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; + } }