fe1b2e496a2abde35f5b9fa58f40294ff4449f4a
[yaffs-website] / web / core / modules / config / src / Form / ConfigSync.php
1 <?php
2
3 namespace Drupal\config\Form;
4
5 use Drupal\Core\Config\ConfigImporterException;
6 use Drupal\Core\Config\ConfigImporter;
7 use Drupal\Core\Config\TypedConfigManagerInterface;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\Extension\ModuleInstallerInterface;
10 use Drupal\Core\Extension\ThemeHandlerInterface;
11 use Drupal\Core\Config\ConfigManagerInterface;
12 use Drupal\Core\Form\FormBase;
13 use Drupal\Core\Config\StorageInterface;
14 use Drupal\Core\Form\FormStateInterface;
15 use Drupal\Core\Lock\LockBackendInterface;
16 use Drupal\Core\Config\StorageComparer;
17 use Drupal\Core\Render\RendererInterface;
18 use Drupal\Core\Url;
19 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
20 use Symfony\Component\DependencyInjection\ContainerInterface;
21
22 /**
23  * Construct the storage changes in a configuration synchronization form.
24  */
25 class ConfigSync extends FormBase {
26
27   /**
28    * The database lock object.
29    *
30    * @var \Drupal\Core\Lock\LockBackendInterface
31    */
32   protected $lock;
33
34   /**
35    * The sync configuration object.
36    *
37    * @var \Drupal\Core\Config\StorageInterface
38    */
39   protected $syncStorage;
40
41   /**
42    * The active configuration object.
43    *
44    * @var \Drupal\Core\Config\StorageInterface
45    */
46   protected $activeStorage;
47
48   /**
49    * The snapshot configuration object.
50    *
51    * @var \Drupal\Core\Config\StorageInterface
52    */
53   protected $snapshotStorage;
54
55   /**
56    * Event dispatcher.
57    *
58    * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
59    */
60   protected $eventDispatcher;
61
62   /**
63    * The configuration manager.
64    *
65    * @var \Drupal\Core\Config\ConfigManagerInterface;
66    */
67   protected $configManager;
68
69   /**
70    * The typed config manager.
71    *
72    * @var \Drupal\Core\Config\TypedConfigManagerInterface
73    */
74   protected $typedConfigManager;
75
76   /**
77    * The module handler.
78    *
79    * @var \Drupal\Core\Extension\ModuleHandlerInterface
80    */
81   protected $moduleHandler;
82
83   /**
84    * The theme handler.
85    *
86    * @var \Drupal\Core\Extension\ThemeHandlerInterface
87    */
88   protected $themeHandler;
89
90   /**
91    * The module installer.
92    *
93    * @var \Drupal\Core\Extension\ModuleInstallerInterface
94    */
95   protected $moduleInstaller;
96
97   /**
98    * The renderer.
99    *
100    * @var \Drupal\Core\Render\RendererInterface
101    */
102   protected $renderer;
103
104   /**
105    * Constructs the object.
106    *
107    * @param \Drupal\Core\Config\StorageInterface $sync_storage
108    *   The source storage.
109    * @param \Drupal\Core\Config\StorageInterface $active_storage
110    *   The target storage.
111    * @param \Drupal\Core\Config\StorageInterface $snapshot_storage
112    *   The snapshot storage.
113    * @param \Drupal\Core\Lock\LockBackendInterface $lock
114    *   The lock object.
115    * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
116    *   Event dispatcher.
117    * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
118    *   Configuration manager.
119    * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
120    *   The typed configuration manager.
121    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
122    *   The module handler.
123    * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
124    *   The module installer.
125    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
126    *   The theme handler.
127    * @param \Drupal\Core\Render\RendererInterface $renderer
128    *   The renderer.
129    */
130   public function __construct(StorageInterface $sync_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) {
131     $this->syncStorage = $sync_storage;
132     $this->activeStorage = $active_storage;
133     $this->snapshotStorage = $snapshot_storage;
134     $this->lock = $lock;
135     $this->eventDispatcher = $event_dispatcher;
136     $this->configManager = $config_manager;
137     $this->typedConfigManager = $typed_config;
138     $this->moduleHandler = $module_handler;
139     $this->moduleInstaller = $module_installer;
140     $this->themeHandler = $theme_handler;
141     $this->renderer = $renderer;
142   }
143
144   /**
145    * {@inheritdoc}
146    */
147   public static function create(ContainerInterface $container) {
148     return new static(
149       $container->get('config.storage.sync'),
150       $container->get('config.storage'),
151       $container->get('config.storage.snapshot'),
152       $container->get('lock.persistent'),
153       $container->get('event_dispatcher'),
154       $container->get('config.manager'),
155       $container->get('config.typed'),
156       $container->get('module_handler'),
157       $container->get('module_installer'),
158       $container->get('theme_handler'),
159       $container->get('renderer')
160     );
161   }
162
163   /**
164    * {@inheritdoc}
165    */
166   public function getFormId() {
167     return 'config_admin_import_form';
168   }
169
170   /**
171    * {@inheritdoc}
172    */
173   public function buildForm(array $form, FormStateInterface $form_state) {
174     $form['actions'] = ['#type' => 'actions'];
175     $form['actions']['submit'] = [
176       '#type' => 'submit',
177       '#value' => $this->t('Import all'),
178     ];
179     $source_list = $this->syncStorage->listAll();
180     $storage_comparer = new StorageComparer($this->syncStorage, $this->activeStorage, $this->configManager);
181     if (empty($source_list) || !$storage_comparer->createChangelist()->hasChanges()) {
182       $form['no_changes'] = [
183         '#type' => 'table',
184         '#header' => [$this->t('Name'), $this->t('Operations')],
185         '#rows' => [],
186         '#empty' => $this->t('There are no configuration changes to import.'),
187       ];
188       $form['actions']['#access'] = FALSE;
189       return $form;
190     }
191     elseif (!$storage_comparer->validateSiteUuid()) {
192       drupal_set_message($this->t('The staged configuration cannot be imported, because it originates from a different site than this site. You can only synchronize configuration between cloned instances of this site.'), 'error');
193       $form['actions']['#access'] = FALSE;
194       return $form;
195     }
196     // A list of changes will be displayed, so check if the user should be
197     // warned of potential losses to configuration.
198     if ($this->snapshotStorage->exists('core.extension')) {
199       $snapshot_comparer = new StorageComparer($this->activeStorage, $this->snapshotStorage, $this->configManager);
200       if (!$form_state->getUserInput() && $snapshot_comparer->createChangelist()->hasChanges()) {
201         $change_list = [];
202         foreach ($snapshot_comparer->getAllCollectionNames() as $collection) {
203           foreach ($snapshot_comparer->getChangelist(NULL, $collection) as $config_names) {
204             if (empty($config_names)) {
205               continue;
206             }
207             foreach ($config_names as $config_name) {
208               $change_list[] = $config_name;
209             }
210           }
211         }
212         sort($change_list);
213         $message = [
214           [
215             '#markup' => $this->t('The following items in your active configuration have changes since the last import that may be lost on the next import.')
216           ],
217           [
218             '#theme' => 'item_list',
219             '#items' => $change_list,
220           ]
221         ];
222         drupal_set_message($this->renderer->renderPlain($message), 'warning');
223       }
224     }
225
226     // Store the comparer for use in the submit.
227     $form_state->set('storage_comparer', $storage_comparer);
228
229     // Add the AJAX library to the form for dialog support.
230     $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
231
232     foreach ($storage_comparer->getAllCollectionNames() as $collection) {
233       if ($collection != StorageInterface::DEFAULT_COLLECTION) {
234         $form[$collection]['collection_heading'] = [
235           '#type' => 'html_tag',
236           '#tag' => 'h2',
237           '#value' => $this->t('@collection configuration collection', ['@collection' => $collection]),
238         ];
239       }
240       foreach ($storage_comparer->getChangelist(NULL, $collection) as $config_change_type => $config_names) {
241         if (empty($config_names)) {
242           continue;
243         }
244
245         // @todo A table caption would be more appropriate, but does not have the
246         //   visual importance of a heading.
247         $form[$collection][$config_change_type]['heading'] = [
248           '#type' => 'html_tag',
249           '#tag' => 'h3',
250         ];
251         switch ($config_change_type) {
252           case 'create':
253             $form[$collection][$config_change_type]['heading']['#value'] = $this->formatPlural(count($config_names), '@count new', '@count new');
254             break;
255
256           case 'update':
257             $form[$collection][$config_change_type]['heading']['#value'] = $this->formatPlural(count($config_names), '@count changed', '@count changed');
258             break;
259
260           case 'delete':
261             $form[$collection][$config_change_type]['heading']['#value'] = $this->formatPlural(count($config_names), '@count removed', '@count removed');
262             break;
263
264           case 'rename':
265             $form[$collection][$config_change_type]['heading']['#value'] = $this->formatPlural(count($config_names), '@count renamed', '@count renamed');
266             break;
267         }
268         $form[$collection][$config_change_type]['list'] = [
269           '#type' => 'table',
270           '#header' => [$this->t('Name'), $this->t('Operations')],
271         ];
272
273         foreach ($config_names as $config_name) {
274           if ($config_change_type == 'rename') {
275             $names = $storage_comparer->extractRenameNames($config_name);
276             $route_options = ['source_name' => $names['old_name'], 'target_name' => $names['new_name']];
277             $config_name = $this->t('@source_name to @target_name', ['@source_name' => $names['old_name'], '@target_name' => $names['new_name']]);
278           }
279           else {
280             $route_options = ['source_name' => $config_name];
281           }
282           if ($collection != StorageInterface::DEFAULT_COLLECTION) {
283             $route_name = 'config.diff_collection';
284             $route_options['collection'] = $collection;
285           }
286           else {
287             $route_name = 'config.diff';
288           }
289           $links['view_diff'] = [
290             'title' => $this->t('View differences'),
291             'url' => Url::fromRoute($route_name, $route_options),
292             'attributes' => [
293               'class' => ['use-ajax'],
294               'data-dialog-type' => 'modal',
295               'data-dialog-options' => json_encode([
296                 'width' => 700
297               ]),
298             ],
299           ];
300           $form[$collection][$config_change_type]['list']['#rows'][] = [
301             'name' => $config_name,
302             'operations' => [
303               'data' => [
304                 '#type' => 'operations',
305                 '#links' => $links,
306               ],
307             ],
308           ];
309         }
310       }
311     }
312     return $form;
313   }
314
315   /**
316    * {@inheritdoc}
317    */
318   public function submitForm(array &$form, FormStateInterface $form_state) {
319     $config_importer = new ConfigImporter(
320       $form_state->get('storage_comparer'),
321       $this->eventDispatcher,
322       $this->configManager,
323       $this->lock,
324       $this->typedConfigManager,
325       $this->moduleHandler,
326       $this->moduleInstaller,
327       $this->themeHandler,
328       $this->getStringTranslation()
329     );
330     if ($config_importer->alreadyImporting()) {
331       drupal_set_message($this->t('Another request may be synchronizing configuration already.'));
332     }
333     else {
334       try {
335         $sync_steps = $config_importer->initialize();
336         $batch = [
337           'operations' => [],
338           'finished' => [get_class($this), 'finishBatch'],
339           'title' => t('Synchronizing configuration'),
340           'init_message' => t('Starting configuration synchronization.'),
341           'progress_message' => t('Completed step @current of @total.'),
342           'error_message' => t('Configuration synchronization has encountered an error.'),
343           'file' => __DIR__ . '/../../config.admin.inc',
344         ];
345         foreach ($sync_steps as $sync_step) {
346           $batch['operations'][] = [[get_class($this), 'processBatch'], [$config_importer, $sync_step]];
347         }
348
349         batch_set($batch);
350       }
351       catch (ConfigImporterException $e) {
352         // There are validation errors.
353         drupal_set_message($this->t('The configuration cannot be imported because it failed validation for the following reasons:'), 'error');
354         foreach ($config_importer->getErrors() as $message) {
355           drupal_set_message($message, 'error');
356         }
357       }
358     }
359   }
360
361   /**
362    * Processes the config import batch and persists the importer.
363    *
364    * @param \Drupal\Core\Config\ConfigImporter $config_importer
365    *   The batch config importer object to persist.
366    * @param string $sync_step
367    *   The synchronization step to do.
368    * @param array $context
369    *   The batch context.
370    */
371   public static function processBatch(ConfigImporter $config_importer, $sync_step, &$context) {
372     if (!isset($context['sandbox']['config_importer'])) {
373       $context['sandbox']['config_importer'] = $config_importer;
374     }
375
376     $config_importer = $context['sandbox']['config_importer'];
377     $config_importer->doSyncStep($sync_step, $context);
378     if ($errors = $config_importer->getErrors()) {
379       if (!isset($context['results']['errors'])) {
380         $context['results']['errors'] = [];
381       }
382       $context['results']['errors'] += $errors;
383     }
384   }
385
386   /**
387    * Finish batch.
388    *
389    * This function is a static function to avoid serializing the ConfigSync
390    * object unnecessarily.
391    */
392   public static function finishBatch($success, $results, $operations) {
393     if ($success) {
394       if (!empty($results['errors'])) {
395         foreach ($results['errors'] as $error) {
396           drupal_set_message($error, 'error');
397           \Drupal::logger('config_sync')->error($error);
398         }
399         drupal_set_message(\Drupal::translation()->translate('The configuration was imported with errors.'), 'warning');
400       }
401       else {
402         drupal_set_message(\Drupal::translation()->translate('The configuration was imported successfully.'));
403       }
404     }
405     else {
406       // An error occurred.
407       // $operations contains the operations that remained unprocessed.
408       $error_operation = reset($operations);
409       $message = \Drupal::translation()->translate('An error occurred while processing %error_operation with arguments: @arguments', ['%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)]);
410       drupal_set_message($message, 'error');
411     }
412   }
413
414 }