diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 7b9c326..540a854 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..47b78bd 100644 --- a/core/lib/Drupal/Core/Config/BatchConfigImporter.php +++ b/core/lib/Drupal/Core/Config/BatchConfigImporter.php @@ -18,6 +18,8 @@ class BatchConfigImporter extends ConfigImporter { * Initializes the config importer in preparation for processing a batch. */ public function initialize() { + $this->createExtensionChangelist(); + // Ensure that the changes have been validated. $this->validate(); @@ -26,8 +28,17 @@ public function initialize() { 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->totalToProcess += count($modules[$op]); + } + $themes = $this->getUnprocessedExtensions('theme'); + foreach (array('enable', 'disable', 'default') as $op) { + $this->totalToProcess += count($themes[$op]); + } + foreach (array('create', 'delete', 'update') as $op) { + $this->totalToProcess += count($this->getUnprocessedConfiguration($op)); } } @@ -40,7 +51,12 @@ public function initialize() { public function processBatch(array &$context) { $operation = $this->getNextOperation(); if (!empty($operation)) { - $this->process($operation['op'], $operation['name']); + if (!empty($operation['type'])) { + $this->processExtension($operation['type'], $operation['op'], $operation['name']); + } + else { + $this->processConfiguration($operation['op'], $operation['name']); + } $context['message'] = t('Synchronizing @name.', array('@name' => $operation['name'])); $context['finished'] = $this->batchProgress(); } @@ -61,25 +77,51 @@ public function processBatch(array &$context) { * @return float * The percentage of progress made expressed as a float between 0 and 1. */ - protected function batchProgress() { - $processed_count = count($this->processed['create']) + count($this->processed['delete']) + count($this->processed['update']); + protected function batchProgress() { + $processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']); + $processed_count += count($this->processedExtensions['theme']['disable']) + count($this->processedExtensions['theme']['enable']); + // Setting the default theme is special. Yuck. + $processed_count += empty($this->processedExtensions['theme']['default']) ? 0 : 1; + $processed_count += count($this->processedConfiguration['create']) + count($this->processedConfiguration['delete']) + count($this->processedConfiguration['update']); return $processed_count / $this->totalToProcess; } /** * Gets the next operation to perform. * + * We process extensions before we process configuration files. + * * @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 getNextOperation() { + 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', 'default', 'disable') as $op) { + $themes = $this->getUnprocessedExtensions('theme'); + if (!empty($themes[$op])) { + return array( + 'op' => $op, + 'type' => 'theme', + 'name' => array_shift($themes[$op]), + ); + } + } + foreach (array('create', 'delete', '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/ConfigBase.php b/core/lib/Drupal/Core/Config/ConfigBase.php index 8dfac43..0fe2eb5 100644 --- a/core/lib/Drupal/Core/Config/ConfigBase.php +++ b/core/lib/Drupal/Core/Config/ConfigBase.php @@ -94,6 +94,7 @@ public function setName($name) { public static function validateName($name) { // The name must be namespaced by owner. if (strpos($name, '.') === FALSE) { + ffs($name); throw new ConfigNameException(String::format('Missing namespace in Config object name @name.', array( '@name' => $name, ))); diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 5e11697..33f461a 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. @@ -90,6 +106,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 @@ -103,14 +133,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 +168,29 @@ 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; return $this; } + protected function getEmptyExtensionsProcessedList() { + return array( + 'module' => array( + 'install' => array(), + 'uninstall' => array(), + ), + 'theme' => array( + 'enable' => array(), + 'disable' => array(), + 'default' => array(), + ), + ); + } + /** - * Checks if there are any unprocessed changes. + * 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 +199,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 +214,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 +226,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 +240,118 @@ 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; + } + + /** + * 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; + } + + /** + * Populate extension change list. + * + * @return void + */ + protected function createExtensionChangelist() { + $current_modules = $this->storageComparer->getTargetStorage()->read('system.module'); + $new_modules = $this->storageComparer->getSourceStorage()->read('system.module'); + $install = array(); + $uninstall = array(); + if (isset($current_modules['enabled'], $new_modules['enabled']) && is_array($current_modules['enabled']) && is_array($new_modules['enabled'])) { + $uninstall = array_diff(array_keys($current_modules['enabled']), array_keys($new_modules['enabled'])); + $install = array_diff(array_keys($new_modules['enabled']), array_keys($current_modules['enabled'])); + } + + $current_themes = $this->storageComparer->getTargetStorage()->read('system.theme'); + $new_themes = $this->storageComparer->getSourceStorage()->read('system.theme'); + $enable = array(); + $disable = array(); + if (isset($current_themes['enabled'], $new_themes['enabled']) && is_array($current_themes['enabled']) && is_array($new_themes['enabled'])) { + $enable = array_diff(array_keys($new_themes['enabled']), array_keys($current_themes['enabled'])); + $disable = array_diff(array_keys($current_themes['enabled']), array_keys($new_themes['enabled'])); + } + + $this->extensionChangelist = array( + 'module' => array( + 'uninstall' => $uninstall, + 'install' => $install, + ), + 'theme' => array( + 'enable' => $enable, + 'default' => array($new_themes['default']), + '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) { + $unprocessed = array(); + $changelist = $this->getExtensionChangelist($type); + + if ($type == 'theme') { + $unprocessed = array( + 'disable' => array_diff($changelist['disable'], $this->processedExtensions[$type]['disable']), + 'enable' => array_diff($changelist['enable'], $this->processedExtensions[$type]['enable']), + // Setting the default theme is special. Yuck. + 'default' => empty($this->processedExtensions[$type]['default']) ? array() : array($changelist['default']), + ); + } + 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 +363,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 +373,22 @@ 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 // 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(); @@ -239,7 +407,8 @@ public function validate() { if (!$this->storageComparer->validateSiteUuid()) { throw new ConfigImporterException('Site UUID in source storage does not match the target storage.'); } - $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, new ConfigImporterEvent($this)); + $importer_event = new ConfigImporterEvent($this); + $this->eventDispatcher->dispatch(ConfigEvents::IMPORT_VALIDATE, $importer_event); $this->validated = TRUE; } return $this; @@ -253,13 +422,43 @@ 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) { + if ($type == 'module') { + $this->moduleHandler->$op(array($name), FALSE); + } + else { + if ($op == 'default') { + // 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', $name) + ->save(); + } + else { + $this->themeHandler->$op(array($name)); + } + } + $this->setProcessedExtension($type, $op, $name); + } + + /** * Writes a configuration change from the source to the target storage. * * @param string $op @@ -277,7 +476,7 @@ protected function importConfig($op, $name) { $config->setData($data ? $data : array()); $config->save(); } - $this->setProcessed($op, $name); + $this->setProcessedConfiguration($op, $name); } /** @@ -325,7 +524,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 +540,128 @@ 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->getUnprocessedConfiguration('update')); + } + + /** + * Handle changes to installed modules and themes. + */ + protected function handleExtensions() { + // 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()); + + $module_updates_count = $this->handleModules($this->getUnprocessedExtensions('module')); + $theme_updates_count = $this->handleThemes($this->getUnprocessedExtensions('theme')); + + \Drupal::service('config.installer') + ->setSyncing(FALSE) + ->resetSourceStorage(); + + if ($module_updates_count || $theme_updates_count) { + // 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. + * + * @param $module_changelist + * Array with 'install' and 'uninstall' keys, each containing a list of + * modules to install or uninstall. + * + * @return int + * The number of modules installed or uninstalled. + */ + protected function handleModules($module_changelist) { + if (!empty($module_changelist['install']) && !$this->moduleHandler->install($module_changelist['install'], FALSE)) { + throw new ConfigImporterException(sprintf('Unable to enable modules')); + } + + if (!empty($module_changelist['uninstall']) && !$this->moduleHandler->uninstall($module_changelist['uninstall'], FALSE)) { + throw new ConfigImporterException(sprintf('Unable to uninstall modules')); + } + + $change_count = count($module_changelist['install']) + count($module_changelist['uninstall']); + if ($change_count > 0) { + $this->importConfig('update', 'system.module'); + } + return $change_count; + } + + /** + * Enable or disable themes depending on configuration to import. + * + * @param $theme_changelist + * Array with 'install' and 'uninstall' keys, each containing a list of + * modules to install or uninstall. + * + * @return int + * The number of themes installed or disabled. + */ + protected function handleThemes($theme_changelist) { + // Enable themes first, to ensure that if the default theme has changed, + // the changed-to-theme has been enabled. + if (!empty($theme_changelist['enable'])) { + $this->themeHandler->enable($themes_changelist['enable']); + } + + if (!empty($theme_changelist['default'])) { + // 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', $theme_changelist['default']) + ->save(); + } + + if (!empty($theme_changelist['disable'])) { + $this->themeHandler->disable($theme_changelist['disable']); + } + + $change_count = count($theme_changelist['enable']) + count($theme_changelist['disable']) + count($theme_changelist['default']); + if ($change_count > 0) { + $this->importConfig('update', 'system.theme'); + } + return $change_count; + } + + 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 3769a7b..ec9f328 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() ->getStorage($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 d1048ea..6fd55c9 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/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php index 75f06d4..72e2232 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 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 35ab1d4..856269b 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/comment/lib/Drupal/comment/CommentManager.php b/core/modules/comment/lib/Drupal/comment/CommentManager.php index 5b1820c..c522fd3 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 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..9f7f707 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php @@ -0,0 +1,109 @@ + '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/ConfigImportRecreateTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php index 9f5ed90..d573c47 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php @@ -45,6 +45,16 @@ public function setUp() { $this->installSchema('system', 'config_snapshot'); $this->installSchema('node', 'node'); + $theme_data = $this->container->get('config.storage')->read('system.theme'); + if (empty($theme_data['enabled'])) { + $this->container->get('config.storage')->write('system.theme', array('default' => 'start', 'enabled' => array('stark'))); + } + $module_data = $this->container->get('config.storage')->read('system.module'); + if (empty($module_data['enabled'])) { + $this->container->get('config.storage')->write('system.module', array('enabled' => array('system'))); + } + $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 +65,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 +100,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 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..3360fbf 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -50,6 +50,16 @@ function setUp() { // so it has to be cleared out manually. unset($GLOBALS['hook_config_test']); + $theme_data = $this->container->get('config.storage')->read('system.theme'); + if (empty($theme_data['enabled'])) { + $this->container->get('config.storage')->write('system.theme', array('default' => 'stark', 'enabled' => array('stark'))); + } + $module_data = $this->container->get('config.storage')->read('system.module'); + if (empty($module_data['enabled'])) { + $this->container->get('config.storage')->write('system.module', array('enabled' => array('system'))); + } + $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 +70,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')); } /** @@ -146,7 +157,10 @@ function testDeleted() { $this->assertTrue(isset($GLOBALS['hook_config_test']['delete'])); // Verify that there is nothing more to import. - $this->assertFalse($this->configImporter->hasUnprocessedChanges()); + foreach (array('delete', 'create', 'update') as $op) { + ffs(array($op => $this->configImporter->getUnprocessedConfiguration($op))); + } + $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); } /** @@ -193,7 +207,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 +262,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/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 3eea9cb..854d80e 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 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 55c27dc..3dfdfb2 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/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 0d80c36..273d877 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -1514,7 +1514,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 3a14c5c..6140e39 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -466,6 +466,8 @@ 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' 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 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 c4f2c85..81f0545 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 a88fc06..e21e3cf 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