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 dd7a0c3..c27a41b 100644 --- a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php +++ b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php @@ -10,16 +10,17 @@ use Drupal\Component\Archiver\ArchiveTar; use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\StorageInterface; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\system\FileDownloadController; use Symfony\Component\Yaml\Dumper; 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; } @@ -134,9 +144,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'] = 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'] = 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' => t('Snapshot'), 'colspan' => '2'), + array('data' => ($storage == 'active') ? t('Active storage') : t('Staging storage'), 'colspan' => '2'), + ), + '#rows' => $formatter->format($diff), + ); + } + } + } + + return $build; + } } diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigImportForm.php b/core/modules/config/lib/Drupal/config/Form/ConfigImportForm.php index ddbdca9..4ddb335 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigImportForm.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigImportForm.php @@ -11,6 +11,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Component\Archiver\ArchiveTar; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Config\StorageComparer; /** * Defines the configuration import form. @@ -30,8 +31,10 @@ class ConfigImportForm extends FormBase { * @param \Drupal\Core\Config\StorageInterface $config_storage * The configuration storage. */ - public function __construct(StorageInterface $config_storage) { + public function __construct(StorageInterface $config_storage, StorageInterface $config_storage_snapshot, StorageInterface $config_storage_active) { $this->configStorage = $config_storage; + $this->configStorageSnapshot = $config_storage_snapshot; + $this->configStorageActive = $config_storage_active; } /** @@ -39,7 +42,9 @@ 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.snapshot'), + $container->get('config.storage') ); } @@ -54,6 +59,15 @@ public function getFormId() { * {@inheritdoc} */ public function buildForm(array $form, array &$form_state) { + $active_snapshot_comparer = new StorageComparer($this->configStorageActive, $this->configStorageSnapshot); + + // Verify that we have an initial snapshot that matches the active + // configuration. + if ($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['description'] = array( '#markup' => '

' . $this->t('Use the upload button below.') . '

', ); diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index 19f417f..42d1c95 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -92,6 +92,13 @@ 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 @@ -123,6 +130,7 @@ public function __construct(StorageInterface $sourceStorage, StorageInterface $t $this->typedConfigManager = $typed_config; $this->moduleHandler = $module_handler; $this->themeHandler = $theme_handler; + $this->snapshotStorage = $snapshotStorage; } /** @@ -138,7 +146,8 @@ public static function create(ContainerInterface $container) { $container->get('url_generator'), $container->get('config.typed'), $container->get('module_handler'), - $container->get('theme_handler') + $container->get('theme_handler'), + $container->get('config.storage.snapshot') ); } @@ -153,6 +162,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 ($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',