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 b184c48..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 @@ -113,14 +122,17 @@ 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; // 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 @@ -167,20 +179,14 @@ public function hasUnprocessedChanges() { * * @param string $name * The name of the configuration processed. - * @param string $op - * The change operation performed, either delete, create or update. */ - protected function setProcessed($name, $op) { - $this->processed[$name] = $op; + protected function setProcessed($name) { + $this->processed[$name] = $this->toProcess[$name]; } /** * Gets a list of unprocessed changes for a given operation. * - * @param string $op - * The change operation to get the unprocessed list for, either delete, - * create or update. - * * @return array * An array of configuration names. */ @@ -192,6 +198,50 @@ public function getUnprocessed() { } /** + * Get an array of unprocessed configuration files to import + * that belong to a certain module. + * + * @param string $module + * The module to get the unprocessed changes for. + * + * @return array + * An array of configuration files to import. + */ + 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; + } + + /** * Imports the changelist to the target storage. * * @throws \Drupal\Core\Config\ConfigException @@ -211,8 +261,13 @@ public function import() { } $this->handleExtensions(); - $this->importInvokeOwner(); - $this->importConfig(); + 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'); @@ -242,20 +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 ($this->getUnprocessed() as $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, $op); + 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); } /** @@ -264,34 +324,42 @@ 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 ($this->getUnprocessed() as $name => $op) { - // 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, $op); + // 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); } /** @@ -305,28 +373,54 @@ protected function handleExtensions() { if (isset($processlist['system.theme'])) { $this->handleThemes(); } + $this->rebuildDependencies(); + } - // Recalculate differences as default config could have been imported. - $this->storageComparer->reset(); - $this->toProcess = array(); - $this->processed = array(); + /** + * 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'); - if (!\Drupal::moduleHandler()->enable(array_flip(array_diff_key($new['enabled'], $current['enabled'])))) { - throw new ConfigImporterException(sprintf('Unable to enable modules')); + $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. - \Drupal::moduleHandler()->disable($disabled_or_uninstalled); - // Uninstall any that in the new list of disabled modules. - if (!\Drupal::moduleHandler()->uninstall(array_diff($disabled_or_uninstalled, $disabled))) { - throw new ConfigImporterException(sprintf('Unable to uninstall modules')); + 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')); + } } } @@ -346,6 +440,9 @@ protected function handleThemes() { 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/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/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 a3d8cc9..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')); } 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 @@ +