diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 72ae554..c02a132 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -22,11 +22,11 @@ class ConfigInstaller implements ConfigInstallerInterface { protected $configFactory; /** - * The active configuration storage. + * The active configuration storages, keyed by collection. * - * @var \Drupal\Core\Config\StorageInterface + * @var \Drupal\Core\Config\StorageInterface[] */ - protected $activeStorage; + protected $activeStorages; /** * The typed configuration manager. @@ -79,7 +79,7 @@ class ConfigInstaller implements ConfigInstallerInterface { */ public function __construct(ConfigFactoryInterface $config_factory, StorageInterface $active_storage, TypedConfigManagerInterface $typed_config, ConfigManagerInterface $config_manager, EventDispatcherInterface $event_dispatcher) { $this->configFactory = $config_factory; - $this->activeStorage = $active_storage; + $this->activeStorages[$active_storage->getCollectionName()] = $active_storage; $this->typedConfig = $typed_config; $this->configManager = $config_manager; $this->eventDispatcher = $event_dispatcher; @@ -119,7 +119,7 @@ public function installDefaultConfig($type, $name) { $enabled_extensions[] = 'core'; foreach ($collection_info->getCollectionNames(TRUE) as $collection) { - $config_to_install = $this->listDefaultConfigCollection($collection, $type, $name, $enabled_extensions); + $config_to_install = $this->listDefaultConfigToInstall($type, $name, $collection, $enabled_extensions); if (!empty($config_to_install)) { $this->createConfiguration($collection, $config_to_install); } @@ -130,21 +130,25 @@ public function installDefaultConfig($type, $name) { } /** - * Installs default configuration for a particular collection. + * Lists default configuration for an extension that is available to install. + * + * This looks in the extension's config/install directory and all of the + * currently enabled extensions config/install directories for configuration + * that begins with the extension's name. * - * @param string $collection - * The configuration collection to install. * @param string $type * The extension type; e.g., 'module' or 'theme'. * @param string $name * The name of the module or theme to install default configuration for. + * @param string $collection + * The configuration collection to install. * @param array $enabled_extensions * A list of all the currently enabled modules and themes. * * @return array * The list of configuration objects to create. */ - protected function listDefaultConfigCollection($collection, $type, $name, array $enabled_extensions) { + protected function listDefaultConfigToInstall($type, $name, $collection, array $enabled_extensions) { // Get all default configuration owned by this extension. $source_storage = $this->getSourceStorage($collection); $config_to_install = $source_storage->listAll($name . '.'); @@ -155,16 +159,17 @@ protected function listDefaultConfigCollection($collection, $type, $name, array $extension_path = drupal_get_path($type, $name); if ($type !== 'core' && is_dir($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY)) { $default_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection); - $other_module_config = array_filter($default_storage->listAll(), function ($value) use ($name) { - return !preg_match('/^' . $name . '\./', $value); - }); - - $other_module_config = array_filter($other_module_config, function ($config_name) use ($enabled_extensions) { + $extension_provided_config = array_filter($default_storage->listAll(), function ($config_name) use ($config_to_install, $enabled_extensions) { + // Ensure that we have not already discovered the config to install. + if (in_array($config_name, $config_to_install)) { + return FALSE; + } + // Ensure the configuration is provided by an enabled module. $provider = Unicode::substr($config_name, 0, strpos($config_name, '.')); return in_array($provider, $enabled_extensions); }); - $config_to_install = array_merge($config_to_install, $other_module_config); + $config_to_install = array_merge($config_to_install, $extension_provided_config); } return $config_to_install; @@ -190,7 +195,7 @@ protected function createConfiguration($collection, array $config_to_install) { } // Remove configuration that already exists in the active storage. - $config_to_install = array_diff($config_to_install, $this->getActiveStorage($collection)->listAll()); + $config_to_install = array_diff($config_to_install, $this->getActiveStorages($collection)->listAll()); foreach ($config_to_install as $name) { // Allow config factory overriders to use a custom configuration object if @@ -200,7 +205,7 @@ protected function createConfiguration($collection, array $config_to_install) { $new_config = $overrider->createConfigObject($name, $collection); } else { - $new_config = new Config($name, $this->getActiveStorage($collection), $this->eventDispatcher, $this->typedConfig); + $new_config = new Config($name, $this->getActiveStorages($collection), $this->eventDispatcher, $this->typedConfig); } if ($data[$name] !== FALSE) { $new_config->setData($data[$name]); @@ -221,7 +226,7 @@ protected function createConfiguration($collection, array $config_to_install) { ->getStorage($entity_type); // It is possible that secondary writes can occur during configuration // creation. Updates of such configuration are allowed. - if ($this->getActiveStorage($collection)->exists($name)) { + if ($this->getActiveStorages($collection)->exists($name)) { $id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix()); $entity = $entity_storage->load($id); $entity = $entity_storage->updateFromStorageRecord($entity, $new_config->get()); @@ -290,7 +295,7 @@ public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECT // Default to using the ExtensionInstallStorage which searches extension's // config directories for default configuration. Only include the profile // configuration during Drupal installation. - $this->sourceStorage = new ExtensionInstallStorage($this->activeStorage, InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, drupal_installation_attempted()); + $this->sourceStorage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection, drupal_installation_attempted()); } if ($this->sourceStorage->getCollectionName() != $collection) { $this->sourceStorage = $this->sourceStorage->createCollection($collection); @@ -308,11 +313,11 @@ public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECT * @return \Drupal\Core\Config\StorageInterface * The configuration storage that provides the default configuration. */ - protected function getActiveStorage($collection = StorageInterface::DEFAULT_COLLECTION) { - if ($this->activeStorage->getCollectionName() != $collection) { - $this->activeStorage = $this->activeStorage->createCollection($collection); + protected function getActiveStorages($collection = StorageInterface::DEFAULT_COLLECTION) { + if (!isset($this->activeStorages[$collection])) { + $this->activeStorages[$collection] = reset($this->activeStorages)->createCollection($collection); } - return $this->activeStorage; + return $this->activeStorages[$collection]; } /** @@ -329,4 +334,31 @@ public function setSyncing($status) { public function isSyncing() { return $this->isSyncing; } + + /** + * {@inheritdoc} + */ + public function findPreExistingConfiguration($type, $name) { + $existing_configuration = array(); + // Gather information about all the supported collections. + $collection_info = $this->configManager->getConfigCollectionInfo(); + + // Read enabled extensions directly from configuration to avoid circular + // dependencies on ModuleHandler and ThemeHandler. + $extension_config = $this->configFactory->get('core.extension'); + $enabled_extensions = array_keys((array) $extension_config->get('module')); + $enabled_extensions += array_keys((array) $extension_config->get('theme')); + // Add the extension that will be enabled to the list of enabled extensions. + $enabled_extensions[] = $name; + foreach ($collection_info->getCollectionNames(TRUE) as $collection) { + $config_to_install = $this->listDefaultConfigToInstall($type, $name, $collection, $enabled_extensions); + $active_storage = $this->getActiveStorages($collection); + foreach ($config_to_install as $config_name) { + if ($active_storage->exists($config_name)) { + $existing_configuration[$collection][] = $config_name; + } + } + } + return $existing_configuration; + } } diff --git a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php index 13e5b0e..1d1e7e0 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php @@ -84,4 +84,26 @@ public function setSyncing($status); */ public function isSyncing(); + /** + * Finds pre-existing configuration objects for the provided extension. + * + * Extensions can not be installed if configuration objects exist in the + * active storage with the same names. This can happen in a number of ways, + * commonly: + * - if a user has created configuration with the same name as that provided + * by the extension. + * - if the extension provides default configuration that does not depend on + * it and the extension has been uninstalled and is about to the + * reinstalled. + * + * @param string $type + * Type of extension to install. + * @param string $name + * Name of extension to install. + * + * @return array + * Array of configuration objects that already exist keyed by collection. + */ + public function findPreExistingConfiguration($type, $name); + } diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index da0fa98..e3fcaf8 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -110,6 +110,19 @@ public function getEntityTypeIdByName($name) { /** * {@inheritdoc} */ + public function loadConfigEntityByName($name) { + $entity_type_id = $this->getEntityTypeIdByName($name); + if ($entity_type_id) { + $entity_type = $this->entityManager->getDefinition($entity_type_id); + $id = substr($name, strlen($entity_type->getConfigPrefix()) + 1); + return $this->entityManager->getStorage($entity_type_id)->load($id); + } + return NULL; + } + + /** + * {@inheritdoc} + */ public function getEntityManager() { return $this->entityManager; } diff --git a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php index c5fdec1..79f4a7e 100644 --- a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php @@ -24,6 +24,17 @@ public function getEntityTypeIdByName($name); /** + * Loads a configuration entity using the configuration name. + * + * @param string $name + * The configuration object name. + * + * @return \Drupal\Core\Entity\EntityInterface|null + * The configuration entity or NULL if it does not exist. + */ + public function loadConfigEntityByName($name); + + /** * Gets the entity manager. * * @return \Drupal\Core\Entity\EntityManagerInterface diff --git a/core/lib/Drupal/Core/Config/PreExistingConfigException.php b/core/lib/Drupal/Core/Config/PreExistingConfigException.php new file mode 100644 index 0000000..8fe8a6a --- /dev/null +++ b/core/lib/Drupal/Core/Config/PreExistingConfigException.php @@ -0,0 +1,101 @@ +configObjects; + } + + /** + * Gets the name of the extension that is being installed. + * + * @return string + * The name of the extension that is being installed. + */ + public function getExtension() { + return $this->extension; + } + + /** + * Creates an exception for an extension and a list of configuration objects. + * + * @param $extension + * The name of the extension that is being installed. + * @param array $config_objects + * A list of configuration objects that already exist in active + * configuration, keyed by config collection. + * + * @return \Drupal\Core\Config\PreExistingConfigException + */ + public static function create($extension, array $config_objects) { + $message = String::format('Configuration objects (@config_names) provided by @extension already exist in active configuration', + array( + '@config_names' => implode(', ', static::flattenConfigObjects($config_objects)), + '@extension' => $extension + ) + ); + $e = new static($message); + $e->configObjects = $config_objects; + $e->extension = $extension; + return $e; + } + + /** + * Flattens the config object array to a single dimensional list. + * + * @param array $config_objects + * A list of configuration objects that already exist in active + * configuration, keyed by config collection. + * + * @return array + * A list of configuration objects that have been prefixed with their + * collection. + */ + public static function flattenConfigObjects(array $config_objects) { + $flat_config_objects = array(); + foreach ($config_objects as $collection => $config_names) { + $config_names = array_map(function ($config_name) use ($collection) { + if ($collection != StorageInterface::DEFAULT_COLLECTION) { + $config_name = str_replace('.', DIRECTORY_SEPARATOR, $collection) . DIRECTORY_SEPARATOR . $config_name; + } + return $config_name; + }, $config_names); + $flat_config_objects = array_merge($flat_config_objects, $config_names); + } + return $flat_config_objects; + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index 6cb8363..dba7a94 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -10,6 +10,8 @@ use Drupal\Component\Serialization\Yaml; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Config\PreExistingConfigException; +use Drupal\Core\Config\StorageInterface; use Drupal\Core\DrupalKernelInterface; use Drupal\Component\Utility\String; @@ -149,6 +151,18 @@ public function install(array $module_list, $enable_dependencies = TRUE) { ))); } + // Install profiles can not have config clashes. Configuration that + // has the same name as a module's configuration will be used instead. + if ($module != drupal_get_profile()) { + // Validate default configuration of this module. Bail if unable to + // install. Should not continue installing more modules because those + // may depend on this one. + $existing_configuration = $config_installer->findPreExistingConfiguration('module', $module); + if (!empty($existing_configuration)) { + throw PreExistingConfigException::create($module, $existing_configuration); + } + } + $extension_config ->set("module.$module", 0) ->set('module', module_config_sort($extension_config->get('module'))) diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index 2f0fd94..d21e256 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -13,6 +13,7 @@ use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigInstallerInterface; use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Routing\RouteBuilderIndicatorInterface; use Drupal\Core\State\StateInterface; use Psr\Log\LoggerInterface; @@ -255,6 +256,13 @@ public function install(array $theme_list, $install_dependencies = TRUE) { ))); } + // Validate default configuration of the theme. If there is existing + // configuration then stop installing. + $existing_configuration = $this->configInstaller->findPreExistingConfiguration('theme', $key); + if (!empty($existing_configuration)) { + throw PreExistingConfigException::create($key, $existing_configuration); + } + // The value is not used; the weight is ignored for themes currently. $extension_config ->set("theme.$key", 0) diff --git a/core/modules/config/src/Tests/ConfigInstallTest.php b/core/modules/config/src/Tests/ConfigInstallTest.php index 06038e8..2da9e53 100644 --- a/core/modules/config/src/Tests/ConfigInstallTest.php +++ b/core/modules/config/src/Tests/ConfigInstallTest.php @@ -7,6 +7,8 @@ namespace Drupal\config\Tests; +use Drupal\Core\Config\PreExistingConfigException; +use Drupal\Core\Config\StorageInterface; use Drupal\simpletest\KernelTestBase; /** @@ -115,6 +117,21 @@ public function testCollectionInstallationCollections() { $this->assertEqual($collection, $data['collection']); } + // Tests that clashing configuration in collections is detected. + try { + \Drupal::service('module_installer')->install(['config_collection_clash_install_test']); + $this->fail('Expected PreExistingConfigException not thrown.'); + } + catch (PreExistingConfigException $e) { + $this->assertEqual($e->getExtension(), 'config_collection_clash_install_test'); + $this->assertEqual($e->getConfigObjects(), [ + 'another_collection' => ['config_collection_install_test.test'], + 'collection.test1' => ['config_collection_install_test.test'], + 'collection.test2' => ['config_collection_install_test.test'], + ]); + $this->assertEqual($e->getMessage(), 'Configuration objects (another_collection/config_collection_install_test.test, collection/test1/config_collection_install_test.test, collection/test2/config_collection_install_test.test) provided by config_collection_clash_install_test already exist in active configuration'); + } + // Test that the we can use the config installer to install all the // available default configuration in a particular collection for enabled // extensions. diff --git a/core/modules/config/src/Tests/ConfigInstallWebTest.php b/core/modules/config/src/Tests/ConfigInstallWebTest.php index 1fecc90..b04ce39 100644 --- a/core/modules/config/src/Tests/ConfigInstallWebTest.php +++ b/core/modules/config/src/Tests/ConfigInstallWebTest.php @@ -2,12 +2,15 @@ /** * @file - * Definition of Drupal\config\Tests\ConfigInstallTest. + * Contains \Drupal\config\Tests\ConfigInstallWebTest. */ namespace Drupal\config\Tests; use Drupal\Core\Config\InstallStorage; +use Drupal\Core\Config\PreExistingConfigException; +use Drupal\Core\Config\StorageInterface; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; use Drupal\Core\Config\FileStorage; @@ -20,11 +23,18 @@ class ConfigInstallWebTest extends WebTestBase { /** + * The admin user used in this test. + */ + protected $adminUser; + + /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); + $this->adminUser = $this->drupalCreateUser(array('administer modules', 'administer themes')); + // Ensure the global variable being asserted by this test does not exist; // a previous test executed in this request/process might have set it. unset($GLOBALS['hook_config_test']); @@ -82,6 +92,18 @@ function testIntegrationModuleReinstallation() { $this->assertIdentical($config_entity->get('label'), 'Customized integration config label'); // Reinstall the integration module. + try { + \Drupal::service('module_installer')->install(array('config_integration_test')); + $this->fail('Expected PreExistingConfigException not thrown.'); + } + catch (PreExistingConfigException $e) { + $this->assertEqual($e->getExtension(), 'config_integration_test'); + $this->assertEqual($e->getConfigObjects(), [StorageInterface::DEFAULT_COLLECTION => ['config_test.dynamic.config_integration_test']]); + $this->assertEqual($e->getMessage(), 'Configuration objects (config_test.dynamic.config_integration_test) provided by config_integration_test already exist in active configuration'); + } + + // Delete the configuration entity so that the install will work. + $config_entity->delete(); \Drupal::service('module_installer')->install(array('config_integration_test')); // Verify the integration module's config was re-installed. @@ -91,10 +113,10 @@ function testIntegrationModuleReinstallation() { $this->assertIdentical($config_static->isNew(), FALSE); $this->assertIdentical($config_static->get('foo'), 'default setting'); - // Verify the customized integration config still exists. - $config_entity = $this->config($default_configuration_entity); + // Verify the integration config is using the default. + $config_entity = \Drupal::config($default_configuration_entity); $this->assertIdentical($config_entity->isNew(), FALSE); - $this->assertIdentical($config_entity->get('label'), 'Customized integration config label'); + $this->assertIdentical($config_entity->get('label'), 'Default integration config label'); } /** @@ -132,21 +154,64 @@ function testInstallProfileConfigOverwrite() { // created from the testing install profile's system.cron.yml file. $config = $this->config($config_name); $this->assertIdentical($config->get(), $expected_profile_data); + } - // Turn on the test module, which will attempt to replace the - // configuration data. This attempt to replace the active configuration - // should be ignored. - \Drupal::service('module_installer')->install(array('config_existing_default_config_test')); - - // Verify that the test module has not been able to change the data. - $config = $this->config($config_name); - $this->assertIdentical($config->get(), $expected_profile_data); - - // Disable and uninstall the test module. - \Drupal::service('module_installer')->uninstall(array('config_existing_default_config_test')); - - // Verify that the data hasn't been altered by removing the test module. - $config = $this->config($config_name); - $this->assertIdentical($config->get(), $expected_profile_data); + /** + * Tests pre-existing configuration detection. + */ + public function testPreExistingConfigInstall() { + $this->drupalLogin($this->adminUser); + + // Try to install config_install_fail_test and config_test. Doing this + // will install the config_test module first because it is a dependency of + // config_install_fail_test. + // @see \Drupal\system\Form\ModulesListForm::submitForm() + $this->drupalPostForm('admin/modules', array('modules[Testing][config_test][enable]' => TRUE, 'modules[Testing][config_install_fail_test][enable]' => TRUE), t('Save configuration')); + $this->assertRaw('Unable to install Configuration install fail test, config_test.dynamic.dotted.default already exists in active configuration.'); + + // Uninstall the config_test module to test the confirm form. + $this->drupalPostForm('admin/modules/uninstall', array('uninstall[config_test]' => TRUE), t('Uninstall')); + $this->drupalPostForm(NULL, array(), t('Uninstall')); + + // Try to install config_install_fail_test without selecting config_test. + // The user is shown a confirm form because the config_test module is a + // dependency. + // @see \Drupal\system\Form\ModulesListConfirmForm::submitForm() + $this->drupalPostForm('admin/modules', array('modules[Testing][config_install_fail_test][enable]' => TRUE), t('Save configuration')); + $this->drupalPostForm(NULL, array(), t('Continue')); + $this->assertRaw('Unable to install Configuration install fail test, config_test.dynamic.dotted.default already exists in active configuration.'); + + // Test that collection configuration clashes during a module install are + // reported correctly. + \Drupal::service('module_installer')->install(['language']); + $this->rebuildContainer(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + \Drupal::languageManager() + ->getLanguageConfigOverride('fr', 'config_test.dynamic.dotted.default') + ->set('label', 'Je suis Charlie') + ->save(); + + $this->drupalPostForm('admin/modules', array('modules[Testing][config_install_fail_test][enable]' => TRUE), t('Save configuration')); + $this->assertRaw('Unable to install Configuration install fail test, config_test.dynamic.dotted.default, language/fr/config_test.dynamic.dotted.default already exist in active configuration.'); + + // Test installing a theme through the UI that has existing configuration. + // This relies on the fact the config_test has been installed and created + // the config_test.dynamic.dotted.default configuration and the translation + // override created still exists. + $this->drupalGet('admin/appearance'); + $url = $this->xpath("//a[contains(@href,'config_clash_test_theme') and contains(@href,'/install?')]/@href")[0]; + $this->drupalGet($this->getAbsoluteUrl($url)); + $this->assertRaw('Unable to install config_clash_test_theme, config_test.dynamic.dotted.default, language/fr/config_test.dynamic.dotted.default already exist in active configuration.'); + + // Test installing a theme through the API that has existing configuration. + try { + \Drupal::service('theme_handler')->install(['config_clash_test_theme']); + $this->fail('Expected PreExistingConfigException not thrown.'); + } + catch (PreExistingConfigException $e) { + $this->assertEqual($e->getExtension(), 'config_clash_test_theme'); + $this->assertEqual($e->getConfigObjects(), [StorageInterface::DEFAULT_COLLECTION => ['config_test.dynamic.dotted.default'], 'language.fr' => ['config_test.dynamic.dotted.default']]); + $this->assertEqual($e->getMessage(), 'Configuration objects (config_test.dynamic.dotted.default, language/fr/config_test.dynamic.dotted.default) provided by config_clash_test_theme already exist in active configuration'); + } } } diff --git a/core/modules/config/src/Tests/ConfigOtherModuleTest.php b/core/modules/config/src/Tests/ConfigOtherModuleTest.php index 5737d78..082e485 100644 --- a/core/modules/config/src/Tests/ConfigOtherModuleTest.php +++ b/core/modules/config/src/Tests/ConfigOtherModuleTest.php @@ -7,6 +7,8 @@ namespace Drupal\config\Tests; +use Drupal\Core\Config\PreExistingConfigException; +use Drupal\Core\Config\StorageInterface; use Drupal\simpletest\WebTestBase; /** @@ -57,10 +59,18 @@ public function testInstallOtherModuleFirst() { // Default configuration provided by config_test should still exist. $this->assertTrue(entity_load('config_test', 'dotted.default', TRUE), 'The configuration is not deleted.'); - // Re-enable module to test that default config is unchanged. - $this->installModule('config_other_module_config_test'); - $config_entity = entity_load('config_test', 'other_module_test', TRUE); - $this->assertEqual($config_entity->get('style'), "The piano ain't got no wrong notes.", 'Re-enabling the module does not install default config over the existing config entity.'); + // Re-enable module to test that pre-existing default configuration throws + // an error. + $msg = "The expected PreExistingConfigException is thrown by reinstalling config_other_module_config_test."; + try { + $this->installModule('config_other_module_config_test'); + $this->fail($msg); + } + catch (PreExistingConfigException $e) { + $this->pass($msg); + $this->assertEqual($e->getExtension(), 'config_other_module_config_test'); + $this->assertEqual($e->getConfigObjects(), [StorageInterface::DEFAULT_COLLECTION => ['config_test.dynamic.other_module_test']]); + } } /** diff --git a/core/modules/config/tests/config_clash_test_theme/config/install/config_test.dynamic.dotted.default.yml b/core/modules/config/tests/config_clash_test_theme/config/install/config_test.dynamic.dotted.default.yml new file mode 100644 index 0000000..eb94849 --- /dev/null +++ b/core/modules/config/tests/config_clash_test_theme/config/install/config_test.dynamic.dotted.default.yml @@ -0,0 +1,7 @@ +# Clashes with default configuration provided by the config_test module. +id: dotted.default +label: 'Config install fail' +weight: 0 +protected_property: Default +# Intentionally commented out to verify default status behavior. +# status: 1 diff --git a/core/modules/config/tests/config_clash_test_theme/config/install/language/fr/config_test.dynamic.dotted.default.yml b/core/modules/config/tests/config_clash_test_theme/config/install/language/fr/config_test.dynamic.dotted.default.yml new file mode 100644 index 0000000..e73dec0 --- /dev/null +++ b/core/modules/config/tests/config_clash_test_theme/config/install/language/fr/config_test.dynamic.dotted.default.yml @@ -0,0 +1,2 @@ +# Clashes with default configuration provided by the config_test module. +label: 'Je suis' diff --git a/core/modules/config/tests/config_clash_test_theme/config_clash_test_theme.info.yml b/core/modules/config/tests/config_clash_test_theme/config_clash_test_theme.info.yml new file mode 100644 index 0000000..2ff354d --- /dev/null +++ b/core/modules/config/tests/config_clash_test_theme/config_clash_test_theme.info.yml @@ -0,0 +1,10 @@ +name: 'Test theme for configuration clash detection' +type: theme +description: 'Test theme for configuration clash detection' +version: VERSION +base theme: classy +core: 8.x +regions: + content: Content + left: Left + right: Right diff --git a/core/modules/config/tests/config_collection_clash_install_test/config/install/another_collection/config_collection_install_test.test.yml b/core/modules/config/tests/config_collection_clash_install_test/config/install/another_collection/config_collection_install_test.test.yml new file mode 100644 index 0000000..0337f93 --- /dev/null +++ b/core/modules/config/tests/config_collection_clash_install_test/config/install/another_collection/config_collection_install_test.test.yml @@ -0,0 +1 @@ +collection: another_collection diff --git a/core/modules/config/tests/config_collection_clash_install_test/config/install/collection/test1/config_collection_install_test.test.yml b/core/modules/config/tests/config_collection_clash_install_test/config/install/collection/test1/config_collection_install_test.test.yml new file mode 100644 index 0000000..8bdebee --- /dev/null +++ b/core/modules/config/tests/config_collection_clash_install_test/config/install/collection/test1/config_collection_install_test.test.yml @@ -0,0 +1 @@ +collection: collection.test1 diff --git a/core/modules/config/tests/config_collection_clash_install_test/config/install/collection/test2/config_collection_install_test.test.yml b/core/modules/config/tests/config_collection_clash_install_test/config/install/collection/test2/config_collection_install_test.test.yml new file mode 100644 index 0000000..b5ae44c --- /dev/null +++ b/core/modules/config/tests/config_collection_clash_install_test/config/install/collection/test2/config_collection_install_test.test.yml @@ -0,0 +1 @@ +collection: collection.test2 diff --git a/core/modules/config/tests/config_collection_clash_install_test/config/install/entity/config_test.dynamic.dotted.default.yml b/core/modules/config/tests/config_collection_clash_install_test/config/install/entity/config_test.dynamic.dotted.default.yml new file mode 100644 index 0000000..f77c303 --- /dev/null +++ b/core/modules/config/tests/config_collection_clash_install_test/config/install/entity/config_test.dynamic.dotted.default.yml @@ -0,0 +1 @@ +label: entity diff --git a/core/modules/config/tests/config_collection_clash_install_test/config_collection_clash_install_test.info.yml b/core/modules/config/tests/config_collection_clash_install_test/config_collection_clash_install_test.info.yml new file mode 100644 index 0000000..8cdab05 --- /dev/null +++ b/core/modules/config/tests/config_collection_clash_install_test/config_collection_clash_install_test.info.yml @@ -0,0 +1,9 @@ +# This should contain a copy of the configuration from the +# config_collection_install_test module. +name: 'Config collection clash test module' +type: module +package: Testing +version: VERSION +core: 8.x +dependencies: + - config_collection_install_test diff --git a/core/modules/config/tests/config_existing_default_config_test/config_existing_default_config_test.info.yml b/core/modules/config/tests/config_existing_default_config_test/config_existing_default_config_test.info.yml deleted file mode 100644 index 75436be..0000000 --- a/core/modules/config/tests/config_existing_default_config_test/config_existing_default_config_test.info.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: 'Configuration existing default config test' -type: module -package: Testing -version: VERSION -core: 8.x diff --git a/core/modules/config/tests/config_install_fail_test/config/install/config_test.dynamic.dotted.default.yml b/core/modules/config/tests/config_install_fail_test/config/install/config_test.dynamic.dotted.default.yml new file mode 100644 index 0000000..eb94849 --- /dev/null +++ b/core/modules/config/tests/config_install_fail_test/config/install/config_test.dynamic.dotted.default.yml @@ -0,0 +1,7 @@ +# Clashes with default configuration provided by the config_test module. +id: dotted.default +label: 'Config install fail' +weight: 0 +protected_property: Default +# Intentionally commented out to verify default status behavior. +# status: 1 diff --git a/core/modules/config/tests/config_install_fail_test/config/install/language/fr/config_test.dynamic.dotted.default.yml b/core/modules/config/tests/config_install_fail_test/config/install/language/fr/config_test.dynamic.dotted.default.yml new file mode 100644 index 0000000..e73dec0 --- /dev/null +++ b/core/modules/config/tests/config_install_fail_test/config/install/language/fr/config_test.dynamic.dotted.default.yml @@ -0,0 +1,2 @@ +# Clashes with default configuration provided by the config_test module. +label: 'Je suis' diff --git a/core/modules/config/tests/config_install_fail_test/config_install_fail_test.info.yml b/core/modules/config/tests/config_install_fail_test/config_install_fail_test.info.yml new file mode 100644 index 0000000..44a9cf3 --- /dev/null +++ b/core/modules/config/tests/config_install_fail_test/config_install_fail_test.info.yml @@ -0,0 +1,7 @@ +name: 'Configuration install fail test' +type: module +package: Testing +version: VERSION +core: 8.x +dependencies: + - config_test diff --git a/core/modules/field/src/Tests/FieldImportChangeTest.php b/core/modules/field/src/Tests/FieldImportChangeTest.php index af06824..f2194f7 100644 --- a/core/modules/field/src/Tests/FieldImportChangeTest.php +++ b/core/modules/field/src/Tests/FieldImportChangeTest.php @@ -19,6 +19,10 @@ class FieldImportChangeTest extends FieldUnitTestBase { /** * Modules to enable. * + * The default configuration provided by field_test_config is imported by + * \Drupal\field\Tests\FieldUnitTestBase::setUp() when it installs field + * configuration. + * * @var array */ public static $modules = array('field_test_config'); @@ -31,8 +35,6 @@ function testImportChange() { $field_id = "entity_test.entity_test.$field_storage_id"; $field_config_name = "field.field.$field_id"; - // Import default config. - $this->installConfig(array('field_test_config')); $active = $this->container->get('config.storage'); $staging = $this->container->get('config.storage.staging'); $this->copyConfig($active, $staging); diff --git a/core/modules/field/src/Tests/FieldImportDeleteTest.php b/core/modules/field/src/Tests/FieldImportDeleteTest.php index 802d144..030adfb 100644 --- a/core/modules/field/src/Tests/FieldImportDeleteTest.php +++ b/core/modules/field/src/Tests/FieldImportDeleteTest.php @@ -21,6 +21,10 @@ class FieldImportDeleteTest extends FieldUnitTestBase { /** * Modules to enable. * + * The default configuration provided by field_test_config is imported by + * \Drupal\field\Tests\FieldUnitTestBase::setUp() when it installs field + * configuration. + * * @var array */ public static $modules = array('field_test_config'); @@ -53,9 +57,6 @@ public function testImportDelete() { // Create a second bundle for the 'Entity test' entity type. entity_test_create_bundle('test_bundle'); - // Import default config. - $this->installConfig(array('field_test_config')); - // Get the uuid's for the field storages. $field_storage_uuid = FieldStorageConfig::load($field_storage_id)->uuid(); $field_storage_uuid_2 = FieldStorageConfig::load($field_storage_id_2)->uuid(); diff --git a/core/modules/forum/config/install/taxonomy.vocabulary.forums.yml b/core/modules/forum/config/install/taxonomy.vocabulary.forums.yml index 6ed487b..951e4a3 100644 --- a/core/modules/forum/config/install/taxonomy.vocabulary.forums.yml +++ b/core/modules/forum/config/install/taxonomy.vocabulary.forums.yml @@ -1,6 +1,9 @@ langcode: en status: true -dependencies: { } +dependencies: + enforced: + module: + - forum name: Forums vid: forums description: 'Forum navigation vocabulary' diff --git a/core/modules/system/src/Controller/ThemeController.php b/core/modules/system/src/Controller/ThemeController.php index 26083cb..7126a7e 100644 --- a/core/modules/system/src/Controller/ThemeController.php +++ b/core/modules/system/src/Controller/ThemeController.php @@ -8,6 +8,7 @@ namespace Drupal\system\Controller; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Routing\RouteBuilderIndicatorInterface; @@ -120,12 +121,28 @@ public function install(Request $request) { $theme = $request->get('theme'); if (isset($theme)) { - if ($this->themeHandler->install(array($theme))) { - $themes = $this->themeHandler->listInfo(); - drupal_set_message($this->t('The %theme theme has been installed.', array('%theme' => $themes[$theme]->info['name']))); + try { + if ($this->themeHandler->install(array($theme))) { + $themes = $this->themeHandler->listInfo(); + drupal_set_message($this->t('The %theme theme has been installed.', array('%theme' => $themes[$theme]->info['name']))); + } + else { + drupal_set_message($this->t('The %theme theme was not found.', array('%theme' => $theme)), 'error'); + } } - else { - drupal_set_message($this->t('The %theme theme was not found.', array('%theme' => $theme)), 'error'); + catch (PreExistingConfigException $e) { + $config_objects = $e->flattenConfigObjects($e->getConfigObjects()); + drupal_set_message( + $this->formatPlural( + count($config_objects), + 'Unable to install @extension, %config_names already exists in active configuration.', + 'Unable to install @extension, %config_names already exist in active configuration.', + array( + '%config_names' => implode(', ', $config_objects), + '@extension' => $theme, + )), + 'error' + ); } return $this->redirect('system.themes_page'); diff --git a/core/modules/system/src/Form/ModulesListConfirmForm.php b/core/modules/system/src/Form/ModulesListConfirmForm.php index 24f1b4b..b3c7977 100644 --- a/core/modules/system/src/Form/ModulesListConfirmForm.php +++ b/core/modules/system/src/Form/ModulesListConfirmForm.php @@ -7,6 +7,7 @@ namespace Drupal\system\Form; +use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Form\ConfirmFormBase; @@ -157,7 +158,24 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // the form doesn't allow modules with unmet dependencies, so the only way // this can happen is if the filesystem changed between form display and // submit, in which case the user has bigger problems. - $this->moduleInstaller->install(array_keys($this->modules['install'])); + try { + $this->moduleInstaller->install(array_keys($this->modules['install'])); + } + catch (PreExistingConfigException $e) { + $config_objects = $e->flattenConfigObjects($e->getConfigObjects()); + drupal_set_message( + $this->formatPlural( + count($config_objects), + 'Unable to install @extension, %config_names already exists in active configuration.', + 'Unable to install @extension, %config_names already exist in active configuration.', + array( + '%config_names' => implode(', ', $config_objects), + '@extension' => $this->modules['install'][$e->getExtension()] + )), + 'error' + ); + return; + } } // Gets module list after install process, flushes caches and displays a diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index 2486ca4..2e80482 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Entity\EntityManagerInterface; @@ -517,7 +518,24 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // There seem to be no dependencies that would need approval. if (!empty($modules['install'])) { - $this->moduleInstaller->install(array_keys($modules['install'])); + try { + $this->moduleInstaller->install(array_keys($modules['install'])); + } + catch (PreExistingConfigException $e) { + $config_objects = $e->flattenConfigObjects($e->getConfigObjects()); + drupal_set_message( + $this->formatPlural( + count($config_objects), + 'Unable to install @extension, %config_names already exists in active configuration.', + 'Unable to install @extension, %config_names already exist in active configuration.', + array( + '%config_names' => implode(', ', $config_objects), + '@extension' => $modules['install'][$e->getExtension()] + )), + 'error' + ); + return; + } } // Gets module list after install process, flushes caches and displays a