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