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