diff --git a/core/core.services.yml b/core/core.services.yml index f3c6cd9..7179d68 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -557,6 +557,7 @@ services: arguments: ['@module_handler'] config_import_subscriber: class: Drupal\Core\EventSubscriber\ConfigImportSubscriber + arguments: ['@string_translation'] tags: - { name: event_subscriber } config_snapshot_subscriber: diff --git a/core/lib/Drupal/Core/Config/ConfigImportValidateEventSubscriberBase.php b/core/lib/Drupal/Core/Config/ConfigImportValidateEventSubscriberBase.php new file mode 100644 index 0000000..61cf26a --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigImportValidateEventSubscriberBase.php @@ -0,0 +1,75 @@ +translationManager = $translation_manager; + } + + /** + * Checks that the configuration synchronisation is valid. + * + * @param ConfigImporterEvent $event + * The config import event. + */ + abstract public function onConfigImporterValidate(ConfigImporterEvent $event); + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidate', 20); + return $events; + } + + /** + * Translates a string to the current language or to a given language. + * + * @param string $string + * A string containing the English string to translate. + * @param array $args + * An associative array of replacements to make after translation. Based + * on the first character of the key, the value is escaped and/or themed. + * See \Drupal\Component\Utility\String::format() for details. + * @param array $options + * An associative array of additional options, with the following elements: + * - 'langcode': The language code to translate to a language other than + * what is used to display the page. + * - 'context': The context the source string belongs to. + * + * @return string + * The translated string. + * + * @see \Drupal\Core\StringTranslation\TranslationInterface::translate() + */ + protected function t($string, array $args = array(), array $options = array()) { + return $this->translationManager->translate($string, $args, $options); + } +} diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 9ddbfb0..ba8c10f 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -135,7 +135,11 @@ class ConfigImporter extends DependencySerialization { protected $processedSystemTheme = FALSE; /** - * List of errors that were logged during a config import. + * A log of any errors encountered. + * + * If errors are logged during the validation event the configuration + * synchronisation will not occur. If errors occur during an import then best + * efforts are made to complete the synchronisation. * * @var array */ @@ -473,19 +477,33 @@ public function import() { * * Events should throw a \Drupal\Core\Config\ConfigImporterException to * prevent an import from occurring. + * + * @throws \Drupal\Core\Config\ConfigImporterException + * Exception thrown if the validate event logged any errors. */ public function validate() { if (!$this->validated) { - 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)); - $this->validated = TRUE; + if (count($this->getErrorLog())) { + throw new ConfigImporterException('There were errors validating the config synchronisation.'); + } + else { + $this->validated = TRUE; + } } return $this; } /** + * Gets any logged errors. + * + * @return array + */ + public function getErrorLog() { + return $this->errorLog; + } + + /** * Processes a configuration change. * * @param string $op diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php index 72e2232..5e5e5bd 100644 --- a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php @@ -8,15 +8,14 @@ namespace Drupal\Core\EventSubscriber; use Drupal\Core\Config\Config; -use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigImporterEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; - +use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase; +use Drupal\Core\Config\ConfigNameException; /** * Config import subscriber for config import events. */ -class ConfigImportSubscriber implements EventSubscriberInterface { +class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase { /** * Validates the configuration to be imported. @@ -29,20 +28,15 @@ class ConfigImportSubscriber implements EventSubscriberInterface { public function onConfigImporterValidate(ConfigImporterEvent $event) { foreach (array('delete', 'create', 'update') as $op) { foreach ($event->getConfigImporter()->getUnprocessedConfiguration($op) as $name) { - Config::validateName($name); + try { + Config::validateName($name); + } + catch (ConfigNameException $e) { + $message = $this->t('The config name @config_name is invalid due to: @reason.', array('@config_name' => $name, '@reason' => $e->getMessage())); + $event->getConfigImporter()->logError($message); + } } } } - /** - * Registers the methods in this class that should be listeners. - * - * @return array - * An array of event listener definitions. - */ - static function getSubscribedEvents() { - $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidate', 40); - return $events; - } - } diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index 907340c..ba65c59 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -8,6 +8,7 @@ namespace Drupal\config\Form; use Drupal\Component\Uuid\UuidInterface; +use Drupal\Core\Config\ConfigImporterException; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ThemeHandlerInterface; @@ -257,21 +258,30 @@ public function submitForm(array &$form, array &$form_state) { drupal_set_message($this->t('Another request may be synchronizing configuration already.')); } else{ - $operations = $config_importer->initialize(); - $batch = array( - 'operations' => array(), - 'finished' => array(get_class($this), 'finishBatch'), - 'title' => t('Synchronizing configuration'), - 'init_message' => t('Starting configuration synchronization.'), - 'progress_message' => t('Completed @current step of @total.'), - 'error_message' => t('Configuration synchronization has encountered an error.'), - 'file' => drupal_get_path('module', 'config') . '/config.admin.inc', - ); - foreach ($operations as $operation) { - $batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $operation)); - } + try { + $operations = $config_importer->initialize(); + $batch = array( + 'operations' => array(), + 'finished' => array(get_class($this), 'finishBatch'), + 'title' => t('Synchronizing configuration'), + 'init_message' => t('Starting configuration synchronization.'), + 'progress_message' => t('Completed @current step of @total.'), + 'error_message' => t('Configuration synchronization has encountered an error.'), + 'file' => drupal_get_path('module', 'config') . '/config.admin.inc', + ); + foreach ($operations as $operation) { + $batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $operation)); + } - batch_set($batch); + batch_set($batch); + } + catch (ConfigImporterException $e) { + // There are validation errors. + drupal_set_message($this->t('The configuration synchronsaiotn failed validaton for the reason(s) listed below.')); + foreach ($config_importer->getErrorLog() as $message) { + drupal_set_message($message, 'error'); + } + } } } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php index 94f35fb..dac73c5 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php @@ -306,6 +306,31 @@ function testImportDiff() { $this->drupalGet('admin/config/development/configuration/sync/diff/' . $config_name); } + /** + * Tests that mutliple validation errors are listed on the page. + */ + public function testImportValidation() { + // Set state value so that + // \Drupal\config_import_test\EventSubscriber::onConfigImportValidate() logs + // validation errors. + \Drupal::state()->set('config_import_test.config_import_validate_fail', TRUE); + // Ensure there is something to import. + $new_site_name = 'Config import test ' . $this->randomString(); + $this->prepareSiteNameUpdate($new_site_name); + + $this->drupalGet('admin/config/development/configuration'); + $this->assertNoText(t('There are no configuration changes.')); + $this->drupalPostForm(NULL, array(), t('Import all')); + + // Verify that the validation messages appear. + $this->assertText('The configuration synchronsaiotn failed validaton for the reason(s) listed below.'); + $this->assertText('Config import validate error 1.'); + $this->assertText('Config import validate error 2.'); + + // Verify site name has not changed. + $this->assertNotEqual($new_site_name, \Drupal::config('system.site')->get('name')); + } + function prepareSiteNameUpdate($new_site_name) { $staging = $this->container->get('config.storage.staging'); // Create updated configuration object. diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php index 7597659..c726859 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -114,7 +114,10 @@ function testSiteUuidValidate() { $this->assertFalse(FALSE, 'ConfigImporterException not thrown, invalid import was not stopped due to mis-matching site UUID.'); } catch (ConfigImporterException $e) { - $this->assertEqual($e->getMessage(), 'Site UUID in source storage does not match the target storage.'); + $this->assertEqual($e->getMessage(), 'There were errors validating the config synchronisation.'); + $error_log = $this->configImporter->getErrorLog(); + $expected = array('Site UUID in source storage does not match the target storage.'); + $this->assertEqual($expected, $error_log); } } diff --git a/core/modules/config/tests/config_import_test/lib/Drupal/config_import_test/EventSubscriber.php b/core/modules/config/tests/config_import_test/lib/Drupal/config_import_test/EventSubscriber.php index 41ff791..676be63 100644 --- a/core/modules/config/tests/config_import_test/lib/Drupal/config_import_test/EventSubscriber.php +++ b/core/modules/config/tests/config_import_test/lib/Drupal/config_import_test/EventSubscriber.php @@ -44,7 +44,11 @@ public function __construct(StateInterface $state) { * @throws \Drupal\Core\Config\ConfigNameException */ public function onConfigImporterValidate(ConfigImporterEvent $event) { - + if ($this->state->get('config_import_test.config_import_validate_fail', FALSE)) { + // Log more than one error to test multiple validation errors. + $event->getConfigImporter()->logError('Config import validate error 1.'); + $event->getConfigImporter()->logError('Config import validate error 2.'); + } } /** @@ -101,6 +105,7 @@ public function onConfigDelete(ConfigCrudEvent $event) { static function getSubscribedEvents() { $events[ConfigEvents::SAVE][] = array('onConfigSave', 40); $events[ConfigEvents::DELETE][] = array('onConfigDelete', 40); + $events[ConfigEvents::IMPORT_VALIDATE] = array('onConfigImporterValidate'); return $events; } diff --git a/core/modules/system/lib/Drupal/system/SystemConfigSubscriber.php b/core/modules/system/lib/Drupal/system/SystemConfigSubscriber.php index e582251..44d7de8 100644 --- a/core/modules/system/lib/Drupal/system/SystemConfigSubscriber.php +++ b/core/modules/system/lib/Drupal/system/SystemConfigSubscriber.php @@ -7,37 +7,32 @@ namespace Drupal\system; -use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigImporterEvent; -use Drupal\Core\Config\ConfigImporterException; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase; +use Drupal\Core\Config\StorageDispatcher; /** * System Config subscriber. */ -class SystemConfigSubscriber implements EventSubscriberInterface { +class SystemConfigSubscriber extends ConfigImportValidateEventSubscriberBase { /** - * {@inheritdoc} - */ - static function getSubscribedEvents() { - $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidate', 20); - return $events; - } - - /** - * Checks that the import source storage is not empty. + * Checks that the configuration synchronisation 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. * * @param ConfigImporterEvent $event * The config import event. - * - * @throws \Drupal\Core\Config\ConfigImporterException - * Exception thrown if the source storage is empty. */ public function onConfigImporterValidate(ConfigImporterEvent $event) { $importList = $event->getConfigImporter()->getStorageComparer()->getSourceStorage()->listAll(); if (empty($importList)) { - throw new ConfigImporterException('This import is empty and if applied would delete all of your configuration, so has been rejected.'); + $event->getConfigImporter()->logError($this->t('This import is empty and if applied would delete all of your configuration, so has been rejected.')); + } + if (!$event->getConfigImporter()->getStorageComparer()->validateSiteUuid()) { + $event->getConfigImporter()->logError($this->t('Site UUID in source storage does not match the target storage.')); } } } diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 550cd28..b995551 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -2440,6 +2440,36 @@ function hook_system_themes_page_alter(&$theme_groups) { } /** + * Alters outbound URLs. + * + * @param $path + * The outbound path to alter, not adjusted for path aliases yet. It won't be + * adjusted for path aliases until all modules are finished altering it. This + * may have been altered by other modules before this one. + * @param $options + * A set of URL options for the URL so elements such as a fragment or a query + * string can be added to the URL. + * @param $original_path + * The original path, before being altered by any modules. + * + * @see url() + */ +function hook_url_outbound_alter(&$path, &$options, $original_path) { + // Use an external RSS feed rather than the Drupal one. + if ($path == 'rss.xml') { + $path = 'http://example.com/rss.xml'; + $options['external'] = TRUE; + } + + // Instead of pointing to user/[uid]/edit, point to user/me/edit. + if (preg_match('|^user/([0-9]*)/edit(/.*)?|', $path, $matches)) { + if (\Drupal::currentUser()->id() == $matches[1]) { + $path = 'user/me/edit' . $matches[2]; + } + } +} + +/** * Provide replacement values for placeholder tokens. * * This hook is invoked when someone calls diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index 3c795f8..543bc5a 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -26,6 +26,7 @@ services: - { name: theme_negotiator, priority: 1000 } system.config_subscriber: class: Drupal\system\SystemConfigSubscriber + arguments: ['@string_translation'] tags: - { name: event_subscriber } system.automatic_cron: