ConfigSingleImportForm.php 15 KB

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