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/ConfigImportForm.php b/core/modules/config/src/Form/ConfigImportForm.php index 7b29e3f..2ee1917 100644 --- a/core/modules/config/src/Form/ConfigImportForm.php +++ b/core/modules/config/src/Form/ConfigImportForm.php @@ -9,8 +9,10 @@ use Drupal\Core\Archiver\ArchiveTar; use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\RendererInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -26,13 +28,53 @@ class ConfigImportForm extends FormBase { protected $configStorage; /** + * The staging configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $stagingStorage; + + /** + * The active configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $activeStorage; + + /** + * The snapshot configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $snapshotStorage; + + /** + * The configuration manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface; + */ + protected $configManager; + + /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** * Constructs a new ConfigImportForm. * * @param \Drupal\Core\Config\StorageInterface $config_storage * The configuration storage. */ - public function __construct(StorageInterface $config_storage) { + public function __construct(StorageInterface $staging_storage, StorageInterface $active_storage, StorageInterface $snapshot_storage, StorageInterface $config_storage, ConfigManagerInterface $config_manager, RendererInterface $renderer) { + $this->stagingStorage = $staging_storage; + $this->activeStorage = $active_storage; + $this->snapshotStorage = $snapshot_storage; $this->configStorage = $config_storage; + $this->configManager = $config_manager; + $this->renderer = $renderer; } /** @@ -40,7 +82,12 @@ public function __construct(StorageInterface $config_storage) { */ public static function create(ContainerInterface $container) { return new static( - $container->get('config.storage.staging') + $container->get('config.storage.staging'), + $container->get('config.storage'), + $container->get('config.storage.snapshot'), + $container->get('config.storage.staging'), + $container->get('config.manager'), + $container->get('renderer') ); } @@ -65,6 +112,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'submit', '#value' => $this->t('Upload'), ); + return $form; } diff --git a/core/modules/config/src/Form/ConfigSync.php b/core/modules/config/src/Form/ConfigSync.php index 7f240e3..be4e21a 100644 --- a/core/modules/config/src/Form/ConfigSync.php +++ b/core/modules/config/src/Form/ConfigSync.php @@ -19,6 +19,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Config\StorageInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Config\StorageComparer; use Drupal\Core\Render\RendererInterface; @@ -109,6 +110,13 @@ class ConfigSync extends FormBase { protected $renderer; /** + * The key value storer. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; + */ + protected $keyValueExpirable; + + /** * Constructs the object. * * @param \Drupal\Core\Config\StorageInterface $staging_storage @@ -119,7 +127,8 @@ class ConfigSync extends FormBase { * The snapshot storage. * @param \Drupal\Core\Lock\LockBackendInterface $lock * The lock object. - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface + * $event_dispatcher * Event dispatcher. * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager * Configuration manager. @@ -131,10 +140,10 @@ class ConfigSync extends FormBase { * 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. */ - 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, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler, RendererInterface $renderer, KeyValueStoreExpirableInterface $key_value_expirable) { $this->stagingStorage = $staging_storage; $this->activeStorage = $active_storage; $this->snapshotStorage = $snapshot_storage; @@ -146,6 +155,7 @@ public function __construct(StorageInterface $staging_storage, StorageInterface $this->moduleInstaller = $module_installer; $this->themeHandler = $theme_handler; $this->renderer = $renderer; + $this->keyValueExpirable = $key_value_expirable; } /** @@ -163,7 +173,8 @@ public static function create(ContainerInterface $container) { $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 +256,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 +307,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 +330,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..735b8ce --- /dev/null +++ b/core/modules/config/src/Form/ConfigSyncConfirmForm.php @@ -0,0 +1,266 @@ +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..6f8813c 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(), '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(), '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..0de3a03 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(), '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..4527d2d 100644 --- a/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php +++ b/core/modules/config/src/Tests/ConfigImportInstallProfileTest.php @@ -31,7 +31,7 @@ class ConfigImportInstallProfileTest extends WebTestBase { public static $modules = array('config'); /** - * A user with the 'synchronize configuration' permission. + * A user with the 'import configuration' permission. * * @var \Drupal\user\UserInterface */ @@ -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..0eeb6ef 100644 --- a/core/modules/config/src/Tests/ConfigImportUITest.php +++ b/core/modules/config/src/Tests/ConfigImportUITest.php @@ -26,7 +26,7 @@ class ConfigImportUITest extends WebTestBase { public static $modules = array('config', 'config_test', 'config_import_test', 'text', 'options'); /** - * A user with the 'synchronize configuration' permission. + * A user with the 'Import configuration' permission. * * @var \Drupal\user\UserInterface */ @@ -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(), 'Import'); $this->assertNoRaw('