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}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

ApiController->attachPresetToRoot
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'])) {
473    if (!isset($data['source_id']) || !isset($data['source'])) {
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);
484    $node_id = $display_builder_instance->attachToRoot($position, $data['source_id'], $data['source']);
485
486    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
486    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
486    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
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  }
ApiController->attachPresetToSlot
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'])) {
525    if (!isset($data['source_id']) || !isset($data['source'])) {
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);
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) {
540    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
540    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
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  }
ApiController->attachToRoot
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);
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);
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;
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) : [];
76      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
76      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
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) : [];
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);
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;
92    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
92    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
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,
95      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
95      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
95      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
96      NULL,
97      $node_id,
98    );
99  }
ApiController->attachToSlot
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);
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);
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;
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) : [];
136      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
136      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
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) : [];
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);
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;
154    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
154    $this->islandId = $is_move ? (string) $request->query->get('from', NULL) : NULL;
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,
157      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
157      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
157      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
158      NULL,
159      $node_id,
160      $parent_id,
161    );
162  }
ApiController->cleanPreset
677  private static function cleanPreset(array &$array): void {
678    unset($array['node_id']);
679
680    foreach ($array as $key => &$value) {
680    foreach ($array as $key => &$value) {
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'] === '') {
685        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
685        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
685        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
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)) {
690      if ($key === 'extra' && empty($value)) {
690      if ($key === 'extra' && empty($value)) {
690      if ($key === 'extra' && empty($value)) {
691        unset($array[$key]);
692      }
693
694      if ($key === 'third_party_settings' && empty($value)) {
694      if ($key === 'third_party_settings' && empty($value)) {
694      if ($key === 'third_party_settings' && empty($value)) {
694      if ($key === 'third_party_settings' && empty($value)) {
695        unset($array[$key]);
696      }
697
698      if ($key === 'variant_id' && $value === NULL) {
698      if ($key === 'variant_id' && $value === NULL) {
698      if ($key === 'variant_id' && $value === NULL) {
698      if ($key === 'variant_id' && $value === NULL) {
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]);
680    foreach ($array as $key => &$value) {
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  }
ApiController->clear
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  }
ApiController->delete
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  }
ApiController->get
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  }
ApiController->paste
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'])) {
328    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
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__') {
339      if ($parent_id === '__root__') {
340        $is_paste_root = TRUE;
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();
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,
352      $is_paste_root ? DisplayBuilderEvents::ON_ATTACH_TO_ROOT : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
352      $is_paste_root ? DisplayBuilderEvents::ON_ATTACH_TO_ROOT : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
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,
355      $is_paste_root ? NULL : $parent_id,
355      $is_paste_root ? NULL : $parent_id,
355      $is_paste_root ? NULL : $parent_id,
356    );
357  }
ApiController->recursiveRefreshNodeId
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) {
662    foreach ($array as &$value) {
662    foreach ($array as &$value) {
663      if (\is_array($value)) {
662    foreach ($array as &$value) {
663      if (\is_array($value)) {
664        self::recursiveRefreshNodeId($value);
662    foreach ($array as &$value) {
662    foreach ($array as &$value) {
663      if (\is_array($value)) {
664        self::recursiveRefreshNodeId($value);
665      }
666    }
667  }
ApiController->redo
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  }
ApiController->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>', [
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  }
ApiController->saveAsPreset
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) {
397    while ($preset_storage->load($id) !== NULL) {
398      $id = $base_id . '_' . $suffix++;
397    while ($preset_storage->load($id) !== NULL) {
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  }
ApiController->thirdPartySettingsUpdate
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);
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  }
ApiController->undo
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  }
ApiController->update
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);
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'])) {
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();
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;
228    catch (FormAjaxException $e) {
229      throw $e;
231    catch (\Exception $e) {
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);
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  }
ApiController->validateIslandForm
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'])) {
572      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
572      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
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);
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);
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);
594        throw new FormValidationException((string) $first_error);
597    catch (FormAjaxException $e) {
598      throw $e;
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;
609    return $values;
610  }