diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml index f2a944a..36e6f89 100644 --- a/core/modules/config/config.routing.yml +++ b/core/modules/config/config.routing.yml @@ -54,3 +54,10 @@ config.export_single: config_name: NULL requirements: _permission: 'export configuration' + +config.snapshot_diff: + path: '/admin/config/development/configuration/sync/snapshot-diff/{storage}' + defaults: + _content: '\Drupal\config\Controller\ConfigController::snapshotDiff' + requirements: + _permission: 'import configuration' diff --git a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php index 88cdeab..abb6cc2 100644 --- a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php +++ b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php @@ -11,15 +11,16 @@ use Drupal\Component\Serialization\Yaml; use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\StorageInterface; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\system\FileDownloadController; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; +use Drupal\Core\Config\StorageComparer; +use Drupal\Core\Controller\ControllerBase; /** * Returns responses for config module routes. */ -class ConfigController implements ContainerInjectionInterface { +class ConfigController extends ControllerBase { /** * The target storage. @@ -36,6 +37,13 @@ class ConfigController implements ContainerInjectionInterface { protected $sourceStorage; /** + * The snapshot. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $snapshotStorage; + + /** * The configuration manager. * * @var \Drupal\Core\Config\ConfigManagerInterface @@ -56,6 +64,7 @@ public static function create(ContainerInterface $container) { return new static( $container->get('config.storage'), $container->get('config.storage.staging'), + $container->get('config.storage.snapshot'), $container->get('config.manager'), new FileDownloadController() ); @@ -71,9 +80,10 @@ public static function create(ContainerInterface $container) { * @param \Drupal\system\FileDownloadController $file_download_controller * The file download controller. */ - public function __construct(StorageInterface $target_storage, StorageInterface $source_storage, ConfigManagerInterface $config_manager, FileDownloadController $file_download_controller) { + public function __construct(StorageInterface $target_storage, StorageInterface $source_storage, StorageInterface $snapshot_storage, ConfigManagerInterface $config_manager, FileDownloadController $file_download_controller) { $this->targetStorage = $target_storage; $this->sourceStorage = $source_storage; + $this->snapshotStorage = $snapshot_storage; $this->configManager = $config_manager; $this->fileDownloadController = $file_download_controller; } @@ -110,15 +120,15 @@ public function diff($source_name, $target_name = NULL) { $build = array(); - $build['#title'] = t('View changes of @config_file', array('@config_file' => $source_name)); + $build['#title'] = $this->t('View changes of @config_file', array('@config_file' => $source_name)); // Add the CSS for the inline diff. $build['#attached']['css'][] = drupal_get_path('module', 'system') . '/css/system.diff.css'; $build['diff'] = array( '#type' => 'table', '#header' => array( - array('data' => t('Old'), 'colspan' => '2'), - array('data' => t('New'), 'colspan' => '2'), + array('data' => $this->t('Old'), 'colspan' => '2'), + array('data' => $this->t('New'), 'colspan' => '2'), ), '#rows' => $formatter->format($diff), ); @@ -131,9 +141,66 @@ public function diff($source_name, $target_name = NULL) { ), ), '#title' => "Back to 'Synchronize configuration' page.", - '#href' => 'admin/config/development/configuration', + '#route_name' => 'config.sync', ); return $build; } + + /** + * Show diff of snapshot and active/staging storage. + * + * @return string + * Table showing a two-way diff between the snapshot and + * active/staged configuration. + */ + public function snapshotDiff($storage = 'active') { + $build = array(); + + // Add the CSS for the inline diff. + $build['#attached']['css'][] = drupal_get_path('module', 'system') . '/css/system.diff.css'; + + if ($storage == 'active') { + $build['#title'] = $this->t('View changes between snapshot and active configuration.'); + $used_storage = $this->targetStorage; + $compare_button = $this->l($this->t('Compare snapshot with staging storage'), 'config.snapshot_diff', array('storage' => 'staging'), array('attributes' => array('class' => array('button')))); + } + else { + $build['#title'] = $this->t('View changes between snapshot and staging configuration.'); + $used_storage = $this->sourceStorage; + $compare_button = $this->l($this->t('Compare snapshot with active storage'), 'config.snapshot_diff', array('storage' => 'active'), array('attributes' => array('class' => array('button')))); + } + + $build['compare']['#markup'] = $compare_button; + + $storage_snapshot_comparer = new StorageComparer($used_storage, $this->snapshotStorage); + + // Collect changes. + $storage_snapshot_comparer->createChangelist(); + $changelist = $storage_snapshot_comparer->getChangelist(); + + // Verify that we have an initial snapshot that matches the active/staging + // configuration. + if ($storage_snapshot_comparer->createChangelist()->hasChanges()) { + // Show changes for each configuration object. + foreach ($changelist as $op) { + foreach ($op as $name) { + $diff = $this->configManager->diff($this->snapshotStorage, $used_storage, $name); + $formatter = new \DrupalDiffFormatter(); + $formatter->show_header = FALSE; + $build[$name] = array( + '#prefix' => '

' . $name . '

', + '#type' => 'table', + '#header' => array( + array('data' => $this->t('Snapshot'), 'colspan' => '2'), + array('data' => ($storage == 'active') ? $this->t('Active storage') : $this->t('Staging storage'), 'colspan' => '2'), + ), + '#rows' => $formatter->format($diff), + ); + } + } + } + + return $build; + } } diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index 19f417f..f70e4f5 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -92,12 +92,21 @@ class ConfigSync extends FormBase { protected $themeHandler; /** + * The snapshot configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $snapshotStorage; + + /** * Constructs the object. * * @param \Drupal\Core\Config\StorageInterface $sourceStorage * The source storage object. * @param \Drupal\Core\Config\StorageInterface $targetStorage * The target storage manager. + * @param \Drupal\Core\Config\StorageInterface $snapshotStorage + * The snapshot storage object. * @param \Drupal\Core\Lock\LockBackendInterface $lock * The lock object. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher @@ -113,9 +122,10 @@ class ConfigSync extends FormBase { * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler * The theme handler */ - public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManager $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { + public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, StorageInterface $snapshotStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManager $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { $this->sourceStorage = $sourceStorage; $this->targetStorage = $targetStorage; + $this->snapshotStorage = $snapshotStorage; $this->lock = $lock; $this->eventDispatcher = $event_dispatcher; $this->configManager = $config_manager; @@ -132,6 +142,7 @@ public static function create(ContainerInterface $container) { return new static( $container->get('config.storage.staging'), $container->get('config.storage'), + $container->get('config.storage.snapshot'), $container->get('lock'), $container->get('event_dispatcher'), $container->get('config.manager'), @@ -153,6 +164,16 @@ public function getFormId() { * {@inheritdoc} */ public function buildForm(array $form, array &$form_state) { + $active_snapshot_comparer = new StorageComparer($this->targetStorage, $this->snapshotStorage); + + // Verify that we have an initial snapshot that matches the active + // configuration. + if (empty($form_state['input']) && $active_snapshot_comparer->createChangelist()->hasChanges()) { + drupal_set_message($this->t('Changes have been made to your active configuration, which might be lost on the next import attempt. You can find and review the differences between snapshot and active/staging storage here: Compare snapshot and active/staging storage.', array( + '@link' => $this->url('config.snapshot_diff', array('storage' => 'active')))), 'warning'); + + } + $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array( '#type' => 'submit', diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php index 0a01bd1..3d218b2 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php @@ -130,4 +130,24 @@ public function testExportImport() { $this->drupalGet('node/add'); $this->assertFieldByName("{$this->field->name}[0][value]", '', 'Widget is displayed'); } + + /** + * Tests that a warning appears when unsynchronized changes exist. + */ + public function testImportWarning() { + $new_slogan = $this->randomName(16); + \Drupal::config('system.site') + ->set('slogan', $new_slogan) + ->save(); + + $this->drupalGet('admin/config/development/configuration'); + $this->assertText('Changes have been made to your active configuration, which might be lost on the next import attempt. You can find and review the differences between snapshot and active/staging storage here'); + $this->clickLink('Compare snapshot and active/staging storage'); + $this->assertText('View changes between snapshot and active configuration'); + $this->assertText('system.site'); + $this->assertText('Active storage'); + $this->assertText($new_slogan, "New slogan $new_slogan found when viewing changes."); + $this->clickLink('Compare snapshot with staging storage'); + $this->assertText('View changes between snapshot and staging configuration'); + } }