Problem/Motivation

We have uncovered a bug in Media that recreated deleted Media Types during module install. The scenario is that you install Media module and delete a Media Type. When you install a modules the media types are recreated.

Steps to test or recreate are as follows

  • Fresh install of Drupal 8.6.1
  • Enable the core media module
  • Go to structure->media types and delete one or two (I removed Audio and Video)
  • Install a module that provides optional configuration - for example Book (I tried Webforms, the problem was first found with Commerce Shipping)
  • Go back to structure->media types and notice the deleted media types are back

Proposed resolution

Fix ConfigInstaller::installOptionalConfig() so that it does not install additional profile configuration that does not depend on the module being installed.

Remaining tasks

User interface changes

None

API changes

None

Data model changes

None

Comments

johnpicozzi created an issue. See original summary.

johnpicozzi’s picture

Issue summary: View changes
johnpicozzi’s picture

Issue summary: View changes
nathandentzau’s picture

I have replicated this and I believe I've found the source of the bundles reappearing. They're being recreated from the optional configuration in the standard install profile. If you delete /core/profiles/standard/optional/media.type.audio.yml file and then delete the audio entity. Then install and enable the webform module (can be any module), the audio bundle is not installed. This seems like an issue with configuration management and unrelated to the media module.

nathandentzau’s picture

alexpott’s picture

ooh nice find. I'd guess this is an unintended impact of #2930996: Config installer doesn't install possible installable config

alexpott’s picture

So it doesn't happen with any module but from the core modules it does happen with Forum - I think it is any module with optional configuration... so Book causes it too.

alexpott’s picture

Version: 8.6.1 » 8.7.x-dev
Component: media system » configuration system
Priority: Normal » Major
Status: Active » Needs review
Issue tags: +Needs tests
StatusFileSize
new1.01 KB

Here's a fix. We only should be checking the profile's optional config for new things to install when we call installOptionalConfig() for the second time during a module install.

This is a major bug. Configuration coming back after being deleted is very confusing and potentially very bad for a site.

alexpott’s picture

Issue tags: -Needs tests
StatusFileSize
new1.75 KB
new2.76 KB

Here's a test that problem.

johnpicozzi’s picture

@alexpott - I tested this with Patch #8 and it resolves the problem. I will test again with patch 9 later this afternoon. Thanks!

alexpott’s picture

Priority: Major » Critical

The more I think about this I think we have a new critical on our hands. Yes there is a work around - delete the config again - but that would mean you'd have to realise this has happened and you might not. And this type of unexpected change could have consequences for your site.

nathandentzau’s picture

+1 RTBC on Patch #9. Solution looks good in code as well. Can we set a target for this to be merged in 8.6.x-dev too?

alexpott’s picture

Issue summary: View changes

The last submitted patch, 9: 3002655-9.test-only.patch, failed testing. View results

johnpicozzi’s picture

+1 RTBC on Patch #9 as well! I tested it on a clean install of 8.6.1 as well as within my project and it worked in both.

larowlan’s picture

+++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
@@ -171,7 +171,11 @@ public function installOptionalConfig(StorageInterface $storage = NULL, $depende
+      if (!empty($dependency)) {
+        // If we're processing a new dependency then add all the optional
+        // profile config into the list.
+        $optional_profile_config = $profile_storage->listAll();

is it possible that some dependencies will enable profile provided optional configuration to be required - just not all of it?

alexpott’s picture

StatusFileSize
new3.97 KB
new5.56 KB

@larowlan yep that can totally occur when installing a new module. For example, when views is installed. We already have tests for this in ConfigInstallProfileOverrideTest - see the entity with the ID completely_new.

I've tried to make what's going on easier to understand by:

  • Improving the docs
  • Having less variables in the method

This patch is better to review in the full context of the \Drupal\Core\Config\ConfigInstaller::installOptionalConfig().

Here's is the whole function context:

diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php
index abcfc8b426..4a198a7252 100644
--- a/core/lib/Drupal/Core/Config/ConfigInstaller.php
+++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php
@@ -158,97 +158,104 @@ public function installDefaultConfig($type, $name) {
   /**
    * {@inheritdoc}
    */
   public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
     $profile = $this->drupalGetProfile();
-    $optional_profile_config = [];
+    $enabled_extensions = $this->getEnabledExtensions();
+    $existing_config = $this->getActiveStorages()->listAll();
+
+    // Create the storages to read configuration from.
     if (!$storage) {
       // Search the install profile's optional configuration too.
       $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $this->installProfile);
       // The extension install storage ensures that overrides are used.
       $profile_storage = NULL;
     }
     elseif (!empty($profile)) {
       // Creates a profile storage to search for overrides.
       $profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
       $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
-      if (!empty($dependency)) {
-        // If we're processing a new dependency then add all the optional
-        // profile config into the list.
-        $optional_profile_config = $profile_storage->listAll();
-      }
     }
     else {
       // Profile has not been set yet. For example during the first steps of the
       // installer or during unit tests.
       $profile_storage = NULL;
     }
 
-    $enabled_extensions = $this->getEnabledExtensions();
-    $existing_config = $this->getActiveStorages()->listAll();
+    // Build the list of possible configuration to create.
+    $list = $storage->listAll();
+    if ($profile_storage && !empty($dependency)) {
+      // Only add the optional profile configuration into the list if we are
+      // have a dependency to check. This ensures that optional profile
+      // configuration is not unexpectedly re-created after being deleted.
+      $list = array_unique(array_merge($list, $profile_storage->listAll()));
+    }
 
-    $list = array_unique(array_merge($storage->listAll(), $optional_profile_config));
+    // Filter the list of configuration to only include configuration that
+    // should be created.
     $list = array_filter($list, function ($config_name) use ($existing_config) {
       // Only list configuration that:
       // - does not already exist
       // - is a configuration entity (this also excludes config that has an
       //   implicit dependency on modules that are not yet installed)
       return !in_array($config_name, $existing_config) && $this->configManager->getEntityTypeIdByName($config_name);
     });
 
     $all_config = array_merge($existing_config, $list);
     $all_config = array_combine($all_config, $all_config);
     $config_to_create = $storage->readMultiple($list);
     // Check to see if the corresponding override storage has any overrides or
     // new configuration that can be installed.
     if ($profile_storage) {
       $config_to_create = $profile_storage->readMultiple($list) + $config_to_create;
     }
     // Sort $config_to_create in the order of the least dependent first.
     $dependency_manager = new ConfigDependencyManager();
     $dependency_manager->setData($config_to_create);
     $config_to_create = array_merge(array_flip($dependency_manager->sortAll()), $config_to_create);
     if (!empty($dependency)) {
       // In order to work out dependencies we need the full config graph.
       $dependency_manager->setData($this->getActiveStorages()->readMultiple($existing_config) + $config_to_create);
       $dependencies = $dependency_manager->getDependentEntities(key($dependency), reset($dependency));
     }
 
     foreach ($config_to_create as $config_name => $data) {
       // Remove configuration where its dependencies cannot be met.
       $remove = !$this->validateDependencies($config_name, $data, $enabled_extensions, $all_config);
       // Remove configuration that is not dependent on $dependency, if it is
       // defined.
       if (!$remove && !empty($dependency)) {
         $remove = !isset($dependencies[$config_name]);
       }
 
       if ($remove) {
         // Remove from the list of configuration to create.
         unset($config_to_create[$config_name]);
         // Remove from the list of all configuration. This ensures that any
         // configuration that depends on this configuration is also removed.
         unset($all_config[$config_name]);
       }
     }
+
+    // Create the optional configuration if there is any left after filtering.
     if (!empty($config_to_create)) {
       $this->createConfiguration(StorageInterface::DEFAULT_COLLECTION, $config_to_create, TRUE);
     }
   }
alexpott’s picture

Title: Deleted Media Types are created after module install » Optional profile configuration can be unexpectedly recreated after being deleted during a module install

Making the title match the issue and sound as critical as this is.

alexpott’s picture

Title: Optional profile configuration can be unexpectedly recreated after being deleted during a module install » Optional profile configuration can be unexpectedly recreated after being deleted

Edited title for clarity.

alexpott’s picture

I was wrong about when this was introduced. We caused this in #2513604: Create default responsive image styles - when we added the ability for profiles to provide completely new optional configuration that would be installed when their dependencies were installed.

larowlan’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me, nice cleanup @alexpott - much easier to work out what is going on.

  • catch committed 808263b on 8.7.x
    Issue #3002655 by alexpott, johnpicozzi, nathandentzau, larowlan:...

  • catch committed 5e1827e on 8.6.x
    Issue #3002655 by alexpott, johnpicozzi, nathandentzau, larowlan:...
catch’s picture

Version: 8.7.x-dev » 8.6.x-dev
Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.7.x and cherry-picked to 8.6.x. Thanks!

catch’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.