diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index e2354f4..8b1f645 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\TypedConfigManager; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Lock\LockBackendInterface; use Drupal\Component\Uuid\UuidInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -89,18 +91,25 @@ class ConfigImporter { protected $validated; /** - * The UUID service. + * The typed config manager. * - * @var \Drupal\Component\Uuid\UuidInterface + * @var \Drupal\Core\Config\TypedConfigManager */ - protected $uuidService; + protected $typedConfigManager; /** - * The typed config manager. + * The module handler. * - * @var \Drupal\Core\Config\TypedConfigManager + * @var \Drupal\Core\Extension\ModuleHandlerInterface */ - protected $typedConfigManager; + protected $moduleHandler; + + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; /** * Constructs a configuration import object. @@ -116,19 +125,22 @@ class ConfigImporter { * The entity manager used to import config entities. * @param \Drupal\Core\Lock\LockBackendInterface * The lock backend to ensure multiple imports do not occur at the same time. - * @param \Drupal\Component\Uuid\UuidInterface $uuid_service - * The UUID service. * @param \Drupal\Core\Config\TypedConfigManager $typed_config * The typed configuration manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler + * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler + * The theme handler */ - public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManagerInterface $entity_manager, LockBackendInterface $lock, UuidInterface $uuid_service, TypedConfigManager $typed_config) { + public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManagerInterface $entity_manager, LockBackendInterface $lock, TypedConfigManager $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { $this->storageComparer = $storage_comparer; $this->eventDispatcher = $event_dispatcher; $this->configFactory = $config_factory; $this->entityManager = $entity_manager; $this->lock = $lock; - $this->uuidService = $uuid_service; $this->typedConfigManager = $typed_config; + $this->moduleHandler = $module_handler; + $this->themeHandler = $theme_handler; $this->processed = $this->storageComparer->getEmptyChangelist(); } @@ -227,6 +239,7 @@ public function import() { // Another process is synchronizing configuration. throw new ConfigImporterException(sprintf('%s is already importing', static::ID)); } + $this->handleExtensions(); $this->importInvokeOwner(); $this->importConfig(); // Allow modules to react to a import. @@ -346,4 +359,116 @@ public function getId() { return static::ID; } + /** + * Checks if a configuration object will be updated by the import. + * + * @param $config_name + * The configuration object name. + * + * @return bool + * TRUE if the configuration object will be updated. + */ + protected function hasUpdate($config_name) { + return in_array($config_name, $this->getUnprocessed('update')); + } + + /** + * Handle changes to installed modules and themes. + */ + protected function handleExtensions() { + // Are there changes to process? + $module_update = $this->hasUpdate('system.module'); + $theme_update = $this->hasUpdate('system.theme'); + if ($module_update || $theme_update) { + // Set the config installer to use the staging directory instead of the + // extensions own default config directories. + \Drupal::service('config.installer') + ->setSyncing(TRUE) + ->setSourceStorage($this->storageComparer->getSourceStorage()); + + if ($module_update) { + $this->handleModules(); + } + + if ($theme_update) { + $this->handleThemes(); + } + + \Drupal::service('config.installer') + ->setSyncing(FALSE) + ->resetSourceStorage(); + + // Recalculate differences as default config could have been imported. + $this->storageComparer->reset(); + $this->processed = $this->storageComparer->getEmptyChangelist(); + drupal_flush_all_caches(); + // Modules have been updated. Services etc might have changed. + // We don't reinject storage comparer because swapping out the active + // store during config import is a complete nonsense. + $this->reInjectMe(); + + } + } + + /** + * Install or uninstall modules depending on configuration to import. + */ + protected function handleModules() { + $current = $this->storageComparer->getTargetStorage()->read('system.module'); + $new = $this->storageComparer->getSourceStorage()->read('system.module'); + + $to_enable = array_diff(array_keys($new['enabled']), array_keys($current['enabled'])); + if (!$this->moduleHandler->install($to_enable)) { + throw new ConfigImporterException(sprintf('Unable to enable modules')); + } + + $to_uninstall = array_diff(array_keys($current['enabled']), array_keys($new['enabled'])); + if (!$this->moduleHandler->uninstall($to_uninstall)) { + throw new ConfigImporterException(sprintf('Unable to uninstall modules')); + } + } + + /** + * Enable or disable themes depending on configuration to import. + */ + protected function handleThemes() { + $current = $this->storageComparer->getTargetStorage()->read('system.theme'); + $new = $this->storageComparer->getSourceStorage()->read('system.theme'); + + $enabled = isset($current['enabled']) ? $current['enabled'] : array(); + + $themes_to_enable = array_diff(array_keys($new['enabled']), array_keys($enabled)); + $themes_to_disable = array_diff(array_keys($enabled), array_keys($new['enabled'])); + + if (!empty($themes_to_enable)) { + $this->themeHandler->enable($themes_to_enable); + } + + // Are we disabling the default theme? This is not possible. Update the + // value to the value from the staged configuration. Enabling themes first + // ensures that if any of the newly enabled themes are the default theme it + // will be already enabled. + if(in_array($current['default'], $themes_to_disable)) { + // Use the configuration factory to write the data since system.theme + // might have been updated by enabling themes. + $this->configFactory + ->get('system.theme') + ->set('default', $new['default']) + ->save(); + } + + if (!empty($themes_to_disable)) { + $this->themeHandler->disable($themes_to_disable); + } + } + + protected function reInjectMe() { + $this->eventDispatcher = \Drupal::service('event_dispatcher'); + $this->configFactory = \Drupal::configFactory(); + $this->entityManager = \Drupal::entityManager(); + $this->lock = \Drupal::lock(); + $this->typedConfigManager = \Drupal::service('config.typed'); + $this->moduleHandler = \Drupal::moduleHandler(); + $this->themeHandler = \Drupal::service('theme_handler'); + } } diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index 0be3354..ca1ebaf 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -49,6 +49,20 @@ class ConfigInstaller implements ConfigInstallerInterface { protected $eventDispatcher; /** + * The configuration storage that provides the default configuration. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $sourceStorage; + + /** + * Is configuration being created as part of a configuration sync. + * + * @var bool + */ + protected $isSyncing = FALSE; + + /** * Constructs the configuration installer. * * @param \Drupal\Core\Config\ConfigFactory $config_factory @@ -75,7 +89,7 @@ public function __construct(ConfigFactory $config_factory, StorageInterface $act */ public function installDefaultConfig($type, $name) { // Get all default configuration owned by this extension. - $source_storage = new ExtensionInstallStorage($this->activeStorage); + $source_storage = $this->getSourceStorage(); $config_to_install = $source_storage->listAll($name . '.'); // Work out if this extension provides default configuration for any other @@ -119,7 +133,15 @@ public function installDefaultConfig($type, $name) { if ($data !== FALSE) { $new_config->setData($data); } - if ($entity_type = config_get_entity_type_by_name($name)) { + if (!$this->isSyncing && $entity_type = config_get_entity_type_by_name($name)) { + // If we are syncing do not create configuration entities. Pluggable + // configuration entities can have dependencies on modules that are + // not yet enabled. In the absence of dependency management for config + // entities this is a good as we can do. The problem with this + // approach is that any code that expects default configuration + // entities to exist (even if there is code the prevents this from + // happening) will be unstable after the module has been enabled and + // before the config entity has been imported. $this->entityManager ->getStorageController($entity_type) ->create($new_config->get()) @@ -133,4 +155,49 @@ public function installDefaultConfig($type, $name) { } } + /** + * {@inheritdoc} + */ + public function setSourceStorage(StorageInterface $storage) { + $this->sourceStorage = $storage; + return $this; + } + + /** + * {@inheritdoc} + */ + public function resetSourceStorage() { + $this->sourceStorage = null; + return $this; + } + + /** + * Gets the configuration storage that provides the default configuration. + * + * @return \Drupal\Core\Config\StorageInterface + * The configuration storage that provides the default configuration. + */ + public function getSourceStorage() { + if (!isset($this->sourceStorage)) { + // If using the the extension install storage class can not + return new ExtensionInstallStorage($this->activeStorage); + } + return $this->sourceStorage; + } + + /** + * {@inheritdoc} + */ + public function setSyncing($status) { + $this->isSyncing = $status; + return $this; + } + + /** + * {@inheritdoc} + */ + public function isSyncing() { + return $this->isSyncing; + } + } diff --git a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php index 927c610..6e7c20d 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php @@ -37,4 +37,38 @@ */ public function installDefaultConfig($type, $name); + /** + * Sets the configuration storage that provides the default configuration. + * + * @param \Drupal\Core\Config\StorageInterface $storage + * + * @return self + * The configuration installer. + */ + public function setSourceStorage(StorageInterface $storage); + + /** + * Resets the configuration storage that provides the default configuration. + * + * @return self + * The configuration installer. + */ + public function resetSourceStorage(); + + /** + * Sets the status of the isSyncing flag. + * + * @param bool $status + * The status of the sync flag. + */ + public function setSyncing($status); + + /** + * Gets the syncing state. + * + * @return bool + * Returns TRUE is syncing flag set. + */ + public function isSyncing(); + } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index a988135..49090b5 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -131,6 +131,7 @@ public function status() { */ public function setSyncing($syncing) { $this->isSyncing = $syncing; + return $this; } /** diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php index 5aa737d..d24d2ac 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php @@ -64,6 +64,9 @@ public function setStatus($status); * * @param bool $status * The status of the sync flag. + * + * @return \Drupal\Core\Config\Entity\ConfigEntityInterface + * The class instance that this method is called on. */ public function setSyncing($status); diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php index 5bedbbe..5762847 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -9,6 +9,7 @@ use Drupal\Core\Config\InstallStorage; use Drupal\Core\Config\StorageException; +use Drupal\Core\Config\StorageInterface; /** * Defines the file storage controller for metadata files. diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index d486973..7e78283 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -551,6 +551,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) { // Required for module installation checks. include_once DRUPAL_ROOT . '/core/includes/install.inc'; + /** @var \Drupal\Core\Config\ConfigInstaller $config_installer */ + $config_installer = \Drupal::service('config.installer'); + $sync_status = $config_installer->isSyncing(); + if ($sync_status) { + $source_storage = $config_installer->getSourceStorage(); + } $modules_installed = array(); foreach ($module_list as $module) { $enabled = $module_config->get("enabled.$module") !== NULL; @@ -633,6 +639,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $version = $versions ? max($versions) : SCHEMA_INSTALLED; // Install default configuration of the module. + $config_installer = \Drupal::service('config.installer'); + if ($sync_status) { + $config_installer + ->setSyncing(TRUE) + ->setSourceStorage($source_storage); + } \Drupal::service('config.installer')->installDefaultConfig('module', $module); // If the module has no current updates, but has some that were @@ -694,7 +706,7 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Skip already uninstalled modules. if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) { - $module_list[$dependent] = TRUE; + $module_list[$dependent] = $dependent; } } } diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index 8e95d7e..bee55aa 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -9,6 +9,8 @@ use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Config\StorageInterface; use Drupal\Core\Lock\LockBackendInterface; @@ -67,18 +69,25 @@ class ConfigSync extends FormBase { protected $urlGenerator; /** - * The UUID service. + * The typed config manager. * - * @var \Drupal\Component\Uuid\UuidInterface + * @var \Drupal\Core\Config\TypedConfigManager */ - protected $uuidService; + protected $typedConfigManager; /** - * The typed config manager. + * The module handler. * - * @var \Drupal\Core\Config\TypedConfigManager + * @var \Drupal\Core\Extension\ModuleHandlerInterface */ - protected $typedConfigManager; + protected $moduleHandler; + + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; /** * Constructs the object. @@ -101,8 +110,12 @@ class ConfigSync extends FormBase { * The UUID Service. * @param \Drupal\Core\Config\TypedConfigManager $typed_config * The typed configuration manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler + * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler + * The theme handler */ - public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManagerInterface $entity_manager, UrlGeneratorInterface $url_generator, UuidInterface $uuid_service, TypedConfigManager $typed_config) { + public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManagerInterface $entity_manager, UrlGeneratorInterface $url_generator, TypedConfigManager $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { $this->sourceStorage = $sourceStorage; $this->targetStorage = $targetStorage; $this->lock = $lock; @@ -110,8 +123,9 @@ public function __construct(StorageInterface $sourceStorage, StorageInterface $t $this->configFactory = $config_factory; $this->entity_manager = $entity_manager; $this->urlGenerator = $url_generator; - $this->uuidService = $uuid_service; $this->typedConfigManager = $typed_config; + $this->moduleHandler = $module_handler; + $this->themeHandler = $theme_handler; } /** @@ -126,8 +140,9 @@ public static function create(ContainerInterface $container) { $container->get('config.factory'), $container->get('entity.manager'), $container->get('url_generator'), - $container->get('uuid'), - $container->get('config.typed') + $container->get('config.typed'), + $container->get('module_handler'), + $container->get('theme_handler') ); } @@ -239,8 +254,9 @@ public function submitForm(array &$form, array &$form_state) { $this->configFactory, $this->entity_manager, $this->lock, - $this->uuidService, - $this->typedConfigManager + $this->typedConfigManager, + $this->moduleHandler, + $this->themeHandler ); if ($config_importer->alreadyImporting()) { drupal_set_message($this->t('Another request may be synchronizing configuration already.')); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php new file mode 100644 index 0000000..4e556f7 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php @@ -0,0 +1,108 @@ + 'Install/uninstall modules', + 'description' => 'Install/uninstall core module and confirm table creation/deletion.', + 'group' => 'Module', + ); + } + + /** + * Tests that a fixed set of modules can be installed and uninstalled. + */ + public function testInstallUninstall() { + + // Get a list of modules to enable. + $all_modules = system_rebuild_module_data(); + $all_modules = array_filter($all_modules, function ($module) { + // Filter hidden, already enabled modules and modules in the Testing + // package. + if (!empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') { + return FALSE; + } + return TRUE; + }); + + // Install every module possible. + \Drupal::moduleHandler()->install(array_keys($all_modules)); + + $this->assertModules(array_keys($all_modules), TRUE); + foreach($all_modules as $module => $info) { + $this->assertModuleConfig($module); + $this->assertModuleTablesExist($module); + } + + // Export active config to staging + $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); + + system_list_reset(); + $this->resetAll(); + + // Delete every field on the site so all modules can be disabled. For + // example, if a comment field exists then module becomes required and can + // not be uninstalled. + $fields = \Drupal::service('field.info')->getFields(); + foreach ($fields as $field) { + entity_invoke_bundle_hook('delete', $field->entity_type, $field->entity_type . '__' . $field->name); + $field->delete(); + } + // Purge the data. + field_purge_batch(1000); + + system_list_reset(); + $all_modules = system_rebuild_module_data(); + $all_modules = array_filter($all_modules, function ($module) { + // Filter required and not enabled modules. + if (!empty($module->info['required']) || $module->status == FALSE) { + return FALSE; + } + return TRUE; + }); + + $this->assertTrue(isset($all_modules['comment']), 'The comment module will be disabled'); + + \Drupal::moduleHandler()->uninstall(array_keys($all_modules)); + + $this->assertModules(array_keys($all_modules), FALSE); + foreach($all_modules as $module => $info) { + $this->assertNoModuleConfig($module); + $this->assertModuleTablesDoNotExist($module); + } + + $this->configImporter()->import(); + + $this->assertModules(array_keys($all_modules), TRUE); + foreach($all_modules as $module => $info) { + $this->assertModuleConfig($module); + $this->assertModuleTablesExist($module); + } + + // Ensure that we have no configuration changes to import. + $admin_user = $this->drupalCreateUser(array('synchronize configuration')); + $this->drupalLogin($admin_user); + $this->drupalGet('admin/config/development/configuration'); + $this->assertText('There are no configuration changes.'); + } +} diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php index e495602..9aa2463 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php @@ -7,6 +7,7 @@ namespace Drupal\config\Tests; +use Drupal\Core\Config\InstallStorage; use Drupal\simpletest\WebTestBase; /** @@ -14,7 +15,7 @@ */ class ConfigImportUITest extends WebTestBase { - public static $modules = array('config', 'config_test'); + public static $modules = array('config', 'config_test', 'config_import_test'); public static function getInfo() { return array( @@ -38,6 +39,7 @@ function setUp() { function testImport() { $name = 'system.site'; $dynamic_name = 'config_test.dynamic.new'; + /** @var \Drupal\Core\Config\StorageInterface $staging */ $staging = $this->container->get('config.storage.staging'); $this->drupalGet('admin/config/development/configuration'); @@ -63,16 +65,50 @@ function testImport() { $staging->write($dynamic_name, $original_dynamic_data); $this->assertIdentical($staging->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + // Enable the Ban and Action modules during import. The Ban module is used + // because it creates a table during the install. The Action module is used + // because it creates a single simple configuration file during the install. + $system_module = \Drupal::config('system.module')->get(); + $system_module['enabled']['action'] = 0; + $system_module['enabled']['ban'] = 0; + $system_module['enabled'] = module_config_sort($system_module['enabled']); + $staging->write('system.module', $system_module); + + // Use the install storage so that we can read configuration from modules + // and themes that are not installed. + $install_storage = new InstallStorage(); + + // Enable the bartik theme and set it as default. + $system_theme = \Drupal::config('system.theme')->get(); + $system_theme['enabled']['bartik'] = '0'; + $system_theme['default'] = 'bartik'; + $staging->write('system.theme', $system_theme); + $staging->write('bartik.settings', $install_storage->read('bartik.settings')); + + // Read the action config from module default config folder. + $action_settings = $install_storage->read('action.settings'); + $action_settings['recursion_limit'] = 50; + $staging->write('action.settings', $action_settings); + // Verify that both appear as ready to import. $this->drupalGet('admin/config/development/configuration'); $this->assertText($name); $this->assertText($dynamic_name); + $this->assertText('system.module'); + $this->assertText('system.theme'); + $this->assertText('action.settings'); + $this->assertText('bartik.settings'); $this->assertFieldById('edit-submit', t('Import all')); // Import and verify that both do not appear anymore. $this->drupalPostForm(NULL, array(), t('Import all')); $this->assertNoText($name); $this->assertNoText($dynamic_name); + $this->assertNoText('system.module'); + $this->assertNoText('system.theme'); + $this->assertNoText('action.settings'); + $this->assertNoText('bartik.settings'); + $this->assertNoFieldById('edit-submit', t('Import all')); // Verify that there are no further changes to import. @@ -86,6 +122,73 @@ function testImport() { // Verify the cache got cleared. $this->assertTrue(isset($GLOBALS['hook_cache_flush'])); + + $this->rebuildContainer(); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('ban'), 'Ban module enabled during import.'); + $this->assertTrue(\Drupal::database()->schema()->tableExists('ban_ip'), 'The database table ban_ip exists.'); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('action'), 'Action module enabled during import.'); + + $theme_info = \Drupal::service('theme_handler')->listInfo(); + $this->assertTrue(isset($theme_info['bartik']) && $theme_info['bartik']->status, 'Bartik theme enabled during import.'); + + // The configuration object system.theme will be saved twice during config + // import. Once during enabling the system and once during importing the + // new default setting. + $this->assertEqual(\Drupal::state()->get('ConfigImportUITest.system.theme.save', 0), 2, 'The system.theme configuration saved twice during import.'); + + // Verify that the action.settings configuration object was only written + // once during the import process and only with the value set in the staged + // configuration. This verifies that the module's default configuration is + // used during configuration import and, additionally, that after installing + // a module, that configuration is not synced twice. + $recursion_limit_values = \Drupal::state()->get('ConfigImportUITest.action.settings.recursion_limit', array()); + $this->assertIdentical($recursion_limit_values, array(50)); + + $system_module = \Drupal::config('system.module')->get(); + unset($system_module['enabled']['action']); + unset($system_module['enabled']['ban']); + $staging->write('system.module', $system_module); + $staging->delete('action.settings'); + + $system_theme = \Drupal::config('system.theme')->get(); + unset($system_theme['enabled']['bartik']); + $system_theme['default'] = 'stark'; + $system_theme['admin'] = 'stark'; + $staging->write('system.theme', $system_theme); + $staging->write('system.theme.disabled', array('bartik' => 0)); + + // Reset counter. + \Drupal::state()->set('ConfigImportUITest.system.theme.save', 0); + + // Verify that both appear as ready to import. + $this->drupalGet('admin/config/development/configuration'); + $this->assertText('system.module'); + $this->assertText('system.theme'); + $this->assertText('system.theme.disabled'); + $this->assertText('action.settings'); + + // Import and verify that both do not appear anymore. + $this->drupalPostForm(NULL, array(), t('Import all')); + $this->assertNoText('system.module'); + $this->assertNoText('system.theme'); + $this->assertNoText('system.theme.disabled'); + $this->assertNoText('action.settings'); + + $this->rebuildContainer(); + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('ban'), 'Ban module uninstalled during import.'); + $this->assertFalse(\Drupal::database()->schema()->tableExists('ban_ip'), 'The database table ban_ip does not exist.'); + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('action'), 'Action module uninstalled during import.'); + // This is because it will be updated to change the default theme, remove + // Bartik and then set the admin theme. + $this->assertEqual(\Drupal::state()->get('ConfigImportUITest.system.theme.save', 0), 3, 'The system.theme configuration saved thrice during import.'); + + $theme_info = \Drupal::service('theme_handler')->listInfo(); + $this->assertTrue(isset($theme_info['bartik']) && !$theme_info['bartik']->status, 'Bartik theme disabled during import.'); + + // Verify that the action.settings configuration object was only deleted + // once during the import process. + $delete_called = \Drupal::state()->get('ConfigImportUITest.action.settings.delete', 0); + $this->assertIdentical($delete_called, 1, "The action.settings configuration was deleted once during configuration import."); } /** diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php index ec445c5..2c40e02 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -61,9 +61,9 @@ function setUp() { $this->container->get('config.factory'), $this->container->get('entity.manager'), $this->container->get('lock'), - $this->container->get('uuid'), $this->container->get('config.typed'), - $this->container->get('module_handler') + $this->container->get('module_handler'), + $this->container->get('theme_handler') ); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); } diff --git a/core/modules/config/tests/config_import_test/config_import_test.info.yml b/core/modules/config/tests/config_import_test/config_import_test.info.yml new file mode 100644 index 0000000..87cdd02 --- /dev/null +++ b/core/modules/config/tests/config_import_test/config_import_test.info.yml @@ -0,0 +1,6 @@ +name: 'Configuration import test' +type: module +package: Testing +version: VERSION +core: 8.x +hidden: true diff --git a/core/modules/config/tests/config_import_test/config_import_test.module b/core/modules/config/tests/config_import_test/config_import_test.module new file mode 100644 index 0000000..936b72b --- /dev/null +++ b/core/modules/config/tests/config_import_test/config_import_test.module @@ -0,0 +1,6 @@ +state = $state; + } + + /** + * Validates the configuration to be imported. + * + * @param \Drupal\Core\Config\ConfigImporterEvent $event + * The Event to process. + * + * @throws \Drupal\Core\Config\ConfigNameException + */ + public function onConfigImporterValidate(ConfigImporterEvent $event) { + + } + + public function onConfigSave(ConfigEvent $event) { + $config = $event->getConfig(); + if ($config->getName() == 'action.settings') { + $values = $this->state->get('ConfigImportUITest.action.settings.recursion_limit', array()); + $values[] = $config->get('recursion_limit'); + $this->state->set('ConfigImportUITest.action.settings.recursion_limit', $values); + } + if ($config->getName() == 'system.theme') { + $value = $this->state->get('ConfigImportUITest.system.theme.save', 0); + $this->state->set('ConfigImportUITest.system.theme.save', $value + 1); + } + } + + public function onConfigDelete(ConfigEvent $event) { + $config = $event->getConfig(); + if ($config->getName() == 'action.settings') { + $value = $this->state->get('ConfigImportUITest.action.settings.delete', 0); + $this->state->set('ConfigImportUITest.action.settings.delete', $value + 1); + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + //$events['config.importer.validate'][] = array('onConfigImporterValidate', 40); + //$events['config.installer.validate'][] = array('onConfigImporterValidate', 40); + $events['config.save'][] = array('onConfigSave', 40); + $events['config.delete'][] = array('onConfigDelete', 40); + return $events; + } + +} \ No newline at end of file diff --git a/core/modules/contact/contact.install b/core/modules/contact/contact.install index c3802e7..1d72488 100644 --- a/core/modules/contact/contact.install +++ b/core/modules/contact/contact.install @@ -15,7 +15,13 @@ function contact_install() { if (empty($site_mail)) { $site_mail = ini_get('sendmail_from'); } - \Drupal::config('contact.category.feedback')->set('recipients', array($site_mail))->save(); + $config = \Drupal::config('contact.category.feedback'); + // Update the recipients if the default configuration entity has been created. + // We should never rely on default config entities as during enabling a module + // during config sync they will not exist. + if (!$config->isNew()) { + \Drupal::config('contact.category.feedback')->set('recipients', array($site_mail))->save(); + } } /** diff --git a/core/modules/field/lib/Drupal/field/Entity/FieldInstance.php b/core/modules/field/lib/Drupal/field/Entity/FieldInstance.php index 5f98b85..73b65e2 100644 --- a/core/modules/field/lib/Drupal/field/Entity/FieldInstance.php +++ b/core/modules/field/lib/Drupal/field/Entity/FieldInstance.php @@ -711,4 +711,11 @@ public function getColumns() { return $this->field->getColumns(); } + /** + * {@inheritdoc} + */ + public function isDeleted() { + return $this->deleted; + } + } diff --git a/core/modules/field/lib/Drupal/field/FieldInstanceInterface.php b/core/modules/field/lib/Drupal/field/FieldInstanceInterface.php index f5df93b..4c08510 100644 --- a/core/modules/field/lib/Drupal/field/FieldInstanceInterface.php +++ b/core/modules/field/lib/Drupal/field/FieldInstanceInterface.php @@ -40,4 +40,12 @@ public function allowBundleRename(); */ public function targetBundle(); + /** + * Gets the deleted flag of the field instance. + * + * @return bool + * Returns TRUE if the instance is deleted. + */ + public function isDeleted(); + } diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install index 55539ae..a43da67 100644 --- a/core/modules/forum/forum.install +++ b/core/modules/forum/forum.install @@ -18,73 +18,75 @@ function forum_install() { $locked['forum'] = 'forum'; \Drupal::state()->set('node.type.locked', $locked); - // Create the 'taxonomy_forums' field if it doesn't already exist. If forum - // is being enabled at the same time as taxonomy after both modules have been - // enabled, the field might exist but still be marked inactive. - if (!field_info_field('node', 'taxonomy_forums')) { - entity_create('field_entity', array( - 'name' => 'taxonomy_forums', - 'entity_type' => 'node', - 'type' => 'taxonomy_term_reference', - 'settings' => array( - 'allowed_values' => array( - array( - 'vocabulary' => 'forums', - 'parent' => 0, + if (!\Drupal::service('config.installer')->isSyncing()) { + // Create the 'taxonomy_forums' field if it doesn't already exist. If forum + // is being enabled at the same time as taxonomy after both modules have been + // enabled, the field might exist but still be marked inactive. + if (!field_info_field('node', 'taxonomy_forums')) { + entity_create('field_entity', array( + 'name' => 'taxonomy_forums', + 'entity_type' => 'node', + 'type' => 'taxonomy_term_reference', + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => 'forums', + 'parent' => 0, + ), ), ), - ), - ))->save(); + ))->save(); - // Create a default forum so forum posts can be created. - $term = entity_create('taxonomy_term', array( - 'name' => t('General discussion'), - 'langcode' => language_default()->id, - 'description' => '', - 'parent' => array(0), - 'vid' => 'forums', - 'forum_container' => 0, - )); - $term->save(); + // Create a default forum so forum posts can be created. + $term = entity_create('taxonomy_term', array( + 'name' => t('General discussion'), + 'langcode' => language_default()->id, + 'description' => '', + 'parent' => array(0), + 'vid' => 'forums', + 'forum_container' => 0, + )); + $term->save(); - // Create the instance on the bundle. - entity_create('field_instance', array( - 'field_name' => 'taxonomy_forums', - 'entity_type' => 'node', - 'label' => 'Forums', - 'bundle' => 'forum', - 'required' => TRUE, - ))->save(); + // Create the instance on the bundle. + entity_create('field_instance', array( + 'field_name' => 'taxonomy_forums', + 'entity_type' => 'node', + 'label' => 'Forums', + 'bundle' => 'forum', + 'required' => TRUE, + ))->save(); - // Assign form display settings for the 'default' form mode. - entity_get_form_display('node', 'forum', 'default') - ->setComponent('taxonomy_forums', array( - 'type' => 'options_select', - )) - ->save(); + // Assign form display settings for the 'default' form mode. + entity_get_form_display('node', 'forum', 'default') + ->setComponent('taxonomy_forums', array( + 'type' => 'options_select', + )) + ->save(); - // Assign display settings for the 'default' and 'teaser' view modes. - entity_get_display('node', 'forum', 'default') - ->setComponent('taxonomy_forums', array( - 'type' => 'taxonomy_term_reference_link', - 'weight' => 10, - )) - ->save(); - entity_get_display('node', 'forum', 'teaser') - ->setComponent('taxonomy_forums', array( - 'type' => 'taxonomy_term_reference_link', - 'weight' => 10, - )) - ->save(); - } - // Add the comment field to the forum node type. - $fields = entity_load_multiple_by_properties('field_entity', array( - 'type' => 'comment', - 'name' => 'comment_forum', - 'include_deleted' => FALSE, - )); - if (empty($fields)) { - Drupal::service('comment.manager')->addDefaultField('node', 'forum', 'comment_forum', COMMENT_OPEN); + // Assign display settings for the 'default' and 'teaser' view modes. + entity_get_display('node', 'forum', 'default') + ->setComponent('taxonomy_forums', array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + )) + ->save(); + entity_get_display('node', 'forum', 'teaser') + ->setComponent('taxonomy_forums', array( + 'type' => 'taxonomy_term_reference_link', + 'weight' => 10, + )) + ->save(); + } + // Add the comment field to the forum node type. + $fields = entity_load_multiple_by_properties('field_entity', array( + 'type' => 'comment', + 'name' => 'comment_forum', + 'include_deleted' => FALSE, + )); + if (empty($fields)) { + Drupal::service('comment.manager')->addDefaultField('node', 'forum', 'comment_forum', COMMENT_OPEN); + } } } diff --git a/core/modules/node/node.install b/core/modules/node/node.install index 91589a0..1e9d2ef 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -447,7 +447,9 @@ function node_uninstall() { $types = config_get_storage_names_with_prefix('node.type.'); foreach ($types as $config_name) { $type = \Drupal::config($config_name)->get('type'); - \Drupal::config('language.settings')->clear('node. ' . $type . '.language.default_configuration')->save(); + if (\Drupal::moduleHandler()->moduleExists('language')) { + \Drupal::config('language.settings')->clear('node. ' . $type . '.language.default_configuration')->save(); + } } // Delete remaining general module variables. diff --git a/core/modules/search/lib/Drupal/search/SearchPageRepository.php b/core/modules/search/lib/Drupal/search/SearchPageRepository.php index e2af5ff..5421586 100644 --- a/core/modules/search/lib/Drupal/search/SearchPageRepository.php +++ b/core/modules/search/lib/Drupal/search/SearchPageRepository.php @@ -87,7 +87,7 @@ public function getDefaultSearchPage() { } // Otherwise, use the first active search page. - return reset($search_pages); + return is_array($search_pages) ? reset($search_pages) : FALSE; } /** diff --git a/core/modules/search/search.module b/core/modules/search/search.module index 11a5e00..a98b990 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -147,11 +147,14 @@ function search_preprocess_block(&$variables) { * Implements hook_menu(). */ function search_menu() { - $items['search'] = array( - 'title' => 'Search', - 'type' => MENU_SUGGESTED_ITEM, - 'route_name' => 'search.view', - ); + // Create a default search page + if (\Drupal::service('search.search_page_repository')->getDefaultSearchPage()) { + $items['search'] = array( + 'title' => 'Search', + 'type' => MENU_SUGGESTED_ITEM, + 'route_name' => 'search.view', + ); + } $items['admin/config/search/settings'] = array( 'title' => 'Search settings', 'description' => 'Configure relevance settings for search and other indexing options.', diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index b9eb05a..3dc1768 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -1470,8 +1470,9 @@ public function configImporter() { $this->container->get('config.factory'), $this->container->get('entity.manager'), $this->container->get('lock'), - $this->container->get('uuid'), - $this->container->get('config.typed') + $this->container->get('config.typed'), + $this->container->get('module_handler'), + $this->container->get('theme_handler') ); } // Always recalculate the changelist when called. diff --git a/core/modules/system/system.module b/core/modules/system/system.module index cc647a2..fe03ff8 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2642,7 +2642,6 @@ function _system_rebuild_module_data() { _system_rebuild_module_data_ensure_required($module, $modules); } - if (isset($modules[$profile])) { // The installation profile is required, if it's a valid module. $modules[$profile]->info['required'] = TRUE;