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