diff --git a/core/includes/config.inc b/core/includes/config.inc index 32ba50d..0b86ba3 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -34,7 +34,8 @@ function config_install_default_config($type, $name) { Drupal::service('event_dispatcher'), Drupal::service('config.factory'), Drupal::entityManager(), - Drupal::lock() + Drupal::lock(), + Drupal::moduleHandler() ); $installer->import(); } diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index d496fe7..d2160b5 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -9,6 +9,8 @@ use Drupal\Core\Config\Context\FreeConfigContext; use Drupal\Core\Entity\EntityManager; +use Drupal\Core\SystemListingInfo; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Lock\LockBackendInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -74,6 +76,13 @@ class ConfigImporter { protected $entityManager; /** + * The module handler. + * + * @var |Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** * The used lock backend instance. * * @var \Drupal\Core\Lock\LockBackendInterface @@ -85,7 +94,14 @@ class ConfigImporter { * * @var array */ - protected $processed; + protected $processed = array(); + + /** + * List of changes to process by the import(). + * + * @var array + */ + protected $toProcess = array(); /** * Indicates changes to import have been validated. @@ -106,16 +122,18 @@ class ConfigImporter { * The config factory that statically caches config objects. * @param \Drupal\Core\Entity\EntityManager $entity_manager * The entity manager used to import config entities. - * @param \Drupal\Core\Lock\LockBackendInterface + * @param \Drupal\Core\Lock\LockBackendInterface $lock * The lock backend to ensure multiple imports do not occur at the same time. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. */ - public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManager $entity_manager, LockBackendInterface $lock) { + public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManager $entity_manager, LockBackendInterface $lock, ModuleHandlerInterface $module_handler) { $this->storageComparer = $storage_comparer; $this->eventDispatcher = $event_dispatcher; $this->configFactory = $config_factory; $this->entityManager = $entity_manager; + $this->moduleHandler = $module_handler; $this->lock = $lock; - $this->processed = $this->storageComparer->getEmptyChangelist(); // Use an override free context for importing so that overrides to do not // pollute the imported data. The context is hard coded to ensure this is // the case. @@ -140,7 +158,8 @@ public function getStorageComparer() { */ public function reset() { $this->storageComparer->reset(); - $this->processed = $this->storageComparer->getEmptyChangelist(); + $this->toProcess = array(); + $this->processed = array(); $this->validated = FALSE; return $this; } @@ -148,56 +167,78 @@ public function reset() { /** * Checks if there are any unprocessed changes. * - * @param array $ops - * The operations to check for changes. Defaults to all operations, i.e. - * array('delete', 'create', 'update'). - * * @return bool * TRUE if there are changes to process and FALSE if not. */ - public function hasUnprocessedChanges($ops = array('delete', 'create', 'update')) { - foreach ($ops as $op) { - if (count($this->getUnprocessed($op))) { - return TRUE; - } - } - return FALSE; + public function hasUnprocessedChanges() { + return count($this->getUnprocessed()) ? TRUE : FALSE; } /** - * Gets list of processed changes. + * Sets a change as processed. * - * @return array - * An array containing a list of processed changes. + * @param string $name + * The name of the configuration processed. */ - public function getProcessed() { - return $this->processed; + protected function setProcessed($name) { + $this->processed[$name] = $this->toProcess[$name]; } /** - * Sets a change as processed. + * Gets a list of unprocessed changes for a given operation. * - * @param string $op - * The change operation performed, either delete, create or update. - * @param string $name - * The name of the configuration processed. + * @return array + * An array of configuration names. */ - protected function setProcessed($op, $name) { - $this->processed[$op][] = $name; + public function getUnprocessed() { + if (empty($this->toProcess)) { + $this->toProcess = $this->storageComparer->getChangelistKeyedByName(); + } + return array_diff_key($this->toProcess, $this->processed); } /** - * Gets a list of unprocessed changes for a given operation. + * Get an array of unprocessed configuration files to import + * that belong to a certain module. * - * @param string $op - * The change operation to get the unprocessed list for, either delete, - * create or update. + * @param string $module + * The module to get the unprocessed changes for. * * @return array - * An array of configuration names. + * An array of configuration files to import. */ - public function getUnprocessed($op) { - return array_diff($this->storageComparer->getChangelist($op), $this->processed[$op]); + public function getUnprocessedChanges($module) { + $to_process = array(); + if (!empty($module)) { + foreach ($this->getUnprocessed() as $name => $op) { + if ($this->changeBelongsToModule($name, $module)) { + $to_process[$name] = $op; + } + } + } + return $to_process; + } + + /** + * This function checks whether the config file belongs to a module. + * E.g. if we pass $name as 'breakpoint.settings.yml' and $module + * as 'breakpoint' this will be true. + * + * @param string $name + * The configuration file name. + * + * @param string $module + * The module name. + * + * @return bool + * TRUE if the config file $name belongs to $module, otherwise FALSE. + */ + protected function changeBelongsToModule($name, $module) { + $parts = explode('.', $name); + if (isset($parts[0]) && $parts[0] == $module) { + return TRUE; + } + return FALSE; } /** @@ -218,8 +259,15 @@ public function import() { // Another process is synchronizing configuration. throw new ConfigImporterException(sprintf('%s is already importing', static::ID)); } - $this->importInvokeOwner(); - $this->importConfig(); + + $this->handleExtensions(); + foreach ($this->getUnprocessed() as $name => $op) { + // If this is not a config entity, import from normal config. + if (!$this->importInvokeOwner($name, $op)) { + $this->importConfig($name, $op); + } + } + // Allow modules to react to a import. $this->notify('import'); @@ -249,22 +297,25 @@ public function validate() { /** * Writes an array of config changes from the source to the target storage. + * + * @param string $name + * The name of the configuration file to import. + * + * @param string $op + * The operation. Will be either 'delete', 'create', 'update'. + * */ - protected function importConfig() { - foreach (array('delete', 'create', 'update') as $op) { - foreach ($this->getUnprocessed($op) as $name) { - $config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context); - if ($op == 'delete') { - $config->delete(); - } - else { - $data = $this->storageComparer->getSourceStorage()->read($name); - $config->setData($data ? $data : array()); - $config->save(); - } - $this->setProcessed($op, $name); - } + public function importConfig($name, $op) { + $config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context); + if ($op == 'delete') { + $config->delete(); + } + else { + $data = $this->storageComparer->getSourceStorage()->read($name); + $config->setData($data ? $data : array()); + $config->save(); } + $this->setProcessed($name); } /** @@ -273,36 +324,125 @@ protected function importConfig() { * Allow modules to take over configuration change operations for higher-level * configuration data. * - * @todo Add support for other extension types; e.g., themes etc. + * @param string $name + * The name of the configuration file to import. + * + * @param string $op + * The operation. Will be either 'delete', 'create', 'update'. + * + * @return bool + * TRUE if the config was imported, otherwise FALSE. */ - protected function importInvokeOwner() { + protected function importInvokeOwner($name, $op) { // First pass deleted, then new, and lastly changed configuration, in order // to handle dependencies correctly. - foreach (array('delete', 'create', 'update') as $op) { - foreach ($this->getUnprocessed($op) as $name) { - // Call to the configuration entity's storage controller to handle the - // configuration change. - $handled_by_module = FALSE; - // Validate the configuration object name before importing it. - // Config::validateName($name); - if ($entity_type = config_get_entity_type_by_name($name)) { - $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context); - $old_config->load(); - - $data = $this->storageComparer->getSourceStorage()->read($name); - $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context); - if ($data !== FALSE) { - $new_config->setData($data); - } - - $method = 'import' . ucfirst($op); - $handled_by_module = $this->entityManager->getStorageController($entity_type)->$method($name, $new_config, $old_config); - } - if (!empty($handled_by_module)) { - $this->setProcessed($op, $name); - } + + // Call to the configuration entity's storage controller to handle the + // configuration change. + $handled_by_module = FALSE; + // Validate the configuration object name before importing it. + // Config::validateName($name); + if ($entity_type = config_get_entity_type_by_name($name)) { + $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context); + $old_config->load(); + + $data = $this->storageComparer->getSourceStorage()->read($name); + $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context); + if ($data !== FALSE) { + $new_config->setData($data); + } + + $method = 'import' . ucfirst($op); + $handled_by_module = $this->entityManager->getStorageController($entity_type)->$method($name, $new_config, $old_config); + } + if (!empty($handled_by_module)) { + $this->setProcessed($name); + } + + return !empty($handled_by_module); + } + + /** + * Handle changes to installed modules and themes. + */ + protected function handleExtensions() { + $processlist = $this->getUnprocessed(); + if (isset($processlist['system.module'])) { + $this->handleModules(); + } + if (isset($processlist['system.theme'])) { + $this->handleThemes(); + } + $this->rebuildDependencies(); + } + + /** + * Rebuild caches in the dependencies for the class. + */ + protected function rebuildDependencies() { + $this->entityManager->clearCachedFieldDefinitions(); + $this->entityManager->clearCachedDefinitions(); + $this->configFactory->clearStaticCache(); + $this->eventDispatcher = \Drupal::service('event_dispatcher'); + } + + /** + * Install, disable or uninstall modules depending on config to import. + */ + protected function handleModules() { + $current = $this->storageComparer->getTargetStorage()->read('system.module'); + $new = $this->storageComparer->getSourceStorage()->read('system.module'); + $module_handler = new ConfigModuleHandler($this->moduleHandler->getModuleList(), $this); + + // Get a list of the modules we need to enable. + // We need to filter out any install profiles so they dont get run through module enable. + $modules = array_diff_key($new['enabled'], $current['enabled']); + $listing = new SystemListingInfo(); + $profiles = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'); + $modules = array_diff_key($modules, $profiles); + + // Ensure all modules that need to be enabled are enabled. + foreach ($modules as $module => $weight) { + $module_list = array ($weight => $module); + if (!$module_handler->enable($module_list)) { + throw new ConfigImporterException(sprintf('Unable to enable module')); } } + $this->importConfig('system.module', 'update'); + $disabled_or_uninstalled = array_flip(array_diff_key($current['enabled'], $new['enabled'])); + $disabled = array_keys($this->storageComparer->getSourceStorage()->read('system.module.disabled')); + // Ensure all modules that should be disabled or uninstalled are disabled. + if (!empty($disabled_or_uninstalled)) { + $module_handler->disable($disabled_or_uninstalled); + if (!empty($disabled)) { + $this->setProcessed('system.module.disabled'); + } + // Uninstall any that in the new list of disabled modules. + if (!$module_handler->uninstall(array_diff($disabled_or_uninstalled, $disabled))) { + throw new ConfigImporterException(sprintf('Unable to uninstall modules')); + } + } + } + + /** + * Enable or disable themes depending on config to import. + */ + protected function handleThemes() { + $current = $this->storageComparer->getTargetStorage()->read('system.theme'); + $new = $this->storageComparer->getSourceStorage()->read('system.theme'); + if (isset($current['enabled'])) { + $current = $current['enabled']; + } + else { + $current = array(); + } + theme_enable(array_flip(array_diff_key($new['enabled'], $current))); + if (!empty($current)) { + theme_disable(array_flip(array_diff_key($current, $new['enabled']))); + } + + // Makes sure other settings in the system.theme.yml are imported. + $this->importConfig('system.theme', 'update'); } /** diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 11622b2..63fb57c 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -30,4 +30,12 @@ class ConfigInstaller extends ConfigImporter { */ const ID = 'config.installer'; + /** + * Handle changes to installed modules and themes. + * + * Provide a null implementation so enabling system module during installer + * does not call theme_enable() unnecessarily. + */ + protected function handleExtensions() {} + } diff --git a/core/lib/Drupal/Core/Config/ConfigModuleHandler.php b/core/lib/Drupal/Core/Config/ConfigModuleHandler.php new file mode 100644 index 0000000..f3f3e7a --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigModuleHandler.php @@ -0,0 +1,41 @@ +configImporter = $importer; + } + + /** + * Implements \Drupal\Core\Extension\ModuleHandlerInterface::installDefaultConfig(). + */ + public function installDefaultConfig($module) { + // We get any unprocessed config files to import for the module that is + // currently being enabled, and we import them. + $to_process = $this->configImporter->getUnprocessedChanges($module); + foreach ($to_process as $name => $op) { + $this->configImporter->importConfig($name, $op); + } + } +} diff --git a/core/lib/Drupal/Core/Config/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php index 834f439..6d16e6d 100644 --- a/core/lib/Drupal/Core/Config/StorageComparer.php +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -170,6 +170,17 @@ public function hasChanges($ops = array('delete', 'create', 'update')) { } /** + * {@inheritdoc} + */ + public function getChangelistKeyedByName() { + $flat_list = array(); + foreach ($this->changelist as $op => $list) { + $flat_list += array_fill_keys($list, $op); + } + return $flat_list; + } + + /** * Gets all the configuration names in the source storage. * * @return array diff --git a/core/lib/Drupal/Core/Config/StorageComparerInterface.php b/core/lib/Drupal/Core/Config/StorageComparerInterface.php index 5ca0f3e..fe19642 100644 --- a/core/lib/Drupal/Core/Config/StorageComparerInterface.php +++ b/core/lib/Drupal/Core/Config/StorageComparerInterface.php @@ -117,4 +117,12 @@ public function reset(); */ public function hasChanges($ops = array('delete', 'create', 'update')); + /** + * Flattens a changelist to an array keyed by name with a value of the operation. + * + * @return array + * A list of changes keyed by name. + */ + public function getChangelistKeyedByName(); + } diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php index ba54de3..7a214ee 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php @@ -26,10 +26,8 @@ class ConfigImportSubscriber implements EventSubscriberInterface { * @throws \Drupal\Core\Config\ConfigNameException */ public function onConfigImporterValidate(ConfigImporterEvent $event) { - foreach (array('delete', 'create', 'update') as $op) { - foreach ($event->getConfigImporter()->getUnprocessed($op) as $name) { - Config::validateName($name); - } + foreach ($event->getConfigImporter()->getUnprocessed() as $name => $op) { + Config::validateName($name); } } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 656e918..e793193 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -143,6 +143,13 @@ public function isLoaded() { } /** + * Implements \Drupal\Core\Extension\ModuleHandlerInterface::installDefaultConfig(). + */ + public function installDefaultConfig($module) { + config_install_default_config('module', $module); + } + + /** * Implements \Drupal\Core\Extension\ModuleHandlerInterface::getModuleList(). */ public function getModuleList() { @@ -674,7 +681,7 @@ public function enable($module_list, $enable_dependencies = TRUE) { $version = $versions ? max($versions) : SCHEMA_INSTALLED; // Install default configuration of the module. - config_install_default_config('module', $module); + $this->installDefaultConfig($module); // If the module has no current updates, but has some that were // previously removed, set the version to the value of diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php index 44284b9..2b4121c 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php @@ -49,6 +49,14 @@ public function loadBootstrapModules(); public function isLoaded(); /** + * Installs the default configuration for a module. + * + * @param $module + * The module to install the configuration for. + */ + public function installDefaultConfig($module); + + /** * Reloads all enabled modules. */ public function reload(); diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc index 2d30e49..9b4b02d 100644 --- a/core/modules/config/config.admin.inc +++ b/core/modules/config/config.admin.inc @@ -11,6 +11,7 @@ use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Config\StorageComparer; use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\ConfigModuleHandler; /** * Helper function to construct the storage changes in a configuration synchronization form. @@ -128,7 +129,8 @@ function config_admin_import_form_submit($form, &$form_state) { Drupal::service('event_dispatcher'), Drupal::service('config.factory'), Drupal::entityManager(), - Drupal::lock() + Drupal::lock(), + Drupal::service('module_handler') ); if ($config_importer->alreadyImporting()) { drupal_set_message(t('Another request may be synchronizing configuration already.')); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportMinimalToStandardTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportMinimalToStandardTest.php new file mode 100644 index 0000000..1a7f2ed --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportMinimalToStandardTest.php @@ -0,0 +1,66 @@ + 'Import Standard profile config', + 'description' => 'Tests importing the configuration for standard profile over the top of minimal.', + 'group' => 'Configuration', + ); + } + + function setUp() { + parent::setUp(); + + $this->web_user = $this->drupalCreateUser(array('synchronize configuration')); + $this->drupalLogin($this->web_user); + } + + /** + * Tests config import to go from Minimal to Standard installation profile. + */ + function testMinimalToStandard() { + // Copy all the standard profiles config files to the staging directory. + $src_dir = drupal_get_path('module', 'config_test_minimal_standard') . '/staging'; + foreach (new \DirectoryIterator($src_dir) as $fileinfo) { + if (!$fileinfo->isDot()) { + $filename = $fileinfo->getFilename(); + $this->assertTrue(file_unmanaged_copy($fileinfo->getPathname(), "public://config_staging/$filename")); + } + } + + // Verify that we have stuff to import. + $this->drupalGet('admin/config/development/sync'); + $this->assertFieldById('edit-submit', t('Import all')); + + // Import and verify that both do not appear anymore. + $this->drupalPost(NULL, array(), t('Import all')); + $this->assertText(t('The configuration was imported successfully.')); + + // Login again and assert there are no config changes left. + $web_user = $this->drupalCreateUser(array('synchronize configuration')); + $this->drupalLogin($web_user); + $this->drupalGet('admin/config/development/sync'); + // Verify that there are no further changes to import. + $this->assertNoFieldById('edit-submit', t('Import all')); + $this->assertText(t('There are no configuration changes.')); + } +} diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php index aa133d5..4402838 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -60,7 +60,8 @@ function setUp() { $this->container->get('event_dispatcher'), $this->container->get('config.factory'), $this->container->get('plugin.manager.entity'), - $this->container->get('lock') + $this->container->get('lock'), + $this->container->get('module_handler') ); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); } @@ -228,5 +229,47 @@ function testUpdated() { // Verify that there is nothing more to import. $this->assertFalse($this->configImporter->hasUnprocessedChanges()); } + + function testModuleInstallImport() { + // Ensure default values of system config. + \Drupal::config('system.module')->set('enabled', array())->save(); + \Drupal::config('system.module.disabled')->save(); + + // Set up staging config data. + $staging = $this->container->get('config.storage.staging'); + $data = $staging->read('system.module'); + $data['enabled']['config_test_module_enable'] = '0'; + $staging->write('system.module', $data); + $staging->write('system.module.disabled', array()); + $staging->write('config_test_module_enable.system', array( + 'foo' => 'bar', + '404' => 'derp', + )); + // Import. + $this->configImporter->reset()->import(); + + $config_enabled_modules = config('system.module')->get('enabled'); + $this->assertTrue(isset($config_enabled_modules['config_test_module_enable']), 'Enabled module in active configuration.'); + $this->assertTrue(db_table_exists('config_test_module_enable'), 'Schema created by module exists.'); + $this->assertEqual(config('config_test_module_enable.system')->get('foo'), 'bar', 'Config for the module has been installed.'); + $this->assertEqual(config('config_test_module_enable.system')->get('404'), 'derp', 'Config changed from the default supplied by module has been installed.'); + + // Test going straight to uninstalled. This will cause the importer to + // disable and then uninstall the module because the module is neither in + // the list of enabled modules or disabled modules. + $data = $staging->read('system.module'); + unset($data['enabled']['config_test_module_enable']); + $staging->write('system.module', $data); + $staging->delete('config_test_module_enable.system'); + + // Import. + $this->configImporter->reset()->import(); + + $config_enabled_modules = config('system.module')->get('enabled'); + $this->assertFalse(isset($config_enabled_modules['config_test_module_enable']), 'Uninstalled module in not active configuration.'); + $this->assertFalse(db_table_exists('config_test_module_enable'), 'Schema uninstalled after importing.'); + $this->assertEqual(config('config_test_module_enable.system')->get(), array(), 'Default config for the module has removed after importing.'); + } + } diff --git a/core/modules/config/tests/config_test_minimal_standard/config_test_minimal_standard.info.yml b/core/modules/config/tests/config_test_minimal_standard/config_test_minimal_standard.info.yml new file mode 100644 index 0000000..9b3b454 --- /dev/null +++ b/core/modules/config/tests/config_test_minimal_standard/config_test_minimal_standard.info.yml @@ -0,0 +1,6 @@ +name: 'Configuration import minimal to standard profiles' +type: module +package: 'Testing' +version: VERSION +core: 8.x +hidden: TRUE diff --git a/core/modules/config/tests/config_test_minimal_standard/config_test_minimal_standard.module b/core/modules/config/tests/config_test_minimal_standard/config_test_minimal_standard.module new file mode 100644 index 0000000..24f593d --- /dev/null +++ b/core/modules/config/tests/config_test_minimal_standard/config_test_minimal_standard.module @@ -0,0 +1,6 @@ +