diff --git a/core/modules/config/config.links.task.yml b/core/modules/config/config.links.task.yml index 5a894f0..9e9383b 100644 --- a/core/modules/config/config.links.task.yml +++ b/core/modules/config/config.links.task.yml @@ -3,32 +3,32 @@ config.sync: base_route: config.sync title: 'Synchronize' -config.full: +config.import: route_name: config.import_full - title: 'Full Import/Export' + title: 'Import' base_route: config.sync -config.single: - route_name: config.import_single - title: 'Single Import/Export' +config.export: + route_name: config.export_full + title: 'Export' base_route: config.sync config.export_full: route_name: config.export_full - title: Export - parent_id: config.full + title: Full + parent_id: config.export config.import_full: route_name: config.import_full - title: Import - parent_id: config.full + title: Full + parent_id: config.import config.export_single: route_name: config.export_single - title: Export - parent_id: config.single + title: Single + parent_id: config.export config.import_single: route_name: config.import_single - title: Import - parent_id: config.single + title: Single + parent_id: config.import diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml index 8eebac3..8c806e8 100644 --- a/core/modules/config/config.routing.yml +++ b/core/modules/config/config.routing.yml @@ -6,6 +6,14 @@ config.sync: requirements: _permission: 'synchronize configuration' +config.sync_confirm: + path: '/admin/config/development/configuration/sync/confirm' + defaults: + _form: 'Drupal\config\Form\ConfigSyncConfirmForm' + _title: 'Confirm configuration synchronization' + requirements: + _permission: 'synchronize configuration' + config.diff: path: '/admin/config/development/configuration/sync/diff/{source_name}/{target_name}' defaults: diff --git a/core/modules/config/src/Form/ConfigSync.php b/core/modules/config/src/Form/ConfigSync.php index 7f240e3..7bb098e 100644 --- a/core/modules/config/src/Form/ConfigSync.php +++ b/core/modules/config/src/Form/ConfigSync.php @@ -7,23 +7,15 @@ namespace Drupal\config\Form; -use Drupal\Component\Uuid\UuidInterface; -use Drupal\Core\Config\ConfigImporterException; use Drupal\Core\Config\ConfigImporter; -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\Config\ConfigManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Config\StorageInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Config\StorageComparer; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Url; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -32,13 +24,6 @@ class ConfigSync extends FormBase { /** - * The database lock object. - * - * @var \Drupal\Core\Lock\LockBackendInterface - */ - protected $lock; - - /** * The staging configuration object. * * @var \Drupal\Core\Config\StorageInterface @@ -60,13 +45,6 @@ class ConfigSync extends FormBase { protected $snapshotStorage; /** - * Event dispatcher. - * - * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface - */ - protected $eventDispatcher; - - /** * The configuration manager. * * @var \Drupal\Core\Config\ConfigManagerInterface; @@ -74,39 +52,18 @@ class ConfigSync extends FormBase { protected $configManager; /** - * 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. + * The renderer. * - * @var \Drupal\Core\Extension\ModuleInstallerInterface + * @var \Drupal\Core\Render\RendererInterface */ - protected $moduleInstaller; + protected $renderer; /** - * The renderer. + * The key value storer. * - * @var \Drupal\Core\Render\RendererInterface + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; */ - protected $renderer; + protected $keyValueExpirable; /** * Constructs the object. @@ -117,35 +74,20 @@ class ConfigSync extends FormBase { * The target storage. * @param \Drupal\Core\Config\StorageInterface $snapshot_storage * The snapshot storage. - * @param \Drupal\Core\Lock\LockBackendInterface $lock - * The lock object. - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher - * Event dispatcher. * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager * Configuration manager. - * @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. - * @param \Drupal\Core\Render\RendererInterface + * @param \Drupal\Core\Render\RendererInterface $renderer * The renderer. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value_expirable + * The expirable key/value store. */ - public function __construct(StorageInterface $staging_storage, StorageInterface $active_storage, StorageInterface $snapshot_storage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler, RendererInterface $renderer) { + public function __construct(StorageInterface $staging_storage, StorageInterface $active_storage, StorageInterface $snapshot_storage, ConfigManagerInterface $config_manager, RendererInterface $renderer, KeyValueStoreExpirableInterface $key_value_expirable) { $this->stagingStorage = $staging_storage; $this->activeStorage = $active_storage; $this->snapshotStorage = $snapshot_storage; - $this->lock = $lock; - $this->eventDispatcher = $event_dispatcher; $this->configManager = $config_manager; - $this->typedConfigManager = $typed_config; - $this->moduleHandler = $module_handler; - $this->moduleInstaller = $module_installer; - $this->themeHandler = $theme_handler; $this->renderer = $renderer; + $this->keyValueExpirable = $key_value_expirable; } /** @@ -156,14 +98,9 @@ public static function create(ContainerInterface $container) { $container->get('config.storage.staging'), $container->get('config.storage'), $container->get('config.storage.snapshot'), - $container->get('lock.persistent'), - $container->get('event_dispatcher'), $container->get('config.manager'), - $container->get('config.typed'), - $container->get('module_handler'), - $container->get('module_installer'), - $container->get('theme_handler'), - $container->get('renderer') + $container->get('renderer'), + $container->get('keyvalue.expirable')->get('config_import') ); } @@ -245,8 +182,8 @@ public function buildForm(array $form, FormStateInterface $form_state) { continue; } - // @todo A table caption would be more appropriate, but does not have the - // visual importance of a heading. + // @todo A table caption would be more appropriate, but does not have + // the visual importance of a heading. $form[$collection][$config_change_type]['heading'] = array( '#type' => 'html_tag', '#tag' => 'h3', @@ -296,7 +233,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { 'class' => array('use-ajax'), 'data-dialog-type' => 'modal', 'data-dialog-options' => json_encode(array( - 'width' => 700 + 'width' => 700, )), ), ); @@ -319,46 +256,11 @@ public function buildForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $config_importer = new ConfigImporter( - $form_state->get('storage_comparer'), - $this->eventDispatcher, - $this->configManager, - $this->lock, - $this->typedConfigManager, - $this->moduleHandler, - $this->moduleInstaller, - $this->themeHandler, - $this->getStringTranslation() - ); - if ($config_importer->alreadyImporting()) { - drupal_set_message($this->t('Another request may be synchronizing configuration already.')); - } - else{ - 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)); - } + $account = $this->currentUser()->id(); + $this->keyValueExpirable->setWithExpire($account, $form_state->get('storage_comparer'), 60); - batch_set($batch); - } - catch (ConfigImporterException $e) { - // There are validation errors. - 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'); - } - } - } + // Redirect to the confirmation form. + $form_state->setRedirect('config.sync_confirm'); } /** diff --git a/core/modules/config/src/Form/ConfigSyncConfirmForm.php b/core/modules/config/src/Form/ConfigSyncConfirmForm.php new file mode 100644 index 0000000..dfbe7a5 --- /dev/null +++ b/core/modules/config/src/Form/ConfigSyncConfirmForm.php @@ -0,0 +1,279 @@ +eventDispatcher = $event_dispatcher; + $this->configManager = $config_manager; + $this->lock = $lock; + $this->typedConfigManager = $typed_config_manager; + $this->moduleHandler = $module_handler; + $this->moduleInstaller = $module_installer; + $this->themeHandler = $theme_handler; + $this->keyValueExpirable = $key_value_expirable; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $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'), + $container->get('keyvalue.expirable')->get('config_import') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $account = $this->currentUser()->id(); + $this->storageComparer = $this->keyValueExpirable->get($account); + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $config_importer = new ConfigImporter( + $this->storageComparer, + $this->eventDispatcher, + $this->configManager, + $this->lock, + $this->typedConfigManager, + $this->moduleHandler, + $this->moduleInstaller, + $this->themeHandler, + $this->getStringTranslation() + ); + if ($config_importer->alreadyImporting()) { + drupal_set_message($this->t('Another request may be synchronizing configuration already.')); + } + else { + 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); + $form_state->setRedirect('config.sync'); + } + catch (ConfigImporterException $e) { + // There are validation errors. + 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'); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return t('Confirm configuration import'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Import'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('config.sync'); + } + + /** + * 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 array $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/Tests/ConfigExportImportUITest.php b/core/modules/config/src/Tests/ConfigExportImportUITest.php index 6a63df7..a0d9a1c 100644 --- a/core/modules/config/src/Tests/ConfigExportImportUITest.php +++ b/core/modules/config/src/Tests/ConfigExportImportUITest.php @@ -173,6 +173,11 @@ public function testExportImport() { $this->assertText($this->contentType->label()); $this->drupalPostForm(NULL, array(), 'Import all'); + + // We now have to confirm the import. + $this->assertText(t('This action cannot be undone.')); + $this->drupalPostForm('admin/config/development/configuration/sync/confirm', array(), t('Import')); + // After importing the snapshot has been updated an there are no warnings. $this->assertNoText(t('Warning message')); $this->assertText(t('There are no configuration changes to import.')); @@ -289,6 +294,11 @@ public function testExportImportCollections() { $this->assertLinkByHref('admin/config/development/configuration/sync/diff_collection/collection.test2/config_test.another_delete'); $this->drupalPostForm(NULL, array(), 'Import all'); + + // We now have to confirm the import. + $this->assertText(t('This action cannot be undone.')); + $this->drupalPostForm('admin/config/development/configuration/sync/confirm', array(), t('Import')); + $this->assertText(t('There are no configuration changes to import.')); // Test data in collections. diff --git a/core/modules/config/src/Tests/ConfigImportAllTest.php b/core/modules/config/src/Tests/ConfigImportAllTest.php index 6e1aaea..5e5d11b 100644 --- a/core/modules/config/src/Tests/ConfigImportAllTest.php +++ b/core/modules/config/src/Tests/ConfigImportAllTest.php @@ -130,6 +130,7 @@ public function testInstallUninstall() { // Import the configuration thereby re-installing all the modules. $this->drupalPostForm('admin/config/development/configuration', array(), t('Import all')); + $this->drupalPostForm('admin/config/development/configuration/sync/confirm', array(), t('Import')); // Modules have been installed that have services. $this->rebuildContainer(); diff --git a/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php b/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php index 64b2d5d..0f63ad0 100644 --- a/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php +++ b/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php @@ -60,6 +60,7 @@ public function testInstallProfileValidation() { $staging->write('core.extension', $core); $this->drupalPostForm('admin/config/development/configuration', array(), t('Import all')); + $this->drupalPostForm('admin/config/development/configuration/sync/confirm', array(), t('Import')); $this->assertText('The configuration cannot be imported because it failed validation for the following reasons:'); $this->assertText('Unable to uninstall the Testing config import profile since it is the install profile.'); @@ -74,6 +75,7 @@ public function testInstallProfileValidation() { $theme['default'] = 'classy'; $staging->write('system.theme', $theme); $this->drupalPostForm('admin/config/development/configuration', array(), t('Import all')); + $this->drupalPostForm('admin/config/development/configuration/sync/confirm', array(), t('Import')); $this->assertText('The configuration was imported successfully.'); $this->rebuildContainer(); $this->assertFalse(\Drupal::moduleHandler()->moduleExists('syslog'), 'The syslog module has been uninstalled.'); diff --git a/core/modules/config/src/Tests/ConfigImportUITest.php b/core/modules/config/src/Tests/ConfigImportUITest.php index dd00edf..442f5db 100644 --- a/core/modules/config/src/Tests/ConfigImportUITest.php +++ b/core/modules/config/src/Tests/ConfigImportUITest.php @@ -123,6 +123,7 @@ function testImport() { // Import and verify that both do not appear anymore. $this->drupalPostForm(NULL, array(), t('Import all')); + $this->drupalPostForm('admin/config/development/configuration/sync/confirm', array(), t('Import')); $this->assertNoRaw('