Commit 95e6d3a5 authored by webchick's avatar webchick

Issue #2236553 by xjm, dags, Gaelan, alexpott: Config import validation needs...

Issue #2236553 by xjm, dags, Gaelan, alexpott: Config import validation needs to be able to list multiple issues and the messages should be translatable.
parent abc7e15f
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigImportValidateEventSubscriberBase.
*/
namespace Drupal\Core\Config;
use Drupal\Core\StringTranslation\TranslationInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Defines a base event listener implementation for config sync validation.
*/
abstract class ConfigImportValidateEventSubscriberBase implements EventSubscriberInterface {
/**
* Checks that the configuration synchronization 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 \Drupal::translation()->translate($string, $args, $options);
}
}
......@@ -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
* synchronization will not occur. If errors occur during an import then best
* efforts are made to complete the synchronization.
*
* @var array
*/
......@@ -195,7 +199,7 @@ public function __construct(StorageComparerInterface $storage_comparer, EventDis
* @param string $message
* The message to log.
*/
protected function logError($message) {
public function logError($message) {
$this->errors[] = $message;
}
......@@ -658,14 +662,19 @@ protected function getNextConfigurationOperation() {
*
* 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->getErrors())) {
throw new ConfigImporterException('There were errors validating the config synchronization.');
}
else {
$this->validated = TRUE;
}
}
return $this;
}
......
......@@ -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.', array('@config_name' => $name));
$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;
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\config\Form;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\ConfigImporterException;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
......@@ -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{
$sync_steps = $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 ($sync_steps as $sync_step) {
$batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $sync_step));
}
try {
$sync_steps = $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 ($sync_steps as $sync_step) {
$batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $sync_step));
}
batch_set($batch);
batch_set($batch);
}
catch (ConfigImporterException $e) {
// There are validation errors.
drupal_set_message($this->t('The configuration synchronization failed validation.'));
foreach ($config_importer->getErrors() as $message) {
drupal_set_message($message, 'error');
}
}
}
}
......
......@@ -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 synchronization failed validation.');
$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.
......
......@@ -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 synchronization.');
$error_log = $this->configImporter->getErrors();
$expected = array('Site UUID in source storage does not match the target storage.');
$this->assertEqual($expected, $error_log);
}
}
......
......@@ -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;
}
......
......@@ -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 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.
*
* @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.'));
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment