diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index cc8e9623e3a7bf77ca6117df157f6cf641693c93..c4622197b8d5448fbd205493f138ebe85f787a21 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 0ab6cc153446d7d5fb61a80039fed4b8e8dcdc3b..67aee132f9887ea3711be1b299e017b95bfbcf26 100644 --- a/core/lib/Drupal/Core/Config/BatchConfigImporter.php +++ b/core/lib/Drupal/Core/Config/BatchConfigImporter.php @@ -14,10 +14,34 @@ */ class BatchConfigImporter extends ConfigImporter { + /** + * The total number of extensions to process. + * + * @var int + */ + protected $totalExtensionsToProcess = 0; + + /** + * The total number of configuration objects to process. + * + * @var int + */ + protected $totalConfigurationToProcess = 0; + /** * Initializes the config importer in preparation for processing a batch. + * + * @return array + * An array of method names that to be called by the batch. If there are + * modules or themes to process then an extra step is added. + * + * @throws ConfigImporterException + * If the configuration is already importing. */ public function initialize() { + $batch_operations = array(); + $this->createExtensionChangelist(); + // Ensure that the changes have been validated. $this->validate(); @@ -25,61 +49,137 @@ public function initialize() { // Another process is synchronizing configuration. throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID)); } - $this->totalToProcess = 0; - foreach(array('create', 'delete', 'update') as $op) { - $this->totalToProcess += count($this->getUnprocessed($op)); + + $modules = $this->getUnprocessedExtensions('module'); + foreach (array('install', 'uninstall') as $op) { + $this->totalExtensionsToProcess += count($modules[$op]); + } + $themes = $this->getUnprocessedExtensions('theme'); + foreach (array('enable', 'disable') as $op) { + $this->totalExtensionsToProcess += count($themes[$op]); + } + + // We have extensions to process. + if ($this->totalExtensionsToProcess > 0) { + $batch_operations[] = 'processExtensionBatch'; } + + $batch_operations[] = 'processConfigurationBatch'; + $batch_operations[] = 'finishBatch'; + return $batch_operations; } /** - * Processes batch. + * Processes extensions as a batch operation. * * @param array $context. * The batch context. */ - public function processBatch(array &$context) { - $operation = $this->getNextOperation(); + public function processExtensionBatch(array &$context) { + $operation = $this->getNextExtensionOperation(); if (!empty($operation)) { - $this->process($operation['op'], $operation['name']); - $context['message'] = t('Synchronizing @name.', array('@name' => $operation['name'])); - $context['finished'] = $this->batchProgress(); + $this->processExtension($operation['type'], $operation['op'], $operation['name']); + $context['message'] = t('Synchronising extensions: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name'])); + $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']); + $processed_count += count($this->processedExtensions['theme']['disable']) + count($this->processedExtensions['theme']['enable']); + $context['finished'] = $processed_count / $this->totalExtensionsToProcess; } else { $context['finished'] = 1; } - if ($context['finished'] >= 1) { - $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this)); - // The import is now complete. - $this->lock->release(static::LOCK_ID); - $this->reset(); + } + + /** + * Processes configuration as a batch operation. + * + * @param array $context. + * The batch context. + */ + public function processConfigurationBatch(array &$context) { + // The first time this is called we need to calculate the total to process. + // This involves recalculating the changelist which will ensure that if + // extensions have been processed any configuration affected will be taken + // into account. + if ($this->totalConfigurationToProcess == 0) { + $this->storageComparer->reset(); + foreach (array('delete', 'create', 'update') as $op) { + $this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op)); + } + } + $operation = $this->getNextConfigurationOperation(); + if (!empty($operation)) { + $this->processConfiguration($operation['op'], $operation['name']); + $context['message'] = t('Synchronizing configuration: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name'])); + $processed_count = count($this->processedConfiguration['create']) + count($this->processedConfiguration['delete']) + count($this->processedConfiguration['update']); + $context['finished'] = $processed_count / $this->totalConfigurationToProcess; + } + else { + $context['finished'] = 1; } } /** - * Gets percentage of progress made. + * Finishes the batch. + * + * @param array $context. + * The batch context. + */ + public function finishBatch(array &$context) { + $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this)); + // The import is now complete. + $this->lock->release(static::LOCK_ID); + $this->reset(); + $context['message'] = t('Finalising configuration synchronisation.'); + $context['finished'] = 1; + } + + /** + * Gets the next extension operation to perform. * - * @return float - * The percentage of progress made expressed as a float between 0 and 1. + * @return array|bool + * An array containing the next operation and extension name to perform it + * on. If there is nothing left to do returns FALSE; */ - protected function batchProgress() { - $processed_count = count($this->processed['create']) + count($this->processed['delete']) + count($this->processed['update']); - return $processed_count / $this->totalToProcess; + protected function getNextExtensionOperation() { + foreach (array('install', 'uninstall') as $op) { + $modules = $this->getUnprocessedExtensions('module'); + if (!empty($modules[$op])) { + return array( + 'op' => $op, + 'type' => 'module', + 'name' => array_shift($modules[$op]), + ); + } + } + foreach (array('enable', 'disable') as $op) { + $themes = $this->getUnprocessedExtensions('theme'); + if (!empty($themes[$op])) { + return array( + 'op' => $op, + 'type' => 'theme', + 'name' => array_shift($themes[$op]), + ); + } + } + return FALSE; } /** - * Gets the next operation to perform. + * Gets the next configuration operation to perform. * * @return array|bool * An array containing the next operation and configuration name to perform * it on. If there is nothing left to do returns FALSE; */ - protected function getNextOperation() { - foreach(array('create', 'delete', 'update') as $op) { - $names = $this->getUnprocessed($op); - if (!empty($names)) { + protected function getNextConfigurationOperation() { + // The order configuration operations is processed is important. Deletes + // have to come first so that recreates can work. + foreach (array('delete', 'create', 'update') as $op) { + $config_names = $this->getUnprocessedConfiguration($op); + if (!empty($config_names)) { return array( 'op' => $op, - 'name' => array_shift($names), + 'name' => array_shift($config_names), ); } } diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 5e11697ca58574cb64f0e90d4a8f3e25d3661479..1303b7f5ef6c77caac1eecd2971f2d4a053094ef 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Config; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Component\Utility\String; use Drupal\Core\Config\Entity\ImportableEntityStorageInterface; use Drupal\Core\DependencyInjection\DependencySerialization; @@ -71,16 +73,30 @@ class ConfigImporter extends DependencySerialization { /** * The typed config manager. * - * @var \Drupal\Core\Config\TypedConfigManager + * @var \Drupal\Core\Config\TypedConfigManagerInterface */ protected $typedConfigManager; /** - * List of changes processed by the import(). + * List of configuration file changes processed by the import(). * * @var array */ - protected $processed; + protected $processedConfiguration; + + /** + * List of extension changes processed by the import(). + * + * @var array + */ + protected $processedExtensions; + + /** + * List of extension changes to be processed by the import(). + * + * @var array + */ + protected $extensionChangelist; /** * Indicates changes to import have been validated. @@ -89,6 +105,27 @@ 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; + + /** + * Flag set to import system.theme during processing theme enable and disables. + * + * @var bool + */ + protected $processedSystemTheme = FALSE; + /** * Constructs a configuration import object. * @@ -103,14 +140,21 @@ 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->processed = $this->storageComparer->getEmptyChangelist(); + $this->moduleHandler = $module_handler; + $this->themeHandler = $theme_handler; + $this->processedConfiguration = $this->storageComparer->getEmptyChangelist(); + $this->processedExtensions = $this->getEmptyExtensionsProcessedList(); } /** @@ -131,13 +175,35 @@ public function getStorageComparer() { */ public function reset() { $this->storageComparer->reset(); - $this->processed = $this->storageComparer->getEmptyChangelist(); + $this->processedConfiguration = $this->storageComparer->getEmptyChangelist(); + $this->processedExtensions = $this->getEmptyExtensionsProcessedList(); + $this->createExtensionChangelist(); $this->validated = FALSE; + $this->processedSystemTheme = FALSE; return $this; } /** - * Checks if there are any unprocessed changes. + * Gets an empty list of extensions to process. + * + * @return array + * An empty list of extensions to process. + */ + protected function getEmptyExtensionsProcessedList() { + return array( + 'module' => array( + 'install' => array(), + 'uninstall' => array(), + ), + 'theme' => array( + 'enable' => array(), + 'disable' => array(), + ), + ); + } + + /** + * Checks if there are any unprocessed configuration changes. * * @param array $ops * The operations to check for changes. Defaults to all operations, i.e. @@ -146,9 +212,9 @@ public function reset() { * @return bool * TRUE if there are changes to process and FALSE if not. */ - public function hasUnprocessedChanges($ops = array('delete', 'create', 'update')) { + public function hasUnprocessedConfigurationChanges($ops = array('delete', 'create', 'update')) { foreach ($ops as $op) { - if (count($this->getUnprocessed($op))) { + if (count($this->getUnprocessedConfiguration($op))) { return TRUE; } } @@ -161,8 +227,8 @@ public function hasUnprocessedChanges($ops = array('delete', 'create', 'update') * @return array * An array containing a list of processed changes. */ - public function getProcessed() { - return $this->processed; + public function getProcessedConfiguration() { + return $this->processedConfiguration; } /** @@ -173,8 +239,8 @@ public function getProcessed() { * @param string $name * The name of the configuration processed. */ - protected function setProcessed($op, $name) { - $this->processed[$op][] = $name; + protected function setProcessedConfiguration($op, $name) { + $this->processedConfiguration[$op][] = $name; } /** @@ -187,8 +253,139 @@ protected function setProcessed($op, $name) { * @return array * An array of configuration names. */ - public function getUnprocessed($op) { - return array_diff($this->storageComparer->getChangelist($op), $this->processed[$op]); + public function getUnprocessedConfiguration($op) { + return array_diff($this->storageComparer->getChangelist($op), $this->processedConfiguration[$op]); + } + + /** + * Gets list of processed extension changes. + * + * @return array + * An array containing a list of processed extension changes. + */ + public function getProcessedExtensions() { + return $this->processedExtensions; + } + + /** + * Determines if the current import has processed extensions. + * + * @return bool + * TRUE if the ConfigImporter has processed extensions. + */ + protected function hasProcessedExtensions() { + $compare = array_diff($this->processedExtensions, getEmptyExtensionsProcessedList()); + return !empty($compare); + } + + /** + * Sets an extension change as processed. + * + * @param string $type + * The type of extension, either 'theme' or 'module'. + * @param string $op + * The change operation performed, either install or uninstall. + * @param string $name + * The name of the extension processed. + */ + protected function setProcessedExtension($type, $op, $name) { + $this->processedExtensions[$type][$op][] = $name; + } + + /** + * Populates the extension change list. + */ + protected function createExtensionChangelist() { + // Read the extensions information to determine changes. + $current_extensions = $this->storageComparer->getTargetStorage()->read('core.extension'); + $new_extensions = $this->storageComparer->getSourceStorage()->read('core.extension'); + + // If there is no extension information in staging then exit. This is + // probably due to an empty staging directory. + if (!$new_extensions) { + return; + } + + // Get a list of modules with dependency weights as values. + $module_data = system_rebuild_module_data(); + // Set the actual module weights. + $module_list = array_combine(array_keys($module_data), array_keys($module_data)); + $module_list = array_map(function ($module) use ($module_data) { + return $module_data[$module]->sort; + }, $module_list); + + // Work out what modules to install and uninstall. + $uninstall = array_diff(array_keys($current_extensions['module']), array_keys($new_extensions['module'])); + $install = array_diff(array_keys($new_extensions['module']), array_keys($current_extensions['module'])); + // Sort the module list by their weights. So that dependencies + // are uninstalled last. + asort($module_list); + $uninstall = array_intersect(array_keys($module_list), $uninstall); + // Sort the module list by their weights (reverse). So that dependencies + // are installed first. + arsort($module_list); + $install = array_intersect(array_keys($module_list), $install); + + // Work out what themes to enable and to disable. + $enable = array_diff(array_keys($new_extensions['theme']), array_keys($current_extensions['theme'])); + $disable = array_diff(array_keys($current_extensions['theme']), array_keys($new_extensions['theme'])); + + $this->extensionChangelist = array( + 'module' => array( + 'uninstall' => $uninstall, + 'install' => $install, + ), + 'theme' => array( + 'enable' => $enable, + 'disable' => $disable, + ), + ); + } + + /** + * Gets a list changes for extensions. + * + * @param string $type + * The type of extension, either 'theme' or 'module'. + * @param string $op + * The change operation to get the unprocessed list for, either install + * or uninstall. + * + * @return array + * An array of extension names. + */ + protected function getExtensionChangelist($type, $op = NULL) { + if ($op) { + return $this->extensionChangelist[$type][$op]; + } + return $this->extensionChangelist[$type]; + } + + /** + * Gets a list of unprocessed changes for extensions. + * + * @param string $type + * The type of extension, either 'theme' or 'module'. + * + * @return array + * An array of extension names. + */ + public function getUnprocessedExtensions($type) { + $changelist = $this->getExtensionChangelist($type); + + if ($type == 'theme') { + $unprocessed = array( + 'enable' => array_diff($changelist['enable'], $this->processedExtensions[$type]['enable']), + 'disable' => array_diff($changelist['disable'], $this->processedExtensions[$type]['disable']), + ); + } + else { + $unprocessed = array( + 'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']), + 'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']), + ); + } + return $unprocessed; } /** @@ -200,7 +397,9 @@ public function getUnprocessed($op) { * The ConfigImporter instance. */ public function import() { - if ($this->hasUnprocessedChanges()) { + if ($this->hasUnprocessedConfigurationChanges()) { + $this->createExtensionChangelist(); + // Ensure that the changes have been validated. $this->validate(); @@ -208,19 +407,20 @@ public function import() { // Another process is synchronizing configuration. throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID)); } + + // Process any extension changes before importing configuration. + $this->handleExtensions(); + // First pass deleted, then new, and lastly changed configuration, in order // to handle dependencies correctly. - // @todo Implement proper dependency ordering using - // https://drupal.org/node/2080823 foreach (array('delete', 'create', 'update') as $op) { - foreach ($this->getUnprocessed($op) as $name) { - $this->process($op, $name); + foreach ($this->getUnprocessedConfiguration($op) as $name) { + $this->processConfiguration($op, $name); } } // Allow modules to react to a import. $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this)); - // The import is now complete. $this->lock->release(static::LOCK_ID); $this->reset(); @@ -253,12 +453,58 @@ public function validate() { * @param string $name * The name of the configuration to process. */ - protected function process($op, $name) { + protected function processConfiguration($op, $name) { if (!$this->importInvokeOwner($op, $name)) { $this->importConfig($op, $name); } } + /** + * Processes an extension change. + * + * @param string $type + * The type of extension, either 'module' or 'theme'. + * @param string $op + * The change operation. + * @param string $name + * The name of the extension to process. + */ + protected function processExtension($type, $op, $name) { + // 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 ($type == 'module') { + $this->moduleHandler->$op(array($name), FALSE); + // Installing a module can cause a kernel boot therefore reinject all the + // services. + $this->reInjectMe(); + // During a module install or uninstall the container is rebuilt and the + // module handler is called from drupal_get_complete_schema(). This causes + // the container's instance of the module handler not to have loaded all + // the enabled modules. + $this->moduleHandler->loadAll(); + } + if ($type == 'theme') { + // Theme disables possible remove default or admin themes therefore we + // need to import this before doing any. If there are no disables and + // the default or admin theme is change this will be picked up whilst + // processing configuration. + if ($op == 'disable' && $this->processedSystemTheme === FALSE) { + $this->importConfig('update', 'system.theme'); + $this->configManager->getConfigFactory()->reset('system.theme'); + $this->processedSystemTheme = TRUE; + } + $this->themeHandler->$op(array($name)); + } + + $this->setProcessedExtension($type, $op, $name); + \Drupal::service('config.installer') + ->setSyncing(FALSE) + ->resetSourceStorage(); + } + /** * Writes a configuration change from the source to the target storage. * @@ -277,7 +523,7 @@ protected function importConfig($op, $name) { $config->setData($data ? $data : array()); $config->save(); } - $this->setProcessed($op, $name); + $this->setProcessedConfiguration($op, $name); } /** @@ -325,7 +571,7 @@ protected function importInvokeOwner($op, $name) { throw new EntityStorageException(String::format('The entity storage "@storage" for the "@entity_type" entity type does not support imports', array('@storage' => get_class($entity_storage), '@entity_type' => $entity_type))); } $entity_storage->$method($name, $new_config, $old_config); - $this->setProcessed($op, $name); + $this->setProcessedConfiguration($op, $name); return TRUE; } return FALSE; @@ -341,4 +587,74 @@ 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 string $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->getUnprocessedConfiguration('update')); + } + + /** + * Handle changes to installed modules and themes. + */ + protected function handleExtensions() { + $processed_extension = FALSE; + foreach (array('install', 'uninstall') as $op) { + $modules = $this->getUnprocessedExtensions('module'); + foreach($modules[$op] as $module) { + $processed_extension = TRUE; + $this->processExtension('module', $op, $module); + } + } + foreach (array('enable', 'disable') as $op) { + $themes = $this->getUnprocessedExtensions('theme'); + foreach($themes[$op] as $theme) { + $processed_extension = TRUE; + $this->processExtension('theme', $op, $theme); + } + } + + if ($processed_extension) { + // Recalculate differences as default config could have been imported. + $this->storageComparer->reset(); + $this->processed = $this->storageComparer->getEmptyChangelist(); + // 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->recalculateChangelist = TRUE; + } + } + + /** + * Gets all the service dependencies from \Drupal. + * + * Since the ConfigImporter handles module installation the kernel and the + * container can be rebuilt and altered during processing. It is necessary to + * keep the services used by the importer in sync. + */ + 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 038dbd63b84e1730db932cee0677b28578cd8dae..7ee1b56f3174882d5fd04ce202613f388081bed3 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -48,6 +48,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. * @@ -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 @@ -130,6 +144,16 @@ 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. This approach means that any code that expects + // default configuration entities to exist 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() ->getStorage($entity_type); @@ -159,4 +183,49 @@ 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)) { + // Default to using the ExtensionInstallStorage which searches extension's + // config directories for default configuration. + $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 927c6105422e178d37d3b1cd7d393c15a65e85cc..6e7c20d0269fc7460bca453751b5dd1a2559aedb 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php @@ -37,4 +37,38 @@ interface ConfigInstallerInterface { */ 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 d1048ea966b2d95f28b343ecd2dae646a96faeda..6fd55c948e67692112d6db7bf238d32c9369e69b 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -90,6 +90,13 @@ public function getEntityManager() { return $this->entityManager; } + /** + * {@inheritdoc} + */ + public function getConfigFactory() { + return $this->configFactory; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php index b5084fe7af56280de04666f5e0e1c105b60ebd47..4da9474d845abc9a7b71cf151f5b575cbf71c5c3 100644 --- a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php @@ -31,6 +31,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. * diff --git a/core/lib/Drupal/Core/Config/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php index ac895373cf1703a543781e4935f62c6a461e01dc..ee467b6be8124d35fe688d8bd3a737d052ff4326 100644 --- a/core/lib/Drupal/Core/Config/StorageComparer.php +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -6,6 +6,8 @@ */ namespace Drupal\Core\Config; + +use Drupal\Component\Utility\String; use Drupal\Core\Config\Entity\ConfigDependencyManager; /** @@ -118,11 +120,23 @@ public function getChangelist($op = NULL) { * The change operation performed. Either delete, create or update. * @param array $changes * Array of changes to add to the changelist. + * @param array $sort_order + * Array to sort that can be used to sort the changelist. This array must + * contain all the items that are in the change list. */ - protected function addChangeList($op, array $changes) { + protected function addChangeList($op, array $changes, array $sort_order = NULL) { // Only add changes that aren't already listed. $changes = array_diff($changes, $this->changelist[$op]); $this->changelist[$op] = array_merge($this->changelist[$op], $changes); + if (isset($sort_order)) { + $count = count($this->changelist[$op]); + // Sort the changlist in the same order as the $sort_order array and + // ensure the array is keyed from 0. + $this->changelist[$op] = array_values(array_intersect($sort_order, $this->changelist[$op])); + if ($count != count($this->changelist[$op])) { + throw new \InvalidArgumentException(String::format('Sorting the @op changelist should not change its length.', array('@op' => $op))); + } + } } /** @@ -188,8 +202,9 @@ protected function addChangelistUpdate() { if (!empty($recreates)) { // Recreates should become deletes and creates. Deletes should be ordered // so that dependencies are deleted first. - $this->addChangeList('create', $recreates); - $this->addChangeList('delete', array_reverse($recreates)); + $this->addChangeList('create', $recreates, $this->sourceNames); + $this->addChangeList('delete', $recreates, array_reverse($this->targetNames)); + } } diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php index 75f06d4d675b72526255bf3cc1ba9154d9f3845a..72e2232b9945fbf26e5fe3108e6ee79a35929753 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php @@ -28,7 +28,7 @@ class ConfigImportSubscriber implements EventSubscriberInterface { */ public function onConfigImporterValidate(ConfigImporterEvent $event) { foreach (array('delete', 'create', 'update') as $op) { - foreach ($event->getConfigImporter()->getUnprocessed($op) as $name) { + foreach ($event->getConfigImporter()->getUnprocessedConfiguration($op) as $name) { Config::validateName($name); } } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 17b4239b0cb48cfed7315ddabf27d9f632c383b2..ac48c175883059f17c87084efb2814c845013f6a 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 = $extension_config->get("module.$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 1474102fc018788201f8699bbc5871ee85a187cd..b43d3e47bea76b54c1cd49caaf10b1987e90f347 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -146,6 +146,15 @@ public function enable(array $theme_list) { // Refresh the theme list as installation of default configuration needs // an updated list to work. $this->reset(); + + // The default config installation storage only knows about the currently + // enabled list of themes, so it has to be reset in order to pick up the + // default config of the newly installed theme. However, do not reset the + // source storage when synchronizing configuration, since that would + // needlessly trigger a reload of the whole configuration to be imported. + if (!$this->configInstaller->isSyncing()) { + $this->configInstaller->resetSourceStorage(); + } // Install default configuration of the theme. $this->configInstaller->installDefaultConfig('theme', $key); } diff --git a/core/modules/block/block.module b/core/modules/block/block.module index 2d4b811a26747c52d34e04ffda87d87a67ee1ad8..183462e111c0b5dbf7c4368072cd31f24d89b5ab 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -8,6 +8,7 @@ use Drupal\block\BlockInterface; use Drupal\Component\Plugin\Exception\PluginException; use Drupal\language\Entity\Language; +use Drupal\system\Entity\Menu; use Symfony\Cmf\Component\Routing\RouteObjectInterface; /** @@ -422,10 +423,12 @@ function block_user_role_delete($role) { /** * Implements hook_menu_delete(). */ -function block_menu_delete($menu) { - foreach (entity_load_multiple('block') as $block) { - if ($block->get('plugin') == 'system_menu_block:' . $menu->id()) { - $block->delete(); +function block_menu_delete(Menu $menu) { + if (!$menu->isSyncing()) { + foreach (entity_load_multiple('block') as $block) { + if ($block->get('plugin') == 'system_menu_block:' . $menu->id()) { + $block->delete(); + } } } } diff --git a/core/modules/comment/lib/Drupal/comment/CommentManager.php b/core/modules/comment/lib/Drupal/comment/CommentManager.php index 5b1820cdc28f7203dad482a1314c96e693e8dc0e..c522fd34c93c34f06846b305765b20a7089da6be 100644 --- a/core/modules/comment/lib/Drupal/comment/CommentManager.php +++ b/core/modules/comment/lib/Drupal/comment/CommentManager.php @@ -184,6 +184,14 @@ public function addDefaultField($entity_type, $bundle, $field_name = 'comment', )) ->save(); + // The comment field should be hidden in all other form displays. + foreach ($this->entityManager->getFormModes($entity_type) as $id => $form_mode) { + $display = entity_get_form_display($entity_type, $bundle, $id); + // Only update existing displays. + if ($display && !$display->isNew()) { + $display->removeComponent($field_name)->save(); + } + } // Set default to display comment list. entity_get_display($entity_type, $bundle, 'default') ->setComponent($field_name, array( @@ -192,6 +200,15 @@ public function addDefaultField($entity_type, $bundle, $field_name = 'comment', 'weight' => 20, )) ->save(); + // The comment field should be hidden in all other view displays. + foreach ($this->entityManager->getViewModes($entity_type) as $id => $view_mode) { + $display = entity_get_display($entity_type, $bundle, $id); + // Only update existing displays. + if ($display && !$display->isNew()) { + $display->removeComponent($field_name)->save(); + } + } + } $this->addBodyField($entity_type, $field_name); } diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index c6000016c1e7016d18fa3497d9202b7d80a85076..c48a77649fc369e7bc428c9571f23817a9feccf6 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; @@ -72,6 +76,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. * @@ -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,24 +248,27 @@ 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.')); } else{ - $config_importer->initialize(); + $operations = $config_importer->initialize(); $batch = array( - 'operations' => array( - array(array(get_class($this), 'processBatch'), array($config_importer)), - ), + 'operations' => array(), 'finished' => array(get_class($this), 'finishBatch'), 'title' => t('Synchronizing configuration'), 'init_message' => t('Starting configuration synchronization.'), - 'progress_message' => t('Synchronized @current configuration files out of @total.'), + 'progress_message' => t('Completed @current step of @total.'), 'error_message' => t('Configuration synchronization has encountered an error.'), 'file' => drupal_get_path('module', 'config') . '/config.admin.inc', ); + foreach ($operations as $operation) { + $batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $operation)); + } batch_set($batch); } @@ -253,13 +282,13 @@ public function submitForm(array &$form, array &$form_state) { * @param $context * The batch context. */ - public static function processBatch(BatchConfigImporter $config_importer, &$context) { + public static function processBatch(BatchConfigImporter $config_importer, $operation, &$context) { if (!isset($context['sandbox']['config_importer'])) { $context['sandbox']['config_importer'] = $config_importer; } $config_importer = $context['sandbox']['config_importer']; - $config_importer->processBatch($context); + $config_importer->$operation($context); } /** 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 0000000000000000000000000000000000000000..038e318ee5ab39029049c632ad619efc4a4c9310 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php @@ -0,0 +1,112 @@ + 'Import configuration from all modules and the standard profile', + 'description' => 'Tests the largest configuration import possible with the modules and profiles provided by core.', + 'group' => 'Configuration', + ); + } + + /** + * 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 uninstalled. 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(); + $modules_to_uninstall = 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($modules_to_uninstall['comment']), 'The comment module will be disabled'); + + // Uninstall all modules that can be uninstalled. + \Drupal::moduleHandler()->uninstall(array_keys($modules_to_uninstall)); + + $this->assertModules(array_keys($modules_to_uninstall), FALSE); + foreach($modules_to_uninstall as $module => $info) { + $this->assertNoModuleConfig($module); + $this->assertModuleTablesDoNotExist($module); + } + + // Import the configuration thereby re-installing all the modules. + $this->configImporter()->import(); + + // Check that all modules that were uninstalled are now reinstalled. + $this->assertModules(array_keys($modules_to_uninstall), TRUE); + foreach($modules_to_uninstall 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/ConfigImportRecreateTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php index 9f5ed90997238d634a4b65160e559b238564f446..3f86c9b273f23d7086d5c99ba63a76b0a0a470f5 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php @@ -45,6 +45,8 @@ public function setUp() { $this->installSchema('system', 'config_snapshot'); $this->installSchema('node', 'node'); + $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); + // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( $this->container->get('config.storage.staging'), @@ -55,9 +57,10 @@ public 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')); } public function testRecreateEntity() { @@ -89,21 +92,19 @@ public function testRecreateEntity() { $this->configImporter->reset(); // A node type, a field, a field instance an entity view display and an // entity form display will be recreated. - $creates = $this->configImporter->getUnprocessed('create'); - $deletes = $this->configImporter->getUnprocessed('delete'); + $creates = $this->configImporter->getUnprocessedConfiguration('create'); + $deletes = $this->configImporter->getUnprocessedConfiguration('delete'); $this->assertEqual(5, count($creates), 'There are 5 configuration items to create.'); $this->assertEqual(5, count($deletes), 'There are 5 configuration items to delete.'); - $this->assertEqual(0, count($this->configImporter->getUnprocessed('update')), 'There are no configuration items to update.'); + $this->assertEqual(0, count($this->configImporter->getUnprocessedConfiguration('update')), 'There are no configuration items to update.'); $this->assertIdentical($creates, array_reverse($deletes), 'Deletes and creates contain the same configuration names in opposite orders due to dependencies.'); $this->configImporter->import(); // Verify that there is nothing more to import. - $this->assertFalse($this->configImporter->reset()->hasUnprocessedChanges()); + $this->assertFalse($this->configImporter->reset()->hasUnprocessedConfigurationChanges()); $content_type = entity_load('node_type', $type_name); $this->assertEqual('Node type one', $content_type->label()); } - } - diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php index 4a38a4df3eba33bae8df31c9d3a9dec5d71e4cd6..084be152d3ece6a30d0e3b98a3f6def169fb41d1 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,9 @@ */ class ConfigImportUITest extends WebTestBase { - public static $modules = array('config', 'config_test'); + // Enable the Options and Text modules to ensure dependencies are handled + // correctly. + public static $modules = array('config', 'config_test', 'config_import_test', 'text', 'options'); public static function getInfo() { return array( @@ -38,6 +41,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 +69,61 @@ function testImport() { $staging->write($dynamic_name, $original_dynamic_data); $this->assertIdentical($staging->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + // Enable the Action and Ban 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. + $core_extension = \Drupal::config('core.extension')->get(); + $core_extension['module']['action'] = 0; + $core_extension['module']['ban'] = 0; + $core_extension['module'] = module_config_sort($core_extension['module']); + $core_extension['theme']['bartik'] = 0; + $staging->write('core.extension', $core_extension); + + // Use the install storage so that we can read configuration from modules + // and themes that are not installed. + $install_storage = new InstallStorage(); + + // Set the Bartik theme as default. + $system_theme = \Drupal::config('system.theme')->get(); + $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); + + // Uninstall the Options and Text modules to ensure that dependencies are + // handled correctly. Options depends on Text so Text should be installed + // first. Since they were enabled during the test setup the core.extension + // file in staging will already contain them. + \Drupal::moduleHandler()->uninstall(array('text', 'options')); + + // Set the state system to record installations and uninstallations. + \Drupal::state()->set('ConfigImportUITest.core.extension.modules_installed', array()); + \Drupal::state()->set('ConfigImportUITest.core.extension.modules_uninstalled', array()); + // Verify that both appear as ready to import. $this->drupalGet('admin/config/development/configuration'); $this->assertText($name); $this->assertText($dynamic_name); + $this->assertText('core.extension'); + $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('core.extension'); + $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 +137,85 @@ function testImport() { // Verify the cache got cleared. $this->assertTrue(isset($GLOBALS['hook_cache_flush'])); + + $this->rebuildContainer(); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('ban'), 'Ban module installed during import.'); + $this->assertTrue(\Drupal::database()->schema()->tableExists('ban_ip'), 'The database table ban_ip exists.'); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('action'), 'Action module installed during import.'); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('options'), 'Options module installed during import.'); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('text'), 'Text module installed during import.'); + + $theme_info = \Drupal::service('theme_handler')->listInfo(); + $this->assertTrue($theme_info['bartik']->status, 'Bartik theme enabled during import.'); + + // Ensure installations and uninstallation occur as expected. + $installed = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_installed', array()); + $uninstalled = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_uninstalled', array()); + $expected = array('ban', 'action', 'text', 'options'); + $this->assertIdentical($expected, $installed, 'Ban, Action, Text and Options modules installed in the correct order.'); + $this->assertTrue(empty($uninstalled), 'No modules uninstalled 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)); + + $core_extension = \Drupal::config('core.extension')->get(); + unset($core_extension['module']['action']); + unset($core_extension['module']['ban']); + unset($core_extension['module']['options']); + unset($core_extension['module']['text']); + unset($core_extension['theme']['bartik']); + $core_extension['disabled']['theme']['bartik'] = 0; + $staging->write('core.extension', $core_extension); + $staging->delete('action.settings'); + $staging->delete('text.settings'); + + $system_theme = \Drupal::config('system.theme')->get(); + $system_theme['default'] = 'stark'; + $system_theme['admin'] = 'stark'; + $staging->write('system.theme', $system_theme); + + // Set the state system to record installations and uninstallations. + \Drupal::state()->set('ConfigImportUITest.core.extension.modules_installed', array()); + \Drupal::state()->set('ConfigImportUITest.core.extension.modules_uninstalled', array()); + + // Verify that both appear as ready to import. + $this->drupalGet('admin/config/development/configuration'); + $this->assertText('core.extension'); + $this->assertText('system.theme'); + $this->assertText('action.settings'); + + // Import and verify that both do not appear anymore. + $this->drupalPostForm(NULL, array(), t('Import all')); + $this->assertNoText('core.extension'); + $this->assertNoText('system.theme'); + $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->assertFalse(\Drupal::moduleHandler()->moduleExists('options'), 'Options module uninstalled during import.'); + $this->assertFalse(\Drupal::moduleHandler()->moduleExists('text'), 'Text module uninstalled during import.'); + + // Ensure installations and uninstallation occur as expected. + $installed = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_installed', array()); + $uninstalled = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_uninstalled', array()); + $expected = array('options', 'text', 'ban', 'action'); + $this->assertIdentical($expected, $uninstalled, 'Options, Text, Action and Ban modules uninstalled in the correct order.'); + $this->assertTrue(empty($installed), 'No modules installed 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 8e99296012aceaa1016878d8bc6b637ba0d2ebae..ff154b78956e5cec029cc88f640ca28cb587e903 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -50,6 +50,8 @@ function setUp() { // so it has to be cleared out manually. unset($GLOBALS['hook_config_test']); + $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); + // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( $this->container->get('config.storage.staging'), @@ -60,9 +62,10 @@ 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')); } /** @@ -145,8 +148,7 @@ function testDeleted() { $this->assertTrue(isset($GLOBALS['hook_config_test']['predelete'])); $this->assertTrue(isset($GLOBALS['hook_config_test']['delete'])); - // Verify that there is nothing more to import. - $this->assertFalse($this->configImporter->hasUnprocessedChanges()); + $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); } /** @@ -193,7 +195,7 @@ function testNew() { $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); // Verify that there is nothing more to import. - $this->assertFalse($this->configImporter->hasUnprocessedChanges()); + $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); } /** @@ -248,7 +250,7 @@ function testUpdated() { $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); // Verify that there is nothing more to import. - $this->assertFalse($this->configImporter->hasUnprocessedChanges()); + $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); } } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php index 137635d1df896b3079847eca7cce35b741accab2..7c6c1cbed4c7f81f4c3665c49d896d4976282a75 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php @@ -32,6 +32,7 @@ public static function getInfo() { public function setUp() { parent::setUp(); $this->installSchema('system', 'config_snapshot'); + $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); } /** diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php index d8e7187de92770b130e0a9523a8ff1d55f170ea1..295ff74d1356a408d982021adc517405af1589bc 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php @@ -36,6 +36,7 @@ public function setUp() { // Update the config snapshot. This allows the parent::setUp() to write // configuration files. \Drupal::service('config.manager')->createSnapshot(\Drupal::service('config.storage'), \Drupal::service('config.storage.snapshot')); + $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 0000000000000000000000000000000000000000..87cdd025cc53153ecd1fefdccb8c7cdcb5bd4b18 --- /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 0000000000000000000000000000000000000000..936b72b74b13fff69ba05e8407b319f3a7d16369 --- /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) { + + } + + /** + * Reacts to a config save and records information in state for testing. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $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() == 'core.extension') { + $installed = $this->state->get('ConfigImportUITest.core.extension.modules_installed', array()); + $uninstalled = $this->state->get('ConfigImportUITest.core.extension.modules_uninstalled', array()); + $original = $config->getOriginal('module'); + $data = $config->get('module'); + $install = array_diff_key($data, $original); + if (!empty($install)) { + $installed[] = key($install); + } + $uninstall = array_diff_key($original, $data); + if (!empty($uninstall)) { + $uninstalled[] = key($uninstall); + } + + $this->state->set('ConfigImportUITest.core.extension.modules_installed', $installed); + $this->state->set('ConfigImportUITest.core.extension.modules_uninstalled', $uninstalled); + } + } + + /** + * Reacts to a config delete and records information in state for testing. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $event + */ + 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[ConfigEvents::SAVE][] = array('onConfigSave', 40); + $events[ConfigEvents::DELETE][] = array('onConfigDelete', 40); + return $events; + } + +} diff --git a/core/modules/contact/contact.install b/core/modules/contact/contact.install index a67134ec68653f1777cbffbec3cc205feea579d0..477f3596a7472e3ad08586326c0a698d1313e4dd 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 8ff4c7ee03d644f690b6651ab571a0456d3231cd..b814f2dc428b6844382a3334e073bee792114776 100644 --- a/core/modules/content_translation/content_translation.install +++ b/core/modules/content_translation/content_translation.install @@ -88,6 +88,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(); + } } /** @@ -105,3 +118,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 3eea9cb40cfaad8be3e658ba1bf53f1e7cb1b6f8..854d80e886bf35bbe419f5358b8aadd85ed4c866 100644 --- a/core/modules/field/lib/Drupal/field/Entity/FieldInstanceConfig.php +++ b/core/modules/field/lib/Drupal/field/Entity/FieldInstanceConfig.php @@ -794,4 +794,11 @@ public function hasCustomStorage() { return $this->field->hasCustomStorage(); } + /** + * {@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 e796caa1c41d9fb9b2b6ae08c69d0a2ad0c7efca..6c842825b0f4416c10f6e53bdcef3f7c3f7a1d9a 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.taxonomy_term.forum_container.yml similarity index 100% rename from core/modules/forum/config/field.field.forum.forum_container.yml rename to core/modules/forum/config/field.field.taxonomy_term.forum_container.yml diff --git a/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml b/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml index ed16957b16a133fd1663fe477dda687bf35450bb..710b8bdd7bced11f734dcb056b01846126fb2cd6 100644 --- a/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml +++ b/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml @@ -15,5 +15,5 @@ settings: { } field_type: list_boolean dependencies: entity: - - field.field.forum.forum_container + - field.field.taxonomy_term.forum_container - taxonomy.vocabulary.forums diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install index b5f0e0a2ccc8953130be71afd5854fa8772bcec2..7f975e9298006cd815bd37791f88b703feb86ad8 100644 --- a/core/modules/forum/forum.install +++ b/core/modules/forum/forum.install @@ -16,72 +16,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_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 a6af612ea4765bd8c140c55120a45c8e01710601..36f83454e3c46aacfe3382250a0666d3757762ca 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 55c27dc8eecb71320be6b6139e385a389fbedf92..3dfdfb237b2bdf88d358030aa94b40bf8ea0b1fe 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/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php index a50a2d485ca1aa6fd45e9325e72aa92c734976b9..36adb8befa4f733afd6e51ae3e581c0199a587be 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php @@ -148,7 +148,7 @@ protected function setUp() { // \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work. // Write directly to active storage to avoid early instantiation of // the event dispatcher which can prevent modules from registering events. - \Drupal::service('config.storage')->write('core.extension', array('module' => array())); + \Drupal::service('config.storage')->write('core.extension', array('module' => array(), 'theme' => array())); // Collect and set a fixed module list. $class = get_class($this); diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 6558f209200bdf30d546ede5c1d53282980809e3..e19169f7b18ff083845a08b787604ae4b04120df 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -1526,7 +1526,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 917a938cda8df9509389b0a516e36c6d9a427af4..82e1ebbb2ad5b48f7c031ea7db33673b3326b34d 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/profiles/standard/config/entity.view_display.node.article.default.yml b/core/profiles/standard/config/entity.view_display.node.article.default.yml index c4f2c85baf20746a4d3600181fc330a498c9a5ea..81f0545957e8df628f1d70bf046441aa9e6aeed7 100644 --- a/core/profiles/standard/config/entity.view_display.node.article.default.yml +++ b/core/profiles/standard/config/entity.view_display.node.article.default.yml @@ -4,6 +4,13 @@ bundle: article mode: default status: true content: + field_image: + label: hidden + type: image + settings: + image_style: large + image_link: '' + weight: -1 body: label: hidden type: text_default @@ -14,13 +21,6 @@ content: weight: 10 label: above settings: { } - field_image: - label: hidden - type: image - settings: - image_style: large - image_link: '' - weight: -1 dependencies: entity: - field.instance.node.article.body diff --git a/core/profiles/standard/config/entity.view_display.node.article.teaser.yml b/core/profiles/standard/config/entity.view_display.node.article.teaser.yml index a88fc068d67255bc29dd9d554f4e3331adc63e3c..e21e3cffdcac89f86ef616cb57aad49bce9a48bd 100644 --- a/core/profiles/standard/config/entity.view_display.node.article.teaser.yml +++ b/core/profiles/standard/config/entity.view_display.node.article.teaser.yml @@ -4,6 +4,13 @@ bundle: article mode: teaser status: true content: + field_image: + label: hidden + type: image + settings: + image_style: medium + image_link: content + weight: -1 body: label: hidden type: text_summary_or_trimmed @@ -15,13 +22,6 @@ content: weight: 10 label: above settings: { } - field_image: - label: hidden - type: image - settings: - image_style: medium - image_link: content - weight: -1 dependencies: entity: - entity.view_mode.node.teaser