Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
20.06% covered (danger)
20.06%
72 / 359
26.81% covered (danger)
26.81%
37 / 138
4.70% covered (danger)
4.70%
7 / 149
18.18% covered (danger)
18.18%
4 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiController
20.06% covered (danger)
20.06%
72 / 359
26.81% covered (danger)
26.81%
37 / 138
4.70% covered (danger)
4.70%
7 / 149
18.18% covered (danger)
18.18%
4 / 22
4434.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
n/a
0 / 0
n/a
0 / 0
100.00% covered (success)
100.00%
1 / 1
1
 attachToRoot
57.58% covered (warning)
57.58%
19 / 33
78.95% covered (warning)
78.95%
15 / 19
13.33% covered (danger)
13.33%
2 / 15
0.00% covered (danger)
0.00%
0 / 1
49.66
 attachToSlot
46.15% covered (danger)
46.15%
18 / 39
57.89% covered (warning)
57.89%
11 / 19
6.67% covered (danger)
6.67%
1 / 15
0.00% covered (danger)
0.00%
0 / 1
60.03
 get
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 thirdPartySettingsUpdate
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 paste
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 delete
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 saveAsPreset
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 save
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 restore
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 revert
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 undo
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 redo
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clear
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 attachPresetToRoot
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 attachPresetToSlot
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 dispatchDisplayBuilderEvent
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateIslandForm
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 responseMessageError
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 recursiveRefreshNodeId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 cleanNodeId
83.33% covered (warning)
83.33%
5 / 6
72.73% covered (warning)
72.73%
8 / 11
9.09% covered (danger)
9.09%
1 / 11
0.00% covered (danger)
0.00%
0 / 1
23.78
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Controller;
6
7use Drupal\Component\Datetime\TimeInterface;
8use Drupal\Core\Entity\FieldableEntityInterface;
9use Drupal\Core\Form\FormAjaxException;
10use Drupal\Core\Form\FormState;
11use Drupal\Core\Form\FormStateInterface;
12use Drupal\Core\Plugin\Context\ContextInterface;
13use Drupal\Core\Render\RendererInterface;
14use Drupal\Core\StringTranslation\TranslatableMarkup;
15use Drupal\Core\TempStore\SharedTempStoreFactory;
16use Drupal\display_builder\Event\DisplayBuilderEvents;
17use Drupal\display_builder\InstanceInterface;
18use Drupal\display_builder\IslandPluginManagerInterface;
19use Drupal\display_builder\Plugin\display_builder\Island\ContextualFormPanel;
20use Drupal\display_builder\RenderableBuilderTrait;
21use Drupal\display_builder\SourceWithSlotsInterface;
22use Drupal\display_builder_entity_view\Plugin\display_builder\Buildable\EntityViewOverride;
23use Drupal\ui_patterns\SourcePluginBase;
24use Drupal\ui_patterns\SourcePluginManager;
25use Symfony\Component\DependencyInjection\Attribute\Autowire;
26use Symfony\Component\HttpFoundation\Request;
27use Symfony\Component\HttpFoundation\Session\SessionInterface;
28use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
29
30/**
31 * Returns responses for Display builder routes.
32 */
33class ApiController extends ApiControllerBase implements ApiControllerInterface {
34
35  use RenderableBuilderTrait;
36
37  public function __construct(
38    protected EventDispatcherInterface $eventDispatcher,
39    protected RendererInterface $renderer,
40    protected TimeInterface $time,
41    #[Autowire(service: 'tempstore.shared')]
42    protected SharedTempStoreFactory $sharedTempStoreFactory,
43    protected SessionInterface $session,
44    private IslandPluginManagerInterface $islandPluginManager,
45    #[Autowire(service: 'plugin.manager.ui_patterns_source')]
46    private SourcePluginManager $sourceManager,
47  ) {
48    parent::__construct($eventDispatcher, $renderer, $time, $sharedTempStoreFactory, $session);
49  }
50
51  /**
52   * {@inheritdoc}
53   */
54  public function attachToRoot(Request $request, InstanceInterface $display_builder_instance): array {
55    $position = (int) $request->request->get('position', 0);
56
57    if ($request->request->has('preset_id')) {
58      $preset_id = (string) $request->request->get('preset_id');
59
60      return $this->attachPresetToRoot($display_builder_instance, $preset_id, $position);
61    }
62
63    $is_move = FALSE;
64
65    if ($request->request->has('node_id')) {
66      $node_id = (string) $request->request->get('node_id');
67
68      if (!$display_builder_instance->moveToRoot($node_id, $position)) {
69        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
70        $debug = [
71          'request' => $request->request->all(),
72          'instance' => $display_builder_instance->toArray(),
73        ];
74
75        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
76      }
77
78      $is_move = TRUE;
79    }
80    elseif ($request->request->has('source_id')) {
81      $source_id = (string) $request->request->get('source_id');
82      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
83      $node_id = $display_builder_instance->attachToRoot($position, $source_id, $data);
84    }
85    else {
86      $message = '[attachToRoot] Missing content (source_id, node_id or preset_id)';
87      $debug = [
88        'request' => $request->request->all(),
89        'instance' => $display_builder_instance->toArray(),
90      ];
91
92      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
93    }
94    $display_builder_instance->save();
95
96    $this->builder = $display_builder_instance;
97    // Let's refresh when we add new source to get the placeholder replacement.
98    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
99
100    return $this->dispatchDisplayBuilderEvent(
101      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
102      NULL,
103      $node_id,
104    );
105  }
106
107  /**
108   * {@inheritdoc}
109   */
110  public function attachToSlot(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $slot): array {
111    $parent_id = $node_id;
112    $position = (int) $request->request->get('position', 0);
113
114    if ($request->request->has('preset_id')) {
115      $preset_id = (string) $request->request->get('preset_id');
116
117      return $this->attachPresetToSlot($display_builder_instance, $preset_id, $parent_id, $slot, $position);
118    }
119
120    $is_move = FALSE;
121
122    // First, we update the data state.
123    if ($request->request->has('node_id')) {
124      $node_id = (string) $request->request->get('node_id');
125
126      if (!$display_builder_instance->moveToSlot($node_id, $parent_id, $slot, $position)) {
127        $message = $this->t('[attachToSlot] moveToSlot failed with invalid data');
128        $debug = [
129          'node_id' => $node_id,
130          'slot' => $slot,
131          'request' => $request->request->all(),
132          'instance' => $display_builder_instance->toArray(),
133        ];
134
135        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
136      }
137
138      $is_move = TRUE;
139    }
140    elseif ($request->request->has('source_id')) {
141      $source_id = (string) $request->request->get('source_id');
142      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
143      $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $source_id, $data);
144    }
145    else {
146      $message = $this->t('[attachToSlot] Missing content (component_id, block_id or node_id)');
147      $debug = [
148        'node_id' => $node_id,
149        'slot' => $slot,
150        'request' => $request->request->all(),
151        'instance' => $display_builder_instance->toArray(),
152      ];
153
154      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
155    }
156    $display_builder_instance->save();
157
158    $this->builder = $display_builder_instance;
159    // Let's refresh when we add new source to get the placeholder replacement.
160    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
161
162    return $this->dispatchDisplayBuilderEvent(
163      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
164      NULL,
165      $node_id,
166      $parent_id,
167    );
168  }
169
170  /**
171   * {@inheritdoc}
172   */
173  public function get(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
174    $this->builder = $display_builder_instance;
175
176    return $this->dispatchDisplayBuilderEvent(
177      DisplayBuilderEvents::ON_ACTIVE,
178      $display_builder_instance->getNode($node_id),
179    );
180  }
181
182  /**
183   * {@inheritdoc}
184   */
185  public function update(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
186    $this->builder = $display_builder_instance;
187    $body = $request->getPayload()->all();
188
189    if (!isset($body['form_id'])) {
190      $message = $this->t('[update] Missing payload!');
191      $debug = [
192        'node_id' => $node_id,
193        'request' => $request->request->all(),
194        'body' => $body,
195        'instance' => $display_builder_instance->toArray(),
196      ];
197
198      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
199    }
200
201    // Load the node to properly alter the form data into config data.
202    $node = $display_builder_instance->getNode($node_id);
203
204    if (isset($body['source']['form_build_id'])) {
205      unset($body['source']['form_build_id'], $body['source']['form_token'], $body['source']['form_id']);
206    }
207
208    if (isset($body['form_build_id'])) {
209      unset($body['form_build_id'], $body['form_token'], $body['form_id']);
210    }
211    $form_state = new FormState();
212    // Default values are the existing values from the state.
213    $form_state->addBuildInfo('args', [
214      [
215        'island_id' => 'contextual_form',
216        'builder_id' => (string) $display_builder_instance->id(),
217        'instance' => $node,
218      ],
219      $display_builder_instance->getContexts(),
220    ]);
221    $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getContexts());
222    // The body received corresponds to raw form values.
223    // We need to set them in the form state to properly
224    // take them into account.
225    $form_state->setValues($body);
226
227    $formClass = ContextualFormPanel::getFormClass();
228    $data = [];
229
230    try {
231      $values = $this->validateIslandForm($formClass, $form_state);
232      $data['source'] = $values;
233    }
234    catch (FormAjaxException $e) {
235      throw $e;
236    }
237    catch (\Exception $e) {
238      $debug = [
239        'node_id' => $node_id,
240        'request' => $request->request->all(),
241        'form' => $form_state->getValues(),
242        'body' => $body,
243        'instance' => $display_builder_instance->toArray(),
244      ];
245
246      return $this->responseMessageError((string) $display_builder_instance->id(), $e->getMessage(), $debug);
247    }
248
249    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
250    $current_source = $this->sourceManager->createInstance(
251      $node['source_id'],
252      SourcePluginBase::buildConfiguration('slot', $slot_definition, $node, [])
253    );
254
255    if ($current_source instanceof SourceWithSlotsInterface) {
256      /** @var \Drupal\display_builder\SourceWithSlotsInterface $new_source */
257      $new_source = $this->sourceManager->createInstance(
258        $node['source_id'],
259        SourcePluginBase::buildConfiguration('slot', $slot_definition, $data, [])
260      );
261      // We keep the slots values (which are not sent by the contextual form)
262      // instead of removing them.
263      $slots = $current_source->getSlotValues();
264
265      foreach ($slots as $slot_id => $slot) {
266        $data['source'] = $new_source->setSlotValue($slot_id, $slot);
267      }
268    }
269
270    $display_builder_instance->setSource($node_id, $node['source_id'], $data['source']);
271    $display_builder_instance->save();
272
273    $this->builder = $display_builder_instance;
274    $this->islandId = (string) $request->query->get('from', NULL);
275
276    return $this->dispatchDisplayBuilderEvent(
277      DisplayBuilderEvents::ON_UPDATE,
278      NULL,
279      $node_id,
280    );
281  }
282
283  /**
284   * {@inheritdoc}
285   */
286  public function thirdPartySettingsUpdate(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $island_id): array {
287    $body = $request->getPayload()->all();
288
289    if (!isset($body['form_id'])) {
290      $message = $this->t('[thirdPartySettingsUpdate] Missing payload!');
291      $debug = [
292        'node_id' => $node_id,
293        'island_id' => $island_id,
294        'request' => $request->request->all(),
295        'body' => $body,
296        'instance' => $display_builder_instance->toArray(),
297      ];
298
299      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
300    }
301
302    $islandDefinition = $this->islandPluginManager->getDefinition($island_id);
303    // Load the instance to properly alter the form data into config data.
304    $node = $display_builder_instance->getNode($node_id);
305    unset($body['form_build_id'], $body['form_token'], $body['form_id']);
306
307    $form_state = new FormState();
308    // Default values are the existing values from the state.
309    $form_state->addBuildInfo('args', [
310      [
311        'island_id' => $island_id,
312        'builder_id' => (string) $display_builder_instance->id(),
313        'instance' => $node,
314      ],
315      [],
316    ]);
317
318    // phpcs:disable Drupal.Files.LineLength.TooLong
319    // @todo should context be injected for third party settings?
320    // $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getContexts());
321    // phpcs:enable Drupal.Files.LineLength.TooLong
322    // The body received corresponds to raw form values.
323    // We need to set them in the form state to properly
324    // take them into account.
325    $form_state->setValues($body);
326
327    $formClass = ($islandDefinition['class'])::getFormClass();
328    $values = $this->validateIslandForm($formClass, $form_state);
329    // We update the state with the new data.
330    $display_builder_instance->setThirdPartySettings($node_id, $island_id, $values);
331    $display_builder_instance->save();
332
333    $this->builder = $display_builder_instance;
334    $this->islandId = $island_id;
335
336    return $this->dispatchDisplayBuilderEvent(
337      DisplayBuilderEvents::ON_UPDATE,
338      NULL,
339      $node_id,
340      NULL,
341    );
342  }
343
344  /**
345   * {@inheritdoc}
346   */
347  public function paste(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $parent_id, string $slot_id, string $slot_position): array {
348    $this->builder = $display_builder_instance;
349    $dataToCopy = $display_builder_instance->getNode($node_id);
350
351    // Keep flag for move or attach to root.
352    $is_paste_root = FALSE;
353
354    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
355      $source_id = $dataToCopy['source_id'];
356      // Use reference to ensure modifications by recursiveRefreshNodeId
357      // persist.
358      $data = &$dataToCopy['source'];
359
360      // Refresh nested node_ids.
361      self::recursiveRefreshNodeId($data);
362
363      // If no parent we are on root.
364      // @todo for duplicate and not parent root seems not detected and copy is inside the slot.
365      if ($parent_id === '__root__') {
366        $is_paste_root = TRUE;
367        $display_builder_instance->attachToRoot(0, $source_id, $data, $dataToCopy['third_party_settings'] ?? []);
368      }
369      else {
370        $display_builder_instance->attachToSlot($parent_id, $slot_id, (int) $slot_position, $source_id, $data, $dataToCopy['third_party_settings'] ?? []);
371      }
372    }
373    $display_builder_instance->save();
374
375    $this->builder = $display_builder_instance;
376
377    return $this->dispatchDisplayBuilderEvent(
378      $is_paste_root ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
379      NULL,
380      $parent_id,
381    );
382  }
383
384  /**
385   * {@inheritdoc}
386   */
387  public function delete(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
388    $parent_id = $display_builder_instance->getParentId($node_id);
389    $display_builder_instance->remove($node_id);
390    $display_builder_instance->save();
391    $this->builder = $display_builder_instance;
392
393    return $this->dispatchDisplayBuilderEvent(
394      DisplayBuilderEvents::ON_DELETE,
395      NULL,
396      $node_id,
397      $parent_id
398    );
399  }
400
401  /**
402   * {@inheritdoc}
403   */
404  public function saveAsPreset(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
405    $label = (string) $this->t('New preset');
406    $data = $display_builder_instance->getNode($node_id);
407    self::cleanNodeId($data);
408
409    $preset_storage = $this->entityTypeManager()->getStorage('pattern_preset');
410    $label = $request->headers->get('hx-prompt', $label) ?: $label;
411    // In HTTP headers, only ASCII is guaranteed to work but historically,
412    // HTTP has allowed header values with the ISO-8859-1 charset.
413    $label = \mb_convert_encoding($label, 'UTF-8', 'ISO-8859-1');
414    $preset = $preset_storage->create([
415      'id' => \uniqid(),
416      'label' => $label,
417      'status' => TRUE,
418      'description' => '',
419      'sources' => $data,
420    ]);
421    $preset->save();
422
423    $this->builder = $display_builder_instance;
424
425    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_PRESET_SAVE);
426  }
427
428  /**
429   * {@inheritdoc}
430   */
431  public function save(Request $request, InstanceInterface $display_builder_instance): array {
432    $display_builder_instance->setSave($display_builder_instance->getCurrentState());
433    $display_builder_instance->save();
434
435    $this->builder = $display_builder_instance;
436
437    return $this->dispatchDisplayBuilderEvent(
438      DisplayBuilderEvents::ON_SAVE,
439      $display_builder_instance->getContexts()
440    );
441  }
442
443  /**
444   * {@inheritdoc}
445   */
446  public function restore(Request $request, InstanceInterface $display_builder_instance): array {
447    $display_builder_instance->restore();
448    $display_builder_instance->save();
449
450    $this->builder = $display_builder_instance;
451
452    // @todo on history change is closest to a data change that we need here
453    // without any instance id. Perhaps we need a new event?
454    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
455  }
456
457  /**
458   * {@inheritdoc}
459   */
460  public function revert(Request $request, InstanceInterface $display_builder_instance): array {
461    $instanceInfos = EntityViewOverride::checkInstanceId((string) $display_builder_instance->id());
462
463    if (isset($instanceInfos['entity_type_id'], $instanceInfos['entity_id'], $instanceInfos['field_name'])) {
464      // Do not get the profile entity ID from Instance context because the
465      // data stored there is not reliable yet.
466      // See: https://www.drupal.org/project/display_builder/issues/3544545
467      $entity = $this->entityTypeManager()->getStorage($instanceInfos['entity_type_id'])
468        ->load($instanceInfos['entity_id']);
469
470      if ($entity instanceof FieldableEntityInterface) {
471        // Remove the saved state as the field values will be deleted.
472        $display_builder_instance->setNewPresent([], 'Revert 1/2: clear overridden data and save');
473        $display_builder_instance->save();
474        $display_builder_instance->setSave($display_builder_instance->getCurrentState());
475
476        // Clear field value.
477        $field = $entity->get($instanceInfos['field_name']);
478        $field->setValue(NULL);
479        $entity->save();
480
481        $contexts = $display_builder_instance->get('contexts')->first()->getValue();
482
483        if (isset($contexts['view_mode'])
484          && $contexts['view_mode'] instanceof ContextInterface
485        ) {
486          $viewMode = $contexts['view_mode']->getContextValue();
487          $display_id = "{$instanceInfos['entity_type_id']}.{$entity->bundle()}.{$viewMode}";
488
489          /** @var \Drupal\display_builder\DisplayBuildableInterface|null $display */
490          $display = $this->entityTypeManager()->getStorage('entity_view_display')
491            ->load($display_id);
492
493          $sources = $display->getSources();
494          $display_builder_instance->setNewPresent($sources, 'Revert 2/2: retrieve existing data from config');
495          $display_builder_instance->save();
496        }
497      }
498    }
499
500    $this->builder = $display_builder_instance;
501
502    // @todo on history change is closest to a data change that we need here
503    // without any instance id. Perhaps we need a new event?
504    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
505  }
506
507  /**
508   * {@inheritdoc}
509   */
510  public function undo(Request $request, InstanceInterface $display_builder_instance): array {
511    $display_builder_instance->undo();
512    $display_builder_instance->save();
513
514    $this->builder = $display_builder_instance;
515
516    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
517  }
518
519  /**
520   * {@inheritdoc}
521   */
522  public function redo(Request $request, InstanceInterface $display_builder_instance): array {
523    $display_builder_instance->redo();
524    $display_builder_instance->save();
525
526    $this->builder = $display_builder_instance;
527
528    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
529  }
530
531  /**
532   * {@inheritdoc}
533   */
534  public function clear(Request $request, InstanceInterface $display_builder_instance): array {
535    $display_builder_instance->clear();
536    $display_builder_instance->save();
537
538    $this->builder = $display_builder_instance;
539
540    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
541  }
542
543  /**
544   * Attach a pattern preset to root.
545   *
546   * Presets are "resolved" after attachment, so they are never moved around.
547   *
548   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
549   *   Display builder instance.
550   * @param string $preset_id
551   *   Pattern preset ID.
552   * @param int $position
553   *   Position.
554   *
555   * @return array
556   *   A renderable array.
557   */
558  protected function attachPresetToRoot(InstanceInterface $display_builder_instance, string $preset_id, int $position): array {
559    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
560
561    /** @var \Drupal\display_builder\PatternPresetInterface $preset */
562    $preset = $presetStorage->load($preset_id);
563    $data = $preset->getSources();
564
565    if (!isset($data['source_id']) || !isset($data['source'])) {
566      $message = $this->t('[attachToRoot] Missing preset source_id data');
567      $debug = [
568        'preset_id' => $preset_id,
569        'position' => $position,
570        'data' => $data,
571        'instance' => $display_builder_instance->toArray(),
572      ];
573
574      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
575    }
576    $node_id = $display_builder_instance->attachToRoot($position, $data['source_id'], $data['source']);
577
578    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
579      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
580    }
581    $display_builder_instance->save();
582    $this->builder = $display_builder_instance;
583
584    return $this->dispatchDisplayBuilderEvent(
585      DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
586      NULL,
587      $node_id,
588    );
589  }
590
591  /**
592   * Attach a pattern preset to a slot .
593   *
594   * Presets are "resolved" after attachment, so they are never moved around.
595   *
596   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
597   *   Display builder instance.
598   * @param string $preset_id
599   *   Pattern preset ID.
600   * @param string $parent_id
601   *   Parent instance ID.
602   * @param string $slot
603   *   Slot.
604   * @param int $position
605   *   Position.
606   *
607   * @return array
608   *   A renderable array.
609   */
610  protected function attachPresetToSlot(InstanceInterface $display_builder_instance, string $preset_id, string $parent_id, string $slot, int $position): array {
611    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
612
613    /** @var \Drupal\display_builder\PatternPresetInterface $preset */
614    $preset = $presetStorage->load($preset_id);
615    $data = $preset->getSources();
616
617    if (!isset($data['source_id']) || !isset($data['source'])) {
618      $message = $this->t('[attachToSlot] Missing preset source_id data');
619      $debug = [
620        'preset_id' => $preset_id,
621        'parent_id' => $parent_id,
622        'slot' => $slot,
623        'position' => $position,
624        'data' => $data,
625        'instance' => $display_builder_instance->toArray(),
626      ];
627
628      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
629    }
630    $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $data['source_id'], $data['source']);
631
632    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
633      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
634    }
635
636    $display_builder_instance->save();
637    $this->builder = $display_builder_instance;
638
639    return $this->dispatchDisplayBuilderEvent(
640      DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
641      NULL,
642      $node_id,
643    );
644  }
645
646  /**
647   * Dispatches a display builder event.
648   *
649   * @param string $event_id
650   *   The event ID.
651   * @param array|null $data
652   *   The data.
653   * @param string|null $node_id
654   *   Optional instance ID.
655   * @param string|null $parent_id
656   *   Optional parent ID.
657   *
658   * @return array
659   *   A renderable array.
660   */
661  protected function dispatchDisplayBuilderEvent(
662    string $event_id,
663    ?array $data = NULL,
664    ?string $node_id = NULL,
665    ?string $parent_id = NULL,
666  ): array {
667    $event = $this->createEventWithEnabledIsland($event_id, $data, $node_id, $parent_id);
668    $this->saveSseData($event_id);
669
670    return $event->getResult();
671  }
672
673  /**
674   * Validates an island form.
675   *
676   * @param string $formClass
677   *   The form class.
678   * @param \Drupal\Core\Form\FormStateInterface $form_state
679   *   The form state.
680   *
681   * @return array
682   *   The validated values.
683   */
684  private function validateIslandForm(string $formClass, FormStateInterface $form_state): array {
685    /** @var \Drupal\Core\Form\FormBuilder $formBuilder */
686    $formBuilder = $this->formBuilder();
687
688    try {
689      $triggering_element = $form_state->getTriggeringElement();
690
691      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
692        // We set a fake triggering element to avoid form API error.
693        $form_state->setTriggeringElement([
694          '#type' => 'submit',
695          '#limit_validation_errors' => FALSE,
696          '#value' => (string) $this->t('Submit'),
697        ]);
698      }
699      $form = $formBuilder->buildForm($formClass, $form_state);
700      $formErrors = $form_state->getErrors();
701
702      if (!empty($formErrors)) {
703        $first_error = \reset($formErrors);
704
705        throw new \Exception((string) $first_error);
706      }
707      $formBuilder->validateForm($formClass, $form, $form_state);
708      $formErrors = $form_state->getErrors();
709
710      if (!empty($formErrors)) {
711        $first_error = \reset($formErrors);
712
713        throw new \Exception((string) $first_error);
714      }
715    }
716    catch (FormAjaxException $e) {
717      throw $e;
718    }
719    // Those values are the validated values, produced by the form.
720    // with all Form API processing.
721    $values = $form_state->getValues();
722
723    // We clean the values from form API keys.
724    if (isset($values['form_build_id'])) {
725      unset($values['form_build_id'], $values['form_token'], $values['form_id']);
726    }
727
728    return $values;
729  }
730
731  /**
732   * Render an error message in the display builder.
733   *
734   * @param string $display_builder_instance_id
735   *   The builder ID.
736   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
737   *   The error message.
738   * @param array $debug
739   *   The debug code related to the error.
740   *
741   * @return array
742   *   A renderable array.
743   */
744  private function responseMessageError(
745    string $display_builder_instance_id,
746    string|TranslatableMarkup $message,
747    array $debug,
748  ): array {
749    // Reduce verbosity.
750    unset($debug['request']['ajax_page_state']['libraries'], $debug['instance']['contexts'], $debug['instance']['past'], $debug['instance']['future'], $debug['instance']['save']);
751
752    $this->getLogger('display_builder')->error('@message <pre>@debug</pre>', [
753      '@message' => $message,
754      '@debug' => \print_r($debug, TRUE),
755    ]);
756
757    $message = new TranslatableMarkup('Error: @error, check logs for more details.', ['@error' => $message]);
758
759    return $this->buildError($display_builder_instance_id, $message, TRUE);
760  }
761
762  /**
763   * Recursively regenerate the node_id key.
764   *
765   * @param array $array
766   *   The array reference.
767   */
768  private static function recursiveRefreshNodeId(array &$array): void {
769    if (isset($array['node_id'])) {
770      $array['node_id'] = \bin2hex(\random_bytes(8));
771    }
772
773    foreach ($array as &$value) {
774      if (\is_array($value)) {
775        self::recursiveRefreshNodeId($value);
776      }
777    }
778  }
779
780  /**
781   * Recursively regenerate the node_id key.
782   *
783   * @param array $array
784   *   The array reference.
785   *
786   * @todo set as utils because clone in ExportForm.php?
787   */
788  private static function cleanNodeId(array &$array): void {
789    unset($array['node_id']);
790
791    foreach ($array as $key => &$value) {
792      if (\is_array($value)) {
793        self::cleanNodeId($value);
794
795        if (isset($value['source_id'], $value['source']['value']) && empty($value['source']['value'])) {
796          unset($array[$key]);
797        }
798      }
799    }
800  }
801
802}