diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 2d0de14..837abc0 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -511,8 +511,6 @@ public function doSyncStep($sync_step, &$context) { * If the configuration is already importing. */ public function initialize() { - $this->createExtensionChangelist(); - // Ensure that the changes have been validated. $this->validate(); @@ -710,8 +708,10 @@ protected function getNextConfigurationOperation() { * @throws \Drupal\Core\Config\ConfigImporterException * Exception thrown if the validate event logged any errors. */ - protected function validate() { + public function validate() { if (!$this->validated) { + // Create the list of installs and uninstalls. + $this->createExtensionChangelist(); // Validate renames. foreach ($this->getUnprocessedConfiguration('rename') as $name) { $names = $this->storageComparer->extractRenameNames($name); diff --git a/core/modules/config/src/Form/ConfigSingleImportForm.php b/core/modules/config/src/Form/ConfigSingleImportForm.php index 15f173d..f87eb6a 100644 --- a/core/modules/config/src/Form/ConfigSingleImportForm.php +++ b/core/modules/config/src/Form/ConfigSingleImportForm.php @@ -8,12 +8,24 @@ namespace Drupal\config\Form; use Drupal\Component\Serialization\Yaml; +use Drupal\config\StorageOverrideWrapper; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\ConfigImporterException; +use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Config\StorageComparer; use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ModuleInstallerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Form\ConfirmFormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Provides a form for importing a single configuration file. @@ -35,6 +47,62 @@ class ConfigSingleImportForm extends ConfirmFormBase { protected $configStorage; /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * Event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The configuration manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface; + */ + protected $configManager; + + /** + * The database lock object. + * + * @var \Drupal\Core\Lock\LockBackendInterface + */ + protected $lock; + + /** + * The typed config manager. + * + * @var \Drupal\Core\Config\TypedConfigManagerInterface + */ + protected $typedConfigManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** + * The module installer. + * + * @var \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected $moduleInstaller; + + /** * If the config exists, this is that object. Otherwise, FALSE. * * @var \Drupal\Core\Config\Config|\Drupal\Core\Config\Entity\ConfigEntityInterface|bool @@ -51,14 +119,42 @@ class ConfigSingleImportForm extends ConfirmFormBase { /** * Constructs a new ConfigSingleImportForm. * + */ + /** * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. * @param \Drupal\Core\Config\StorageInterface $config_storage * The config storage. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher used to notify subscribers of config import events. + * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager + * The configuration manager. + * @param \Drupal\Core\Lock\LockBackendInterface $lock + * The lock backend to ensure multiple imports do not occur at the same time. + * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config + * The typed configuration manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler + * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer + * The module installer. + * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler + * The theme handler */ - public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage) { + public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage, RendererInterface $renderer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler) { $this->entityManager = $entity_manager; $this->configStorage = $config_storage; + $this->renderer = $renderer; + + // Services necessary for \Drupal\Core\Config\ConfigImporter. + $this->eventDispatcher = $event_dispatcher; + $this->configManager = $config_manager; + $this->lock = $lock; + $this->typedConfigManager = $typed_config; + $this->moduleHandler = $module_handler; + $this->moduleInstaller = $module_installer; + $this->themeHandler = $theme_handler; } /** @@ -67,7 +163,15 @@ public function __construct(EntityManagerInterface $entity_manager, StorageInter public static function create(ContainerInterface $container) { return new static( $container->get('entity.manager'), - $container->get('config.storage') + $container->get('config.storage'), + $container->get('renderer'), + $container->get('event_dispatcher'), + $container->get('config.manager'), + $container->get('lock.persistent'), + $container->get('config.typed'), + $container->get('module_handler'), + $container->get('module_installer'), + $container->get('theme_handler') ); } @@ -204,6 +308,8 @@ public function validateForm(array &$form, FormStateInterface $form_state) { $form_state->setErrorByName('import', $this->t('Missing ID key "@id_key" for this @entity_type import.', array('@id_key' => $id_key, '@entity_type' => $definition->getLabel()))); return; } + + $config_name = $definition->getConfigPrefix() . '.' . $data[$id_key]; // If there is an existing entity, ensure matching ID and UUID. if ($entity = $entity_storage->load($data[$id_key])) { $this->configExists = $entity; @@ -222,10 +328,53 @@ public function validateForm(array &$form, FormStateInterface $form_state) { } } else { - $config = $this->config($form_state->getValue('config_name')); + $config_name = $form_state->getValue('config_name'); + $config = $this->config($config_name); $this->configExists = !$config->isNew() ? $config : FALSE; } + // Use ConfigImporter validation. + if (!$form_state->getErrors()) { + $source_storage = new StorageOverrideWrapper($this->configStorage); + $source_storage->override($config_name, $data); + $storage_comparer = new StorageComparer( + $source_storage, + $this->configStorage, + $this->configManager + ); + + if (!$storage_comparer->createChangelist()->hasChanges()) { + $form_state->setErrorByName('import', $this->t('There are no changes to import.')); + } + else { + $config_importer = new ConfigImporter( + $storage_comparer, + $this->eventDispatcher, + $this->configManager, + $this->lock, + $this->typedConfigManager, + $this->moduleHandler, + $this->moduleInstaller, + $this->themeHandler, + $this->getStringTranslation() + ); + + try { + $config_importer->validate(); + $form_state->set('config_importer', $config_importer); + } + catch (ConfigImporterException $e) { + // There are validation errors. + $item_list = array( + '#theme' => 'item_list', + '#items' => $config_importer->getErrors(), + '#title' => $this->t('The configuration cannot be imported because it failed validation for the following reasons:'), + ); + $form_state->setErrorByName('import', $this->renderer->render($item_list)); + } + } + } + // Store the decoded version of the submitted import. $form_state->setValueForElement($form['import'], $data); } @@ -241,28 +390,89 @@ public function submitForm(array &$form, FormStateInterface $form_state) { return; } - // If a simple configuration file was added, set the data and save. - if ($this->data['config_type'] === 'system.simple') { - $this->configFactory()->getEditable($this->data['config_name'])->setData($this->data['import'])->save(); - drupal_set_message($this->t('The %name configuration was imported.', array('%name' => $this->data['config_name']))); + /** @var \Drupal\Core\Config\ConfigImporter $config_importer */ + $config_importer = $form_state->get('config_importer'); + if ($config_importer->alreadyImporting()) { + drupal_set_message($this->t('Another request may be importing configuration already.'), 'error'); } - // For a config entity, create an entity and save it. - else { + else{ try { - $entity_storage = $this->entityManager->getStorage($this->data['config_type']); - if ($this->configExists) { - $entity = $entity_storage->updateFromStorageRecord($this->configExists, $this->data['import']); + $sync_steps = $config_importer->initialize(); + $batch = array( + 'operations' => array(), + 'finished' => array(get_class($this), 'finishBatch'), + 'title' => t('Importing configuration'), + 'init_message' => t('Starting configuration import.'), + 'progress_message' => t('Completed @current step of @total.'), + 'error_message' => t('Configuration import has encountered an error.'), + ); + foreach ($sync_steps as $sync_step) { + $batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $sync_step)); } - else { - $entity = $entity_storage->createFromStorageRecord($this->data['import']); + + batch_set($batch); + } + catch (ConfigImporterException $e) { + // There are validation errors. + drupal_set_message($this->t('The configuration import failed for the following reasons:'), 'error'); + foreach ($config_importer->getErrors() as $message) { + drupal_set_message($message, 'error'); } - $entity->save(); - drupal_set_message($this->t('The @entity_type %label was imported.', array('@entity_type' => $entity->getEntityTypeId(), '%label' => $entity->label()))); } - catch (\Exception $e) { - drupal_set_message($e->getMessage(), 'error'); + } + } + + /** + * Processes the config import batch and persists the importer. + * + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * The batch config importer object to persist. + * @param string $sync_step + * The synchronization step to do. + * @param $context + * The batch context. + */ + public static function processBatch(ConfigImporter $config_importer, $sync_step, &$context) { + if (!isset($context['sandbox']['config_importer'])) { + $context['sandbox']['config_importer'] = $config_importer; + } + + $config_importer = $context['sandbox']['config_importer']; + $config_importer->doSyncStep($sync_step, $context); + if ($errors = $config_importer->getErrors()) { + if (!isset($context['results']['errors'])) { + $context['results']['errors'] = array(); + } + $context['results']['errors'] += $errors; + } + } + + /** + * Finish batch. + * + * This function is a static function to avoid serializing the ConfigSync + * object unnecessarily. + */ + public static function finishBatch($success, $results, $operations) { + if ($success) { + if (!empty($results['errors'])) { + foreach ($results['errors'] as $error) { + drupal_set_message($error, 'error'); + \Drupal::logger('config_sync')->error($error); + } + drupal_set_message(\Drupal::translation()->translate('The configuration was imported with errors.'), 'warning'); + } + else { + drupal_set_message(\Drupal::translation()->translate('The configuration was imported successfully.')); } } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = \Drupal::translation()->translate('An error occurred while processing %error_operation with arguments: @arguments', array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE))); + drupal_set_message($message, 'error'); + } } } diff --git a/core/modules/config/src/StorageOverrideWrapper.php b/core/modules/config/src/StorageOverrideWrapper.php new file mode 100644 index 0000000..c4fc49b --- /dev/null +++ b/core/modules/config/src/StorageOverrideWrapper.php @@ -0,0 +1,187 @@ +storage = $storage; + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function exists($name) { + return isset($this->overrideData[$this->collection][$name]) || $this->storage->exists($name); + } + + /** + * {@inheritdoc} + */ + public function read($name) { + if (isset($this->overrideData[$this->collection][$name])) { + return $this->overrideData[$this->collection][$name]; + } + return $this->storage->read($name); + } + + /** + * {@inheritdoc} + */ + public function readMultiple(array $names) { + $data = $this->storage->readMultiple(($names)); + foreach ($names as $name) { + if (isset($this->overrideData[$this->collection][$name])) { + $data[$name] = $this->overrideData[$this->collection][$name]; + } + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function write($name, array $data) { + if (isset($this->overrideData[$this->collection][$name])) { + unset($this->overrideData[$this->collection][$name]); + } + return $this->storage->write($name, $data); + } + + /** + * {@inheritdoc} + */ + public function delete($name) { + if (isset($this->overrideData[$this->collection][$name])) { + unset($this->overrideData[$this->collection][$name]); + } + return $this->storage->delete($name); + } + + /** + * {@inheritdoc} + */ + public function rename($name, $new_name) { + if (isset($this->overrideData[$this->collection][$name])) { + $this->overrideData[$this->collection][$new_name] = $this->overrideData[$this->collection][$name]; + unset($this->overrideData[$this->collection][$name]); + } + return $this->rename($name, $new_name); + } + + /** + * {@inheritdoc} + */ + public function encode($data) { + return $this->storage->encode($data); + } + + /** + * {@inheritdoc} + */ + public function decode($raw) { + return $this->storage->decode($raw); + } + + /** + * {@inheritdoc} + */ + public function listAll($prefix = '') { + $names = $this->storage->listAll($prefix); + $additional_names = []; + if ($prefix === '') { + $additional_names = array_keys($this->overrideData[$this->collection]); + } + else { + foreach (array_keys($this->overrideData[$this->collection]) as $name) { + if (strpos($name, $prefix) === 0) { + $additional_names[] = $name; + } + } + } + if (!empty($additional_names)) { + $names = array_unique(array_merge($names, $additional_names)); + } + return $names; + } + + /** + * {@inheritdoc} + */ + public function deleteAll($prefix = '') { + if ($prefix === '') { + $this->overrideData[$this->collection] = []; + } + else { + foreach (array_keys($this->overrideData[$this->collection]) as $name) { + if (strpos($name, $prefix) === 0) { + unset($this->overrideData[$this->collection][$name]); + } + } + } + return $this->storage->deleteAll($prefix); + } + + /** + * {@inheritdoc} + */ + public function createCollection($collection) { + $this->collection = $collection; + return $this->storage->createCollection($collection); + } + + /** + * {@inheritdoc} + */ + public function getAllCollectionNames() { + return $this->storage->getAllCollectionNames(); + } + + /** + * {@inheritdoc} + */ + public function getCollectionName() { + return $this->collection; + } + + public function override($name, array $data) { + $this->overrideData[$this->collection][$name] = $data; + return $this; + } +} diff --git a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php index 7b3336d..817bf22 100644 --- a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php +++ b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php @@ -66,7 +66,7 @@ public function testImport() { $this->assertIdentical($entity->label(), 'First'); $this->assertIdentical($entity->id(), 'first'); $this->assertTrue($entity->status()); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); // Attempt an import with an existing ID but missing UUID. $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); @@ -82,8 +82,7 @@ public function testImport() { $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); $this->assertRaw(t('Are you sure you want to create a new %name @type?', array('%name' => 'custom_id', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); - $entity = $storage->load('custom_id'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); // Perform an import with a unique ID and UUID. $import = <<assertRaw(t('Are you sure you want to create a new %name @type?', array('%name' => 'second', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); $entity = $storage->load('second'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); $this->assertIdentical($entity->label(), 'Second'); $this->assertIdentical($entity->id(), 'second'); $this->assertFalse($entity->status()); @@ -126,8 +125,27 @@ public function testImport() { $this->assertRaw(t('Are you sure you want to update the %name @type?', array('%name' => 'second', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); $entity = $storage->load('second'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); $this->assertIdentical($entity->label(), 'Second updated'); + + // Try to perform an update which adds missing dependencies. + $import = << 'config_test', + 'import' => $import, + ); + $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); + $this->assertRaw(t('Configuration %name depends on the %owner module that will not be installed after import.', ['%name' => 'config_test.dynamic.second', '%owner' => 'does_not_exist'])); } /** @@ -150,6 +168,20 @@ public function testImportSimpleConfiguration() { $this->drupalPostForm(NULL, array(), t('Confirm')); $this->drupalGet(''); $this->assertText('Test simple import'); + + // Ensure that ConfigImporter validation is running when importing simple + // configuration. + $config_data = $this->config('core.extension')->get(); + // Simulate uninstalling the Config module. + unset($config_data['module']['config']); + $edit = array( + 'config_type' => 'system.simple', + 'config_name' => 'core.extension', + 'import' => Yaml::encode($config_data), + ); + $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); + $this->assertText(t('Can not uninstall the Configuration module as part of a configuration synchronization through the user interface.')); + } /**