diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 72ae554..ace69e9 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; @@ -155,16 +155,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 +191,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 +201,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 +222,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 +291,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 +309,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 +330,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->listDefaultConfigCollection($collection, $type, $name, $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 a97cf72..9fcb29b 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..9da1716 --- /dev/null +++ b/core/lib/Drupal/Core/Config/PreExistingConfigException.php @@ -0,0 +1,102 @@ +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 73278d3..c3a5666 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 89ad503..e2e166e 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..ae23d07 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'); } /** @@ -149,4 +171,59 @@ function testInstallProfileConfigOverwrite() { $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 becuase 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 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 UI that has existing configuration. + $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.'); + + // Theme install through the API. + 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..4a6342f --- /dev/null +++ b/core/modules/config/tests/config_clash_test_theme/config_clash_test_theme.info.yml @@ -0,0 +1,11 @@ +# +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..8ea5dd5 --- /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 \ No newline at end of file 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..fbfa817 --- /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 \ No newline at end of file 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..9cb93ae --- /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 \ No newline at end of file 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_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 3bb6d31..46b48dd 100644 --- a/core/modules/system/src/Controller/ThemeController.php +++ b/core/modules/system/src/Controller/ThemeController.php @@ -7,6 +7,7 @@ namespace Drupal\system\Controller; +use Drupal\Core\Config\PreExistingConfigException; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Routing\RouteBuilderIndicatorInterface; @@ -115,12 +116,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 diff --git a/core/modules/system/src/Tests/Module/InstallUninstallTest.php b/core/modules/system/src/Tests/Module/InstallUninstallTest.php index 125b754..a2c8cd1 100644 --- a/core/modules/system/src/Tests/Module/InstallUninstallTest.php +++ b/core/modules/system/src/Tests/Module/InstallUninstallTest.php @@ -7,7 +7,10 @@ namespace Drupal\system\Tests\Module; +use Drupal\Core\Entity\Entity\EntityViewMode; +use Drupal\Core\Field\Entity\BaseFieldOverride; use Drupal\Core\Logger\RfcLogLevel; +use Drupal\node\Entity\NodeType; /** * Install/uninstall core module and confirm table creation/deletion. diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 255449e..5b30997 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -145,6 +145,7 @@ function system_help($route_name, RouteMatchInterface $route_match) { case 'system.status': return '

' . t("Here you can find a short overview of your site's parameters as well as any problems detected with your installation. It may be useful to copy and paste this information into support requests filed on drupal.org's support forums and project issue queues. Before filing a support request, ensure that your web server meets the system requirements.", array('@system-requirements' => 'http://drupal.org/requirements')) . '

'; + } }