diff --git a/core/core.services.yml b/core/core.services.yml
index c6161ab..ddab635 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -979,6 +979,7 @@ services:
class: Drupal\Core\EventSubscriber\ConfigImportSubscriber
tags:
- { name: event_subscriber }
+ arguments: ['@theme_handler']
config_snapshot_subscriber:
class: Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
tags:
diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php
index 4f28425..81bc211 100644
--- a/core/lib/Drupal/Core/Config/ConfigImporter.php
+++ b/core/lib/Drupal/Core/Config/ConfigImporter.php
@@ -240,11 +240,12 @@ public function getStorageComparer() {
*/
public function reset() {
$this->storageComparer->reset();
+ // Empty all the lists.
foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
$this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
}
- $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
- $this->createExtensionChangelist();
+ $this->extensionChangelist = $this->processedExtensions = $this->getEmptyExtensionsProcessedList();
+
$this->validated = FALSE;
$this->processedSystemTheme = FALSE;
return $this;
@@ -359,6 +360,9 @@ protected function setProcessedExtension($type, $op, $name) {
* Populates the extension change list.
*/
protected function createExtensionChangelist() {
+ // Create an empty changelist.
+ $this->extensionChangelist = $this->getEmptyExtensionsProcessedList();
+
// Read the extensions information to determine changes.
$current_extensions = $this->storageComparer->getTargetStorage()->read('core.extension');
$new_extensions = $this->storageComparer->getSourceStorage()->read('core.extension');
@@ -396,7 +400,7 @@ protected function createExtensionChangelist() {
// 0 1 actions
// @todo Move this sorting functionality to the extension system.
array_multisort(array_values($module_list), SORT_ASC, array_keys($module_list), SORT_DESC, $module_list);
- $uninstall = array_intersect(array_keys($module_list), $uninstall);
+ $this->extensionChangelist['module']['uninstall'] = array_intersect(array_keys($module_list), $uninstall);
// Determine which modules to install.
$install = array_keys(array_diff_key($new_extensions['module'], $current_extensions['module']));
@@ -404,22 +408,11 @@ protected function createExtensionChangelist() {
// (with dependencies installed first, and modules of the same weight sorted
// in alphabetical order).
$module_list = array_reverse($module_list);
- $install = array_intersect(array_keys($module_list), $install);
+ $this->extensionChangelist['module']['install'] = array_intersect(array_keys($module_list), $install);
// Work out what themes to install and to uninstall.
- $theme_install = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
- $theme_uninstall = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
-
- $this->extensionChangelist = array(
- 'module' => array(
- 'uninstall' => $uninstall,
- 'install' => $install,
- ),
- 'theme' => array(
- 'install' => $theme_install,
- 'uninstall' => $theme_uninstall,
- ),
- );
+ $this->extensionChangelist['theme']['install'] = array_keys(array_diff_key($new_extensions['theme'], $current_extensions['theme']));
+ $this->extensionChangelist['theme']['uninstall'] = array_keys(array_diff_key($current_extensions['theme'], $new_extensions['theme']));
}
/**
@@ -434,7 +427,7 @@ protected function createExtensionChangelist() {
* @return array
* An array of extension names.
*/
- protected function getExtensionChangelist($type, $op = NULL) {
+ public function getExtensionChangelist($type, $op = NULL) {
if ($op) {
return $this->extensionChangelist[$type][$op];
}
diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
index f17d5bd..507da95 100644
--- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php
@@ -8,9 +8,11 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Config\Config;
+use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Config\ConfigImporterEvent;
use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
use Drupal\Core\Config\ConfigNameException;
+use Drupal\Core\Extension\ThemeHandlerInterface;
/**
* Config import subscriber for config import events.
@@ -18,6 +20,37 @@
class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
/**
+ * Theme data.
+ *
+ * @var \Drupal\Core\Extension\Extension[]
+ */
+ protected $themeData;
+
+ /**
+ * Module data.
+ *
+ * @var \Drupal\Core\Extension\Extension[]
+ */
+ protected $moduleData;
+
+ /**
+ * The theme handler.
+ *
+ * @var \Drupal\Core\Extension\ThemeHandlerInterface
+ */
+ protected $themeHandler;
+
+ /**
+ * Constructs the ConfigImportSubscriber.
+ *
+ * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
+ * The theme handler.
+ */
+ public function __construct(ThemeHandlerInterface $theme_handler) {
+ $this->themeHandler = $theme_handler;
+ }
+
+ /**
* Validates the configuration to be imported.
*
* @param \Drupal\Core\Config\ConfigImporterEvent $event
@@ -37,6 +70,253 @@ public function onConfigImporterValidate(ConfigImporterEvent $event) {
}
}
}
+ $config_importer = $event->getConfigImporter();
+ if ($config_importer->getStorageComparer()->getSourceStorage()->exists('core.extension')) {
+ $this->validateModules($config_importer);
+ $this->validateThemes($config_importer);
+ $this->validateDependencies($config_importer);
+ }
+ else {
+ $config_importer->logError($this->t('The core.extension configuration does not exist.'));
+ }
+ }
+
+ /**
+ * Validates module installations and uninstallations.
+ *
+ * @param \Drupal\Core\Config\ConfigImporter $config_importer
+ * The configuration importer.
+ */
+ protected function validateModules(ConfigImporter $config_importer) {
+ $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
+ // Get a list of modules with dependency weights as values.
+ $module_data = $this->getModuleData();
+ $nonexistent_modules = array_keys(array_diff_key($core_extension['module'], $module_data));
+ foreach ($nonexistent_modules as $module) {
+ $config_importer->logError($this->t('Unable to install the %module module since it does not exist.', array('%module' => $module)));
+ }
+
+ // Ensure that all modules being installed have their dependencies met.
+ $installs = $config_importer->getExtensionChangelist('module', 'install');
+ foreach ($installs as $module) {
+ $missing_dependencies = [];
+ foreach (array_keys($module_data[$module]->requires) as $required_module) {
+ if (!isset($core_extension['module'][$required_module])) {
+ $missing_dependencies[] = $module_data[$required_module]->info['name'];
+ }
+ }
+ if (!empty($missing_dependencies)) {
+ $module_name = $module_data[$module]->info['name'];
+ $message = $this->formatPlural(count($missing_dependencies),
+ 'Unable to install the %module module since it requires the %required_module module.',
+ 'Unable to install the %module module since it requires the %required_module modules.',
+ array('%module' => $module_name, '%required_module' => implode(', ', $missing_dependencies))
+ );
+ $config_importer->logError($message);
+ }
+ }
+
+ // Ensure that all modules being uninstalled are not required by modules
+ // that will be installed after the import.
+ $uninstalls = $config_importer->getExtensionChangelist('module', 'uninstall');
+ foreach ($uninstalls as $module) {
+ foreach (array_keys($module_data[$module]->required_by) as $dependent_module) {
+ if ($module_data[$dependent_module]->status && !in_array($dependent_module, $uninstalls, TRUE)) {
+ $module_name = $module_data[$module]->info['name'];
+ $dependent_module_name = $module_data[$dependent_module]->info['name'];
+ $config_importer->logError($this->t('Unable to uninstall the %module module since the %dependent_module module is installed.', array('%module' => $module_name, '%dependent_module' => $dependent_module_name)));
+ }
+ }
+ }
+ }
+
+ /**
+ * Validates theme installations and uninstallations.
+ *
+ * @param \Drupal\Core\Config\ConfigImporter $config_importer
+ * The configuration importer.
+ */
+ protected function validateThemes(ConfigImporter $config_importer) {
+ $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
+ // Get all themes including those that are not installed.
+ $theme_data = $this->getThemeData();
+ $installs = $config_importer->getExtensionChangelist('theme', 'install');
+ foreach ($installs as $key => $theme) {
+ if (!isset($theme_data[$theme])) {
+ $config_importer->logError($this->t('Unable to install the %theme theme since it does not exist.', array('%theme' => $theme)));
+ // Remove non existing installs from list so we can validate theme
+ // dependencies later.
+ unset($installs[$key]);
+ }
+ }
+
+ // Ensure that all themes being installed have their dependencies met.
+ foreach ($installs as $theme) {
+ foreach (array_keys($theme_data[$theme]->requires) as $required_theme) {
+ if (!isset($core_extension['theme'][$required_theme])) {
+ $theme_name = $theme_data[$theme]->info['name'];
+ $required_theme_name = $theme_data[$required_theme]->info['name'];
+ $config_importer->logError($this->t('Unable to install the %theme theme since it requires the %required_theme theme.', array('%theme' => $theme_name, '%required_theme' => $required_theme_name)));
+ }
+ }
+ }
+
+ // Ensure that all themes being uninstalled are not required by themes that
+ // will be installed after the import.
+ $uninstalls = $config_importer->getExtensionChangelist('theme', 'uninstall');
+ foreach ($uninstalls as $theme) {
+ foreach (array_keys($theme_data[$theme]->required_by) as $dependent_theme) {
+ if ($theme_data[$dependent_theme]->status && !in_array($dependent_theme, $uninstalls, TRUE)) {
+ $theme_name = $theme_data[$theme]->info['name'];
+ $dependent_theme_name = $theme_data[$dependent_theme]->info['name'];
+ $config_importer->logError($this->t('Unable to uninstall the %theme theme since the %dependent_theme theme is installed.', array('%theme' => $theme_name, '%dependent_theme' => $dependent_theme_name)));
+ }
+ }
+ }
+ }
+
+ /**
+ * Validates configuration being imported does not have unmet dependencies.
+ *
+ * @param \Drupal\Core\Config\ConfigImporter $config_importer
+ * The configuration importer.
+ */
+ protected function validateDependencies(ConfigImporter $config_importer) {
+ $core_extension = $config_importer->getStorageComparer()->getSourceStorage()->read('core.extension');
+ $existing_dependencies = [
+ 'config' => $config_importer->getStorageComparer()->getSourceStorage()->listAll(),
+ 'module' => array_keys($core_extension['module']),
+ 'theme' => array_keys($core_extension['theme']),
+ ];
+
+ $theme_data = $this->getThemeData();
+ $module_data = $this->getModuleData();
+
+ // Validate the dependencies of all the configuration. We have to validate
+ // the entire tree because existing configuration might depend on
+ // configuration that is being deleted.
+ foreach ($config_importer->getStorageComparer()->getSourceStorage()->listAll() as $name) {
+ // Ensure that the config owner is installed. This checks all
+ // configuration including configuration entities.
+ list($owner,) = explode('.', $name, 2);
+ if ($owner !== 'core') {
+ $message = FALSE;
+ if (!isset($core_extension['module'][$owner]) && isset($module_data[$owner])) {
+ $message = $this->t('Configuration %name depends on the %owner module that will not be installed after import.', array(
+ '%name' => $name,
+ '%owner' => $module_data[$owner]->info['name']
+ ));
+ }
+ elseif (!isset($core_extension['theme'][$owner]) && isset($theme_data[$owner])) {
+ $message = $this->t('Configuration %name depends on the %owner theme that will not be installed after import.', array(
+ '%name' => $name,
+ '%owner' => $theme_data[$owner]->info['name']
+ ));
+ }
+ elseif (!isset($core_extension['module'][$owner]) && !isset($core_extension['theme'][$owner])) {
+ $message = $this->t('Configuration %name depends on the %owner extension that will not be installed after import.', array(
+ '%name' => $name,
+ '%owner' => $owner
+ ));
+ }
+
+ if ($message) {
+ $config_importer->logError($message);
+ continue;
+ }
+ }
+
+ $data = $config_importer->getStorageComparer()->getSourceStorage()->read($name);
+ // Configuration entities have dependencies on modules, themes and other
+ // configuration entities that we can validate. Their content dependencies
+ // are not validated since we assume that they are soft dependencies.
+ // Configuration entities can be identified by having dependencies and
+ // UUID keys.
+ if (isset($data['dependencies']) && isset($data['uuid'])) {
+ $dependencies_to_check = array_intersect_key($data['dependencies'], array_flip(['module', 'theme', 'config']));
+ foreach ($dependencies_to_check as $type => $dependencies) {
+ $diffs = array_diff($dependencies, $existing_dependencies[$type]);
+ if (!empty($diffs)) {
+ $message = FALSE;
+ switch ($type) {
+ case 'module':
+ $message = $this->formatPlural(
+ count($diffs),
+ 'Configuration %name depends on the %module module that will not be installed after import.',
+ 'Configuration %name depends on modules (%module) that will not be installed after import.',
+ array('%name' => $name, '%module' => implode(', ', $this->getNames($diffs, $module_data)))
+ );
+ break;
+ case 'theme':
+ $message = $this->formatPlural(
+ count($diffs),
+ 'Configuration %name depends on the %theme theme that will not be installed after import.',
+ 'Configuration %name depends on themes (%theme) that will not be installed after import.',
+ array('%name' => $name, '%theme' => implode(', ', $this->getNames($diffs, $theme_data)))
+ );
+ break;
+ case 'config':
+ $message = $this->formatPlural(
+ count($diffs),
+ 'Configuration %name depends on the %config configuration that will not exist after import.',
+ 'Configuration %name depends on configuration (%config) that will not exist after import.',
+ array('%name' => $name, '%config' => implode(', ', $diffs))
+ );
+ break;
+ }
+
+ if ($message) {
+ $config_importer->logError($message);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets theme data.
+ *
+ * @return \Drupal\Core\Extension\Extension[]
+ */
+ protected function getThemeData() {
+ if (!isset($this->themeData)) {
+ $this->themeData = $this->themeHandler->rebuildThemeData();
+ }
+ return $this->themeData;
+ }
+
+ /**
+ * Gets module data.
+ *
+ * @return \Drupal\Core\Extension\Extension[]
+ */
+ protected function getModuleData() {
+ if (!isset($this->moduleData)) {
+ $this->moduleData = system_rebuild_module_data();
+ }
+ return $this->moduleData;
+ }
+
+ /**
+ * Gets human readable extension names.
+ *
+ * @param array $names
+ * A list of extension machine names.
+ * @param \Drupal\Core\Extension\Extension[] $extension_data
+ * Extension data.
+ *
+ * @return array
+ * A list of human-readable extension names or machine names if not
+ * available.
+ */
+ protected function getNames(array $names, array $extension_data) {
+ return array_map(function ($name) use ($extension_data) {
+ if (isset($extension_data[$name])) {
+ $name = $extension_data[$name]->info['name'];
+ }
+ return $name;
+ }, $names);
}
}
diff --git a/core/modules/config/src/Form/ConfigSync.php b/core/modules/config/src/Form/ConfigSync.php
index 0a91758..e0e5691 100644
--- a/core/modules/config/src/Form/ConfigSync.php
+++ b/core/modules/config/src/Form/ConfigSync.php
@@ -340,7 +340,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
}
catch (ConfigImporterException $e) {
// There are validation errors.
- drupal_set_message($this->t('The configuration synchronization failed validation.'));
+ drupal_set_message($this->t('The configuration cannot be imported because it failed validation for the following reasons:'), 'error');
foreach ($config_importer->getErrors() as $message) {
drupal_set_message($message, 'error');
}
diff --git a/core/modules/config/src/Tests/ConfigImportUITest.php b/core/modules/config/src/Tests/ConfigImportUITest.php
index ba44925..cc36e36 100644
--- a/core/modules/config/src/Tests/ConfigImportUITest.php
+++ b/core/modules/config/src/Tests/ConfigImportUITest.php
@@ -326,7 +326,7 @@ public function testImportValidation() {
$this->drupalPostForm(NULL, array(), t('Import all'));
// Verify that the validation messages appear.
- $this->assertText('The configuration synchronization failed validation.');
+ $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:');
$this->assertText('Config import validate error 1.');
$this->assertText('Config import validate error 2.');
@@ -453,4 +453,39 @@ public function testEntityBundleDelete() {
$this->assertNoText(format_string('core.entity_form_display.node.@type.default', array('@type' => $node_type->id())));
}
+ /**
+ * Tests config importer cannot uninstall extensions which are depended on.
+ *
+ * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+ */
+ public function testExtensionValidation() {
+ \Drupal::service('module_installer')->install(['node']);
+ \Drupal::service('theme_handler')->install(['bartik']);
+ $this->rebuildContainer();
+
+ $staging = $this->container->get('config.storage.staging');
+ $this->copyConfig($this->container->get('config.storage'), $staging);
+ $core = $staging->read('core.extension');
+ // Node depends on text.
+ unset($core['module']['text']);
+ $module_data = system_rebuild_module_data();
+ $this->assertTrue(isset($module_data['node']->requires['text']), 'The Node module depends on the Text module.');
+ // Bartik depends on classy.
+ unset($core['theme']['classy']);
+ $theme_data = \Drupal::service('theme_handler')->rebuildThemeData();
+ $this->assertTrue(isset($theme_data['bartik']->requires['classy']), 'The Bartik theme depends on the Classy theme.');
+ // This module does not exist.
+ $core['module']['does_not_exist'] = 0;
+ // This theme does not exist.
+ $core['theme']['does_not_exist'] = 0;
+ $staging->write('core.extension', $core);
+
+ $this->drupalPostForm('admin/config/development/configuration', array(), t('Import all'));
+ $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:');
+ $this->assertText('Unable to uninstall the Text module since the Node module is installed.');
+ $this->assertText('Unable to uninstall the Classy theme since the Bartik theme is installed.');
+ $this->assertText('Unable to install the does_not_exist module since it does not exist.');
+ $this->assertText('Unable to install the does_not_exist theme since it does not exist.');
+ }
+
}
diff --git a/core/modules/config/src/Tests/ConfigImporterTest.php b/core/modules/config/src/Tests/ConfigImporterTest.php
index ebd6378..6f8c10c 100644
--- a/core/modules/config/src/Tests/ConfigImporterTest.php
+++ b/core/modules/config/src/Tests/ConfigImporterTest.php
@@ -105,7 +105,7 @@ function testSiteUuidValidate() {
$staging->write('system.site', $config_data);
try {
$this->configImporter->reset()->import();
- $this->assertFalse(FALSE, 'ConfigImporterException not thrown, invalid import was not stopped due to mis-matching site UUID.');
+ $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to mis-matching site UUID.');
}
catch (ConfigImporterException $e) {
$this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
@@ -541,4 +541,111 @@ function testIsInstallable() {
$this->assertTrue($this->container->get('config.storage')->exists($config_name));
}
+ /**
+ * Tests dependency validation during configuration import.
+ *
+ * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+ * @see \Drupal\Core\Config\ConfigImporter::createExtensionChangelist()
+ */
+ public function testUnmetDependency() {
+ $storage = $this->container->get('config.storage');
+ $staging = $this->container->get('config.storage.staging');
+
+ // Test an unknown configuration owner.
+ $staging->write('unknown.config', ['test' => 'test']);
+
+ // Make a config entity have unmet dependencies.
+ $config_entity_data = $staging->read('config_test.dynamic.dotted.default');
+ $config_entity_data['dependencies'] = ['module' => ['unknown']];
+ $staging->write('config_test.dynamic.dotted.module', $config_entity_data);
+ $config_entity_data['dependencies'] = ['theme' => ['unknown']];
+ $staging->write('config_test.dynamic.dotted.theme', $config_entity_data);
+ $config_entity_data['dependencies'] = ['config' => ['unknown']];
+ $staging->write('config_test.dynamic.dotted.config', $config_entity_data);
+
+ // Make an active config depend on something that is missing in staging.
+ // The whole configuration needs to be consistent not only the updated one.
+ $config_entity_data['dependencies'] = [];
+ $storage->write('config_test.dynamic.dotted.deleted', $config_entity_data);
+ $config_entity_data['dependencies'] = ['config' => ['config_test.dynamic.dotted.deleted']];
+ $storage->write('config_test.dynamic.dotted.existing', $config_entity_data);
+ $staging->write('config_test.dynamic.dotted.existing', $config_entity_data);
+
+ $extensions = $staging->read('core.extension');
+ // Add a module and a theme that do not exist.
+ $extensions['module']['unknown_module'] = 0;
+ $extensions['theme']['unknown_theme'] = 0;
+ // Add a module and a theme that do depend on uninstalled extensions.
+ $extensions['module']['book'] = 0;
+ $extensions['theme']['bartik'] = 0;
+
+ $staging->write('core.extension', $extensions);
+ try {
+ $this->configImporter->reset()->import();
+ $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to missing dependencies.');
+ }
+ catch (ConfigImporterException $e) {
+ $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
+ $error_log = $this->configImporter->getErrors();
+ $expected = [
+ 'Unable to install the unknown_module module since it does not exist.',
+ 'Unable to install the Book module since it requires the Node, Text, Field, Filter, User, Entity Reference modules.',
+ 'Unable to install the unknown_theme theme since it does not exist.',
+ 'Unable to install the Bartik theme since it requires the Classy theme.',
+ 'Configuration config_test.dynamic.dotted.config depends on the unknown configuration that will not exist after import.',
+ 'Configuration config_test.dynamic.dotted.existing depends on the config_test.dynamic.dotted.deleted configuration that will not exist after import.',
+ 'Configuration config_test.dynamic.dotted.module depends on the unknown module that will not be installed after import.',
+ 'Configuration config_test.dynamic.dotted.theme depends on the unknown theme that will not be installed after import.',
+ 'Configuration unknown.config depends on the unknown extension that will not be installed after import.',
+ ];
+ foreach ($expected as $expected_message) {
+ $this->assertTrue(in_array($expected_message, $error_log), $expected_message);
+ }
+ }
+
+ // Make a config entity have mulitple unmet dependencies.
+ $config_entity_data = $staging->read('config_test.dynamic.dotted.default');
+ $config_entity_data['dependencies'] = ['module' => ['unknown', 'dblog']];
+ $staging->write('config_test.dynamic.dotted.module', $config_entity_data);
+ $config_entity_data['dependencies'] = ['theme' => ['unknown', 'seven']];
+ $staging->write('config_test.dynamic.dotted.theme', $config_entity_data);
+ $config_entity_data['dependencies'] = ['config' => ['unknown', 'unknown2']];
+ $staging->write('config_test.dynamic.dotted.config', $config_entity_data);
+ try {
+ $this->configImporter->reset()->import();
+ $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to missing dependencies.');
+ }
+ catch (ConfigImporterException $e) {
+ $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
+ $error_log = $this->configImporter->getErrors();
+ $expected = [
+ 'Configuration config_test.dynamic.dotted.config depends on configuration (unknown, unknown2) that will not exist after import.',
+ 'Configuration config_test.dynamic.dotted.module depends on modules (unknown, Database Logging) that will not be installed after import.',
+ 'Configuration config_test.dynamic.dotted.theme depends on themes (unknown, Seven) that will not be installed after import.',
+ ];
+ foreach ($expected as $expected_message) {
+ $this->assertTrue(in_array($expected_message, $error_log), $expected_message);
+ }
+ }
+ }
+
+ /**
+ * Tests missing core.extension during configuration import.
+ *
+ * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
+ */
+ public function testMissingCoreExtension() {
+ $staging = $this->container->get('config.storage.staging');
+ $staging->delete('core.extension');
+ try {
+ $this->configImporter->reset()->import();
+ $this->fail('ConfigImporterException not thrown, invalid import was not stopped due to missing dependencies.');
+ }
+ catch (ConfigImporterException $e) {
+ $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
+ $error_log = $this->configImporter->getErrors();
+ $this->assertEqual(['The core.extension configuration does not exist.'], $error_log);
+ }
+ }
+
}
diff --git a/core/modules/field/src/Tests/FieldImportDeleteUninstallUiTest.php b/core/modules/field/src/Tests/FieldImportDeleteUninstallUiTest.php
index 53998e7..de793dd 100644
--- a/core/modules/field/src/Tests/FieldImportDeleteUninstallUiTest.php
+++ b/core/modules/field/src/Tests/FieldImportDeleteUninstallUiTest.php
@@ -23,7 +23,7 @@ class FieldImportDeleteUninstallUiTest extends FieldTestBase {
*
* @var array
*/
- public static $modules = array('entity_test', 'telephone', 'config', 'filter', 'text');
+ public static $modules = array('entity_test', 'telephone', 'config', 'filter', 'datetime');
protected function setUp() {
parent::setUp();
@@ -48,14 +48,14 @@ public function testImportDeleteUninstall() {
))->save();
// Create a text field.
- $text_field_storage = entity_create('field_storage_config', array(
- 'field_name' => 'field_text',
+ $date_field_storage = entity_create('field_storage_config', array(
+ 'field_name' => 'field_date',
'entity_type' => 'entity_test',
- 'type' => 'text',
+ 'type' => 'datetime',
));
- $text_field_storage->save();
+ $date_field_storage->save();
entity_create('field_config', array(
- 'field_storage' => $text_field_storage,
+ 'field_storage' => $date_field_storage,
'bundle' => 'entity_test',
))->save();
@@ -63,14 +63,14 @@ public function testImportDeleteUninstall() {
$entity = entity_create('entity_test');
$value = '+0123456789';
$entity->field_tel = $value;
- $entity->field_text = $this->randomMachineName(20);
+ $entity->field_date = time();
$entity->name->value = $this->randomMachineName();
$entity->save();
// Delete the text field before exporting configuration so that we can test
// that deleted fields that are provided by modules that will be uninstalled
// are also purged and that the UI message includes such fields.
- $text_field_storage->delete();
+ $date_field_storage->delete();
// Verify entity has been created properly.
$id = $entity->id();
@@ -95,22 +95,13 @@ public function testImportDeleteUninstall() {
// synchronization is correct.
$this->assertText('This synchronization will delete data from the field entity_test.field_tel.');
- // Stage an uninstall of the text module to test the message for multiple
- // fields.
- unset($core_extension['module']['text']);
+ // Stage an uninstall of the datetime module to test the message for
+ // multiple fields.
+ unset($core_extension['module']['datetime']);
$staging->write('core.extension', $core_extension);
+
$this->drupalGet('admin/config/development/configuration');
- $this->assertText('This synchronization will delete data from the fields: entity_test.field_tel, entity_test.field_text.');
- // Delete all the text fields in staging, entity_test_install() adds quite
- // a few.
- foreach (\Drupal::entityManager()->getFieldMap() as $entity_type => $fields) {
- foreach ($fields as $field_name => $info) {
- if ($info['type'] == 'text') {
- $staging->delete("field.storage.$entity_type.$field_name");
- $staging->delete("field.field.$entity_type.$entity_type.$field_name");
- }
- }
- }
+ $this->assertText('This synchronization will delete data from the fields: entity_test.field_tel, entity_test.field_date.');
// This will purge all the data, delete the field and uninstall the
// Telephone and Text modules.
diff --git a/core/modules/system/src/SystemConfigSubscriber.php b/core/modules/system/src/SystemConfigSubscriber.php
index 765b824..204d685 100644
--- a/core/modules/system/src/SystemConfigSubscriber.php
+++ b/core/modules/system/src/SystemConfigSubscriber.php
@@ -7,32 +7,58 @@
namespace Drupal\system;
+use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\ConfigImporterEvent;
-use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
-use Drupal\Core\Config\StorageDispatcher;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* System Config subscriber.
*/
-class SystemConfigSubscriber extends ConfigImportValidateEventSubscriberBase {
+class SystemConfigSubscriber implements EventSubscriberInterface {
+ use StringTranslationTrait;
/**
* Checks that the configuration synchronization is valid.
*
- * This event listener implements two checks:
- * - prevents deleting all configuration.
- * - checks that the system.site:uuid's in the source and target match.
+ * This event listener prevents deleting all configuration. If there is
+ * nothing to import then event propagation is stopped because there is no
+ * config import to validate.
*
- * @param ConfigImporterEvent $event
+ * @param \Drupal\Core\Config\ConfigImporterEvent $event
* The config import event.
*/
- public function onConfigImporterValidate(ConfigImporterEvent $event) {
+ public function onConfigImporterValidateNotEmpty(ConfigImporterEvent $event) {
$importList = $event->getConfigImporter()->getStorageComparer()->getSourceStorage()->listAll();
if (empty($importList)) {
$event->getConfigImporter()->logError($this->t('This import is empty and if applied would delete all of your configuration, so has been rejected.'));
+ $event->stopPropagation();
}
+ }
+
+ /**
+ * Checks that the configuration synchronization is valid.
+ *
+ * This event listener checks that the system.site:uuid's in the source and
+ * target match.
+ *
+ * @param ConfigImporterEvent $event
+ * The config import event.
+ */
+ public function onConfigImporterValidateSiteUUID(ConfigImporterEvent $event) {
if (!$event->getConfigImporter()->getStorageComparer()->validateSiteUuid()) {
$event->getConfigImporter()->logError($this->t('Site UUID in source storage does not match the target storage.'));
}
}
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ // The empty check has a high priority so that is can stop propagation if
+ // there is no configuration to import.
+ $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidateNotEmpty', 512);
+ $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidateSiteUUID', 256);
+ return $events;
+ }
}