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