diff -u b/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php --- b/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -111,10 +111,19 @@ $prefix = $name . '.'; } + // Gets a profile storage to search for overrides if necessary. + $profile_storage = $this->getProfileStorage($name); + // Gather information about all the supported collections. $collection_info = $this->configManager->getConfigCollectionInfo(); foreach ($collection_info->getCollectionNames() as $collection) { - $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix); + $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storage); + // If we're installing a profile ensure configuration that is overriding + // is excluded. + if ($name == $this->drupalGetProfile()) { + $existing_configuration = $this->getActiveStorages($collection)->listAll(); + $config_to_create = array_diff_key($config_to_create, array_flip($existing_configuration)); + } if (!empty($config_to_create)) { $this->createConfiguration($collection, $config_to_create); } @@ -152,17 +161,20 @@ */ public function installOptionalConfig(StorageInterface $storage = NULL, $prefix = '') { if (!$storage) { - $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, $this->drupalInstallationAttempted()); + // Optional configuration in the profile can never be used. + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE); } $collection_info = $this->configManager->getConfigCollectionInfo(); $enabled_extensions = $this->getEnabledExtensions(); - $existing_config = $this->configFactory->listAll($prefix); + // Gets a profile storage to search for overrides if necessary. + $profile_storage = $this->getProfileStorage(); + foreach ($collection_info->getCollectionNames() as $collection) { if (!$this->configManager->supportsConfigurationEntities($collection)) { continue; } - - $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix); + $existing_config = $this->getActiveStorages($collection)->listAll($prefix); + $config_to_create = $this->getConfigToCreate($storage, $collection, $prefix, $profile_storage); $all_config = array_merge($existing_config, array_keys($config_to_create)); foreach ($config_to_create as $config_name => $data) { // Exclude configuration that: @@ -190,14 +202,25 @@ * The configuration collection to use. * @param string $prefix * (optional) Limit to configuration starting with the provided string. - * @return array An array of configuration data read from the source storage keyed by the - * An array of configuration data read from the source storage keyed by the + * + * @return array + * An array of configuration data read from the source storage keyed by the + * configuration object name. */ - protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '') { + protected function getConfigToCreate(StorageInterface $storage, $collection, $prefix = '', StorageInterface $profile_storage = NULL) { if ($storage->getCollectionName() != $collection) { $storage = $storage->createCollection($collection); } - return $storage->readMultiple($storage->listAll($prefix)); + $data = $storage->readMultiple($storage->listAll($prefix)); + + // Check to see if the corresponding override storage has any overrides. + if ($profile_storage) { + if ($profile_storage->getCollectionName() != $collection) { + $profile_storage = $profile_storage->createCollection($collection); + } + $data = $profile_storage->readMultiple(array_keys($data)) + $data; + } + return $data; } /** @@ -392,9 +415,11 @@ $enabled_extensions = $this->getEnabledExtensions(); // Add the extension that will be enabled to the list of enabled extensions. $enabled_extensions[] = $name; + // Gets a profile storage to search for overrides if necessary. + $profile_storage = $this->getProfileStorage($name);; // Check the dependencies of configuration provided by the module. - $invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions); + $invalid_default_config = $this->findDefaultConfigWithUnmetDependencies($storage, $enabled_extensions, $profile_storage); if (!empty($invalid_default_config)) { throw UnmetDependenciesException::create($name, $invalid_default_config); } @@ -421,8 +446,8 @@ * @return array * List of configuration that has unmet dependencies */ - protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions) { - $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION); + protected function findDefaultConfigWithUnmetDependencies(StorageInterface $storage, array $enabled_extensions, StorageInterface $profile_storage = NULL) { + $config_to_create = $this->getConfigToCreate($storage, StorageInterface::DEFAULT_COLLECTION, '', $profile_storage); $all_config = array_merge($this->configFactory->listAll(), array_keys($config_to_create)); return array_filter(array_keys($config_to_create), function($config_name) use ($enabled_extensions, $all_config, $config_to_create) { return !$this->validateDependencies($config_name, $config_to_create[$config_name], $enabled_extensions, $all_config); @@ -509,6 +534,32 @@ } /** + * Gets the profile storage to use to check for profile overrides. + * + * @param string $installing_name + * (optional) The name of the extension currently being installed. + * + * @return \Drupal\Core\Config\StorageInterface|null + * A storage to access configuration from the installation profile. If a + * Drupal installation is not in progress or we're installing the profile + * itself, then it will return NULL as the profile storage should not be + * used. + */ + protected function getProfileStorage($installing_name = '') { + $profile = $this->drupalGetProfile(); + if ($this->drupalInstallationAttempted() && $profile != $installing_name) { + // Profiles should not contain optional configuration so always use the + // install directory. + $profile_install_path = $this->getDefaultConfigDirectory('module', $profile); + $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION); + } + else { + $profile_storage = NULL; + } + return $profile_storage; + } + + /** * Gets an extension's default configuration directory. * * @param string $type diff -u b/core/lib/Drupal/Core/Config/UnmetDependenciesException.php b/core/lib/Drupal/Core/Config/UnmetDependenciesException.php --- b/core/lib/Drupal/Core/Config/UnmetDependenciesException.php +++ b/core/lib/Drupal/Core/Config/UnmetDependenciesException.php @@ -75,8 +75,7 @@ * @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. + * A list of configuration object names that have unmet dependencies * * @return \Drupal\Core\Config\PreExistingConfigException */ diff -u b/core/modules/config/src/Tests/ConfigInstallWebTest.php b/core/modules/config/src/Tests/ConfigInstallWebTest.php --- b/core/modules/config/src/Tests/ConfigInstallWebTest.php +++ b/core/modules/config/src/Tests/ConfigInstallWebTest.php @@ -7,12 +7,10 @@ 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; /** * Tests installation and removal of configuration objects in install, disable @@ -120,43 +118,6 @@ } /** - * Tests install profile config changes. - */ - function testInstallProfileConfigOverwrite() { - $config_name = 'system.cron'; - // The expected configuration from the system module. - $expected_original_data = array( - 'threshold' => array( - 'autorun' => 0, - 'requirements_warning' => 172800, - 'requirements_error' => 1209600, - ), - ); - // The expected active configuration altered by the install profile. - $expected_profile_data = array( - 'threshold' => array( - 'autorun' => 0, - 'requirements_warning' => 259200, - 'requirements_error' => 1209600, - ), - ); - - // Verify that the original data matches. We have to read the module config - // file directly, because the install profile default system.cron.yml - // configuration file was used to create the active configuration. - $config_dir = drupal_get_path('module', 'system') . '/'. InstallStorage::CONFIG_INSTALL_DIRECTORY; - $this->assertTrue(is_dir($config_dir)); - $source_storage = new FileStorage($config_dir); - $data = $source_storage->read($config_name); - $this->assertIdentical($data, $expected_original_data); - - // Verify that active configuration matches the expected data, which was - // created from the testing install profile's system.cron.yml file. - $config = $this->config($config_name); - $this->assertIdentical($config->get(), $expected_profile_data); - } - - /** * Tests pre-existing configuration detection. */ public function testPreExistingConfigInstall() { only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/src/Tests/ConfigInstallProfileOverrideTest.php @@ -0,0 +1,90 @@ + array( + 'autorun' => 0, + 'requirements_warning' => 172800, + 'requirements_error' => 1209600, + ), + ); + // The expected active configuration altered by the install profile. + $expected_profile_data = array( + 'threshold' => array( + 'autorun' => 0, + 'requirements_warning' => 259200, + 'requirements_error' => 1209600, + ), + ); + + // Verify that the original data matches. We have to read the module config + // file directly, because the install profile default system.cron.yml + // configuration file was used to create the active configuration. + $config_dir = drupal_get_path('module', 'system') . '/'. InstallStorage::CONFIG_INSTALL_DIRECTORY; + $this->assertTrue(is_dir($config_dir)); + $source_storage = new FileStorage($config_dir); + $data = $source_storage->read($config_name); + $this->assertIdentical($data, $expected_original_data); + + // Verify that active configuration matches the expected data, which was + // created from the testing install profile's system.cron.yml file. + $config = $this->config($config_name); + $this->assertIdentical($config->get(), $expected_profile_data); + + // Ensure that the configuration entity has the expected dependencies and + // overrides. + $action = Action::load('user_block_user_action'); + $this->assertEqual($action->label(), 'Overridden block the selected user(s)'); + $action = Action::load('user_cancel_user_action'); + $this->assertEqual($action->label(), 'Cancel the selected user account(s)', 'Default configuration that is not overridden is not affected.'); + + // Ensure that optional configuration can be overridden. + $tour = Tour::load('language'); + $this->assertEqual(count($tour->getTips()), 1, 'Optional configuration can be overridden. The language tour only has one tip'); + $tour = Tour::load('language-add'); + $this->assertEqual(count($tour->getTips()), 3, 'Optional configuration that is not overridden is not affected.'); + + // Ensure that optional configuration is not installed from a profile. + $id = 'testing_config_overrides'; + $this->assertNull(Tour::load($id), 'The tour contained in the profile\'s optional directory does not exist.'); + // Make that we don't get false positives from the assertion above. + $optional_dir = drupal_get_path('module', 'testing_config_overrides') . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; + $storage = new FileStorage($optional_dir); + $this->assertTrue($storage->exists('tour.tour.' . $id), 'The tour does exist in the profile\'s optional directory.'); + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/src/Tests/ConfigInstallProfileUnmetDependenciesTest.php @@ -0,0 +1,95 @@ +siteDirectory . '/profiles/testing_config_overrides'; + mkdir($dest, 0777, TRUE); + $source = DRUPAL_ROOT . '/core/profiles/testing_config_overrides'; + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST); + foreach ($iterator as $item) { + if ($item->isDir()) { + mkdir($dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } + else { + copy($item, $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName()); + } + } + $config_file = $dest . DIRECTORY_SEPARATOR . InstallStorage::CONFIG_INSTALL_DIRECTORY . DIRECTORY_SEPARATOR . 'system.action.user_block_user_action.yml'; + $action = Yaml::decode(file_get_contents($config_file)); + $action['dependencies']['module'][] = 'action'; + file_put_contents($config_file, Yaml::encode($action)); + parent::setUp(); + } + + /** + * {@inheritdoc} + * + * Override the error method so we can test for the expected exception. + */ + protected function error($message = '', $group = 'Other', array $caller = NULL) { + if ($group == 'User notice') { + // Since 'User notice' is set by trigger_error() which is used for debug + // set the message to a status of 'debug'. + return $this->assert('debug', $message, 'Debug', $caller); + } + if ($group == 'Drupal\Core\Config\UnmetDependenciesException') { + $this->expectedException = TRUE; + return FALSE; + } + return $this->assert('exception', $message, $group, $caller); + } + + /** + * {@inheritdoc} + */ + protected function setUpSite() { + // This step is not reached due to the exception. + } + + + /** + * Confirms that the installation succeeded. + */ + public function testInstalled() { + if ($this->expectedException) { + $this->pass('Expected Drupal\Core\Config\UnmetDependenciesException exception thrown'); + } + else { + $this->fail('Expected Drupal\Core\Config\UnmetDependenciesException exception thrown'); + } + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/profiles/testing_config_overrides/config/install/system.action.user_block_user_action.yml @@ -0,0 +1,9 @@ +id: user_block_user_action +label: 'Overridden block the selected user(s)' +status: true +langcode: en +type: user +plugin: user_block_user_action +dependencies: + module: + - user only in patch2: unchanged: --- /dev/null +++ b/core/profiles/testing_config_overrides/config/install/tour.tour.language.yml @@ -0,0 +1,13 @@ +id: language +module: language +label: Language +langcode: en +routes: + - route_name: entity.configurable_language.collection +tips: + language-overview: + id: language-overview + plugin: text + label: Languages + body: '

The "Languages" page allows you to add, edit, delete, and reorder languages for the site.

' + weight: 1 only in patch2: unchanged: --- /dev/null +++ b/core/profiles/testing_config_overrides/config/optional/tour.tour.testing_config_overrides.yml @@ -0,0 +1,13 @@ +id: testing_config_overrides +module: testing_config_overrides +label: Config override test +langcode: en +routes: + - route_name: entity.configurable_language.collection +tips: + language-overview: + id: language-overview + plugin: text + label: Languages + body: '

The "Languages" page allows you to add, edit, delete, and reorder languages for the site.

' + weight: 1 only in patch2: unchanged: --- /dev/null +++ b/core/profiles/testing_config_overrides/testing_config_overrides.info.yml @@ -0,0 +1,10 @@ +name: Testing config overrides +type: profile +description: 'Minimal profile for running tests with config overrides in a profile.' +version: VERSION +core: 8.x +hidden: true +dependencies: + - action + - language + - tour