diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index e1ae91a..325e7c3 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -612,4 +612,14 @@ public static function formBuilder() { return static::$container->get('form_builder'); } + /** + * Gets the syncing state. + * + * @return bool + * Returns TRUE is syncing flag set. + */ + public function isConfigSyncing() { + return static::$container->get('config.installer')->isSyncing(); + } + } diff --git a/core/lib/Drupal/Core/Config/BatchConfigImporter.php b/core/lib/Drupal/Core/Config/BatchConfigImporter.php index 0ab6cc1..ba33dcf 100644 --- a/core/lib/Drupal/Core/Config/BatchConfigImporter.php +++ b/core/lib/Drupal/Core/Config/BatchConfigImporter.php @@ -26,6 +26,10 @@ public function initialize() { throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID)); } $this->totalToProcess = 0; + + // @todo batch this properly. + $this->handleExtensions(); + foreach(array('create', 'delete', 'update') as $op) { $this->totalToProcess += count($this->getUnprocessed($op)); } diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 090b883..de1570c 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -7,9 +7,9 @@ namespace Drupal\Core\Config; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Component\Utility\String; -use Drupal\Core\Config\ConfigEvents; -use Drupal\Core\Config\Entity\ConfigStorageControllerInterface; use Drupal\Core\Config\Entity\ImportableEntityStorageInterface; use Drupal\Core\DependencyInjection\DependencySerialization; use Drupal\Core\Entity\EntityStorageException; @@ -73,7 +73,7 @@ class ConfigImporter extends DependencySerialization { /** * The typed config manager. * - * @var \Drupal\Core\Config\TypedConfigManager + * @var \Drupal\Core\Config\TypedConfigManagerInterface */ protected $typedConfigManager; @@ -92,6 +92,20 @@ class ConfigImporter extends DependencySerialization { protected $validated; /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** * Constructs a configuration import object. * * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer @@ -105,13 +119,19 @@ class ConfigImporter extends DependencySerialization { * The lock backend to ensure multiple imports do not occur at the same time. * @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, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManager $typed_config) { + public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { $this->storageComparer = $storage_comparer; $this->eventDispatcher = $event_dispatcher; $this->configManager = $config_manager; $this->lock = $lock; $this->typedConfigManager = $typed_config; + $this->moduleHandler = $module_handler; + $this->themeHandler = $theme_handler; $this->processed = $this->storageComparer->getEmptyChangelist(); } @@ -210,6 +230,10 @@ public function import() { // Another process is synchronizing configuration. throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID)); } + + // Where to put this? + $this->handleExtensions(); + // First pass deleted, then new, and lastly changed configuration, in order // to handle dependencies correctly. // @todo Implement proper dependency ordering using @@ -343,4 +367,126 @@ public function alreadyImporting() { return !$this->lock->lockMayBeAvailable(static::LOCK_ID); } + /** + * Returns the identifier for events and locks. + * + * @return string + * The identifier for events and locks. + */ + public function getId() { + return static::LOCK_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->configManager->getConfigFactory() + ->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 b039b8f..8da1930 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\ConfigFactoryInterface $config_factory @@ -75,7 +89,7 @@ public function __construct(ConfigFactoryInterface $config_factory, StorageInter */ 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 @@ -125,6 +139,18 @@ public function installDefaultConfig($type, $name) { $new_config->setData($data[$name]); } if ($entity_type = $this->configManager->getEntityTypeIdByName($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. + if ($this->isSyncing) { + continue; + } $entity_storage = $this->configManager ->getEntityManager() ->getStorageController($entity_type); @@ -133,6 +159,9 @@ public function installDefaultConfig($type, $name) { if ($this->activeStorage->exists($name)) { $id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix()); $entity = $entity_storage->load($id); + if ($this->isSyncing) { + $entity->setSyncing(TRUE); + } foreach ($new_config->get() as $property => $value) { $entity->set($property, $value); } @@ -154,4 +183,48 @@ public function installDefaultConfig($type, $name) { $this->configFactory->reset(); } + /** + * {@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 + $this->sourceStorage = 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/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index 418b912..3561924 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -93,6 +93,13 @@ public function getEntityManager() { /** * {@inheritdoc} */ + public function getConfigFactory() { + return $this->configFactory; + } + + /** + * {@inheritdoc} + */ public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $name) { // @todo Replace with code that can be autoloaded. // https://drupal.org/node/1848266 diff --git a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php index b5084fe..4da9474 100644 --- a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php @@ -32,6 +32,14 @@ public function getEntityTypeIdByName($name); public function getEntityManager(); /** + * Gets the config factory. + * + * @return \Drupal\Core\Config\ConfigFactoryInterface + * The entity manager. + */ + public function getConfigFactory(); + + /** * Return a formatted diff of a named config between two storages. * * @param \Drupal\Core\Config\StorageInterface $source_storage diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index a20b9b9..e0807e5 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -582,6 +582,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; @@ -671,6 +677,18 @@ public function install(array $module_list, $enable_dependencies = TRUE) { } // Install default configuration of the module. + $config_installer = \Drupal::service('config.installer'); + if ($sync_status) { + $config_installer + ->setSyncing(TRUE) + ->setSourceStorage($source_storage); + } + else { + // If we're not in a config synchronisation reset the source storage + // so that the extension install storage will pick up the new + // configuration. + $config_installer->resetSourceStorage(); + } \Drupal::service('config.installer')->installDefaultConfig('module', $module); // If the module has no current updates, but has some that were @@ -732,7 +750,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/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index db22e5f..c4218a4 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -145,6 +145,11 @@ public function enable(array $theme_list) { // Refresh the theme list as installation of default configuration needs // an updated list to work. $this->reset(); + // If we're not in a config synchronisation reset the source storage so + // that the extension install storage will pick up the new configuration. + if (!$this->configInstaller->isSyncing()) { + $this->configInstaller->resetSourceStorage(); + } // Install default configuration of the theme. $this->configInstaller->installDefaultConfig('theme', $key); } diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index c600001..8ae2b63 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -7,6 +7,10 @@ namespace Drupal\config\Form; +use Drupal\Component\Uuid\UuidInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Config\StorageInterface; @@ -73,6 +77,20 @@ class ConfigSync extends FormBase { protected $typedConfigManager; /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** * Constructs the object. * * @param \Drupal\Core\Config\StorageInterface $sourceStorage @@ -89,8 +107,12 @@ class ConfigSync extends FormBase { * The url generator 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, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManager $typed_config) { + public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManager $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { $this->sourceStorage = $sourceStorage; $this->targetStorage = $targetStorage; $this->lock = $lock; @@ -98,6 +120,8 @@ public function __construct(StorageInterface $sourceStorage, StorageInterface $t $this->configManager = $config_manager; $this->urlGenerator = $url_generator; $this->typedConfigManager = $typed_config; + $this->moduleHandler = $module_handler; + $this->themeHandler = $theme_handler; } /** @@ -111,7 +135,9 @@ public static function create(ContainerInterface $container) { $container->get('event_dispatcher'), $container->get('config.manager'), $container->get('url_generator'), - $container->get('config.typed') + $container->get('config.typed'), + $container->get('module_handler'), + $container->get('theme_handler') ); } @@ -222,7 +248,9 @@ public function submitForm(array &$form, array &$form_state) { $this->eventDispatcher, $this->configManager, $this->lock, - $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..a915b20 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php @@ -0,0 +1,110 @@ + '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. + $storage_comparer = new StorageComparer( + $this->container->get('config.storage.staging'), + $this->container->get('config.storage') + ); + $this->assertIdentical($storage_comparer->createChangelist()->getChangelist(), $storage_comparer->getEmptyChangelist()); + } +} diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php index 51f9182..d2eb827 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'); @@ -65,16 +67,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. @@ -88,6 +124,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 b01ddab..a132946 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -60,7 +60,9 @@ function setUp() { $this->container->get('event_dispatcher'), $this->container->get('config.manager'), $this->container->get('lock'), - $this->container->get('config.typed') + $this->container->get('config.typed'), + $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(ConfigCrudEvent $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(ConfigCrudEvent $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[ConfigEvents::SAVE][] = array('onConfigSave', 40); + $events[ConfigEvents::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 a67134e..477f359 100644 --- a/core/modules/contact/contact.install +++ b/core/modules/contact/contact.install @@ -15,5 +15,11 @@ 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/content_translation/content_translation.install b/core/modules/content_translation/content_translation.install index e474d69..9c365d7 100644 --- a/core/modules/content_translation/content_translation.install +++ b/core/modules/content_translation/content_translation.install @@ -87,6 +87,19 @@ function content_translation_install() { // hook_module_implements_alter() is run among the last ones. module_set_weight('content_translation', 10); \Drupal::service('language_negotiator')->saveConfiguration(Language::TYPE_CONTENT, array(LanguageNegotiationUrl::METHOD_ID => 0)); + + $config_names = \Drupal::configFactory()->listAll('field.field.'); + foreach ($config_names as $name) { + \Drupal::config($name) + ->set('settings.translation_sync', FALSE) + ->save(); + } + $config_names = \Drupal::configFactory()->listAll('field.instance.'); + foreach ($config_names as $name) { + \Drupal::config($name) + ->set('settings.translation_sync', FALSE) + ->save(); + } } /** @@ -104,3 +117,21 @@ function content_translation_enable() { $message = t('Enable translation for content types, taxonomy vocabularies, accounts, or any other element you wish to translate.', $t_args); drupal_set_message($message, 'warning'); } + +/** + * Implements hook_uninstall(). + */ +function content_translation_uninstall() { + $config_names = \Drupal::configFactory()->listAll('field.field.'); + foreach ($config_names as $name) { + \Drupal::config($name) + ->clear('settings.translation_sync') + ->save(); + } + $config_names = \Drupal::configFactory()->listAll('field.instance.'); + foreach ($config_names as $name) { + \Drupal::config($name) + ->clear('settings.translation_sync') + ->save(); + } +} diff --git a/core/modules/field/lib/Drupal/field/Entity/FieldInstanceConfig.php b/core/modules/field/lib/Drupal/field/Entity/FieldInstanceConfig.php index 5cc9f67..035d81d 100644 --- a/core/modules/field/lib/Drupal/field/Entity/FieldInstanceConfig.php +++ b/core/modules/field/lib/Drupal/field/Entity/FieldInstanceConfig.php @@ -765,4 +765,11 @@ public function getColumns() { return $this->field->getColumns(); } + /** + * {@inheritdoc} + */ + public function isDeleted() { + return $this->deleted; + } + } diff --git a/core/modules/field/lib/Drupal/field/FieldInstanceConfigInterface.php b/core/modules/field/lib/Drupal/field/FieldInstanceConfigInterface.php index e796caa..6c84282 100644 --- a/core/modules/field/lib/Drupal/field/FieldInstanceConfigInterface.php +++ b/core/modules/field/lib/Drupal/field/FieldInstanceConfigInterface.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/config/field.field.forum.forum_container.yml b/core/modules/forum/config/field.field.forum.forum_container.yml index af5ebf2..1c7d8a4 100644 --- a/core/modules/forum/config/field.field.forum.forum_container.yml +++ b/core/modules/forum/config/field.field.forum.forum_container.yml @@ -1,4 +1,4 @@ -id: taxonomy_term.forum_container +id: forum.forum_container status: true langcode: en name: forum_container diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install index b5f0e0a..da46646 100644 --- a/core/modules/forum/forum.install +++ b/core/modules/forum/forum.install @@ -16,72 +16,74 @@ 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_config', 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_config', array( + 'name' => 'taxonomy_forums', + 'entity_type' => 'node', + 'type' => 'taxonomy_term_reference', + 'settings' => array( + 'allowed_values' => array( + array( + 'vocabulary' => 'forums', + 'parent' => 0, + ), ), ), - ), - ))->save(); - - // Create a default forum so forum posts can be created. - $term = entity_create('taxonomy_term', array( - 'name' => t('General discussion'), - 'description' => '', - 'parent' => array(0), - 'vid' => 'forums', - 'forum_container' => 0, + ))->save(); + + // Create a default forum so forum posts can be created. + $term = entity_create('taxonomy_term', array( + 'name' => t('General discussion'), + 'description' => '', + 'parent' => array(0), + 'vid' => 'forums', + 'forum_container' => 0, + )); + $term->save(); + + // Create the instance on the bundle. + entity_create('field_instance_config', 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 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_config', array( + 'type' => 'comment', + 'name' => 'comment_forum', + 'include_deleted' => FALSE, )); - $term->save(); - - // Create the instance on the bundle. - entity_create('field_instance_config', 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 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_config', array( - 'type' => 'comment', - 'name' => 'comment_forum', - 'include_deleted' => FALSE, - )); - if (empty($fields)) { - Drupal::service('comment.manager')->addDefaultField('node', 'forum', 'comment_forum'); + if (empty($fields)) { + Drupal::service('comment.manager')->addDefaultField('node', 'forum', 'comment_forum'); + } } } diff --git a/core/modules/node/node.install b/core/modules/node/node.install index a6af612..36f8345 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -459,7 +459,9 @@ function node_uninstall() { $types = \Drupal::configFactory()->listAll('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 db1e57f..37d0dd0 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 62822e5..919b349 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -142,11 +142,14 @@ function search_preprocess_block(&$variables) { * Implements hook_menu_link_defaults(). */ function search_menu_link_defaults() { - $links['search.view'] = array( - 'link_title' => 'Search', - 'route_name' => 'search.view', - 'type' => MENU_SUGGESTED_ITEM, - ); + // Create a default search page + if (\Drupal::service('search.search_page_repository')->getDefaultSearchPage()) { + $links['search.view'] = array( + 'link_title' => 'Search', + 'route_name' => 'search.view', + 'type' => MENU_SUGGESTED_ITEM, + ); + } $links['search.settings'] = array( 'link_title' => 'Search settings', 'parent' => 'system.admin_config_search', diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 065d4c8..40c5f90 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -1510,7 +1510,9 @@ public function configImporter() { $this->container->get('event_dispatcher'), $this->container->get('config.manager'), $this->container->get('lock'), - $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/simpletest/simpletest.install b/core/modules/simpletest/simpletest.install index 917a938..82e1ebb 100644 --- a/core/modules/simpletest/simpletest.install +++ b/core/modules/simpletest/simpletest.install @@ -182,7 +182,7 @@ function simpletest_uninstall() { // Do not clean the environment in case the Simpletest module is uninstalled // in a (recursive) test for itself, since simpletest_clean_environment() // would also delete the test site of the parent test process. - if (!DRUPAL_TEST_IN_CHILD_SITE) { + if (!drupal_valid_test_ua()) { simpletest_clean_environment(); } // Delete verbose test output and any other testing framework files. diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 157805c..6140e39 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -466,9 +466,11 @@ system.theme.global: type: boolean label: 'Use default' +# The weight for disabled themes is ignored but the same format for +# system.theme:enabled is used for consistency. system.theme.disabled: type: sequence label: 'Disabled themes' sequence: - - type: string - label: 'Theme' + - type: integer + label: 'Weight' diff --git a/core/modules/system/config/system.theme.disabled.yml b/core/modules/system/config/system.theme.disabled.yml new file mode 100644 index 0000000..bc8d1d5 --- /dev/null +++ b/core/modules/system/config/system.theme.disabled.yml @@ -0,0 +1,3 @@ +# An empty array representing no disabled themes. This file needs to exist since +# all simple configuration must exist. +{ } \ No newline at end of file