1<?php
2
3namespace Drupal\config\Form;
4
5use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
6use Drupal\config\StorageReplaceDataWrapper;
7use Drupal\Core\Config\ConfigImporter;
8use Drupal\Core\Config\ConfigImporterException;
9use Drupal\Core\Config\ConfigManagerInterface;
10use Drupal\Core\Config\Entity\ConfigEntityInterface;
11use Drupal\Core\Config\Importer\ConfigImporterBatch;
12use Drupal\Core\Config\StorageComparer;
13use Drupal\Core\Config\StorageInterface;
14use Drupal\Core\Config\TypedConfigManagerInterface;
15use Drupal\Core\Entity\EntityTypeManagerInterface;
16use Drupal\Core\Extension\ModuleExtensionList;
17use Drupal\Core\Extension\ModuleHandlerInterface;
18use Drupal\Core\Extension\ModuleInstallerInterface;
19use Drupal\Core\Extension\ThemeHandlerInterface;
20use Drupal\Core\Form\ConfirmFormBase;
21use Drupal\Core\Form\FormStateInterface;
22use Drupal\Core\Lock\LockBackendInterface;
23use Drupal\Core\Render\RendererInterface;
24use Drupal\Core\Serialization\Yaml;
25use Drupal\Core\Url;
26use Symfony\Component\DependencyInjection\ContainerInterface;
27use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
28
29/**
30 * Provides a form for importing a single configuration file.
31 *
32 * @internal
33 */
34class ConfigSingleImportForm extends ConfirmFormBase {
35
36  /**
37   * The entity type manager.
38   *
39   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
40   */
41  protected $entityTypeManager;
42
43  /**
44   * The config storage.
45   *
46   * @var \Drupal\Core\Config\StorageInterface
47   */
48  protected $configStorage;
49
50  /**
51   * The renderer service.
52   *
53   * @var \Drupal\Core\Render\RendererInterface
54   */
55  protected $renderer;
56
57  /**
58   * The event dispatcher.
59   *
60   * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
61   */
62  protected $eventDispatcher;
63
64  /**
65   * The configuration manager.
66   *
67   * @var \Drupal\Core\Config\ConfigManagerInterface
68   */
69  protected $configManager;
70
71  /**
72   * The database lock object.
73   *
74   * @var \Drupal\Core\Lock\LockBackendInterface
75   */
76  protected $lock;
77
78  /**
79   * The typed config manager.
80   *
81   * @var \Drupal\Core\Config\TypedConfigManagerInterface
82   */
83  protected $typedConfigManager;
84
85  /**
86   * The module handler.
87   *
88   * @var \Drupal\Core\Extension\ModuleHandlerInterface
89   */
90  protected $moduleHandler;
91
92  /**
93   * The theme handler.
94   *
95   * @var \Drupal\Core\Extension\ThemeHandlerInterface
96   */
97  protected $themeHandler;
98
99  /**
100   * The module extension list.
101   *
102   * @var \Drupal\Core\Extension\ModuleExtensionList
103   */
104  protected $moduleExtensionList;
105
106  /**
107   * The module installer.
108   *
109   * @var \Drupal\Core\Extension\ModuleInstallerInterface
110   */
111  protected $moduleInstaller;
112
113  /**
114   * If the config exists, this is that object. Otherwise, FALSE.
115   *
116   * @var \Drupal\Core\Config\Config|\Drupal\Core\Config\Entity\ConfigEntityInterface|bool
117   */
118  protected $configExists = FALSE;
119
120  /**
121   * The submitted data needing to be confirmed.
122   *
123   * @var array
124   */
125  protected $data = [];
126
127  /**
128   * Constructs a new ConfigSingleImportForm.
129   *
130   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
131   *   The entity type manager.
132   * @param \Drupal\Core\Config\StorageInterface $config_storage
133   *   The config storage.
134   * @param \Drupal\Core\Render\RendererInterface $renderer
135   *   The renderer service.
136   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
137   *   The event dispatcher used to notify subscribers of config import events.
138   * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
139   *   The configuration manager.
140   * @param \Drupal\Core\Lock\LockBackendInterface $lock
141   *   The lock backend to ensure multiple imports do not occur at the same time.
142   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
143   *   The typed configuration manager.
144   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
145   *   The module handler.
146   * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
147   *   The module installer.
148   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
149   *   The theme handler.
150   * @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
151   *   The module extension list.
152   */
153  public function __construct(EntityTypeManagerInterface $entity_type_manager, StorageInterface $config_storage, RendererInterface $renderer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler, ModuleExtensionList $extension_list_module) {
154    $this->entityTypeManager = $entity_type_manager;
155    $this->configStorage = $config_storage;
156    $this->renderer = $renderer;
157
158    // Services necessary for \Drupal\Core\Config\ConfigImporter.
159    $this->eventDispatcher = $event_dispatcher;
160    $this->configManager = $config_manager;
161    $this->lock = $lock;
162    $this->typedConfigManager = $typed_config;
163    $this->moduleHandler = $module_handler;
164    $this->moduleInstaller = $module_installer;
165    $this->themeHandler = $theme_handler;
166    $this->moduleExtensionList = $extension_list_module;
167  }
168
169  /**
170   * {@inheritdoc}
171   */
172  public static function create(ContainerInterface $container) {
173    return new static(
174      $container->get('entity_type.manager'),
175      $container->get('config.storage'),
176      $container->get('renderer'),
177      $container->get('event_dispatcher'),
178      $container->get('config.manager'),
179      $container->get('lock.persistent'),
180      $container->get('config.typed'),
181      $container->get('module_handler'),
182      $container->get('module_installer'),
183      $container->get('theme_handler'),
184      $container->get('extension.list.module')
185    );
186  }
187
188  /**
189   * {@inheritdoc}
190   */
191  public function getFormId() {
192    return 'config_single_import_form';
193  }
194
195  /**
196   * {@inheritdoc}
197   */
198  public function getCancelUrl() {
199    return new Url('config.import_single');
200  }
201
202  /**
203   * {@inheritdoc}
204   */
205  public function getQuestion() {
206    if ($this->data['config_type'] === 'system.simple') {
207      $name = $this->data['config_name'];
208      $type = $this->t('simple configuration');
209    }
210    else {
211      $definition = $this->entityTypeManager->getDefinition($this->data['config_type']);
212      $name = $this->data['import'][$definition->getKey('id')];
213      $type = $definition->getSingularLabel();
214    }
215
216    $args = [
217      '%name' => $name,
218      '@type' => strtolower($type),
219    ];
220    if ($this->configExists) {
221      $question = $this->t('Are you sure you want to update the %name @type?', $args);
222    }
223    else {
224      $question = $this->t('Are you sure you want to create a new %name @type?', $args);
225    }
226    return $question;
227  }
228
229  /**
230   * {@inheritdoc}
231   */
232  public function buildForm(array $form, FormStateInterface $form_state) {
233    // When this is the confirmation step fall through to the confirmation form.
234    if ($this->data) {
235      return parent::buildForm($form, $form_state);
236    }
237
238    $entity_types = [];
239    foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $definition) {
240      if ($definition->entityClassImplements(ConfigEntityInterface::class)) {
241        $entity_types[$entity_type] = $definition->getLabel();
242      }
243    }
244    // Sort the entity types by label, then add the simple config to the top.
245    uasort($entity_types, 'strnatcasecmp');
246    $config_types = [
247      'system.simple' => $this->t('Simple configuration'),
248    ] + $entity_types;
249    $form['config_type'] = [
250      '#title' => $this->t('Configuration type'),
251      '#type' => 'select',
252      '#options' => $config_types,
253      '#required' => TRUE,
254    ];
255    $form['config_name'] = [
256      '#title' => $this->t('Configuration name'),
257      '#description' => $this->t('Enter the name of the configuration file without the <em>.yml</em> extension. (e.g. <em>system.site</em>)'),
258      '#type' => 'textfield',
259      '#states' => [
260        'required' => [
261          ':input[name="config_type"]' => ['value' => 'system.simple'],
262        ],
263        'visible' => [
264          ':input[name="config_type"]' => ['value' => 'system.simple'],
265        ],
266      ],
267    ];
268    $form['import'] = [
269      '#title' => $this->t('Paste your configuration here'),
270      '#type' => 'textarea',
271      '#rows' => 24,
272      '#required' => TRUE,
273    ];
274    $form['advanced'] = [
275      '#type' => 'details',
276      '#title' => $this->t('Advanced'),
277    ];
278    $form['advanced']['custom_entity_id'] = [
279      '#title' => $this->t('Custom Entity ID'),
280      '#type' => 'textfield',
281      '#description' => $this->t('Specify a custom entity ID. This will override the entity ID in the configuration above.'),
282    ];
283    $form['actions'] = ['#type' => 'actions'];
284    $form['actions']['submit'] = [
285      '#type' => 'submit',
286      '#value' => $this->t('Import'),
287      '#button_type' => 'primary',
288    ];
289    return $form;
290  }
291
292  /**
293   * {@inheritdoc}
294   */
295  public function validateForm(array &$form, FormStateInterface $form_state) {
296    // The confirmation step needs no additional validation.
297    if ($this->data) {
298      return;
299    }
300
301    try {
302      // Decode the submitted import.
303      $data = Yaml::decode($form_state->getValue('import'));
304    }
305    catch (InvalidDataTypeException $e) {
306      $form_state->setErrorByName('import', $this->t('The import failed with the following message: %message', ['%message' => $e->getMessage()]));
307    }
308
309    // Validate for config entities.
310    if ($form_state->getValue('config_type') && $form_state->getValue('config_type') !== 'system.simple') {
311      $definition = $this->entityTypeManager->getDefinition($form_state->getValue('config_type'));
312      $id_key = $definition->getKey('id');
313
314      // If a custom entity ID is specified, override the value in the
315      // configuration data being imported.
316      if (!$form_state->isValueEmpty('custom_entity_id')) {
317        $data[$id_key] = $form_state->getValue('custom_entity_id');
318      }
319
320      $entity_storage = $this->entityTypeManager->getStorage($form_state->getValue('config_type'));
321      // If an entity ID was not specified, set an error.
322      if (!isset($data[$id_key])) {
323        $form_state->setErrorByName('import', $this->t('Missing ID key "@id_key" for this @entity_type import.', ['@id_key' => $id_key, '@entity_type' => $definition->getLabel()]));
324        return;
325      }
326
327      $config_name = $definition->getConfigPrefix() . '.' . $data[$id_key];
328      // If there is an existing entity, ensure matching ID and UUID.
329      if ($entity = $entity_storage->load($data[$id_key])) {
330        $this->configExists = $entity;
331        if (!isset($data['uuid'])) {
332          $form_state->setErrorByName('import', $this->t('An entity with this machine name already exists but the import did not specify a UUID.'));
333          return;
334        }
335        if ($data['uuid'] !== $entity->uuid()) {
336          $form_state->setErrorByName('import', $this->t('An entity with this machine name already exists but the UUID does not match.'));
337          return;
338        }
339      }
340      // If there is no entity with a matching ID, check for a UUID match.
341      elseif (isset($data['uuid']) && $entity_storage->loadByProperties(['uuid' => $data['uuid']])) {
342        $form_state->setErrorByName('import', $this->t('An entity with this UUID already exists but the machine name does not match.'));
343      }
344    }
345    else {
346      $config_name = $form_state->getValue('config_name');
347      $config = $this->config($config_name);
348      $this->configExists = !$config->isNew() ? $config : FALSE;
349    }
350
351    // Use ConfigImporter validation.
352    if (!$form_state->getErrors()) {
353      $source_storage = new StorageReplaceDataWrapper($this->configStorage);
354      $source_storage->replaceData($config_name, $data);
355      $storage_comparer = new StorageComparer($source_storage, $this->configStorage);
356
357      $storage_comparer->createChangelist();
358      if (!$storage_comparer->hasChanges()) {
359        $form_state->setErrorByName('import', $this->t('There are no changes to import.'));
360      }
361      else {
362        $config_importer = new ConfigImporter(
363          $storage_comparer,
364          $this->eventDispatcher,
365          $this->configManager,
366          $this->lock,
367          $this->typedConfigManager,
368          $this->moduleHandler,
369          $this->moduleInstaller,
370          $this->themeHandler,
371          $this->getStringTranslation(),
372          $this->moduleExtensionList
373        );
374
375        try {
376          $config_importer->validate();
377          $form_state->set('config_importer', $config_importer);
378        }
379        catch (ConfigImporterException $e) {
380          // There are validation errors.
381          $item_list = [
382            '#theme' => 'item_list',
383            '#items' => $config_importer->getErrors(),
384            '#title' => $this->t('The configuration cannot be imported because it failed validation for the following reasons:'),
385          ];
386          $form_state->setErrorByName('import', $this->renderer->render($item_list));
387        }
388      }
389    }
390
391    // Store the decoded version of the submitted import.
392    $form_state->setValueForElement($form['import'], $data);
393  }
394
395  /**
396   * {@inheritdoc}
397   */
398  public function submitForm(array &$form, FormStateInterface $form_state) {
399    // If this form has not yet been confirmed, store the values and rebuild.
400    if (!$this->data) {
401      $form_state->setRebuild();
402      $this->data = $form_state->getValues();
403      return;
404    }
405
406    /** @var \Drupal\Core\Config\ConfigImporter $config_importer */
407    $config_importer = $form_state->get('config_importer');
408    if ($config_importer->alreadyImporting()) {
409      $this->messenger()->addError($this->t('Another request may be importing configuration already.'));
410    }
411    else {
412      try {
413        $sync_steps = $config_importer->initialize();
414        $batch = [
415          'operations' => [],
416          'finished' => [ConfigImporterBatch::class, 'finish'],
417          'title' => $this->t('Importing configuration'),
418          'init_message' => $this->t('Starting configuration import.'),
419          'progress_message' => $this->t('Completed @current step of @total.'),
420          'error_message' => $this->t('Configuration import has encountered an error.'),
421        ];
422        foreach ($sync_steps as $sync_step) {
423          $batch['operations'][] = [[ConfigImporterBatch::class, 'process'], [$config_importer, $sync_step]];
424        }
425
426        batch_set($batch);
427      }
428      catch (ConfigImporterException $e) {
429        // There are validation errors.
430        $this->messenger()->addError($this->t('The configuration import failed for the following reasons:'));
431        foreach ($config_importer->getErrors() as $message) {
432          $this->messenger()->addError($message);
433        }
434      }
435    }
436  }
437
438}
439