Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
32.75% covered (danger)
32.75%
94 / 287
39.18% covered (danger)
39.18%
38 / 97
16.22% covered (danger)
16.22%
12 / 74
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiController
32.75% covered (danger)
32.75%
94 / 287
39.18% covered (danger)
39.18%
38 / 97
16.22% covered (danger)
16.22%
12 / 74
46.67% covered (danger)
46.67%
7 / 15
1580.75
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
 reloadIsland
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 attachToRoot
57.58% covered (warning)
57.58%
19 / 33
75.00% covered (warning)
75.00%
12 / 16
22.22% covered (danger)
22.22%
2 / 9
0.00% covered (danger)
0.00%
0 / 1
30.05
 attachToSlot
46.15% covered (danger)
46.15%
18 / 39
56.25% covered (warning)
56.25%
9 / 16
11.11% covered (danger)
11.11%
1 / 9
0.00% covered (danger)
0.00%
0 / 1
41.41
 get
100.00% covered (success)
100.00%
5 / 5
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
 update
21.57% covered (danger)
21.57%
11 / 51
16.67% covered (danger)
16.67%
2 / 12
14.29% covered (danger)
14.29%
1 / 7
0.00% covered (danger)
0.00%
0 / 1
28.67
 thirdPartySettingsUpdate
30.56% covered (danger)
30.56%
11 / 36
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 undo
100.00% covered (success)
100.00%
4 / 4
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
 redo
100.00% covered (success)
100.00%
4 / 4
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
 clear
100.00% covered (success)
100.00%
4 / 4
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
 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
 resolveSkipIslandId
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
6 / 8
40.00% covered (danger)
40.00%
2 / 5
100.00% covered (success)
100.00%
1 / 1
7.46
 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
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
3 / 3
50.00% covered (danger)
50.00%
1 / 2
100.00% covered (success)
100.00%
1 / 1
2.50
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\RequestStack;
24use Symfony\Component\HttpFoundation\Session\SessionInterface;
25use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
26
27/**
28 * Returns responses for Display builder routes.
29 */
30class ApiController extends ApiControllerBase implements ApiControllerInterface {
31
32  use RenderableBuilderTrait;
33
34  public function __construct(
35    protected EventDispatcherInterface $eventDispatcher,
36    protected RendererInterface $renderer,
37    protected TimeInterface $time,
38    #[Autowire(service: 'tempstore.shared')]
39    protected SharedTempStoreFactory $sharedTempStoreFactory,
40    protected SessionInterface $session,
41    protected RequestStack $requestStack,
42    private IslandPluginManagerInterface $islandPluginManager,
43  ) {
44    parent::__construct($eventDispatcher, $renderer, $time, $sharedTempStoreFactory, $session, $requestStack);
45  }
46
47  /**
48   * {@inheritdoc}
49   */
50  public function reloadIsland(Request $request, InstanceInterface $display_builder_instance, string $island_id): array {
51    $profile = $display_builder_instance->getProfile();
52
53    if (!isset($profile->getEnabledIslands()[$island_id])) {
54      $message = $this->t('[reloadIsland] Island @island is not enabled on this profile', ['@island' => $island_id]);
55      $debug = [
56        'island_id' => $island_id,
57        'instance' => $display_builder_instance,
58      ];
59
60      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
61    }
62
63    $contexts = $display_builder_instance->getAvailableContexts();
64    $definitions = \array_intersect_key(
65      $this->islandPluginManager->getDefinitions(),
66      [$island_id => TRUE],
67    );
68    $islands = $this->islandPluginManager->createInstances($definitions, $contexts, $profile->getIslandConfigurations());
69    $island = $islands[$island_id] ?? NULL;
70
71    if ($island === NULL) {
72      $message = $this->t('[reloadIsland] Island @island is not available in this context', ['@island' => $island_id]);
73      $debug = [
74        'island_id' => $island_id,
75        'instance' => $display_builder_instance,
76      ];
77
78      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
79    }
80
81    return [$island->reload($display_builder_instance)];
82  }
83
84  /**
85   * {@inheritdoc}
86   */
87  public function attachToRoot(Request $request, InstanceInterface $display_builder_instance): array {
88    $position = (int) $request->request->get('position', 0);
89
90    if ($request->request->has('preset_id')) {
91      $preset_id = (string) $request->request->get('preset_id');
92
93      return $this->attachPresetToRoot($display_builder_instance, $preset_id, $position);
94    }
95
96    $is_move = FALSE;
97
98    if ($request->request->has('node_id')) {
99      $node_id = (string) $request->request->get('node_id');
100
101      if (!$display_builder_instance->moveToRoot($node_id, $position)) {
102        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
103        $debug = [
104          'request' => $request->request->all(),
105          'instance' => $display_builder_instance,
106        ];
107
108        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
109      }
110
111      $is_move = TRUE;
112    }
113    elseif ($request->request->has('source_id')) {
114      $source_id = (string) $request->request->get('source_id');
115      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
116      $node_id = $display_builder_instance->attachToRoot($position, $source_id, $data);
117    }
118    else {
119      $message = '[attachToRoot] Missing content (source_id, node_id or preset_id)';
120      $debug = [
121        'request' => $request->request->all(),
122        'instance' => $display_builder_instance,
123      ];
124
125      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
126    }
127    $display_builder_instance->save();
128
129    $this->builder = $display_builder_instance;
130    // Let's refresh when we add new source to get the placeholder replacement.
131    $this->islandId = $this->resolveSkipIslandId($request, $is_move);
132
133    return $this->dispatchDisplayBuilderEvent(
134      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
135      NULL,
136      $node_id,
137    );
138  }
139
140  /**
141   * {@inheritdoc}
142   */
143  public function attachToSlot(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $slot): array {
144    $parent_id = $node_id;
145    $position = (int) $request->request->get('position', 0);
146
147    if ($request->request->has('preset_id')) {
148      $preset_id = (string) $request->request->get('preset_id');
149
150      return $this->attachPresetToSlot($display_builder_instance, $preset_id, $parent_id, $slot, $position);
151    }
152
153    $is_move = FALSE;
154
155    // First, we update the data state.
156    if ($request->request->has('node_id')) {
157      $node_id = (string) $request->request->get('node_id');
158
159      if (!$display_builder_instance->moveToSlot($node_id, $parent_id, $slot, $position)) {
160        $message = $this->t('[attachToSlot] moveToSlot failed with invalid data');
161        $debug = [
162          'node_id' => $node_id,
163          'slot' => $slot,
164          'request' => $request->request->all(),
165          'instance' => $display_builder_instance,
166        ];
167
168        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
169      }
170
171      $is_move = TRUE;
172    }
173    elseif ($request->request->has('source_id')) {
174      $source_id = (string) $request->request->get('source_id');
175      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
176      $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $source_id, $data);
177    }
178    else {
179      $message = $this->t('[attachToSlot] Missing content (component_id, block_id or node_id)');
180      $debug = [
181        'node_id' => $node_id,
182        'slot' => $slot,
183        'request' => $request->request->all(),
184        'instance' => $display_builder_instance,
185      ];
186
187      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
188    }
189    $display_builder_instance->save();
190
191    $this->builder = $display_builder_instance;
192    // Let's refresh when we add new source to get the placeholder replacement.
193    $this->islandId = $this->resolveSkipIslandId($request, $is_move);
194
195    return $this->dispatchDisplayBuilderEvent(
196      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
197      NULL,
198      $node_id,
199      $parent_id,
200    );
201  }
202
203  /**
204   * {@inheritdoc}
205   */
206  public function get(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
207    $this->builder = $display_builder_instance;
208
209    return $this->dispatchDisplayBuilderEvent(
210      DisplayBuilderEvents::ON_ACTIVE,
211      $display_builder_instance->getNode($node_id),
212    );
213  }
214
215  /**
216   * {@inheritdoc}
217   */
218  public function update(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
219    $this->builder = $display_builder_instance;
220    $body = $request->getPayload()->all();
221
222    if (!isset($body['form_id'])) {
223      $message = $this->t('[update] Missing payload!');
224      $debug = [
225        'node_id' => $node_id,
226        'request' => $request->request->all(),
227        'body' => $body,
228        'instance' => $display_builder_instance,
229      ];
230
231      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
232    }
233
234    // Load the node to properly alter the form data into config data.
235    $node = $display_builder_instance->getNode($node_id);
236
237    if (isset($body['source']['form_build_id'])) {
238      unset($body['source']['form_build_id'], $body['source']['form_token'], $body['source']['form_id']);
239    }
240
241    if (isset($body['form_build_id'])) {
242      unset($body['form_build_id'], $body['form_token'], $body['form_id']);
243    }
244    $form_state = new FormState();
245    // Default values are the existing values from the state.
246    $form_state->addBuildInfo('args', [
247      [
248        'island_id' => 'contextual_form',
249        'builder_id' => (string) $display_builder_instance->id(),
250        'instance' => $node,
251      ],
252      $display_builder_instance->getAvailableContexts(),
253    ]);
254    $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getAvailableContexts());
255    // The body received corresponds to raw form values.
256    // We need to set them in the form state to properly
257    // take them into account.
258    $form_state->setValues($body);
259
260    $formClass = ContextualFormPanel::getFormClass();
261    $data = [];
262
263    try {
264      $values = $this->validateIslandForm($formClass, $form_state);
265      $data['source'] = $values;
266    }
267    catch (FormAjaxException $e) {
268      throw $e;
269    }
270    catch (\Exception $e) {
271      $debug = [
272        'node_id' => $node_id,
273        'request' => $request->request->all(),
274        'form' => $form_state->getValues(),
275        'body' => $body,
276        'instance' => $display_builder_instance,
277      ];
278
279      return $this->responseMessageError((string) $display_builder_instance->id(), $e->getMessage(), $debug);
280    }
281
282    $display_builder_instance->setSource($node_id, $node['source_id'], $data['source']);
283    $display_builder_instance->save();
284
285    $this->builder = $display_builder_instance;
286    $this->islandId = (string) $request->query->get('from', NULL);
287
288    return $this->dispatchDisplayBuilderEvent(
289      DisplayBuilderEvents::ON_UPDATE,
290      NULL,
291      $node_id,
292    );
293  }
294
295  /**
296   * {@inheritdoc}
297   */
298  public function thirdPartySettingsUpdate(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $island_id): array {
299    $body = $request->getPayload()->all();
300
301    if (!isset($body['form_id'])) {
302      $message = $this->t('[thirdPartySettingsUpdate] Missing payload!');
303      $debug = [
304        'node_id' => $node_id,
305        'island_id' => $island_id,
306        'request' => $request->request->all(),
307        'body' => $body,
308        'instance' => $display_builder_instance,
309      ];
310
311      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
312    }
313
314    $islandDefinition = $this->islandPluginManager->getDefinition($island_id);
315    // Load the instance to properly alter the form data into config data.
316    $node = $display_builder_instance->getNode($node_id);
317    unset($body['form_build_id'], $body['form_token'], $body['form_id']);
318
319    $form_state = new FormState();
320    // Default values are the existing values from the state.
321    $form_state->addBuildInfo('args', [
322      [
323        'island_id' => $island_id,
324        'builder_id' => (string) $display_builder_instance->id(),
325        'instance' => $node,
326      ],
327      [],
328    ]);
329
330    // phpcs:disable Drupal.Files.LineLength.TooLong
331    // @todo should context be injected for third party settings?
332    // $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getAvailableContexts());
333    // phpcs:enable Drupal.Files.LineLength.TooLong
334    // The body received corresponds to raw form values.
335    // We need to set them in the form state to properly
336    // take them into account.
337    $form_state->setValues($body);
338
339    $formClass = ($islandDefinition['class'])::getFormClass();
340    $values = $this->validateIslandForm($formClass, $form_state);
341    // We update the state with the new data.
342    $display_builder_instance->setThirdPartySettings($node_id, $island_id, $values);
343    $display_builder_instance->save();
344
345    $this->builder = $display_builder_instance;
346    $this->islandId = $island_id;
347
348    return $this->dispatchDisplayBuilderEvent(
349      DisplayBuilderEvents::ON_UPDATE,
350      NULL,
351      $node_id,
352      NULL,
353    );
354  }
355
356  /**
357   * {@inheritdoc}
358   */
359  public function undo(Request $request, InstanceInterface $display_builder_instance): array {
360    /** @var \Drupal\display_builder\Entity\InstanceStorage $storage */
361    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
362    /** @var \Drupal\display_builder\InstanceInterface $display_builder_instance */
363    $display_builder_instance = $storage->undo($display_builder_instance);
364
365    $this->builder = $display_builder_instance;
366
367    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
368  }
369
370  /**
371   * {@inheritdoc}
372   */
373  public function redo(Request $request, InstanceInterface $display_builder_instance): array {
374    /** @var \Drupal\display_builder\Entity\InstanceStorage $storage */
375    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
376    /** @var \Drupal\display_builder\InstanceInterface $display_builder_instance */
377    $display_builder_instance = $storage->redo($display_builder_instance);
378
379    $this->builder = $display_builder_instance;
380
381    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
382  }
383
384  /**
385   * {@inheritdoc}
386   */
387  public function clear(Request $request, InstanceInterface $display_builder_instance): array {
388    $display_builder_instance->clear();
389    $display_builder_instance->save();
390
391    $this->builder = $display_builder_instance;
392
393    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
394  }
395
396  /**
397   * Attach a pattern preset to root.
398   *
399   * Presets are "resolved" after attachment, so they are never moved around.
400   *
401   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
402   *   Display builder instance.
403   * @param string $preset_id
404   *   Pattern preset ID.
405   * @param int $position
406   *   Position.
407   *
408   * @return array
409   *   A renderable array.
410   */
411  protected function attachPresetToRoot(InstanceInterface $display_builder_instance, string $preset_id, int $position): array {
412    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
413
414    /** @var \Drupal\display_builder\Entity\PatternPresetInterface $preset */
415    $preset = $presetStorage->load($preset_id);
416    $data = $preset->getSources();
417
418    if (!isset($data['source_id']) || !isset($data['source'])) {
419      $message = $this->t('[attachToRoot] Missing preset source_id data');
420      $debug = [
421        'preset_id' => $preset_id,
422        'position' => $position,
423        'data' => $data,
424        'instance' => $display_builder_instance,
425      ];
426
427      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
428    }
429    $node_id = $display_builder_instance->attachToRoot($position, $data['source_id'], $data['source']);
430
431    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
432      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
433    }
434    $display_builder_instance->save();
435    $this->builder = $display_builder_instance;
436
437    return $this->dispatchDisplayBuilderEvent(
438      DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
439      NULL,
440      $node_id,
441    );
442  }
443
444  /**
445   * Attach a pattern preset to a slot .
446   *
447   * Presets are "resolved" after attachment, so they are never moved around.
448   *
449   * @param \Drupal\display_builder\InstanceInterface $display_builder_instance
450   *   Display builder instance.
451   * @param string $preset_id
452   *   Pattern preset ID.
453   * @param string $parent_id
454   *   Parent instance ID.
455   * @param string $slot
456   *   Slot.
457   * @param int $position
458   *   Position.
459   *
460   * @return array
461   *   A renderable array.
462   */
463  protected function attachPresetToSlot(InstanceInterface $display_builder_instance, string $preset_id, string $parent_id, string $slot, int $position): array {
464    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
465
466    /** @var \Drupal\display_builder\Entity\PatternPresetInterface $preset */
467    $preset = $presetStorage->load($preset_id);
468    $data = $preset->getSources();
469
470    if (!isset($data['source_id']) || !isset($data['source'])) {
471      $message = $this->t('[attachToSlot] Missing preset source_id data');
472      $debug = [
473        'preset_id' => $preset_id,
474        'parent_id' => $parent_id,
475        'slot' => $slot,
476        'position' => $position,
477        'data' => $data,
478        'instance' => $display_builder_instance,
479      ];
480
481      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
482    }
483    $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $data['source_id'], $data['source']);
484
485    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
486      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
487    }
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   * Determines which island (if any) to skip from the event dispatch fan-out.
501   *
502   * Builder, Wireframe, and Tree share the same Sortable group (@see
503   * components/dropzone/dropzone.js), so an existing node can be dragged
504   * from one panel's dropzone straight into another's. Sortable only
505   * relocates the DOM node - it never re-renders it - so after a
506   * cross-panel move the dropped element is still wearing whichever
507   * panel originally rendered it (@see
508   * BuilderPanel::buildNodeAttributes(), display_builder.js's addVals()).
509   * Skipping the destination island's own re-render is only safe when the
510   * move stayed within that same island; otherwise its dropzone would be
511   * left showing another island's markup until the next full reload.
512   *
513   * @param \Symfony\Component\HttpFoundation\Request $request
514   *   The request.
515   * @param bool $is_move
516   *   Whether this attach is actually moving an existing node.
517   *
518   * @return string|null
519   *   The island ID to skip, or NULL to skip none.
520   */
521  private function resolveSkipIslandId(Request $request, bool $is_move): ?string {
522    if (!$is_move) {
523      return NULL;
524    }
525
526    $destination_island = (string) $request->query->get('from', '');
527    $source_island = (string) $request->request->get('source_island', '');
528
529    return ($source_island !== '' && $source_island === $destination_island) ? $destination_island : NULL;
530  }
531
532  /**
533   * Validates an island form.
534   *
535   * @param string $formClass
536   *   The form class.
537   * @param \Drupal\Core\Form\FormStateInterface $form_state
538   *   The form state.
539   *
540   * @return array
541   *   The validated values.
542   */
543  private function validateIslandForm(string $formClass, FormStateInterface $form_state): array {
544    /** @var \Drupal\Core\Form\FormBuilder $formBuilder */
545    $formBuilder = $this->formBuilder();
546
547    try {
548      $triggering_element = $form_state->getTriggeringElement();
549
550      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
551        // We set a fake triggering element to avoid form API error.
552        $form_state->setTriggeringElement([
553          '#type' => 'submit',
554          '#limit_validation_errors' => FALSE,
555          '#value' => (string) $this->t('Submit'),
556        ]);
557      }
558      $form = $formBuilder->buildForm($formClass, $form_state);
559      $formErrors = $form_state->getErrors();
560
561      if (!empty($formErrors)) {
562        $first_error = \reset($formErrors);
563
564        throw new FormValidationException((string) $first_error);
565      }
566      $formBuilder->validateForm($formClass, $form, $form_state);
567      $formErrors = $form_state->getErrors();
568
569      if (!empty($formErrors)) {
570        $first_error = \reset($formErrors);
571
572        throw new FormValidationException((string) $first_error);
573      }
574    }
575    catch (FormAjaxException $e) {
576      throw $e;
577    }
578    // Those values are the validated values, produced by the form.
579    // with all Form API processing.
580    $values = $form_state->getValues();
581
582    // We clean the values from form API keys.
583    if (isset($values['form_build_id'])) {
584      unset($values['form_build_id'], $values['form_token'], $values['form_id']);
585    }
586
587    return $values;
588  }
589
590  /**
591   * Render an error message in the display builder.
592   *
593   * @param string $display_builder_instance_id
594   *   The builder ID.
595   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
596   *   The error message.
597   * @param array $debug
598   *   The debug code related to the error.
599   *
600   * @return array
601   *   A renderable array.
602   */
603  private function responseMessageError(
604    string $display_builder_instance_id,
605    string|TranslatableMarkup $message,
606    array $debug,
607  ): array {
608    // Reduce verbosity.
609    unset($debug['request']['ajax_page_state']['libraries']);
610
611    $instance = $debug['instance'] ?? NULL;
612
613    if ($instance) {
614      $tree = new SourceTree($debug['instance']->getCurrentState());
615      unset($debug['instance']);
616      $debug['tree'] = $tree->getNormalizedStructure()['structure'];
617    }
618
619    $this->getLogger('display_builder')->error('@message <pre>@debug</pre>', [
620      '@message' => $message,
621      '@debug' => \print_r($debug, TRUE),
622    ]);
623
624    $message = new TranslatableMarkup('An error occurred, refresh the current page to continue from a valid state.<br>Check logs for more details.');
625
626    return $this->buildError($display_builder_instance_id, $message, TRUE);
627  }
628
629}

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
411  protected function attachPresetToRoot(InstanceInterface $display_builder_instance, string $preset_id, int $position): array {
412    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
413
414    /** @var \Drupal\display_builder\Entity\PatternPresetInterface $preset */
415    $preset = $presetStorage->load($preset_id);
416    $data = $preset->getSources();
417
418    if (!isset($data['source_id']) || !isset($data['source'])) {
418    if (!isset($data['source_id']) || !isset($data['source'])) {
418    if (!isset($data['source_id']) || !isset($data['source'])) {
419      $message = $this->t('[attachToRoot] Missing preset source_id data');
420      $debug = [
421        'preset_id' => $preset_id,
422        'position' => $position,
423        'data' => $data,
424        'instance' => $display_builder_instance,
425      ];
426
427      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
429    $node_id = $display_builder_instance->attachToRoot($position, $data['source_id'], $data['source']);
430
431    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
431    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
431    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
431    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
432      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
433    }
434    $display_builder_instance->save();
435    $this->builder = $display_builder_instance;
436
437    return $this->dispatchDisplayBuilderEvent(
438      DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
439      NULL,
440      $node_id,
441    );
442  }
ApiController->attachPresetToSlot
463  protected function attachPresetToSlot(InstanceInterface $display_builder_instance, string $preset_id, string $parent_id, string $slot, int $position): array {
464    $presetStorage = $this->entityTypeManager()->getStorage('pattern_preset');
465
466    /** @var \Drupal\display_builder\Entity\PatternPresetInterface $preset */
467    $preset = $presetStorage->load($preset_id);
468    $data = $preset->getSources();
469
470    if (!isset($data['source_id']) || !isset($data['source'])) {
470    if (!isset($data['source_id']) || !isset($data['source'])) {
470    if (!isset($data['source_id']) || !isset($data['source'])) {
471      $message = $this->t('[attachToSlot] Missing preset source_id data');
472      $debug = [
473        'preset_id' => $preset_id,
474        'parent_id' => $parent_id,
475        'slot' => $slot,
476        'position' => $position,
477        'data' => $data,
478        'instance' => $display_builder_instance,
479      ];
480
481      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
483    $node_id = $display_builder_instance->attachToSlot($parent_id, $slot, $position, $data['source_id'], $data['source']);
484
485    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
485    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
485    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
485    foreach ($data['third_party_settings'] ?? [] as $provider => $settings) {
486      $display_builder_instance->setThirdPartySettings($node_id, $provider, $settings ?? []);
487    }
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->attachToRoot
87  public function attachToRoot(Request $request, InstanceInterface $display_builder_instance): array {
88    $position = (int) $request->request->get('position', 0);
89
90    if ($request->request->has('preset_id')) {
91      $preset_id = (string) $request->request->get('preset_id');
92
93      return $this->attachPresetToRoot($display_builder_instance, $preset_id, $position);
96    $is_move = FALSE;
97
98    if ($request->request->has('node_id')) {
99      $node_id = (string) $request->request->get('node_id');
100
101      if (!$display_builder_instance->moveToRoot($node_id, $position)) {
102        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
103        $debug = [
104          'request' => $request->request->all(),
105          'instance' => $display_builder_instance,
106        ];
107
108        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
98    if ($request->request->has('node_id')) {
99      $node_id = (string) $request->request->get('node_id');
100
101      if (!$display_builder_instance->moveToRoot($node_id, $position)) {
102        $message = $this->t('[attachToRoot] moveToRoot failed with invalid data');
103        $debug = [
104          'request' => $request->request->all(),
105          'instance' => $display_builder_instance,
106        ];
107
108        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
109      }
110
111      $is_move = TRUE;
113    elseif ($request->request->has('source_id')) {
114      $source_id = (string) $request->request->get('source_id');
115      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
115      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
115      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
113    elseif ($request->request->has('source_id')) {
114      $source_id = (string) $request->request->get('source_id');
115      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
119      $message = '[attachToRoot] Missing content (source_id, node_id or preset_id)';
120      $debug = [
121        'request' => $request->request->all(),
122        'instance' => $display_builder_instance,
123      ];
124
125      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
127    $display_builder_instance->save();
128
129    $this->builder = $display_builder_instance;
130    // Let's refresh when we add new source to get the placeholder replacement.
131    $this->islandId = $this->resolveSkipIslandId($request, $is_move);
132
133    return $this->dispatchDisplayBuilderEvent(
134      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
134      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
134      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
134      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_ROOT,
135      NULL,
136      $node_id,
137    );
138  }
ApiController->attachToSlot
143  public function attachToSlot(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $slot): array {
144    $parent_id = $node_id;
145    $position = (int) $request->request->get('position', 0);
146
147    if ($request->request->has('preset_id')) {
148      $preset_id = (string) $request->request->get('preset_id');
149
150      return $this->attachPresetToSlot($display_builder_instance, $preset_id, $parent_id, $slot, $position);
153    $is_move = FALSE;
154
155    // First, we update the data state.
156    if ($request->request->has('node_id')) {
157      $node_id = (string) $request->request->get('node_id');
158
159      if (!$display_builder_instance->moveToSlot($node_id, $parent_id, $slot, $position)) {
160        $message = $this->t('[attachToSlot] moveToSlot failed with invalid data');
161        $debug = [
162          'node_id' => $node_id,
163          'slot' => $slot,
164          'request' => $request->request->all(),
165          'instance' => $display_builder_instance,
166        ];
167
168        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
156    if ($request->request->has('node_id')) {
157      $node_id = (string) $request->request->get('node_id');
158
159      if (!$display_builder_instance->moveToSlot($node_id, $parent_id, $slot, $position)) {
160        $message = $this->t('[attachToSlot] moveToSlot failed with invalid data');
161        $debug = [
162          'node_id' => $node_id,
163          'slot' => $slot,
164          'request' => $request->request->all(),
165          'instance' => $display_builder_instance,
166        ];
167
168        return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
169      }
170
171      $is_move = TRUE;
173    elseif ($request->request->has('source_id')) {
174      $source_id = (string) $request->request->get('source_id');
175      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
175      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
175      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
173    elseif ($request->request->has('source_id')) {
174      $source_id = (string) $request->request->get('source_id');
175      $data = $request->request->has('source') ? \json_decode((string) $request->request->get('source'), TRUE) : [];
179      $message = $this->t('[attachToSlot] Missing content (component_id, block_id or node_id)');
180      $debug = [
181        'node_id' => $node_id,
182        'slot' => $slot,
183        'request' => $request->request->all(),
184        'instance' => $display_builder_instance,
185      ];
186
187      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
189    $display_builder_instance->save();
190
191    $this->builder = $display_builder_instance;
192    // Let's refresh when we add new source to get the placeholder replacement.
193    $this->islandId = $this->resolveSkipIslandId($request, $is_move);
194
195    return $this->dispatchDisplayBuilderEvent(
196      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
196      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
196      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
196      $is_move ? DisplayBuilderEvents::ON_MOVE : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
197      NULL,
198      $node_id,
199      $parent_id,
200    );
201  }
ApiController->clear
387  public function clear(Request $request, InstanceInterface $display_builder_instance): array {
388    $display_builder_instance->clear();
389    $display_builder_instance->save();
390
391    $this->builder = $display_builder_instance;
392
393    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
394  }
ApiController->get
206  public function get(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
207    $this->builder = $display_builder_instance;
208
209    return $this->dispatchDisplayBuilderEvent(
210      DisplayBuilderEvents::ON_ACTIVE,
211      $display_builder_instance->getNode($node_id),
212    );
213  }
ApiController->redo
373  public function redo(Request $request, InstanceInterface $display_builder_instance): array {
374    /** @var \Drupal\display_builder\Entity\InstanceStorage $storage */
375    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
376    /** @var \Drupal\display_builder\InstanceInterface $display_builder_instance */
377    $display_builder_instance = $storage->redo($display_builder_instance);
378
379    $this->builder = $display_builder_instance;
380
381    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
382  }
ApiController->reloadIsland
50  public function reloadIsland(Request $request, InstanceInterface $display_builder_instance, string $island_id): array {
51    $profile = $display_builder_instance->getProfile();
52
53    if (!isset($profile->getEnabledIslands()[$island_id])) {
54      $message = $this->t('[reloadIsland] Island @island is not enabled on this profile', ['@island' => $island_id]);
55      $debug = [
56        'island_id' => $island_id,
57        'instance' => $display_builder_instance,
58      ];
59
60      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
63    $contexts = $display_builder_instance->getAvailableContexts();
64    $definitions = \array_intersect_key(
65      $this->islandPluginManager->getDefinitions(),
66      [$island_id => TRUE],
67    );
68    $islands = $this->islandPluginManager->createInstances($definitions, $contexts, $profile->getIslandConfigurations());
69    $island = $islands[$island_id] ?? NULL;
70
71    if ($island === NULL) {
72      $message = $this->t('[reloadIsland] Island @island is not available in this context', ['@island' => $island_id]);
73      $debug = [
74        'island_id' => $island_id,
75        'instance' => $display_builder_instance,
76      ];
77
78      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
81    return [$island->reload($display_builder_instance)];
82  }
ApiController->resolveSkipIslandId
521  private function resolveSkipIslandId(Request $request, bool $is_move): ?string {
522    if (!$is_move) {
523      return NULL;
526    $destination_island = (string) $request->query->get('from', '');
527    $source_island = (string) $request->request->get('source_island', '');
528
529    return ($source_island !== '' && $source_island === $destination_island) ? $destination_island : NULL;
529    return ($source_island !== '' && $source_island === $destination_island) ? $destination_island : NULL;
529    return ($source_island !== '' && $source_island === $destination_island) ? $destination_island : NULL;
529    return ($source_island !== '' && $source_island === $destination_island) ? $destination_island : NULL;
529    return ($source_island !== '' && $source_island === $destination_island) ? $destination_island : NULL;
529    return ($source_island !== '' && $source_island === $destination_island) ? $destination_island : NULL;
530  }
ApiController->responseMessageError
604    string $display_builder_instance_id,
605    string|TranslatableMarkup $message,
606    array $debug,
607  ): array {
608    // Reduce verbosity.
609    unset($debug['request']['ajax_page_state']['libraries']);
610
611    $instance = $debug['instance'] ?? NULL;
612
613    if ($instance) {
614      $tree = new SourceTree($debug['instance']->getCurrentState());
615      unset($debug['instance']);
616      $debug['tree'] = $tree->getNormalizedStructure()['structure'];
617    }
618
619    $this->getLogger('display_builder')->error('@message <pre>@debug</pre>', [
619    $this->getLogger('display_builder')->error('@message <pre>@debug</pre>', [
620      '@message' => $message,
621      '@debug' => \print_r($debug, TRUE),
622    ]);
623
624    $message = new TranslatableMarkup('An error occurred, refresh the current page to continue from a valid state.<br>Check logs for more details.');
625
626    return $this->buildError($display_builder_instance_id, $message, TRUE);
627  }
ApiController->thirdPartySettingsUpdate
298  public function thirdPartySettingsUpdate(Request $request, InstanceInterface $display_builder_instance, string $node_id, string $island_id): array {
299    $body = $request->getPayload()->all();
300
301    if (!isset($body['form_id'])) {
302      $message = $this->t('[thirdPartySettingsUpdate] Missing payload!');
303      $debug = [
304        'node_id' => $node_id,
305        'island_id' => $island_id,
306        'request' => $request->request->all(),
307        'body' => $body,
308        'instance' => $display_builder_instance,
309      ];
310
311      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
314    $islandDefinition = $this->islandPluginManager->getDefinition($island_id);
315    // Load the instance to properly alter the form data into config data.
316    $node = $display_builder_instance->getNode($node_id);
317    unset($body['form_build_id'], $body['form_token'], $body['form_id']);
318
319    $form_state = new FormState();
320    // Default values are the existing values from the state.
321    $form_state->addBuildInfo('args', [
322      [
323        'island_id' => $island_id,
324        'builder_id' => (string) $display_builder_instance->id(),
325        'instance' => $node,
326      ],
327      [],
328    ]);
329
330    // phpcs:disable Drupal.Files.LineLength.TooLong
331    // @todo should context be injected for third party settings?
332    // $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getAvailableContexts());
333    // phpcs:enable Drupal.Files.LineLength.TooLong
334    // The body received corresponds to raw form values.
335    // We need to set them in the form state to properly
336    // take them into account.
337    $form_state->setValues($body);
338
339    $formClass = ($islandDefinition['class'])::getFormClass();
340    $values = $this->validateIslandForm($formClass, $form_state);
341    // We update the state with the new data.
342    $display_builder_instance->setThirdPartySettings($node_id, $island_id, $values);
343    $display_builder_instance->save();
344
345    $this->builder = $display_builder_instance;
346    $this->islandId = $island_id;
347
348    return $this->dispatchDisplayBuilderEvent(
349      DisplayBuilderEvents::ON_UPDATE,
350      NULL,
351      $node_id,
352      NULL,
353    );
354  }
ApiController->undo
359  public function undo(Request $request, InstanceInterface $display_builder_instance): array {
360    /** @var \Drupal\display_builder\Entity\InstanceStorage $storage */
361    $storage = $this->entityTypeManager()->getStorage('display_builder_instance');
362    /** @var \Drupal\display_builder\InstanceInterface $display_builder_instance */
363    $display_builder_instance = $storage->undo($display_builder_instance);
364
365    $this->builder = $display_builder_instance;
366
367    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_HISTORY_CHANGE);
368  }
ApiController->update
218  public function update(Request $request, InstanceInterface $display_builder_instance, string $node_id): array {
219    $this->builder = $display_builder_instance;
220    $body = $request->getPayload()->all();
221
222    if (!isset($body['form_id'])) {
223      $message = $this->t('[update] Missing payload!');
224      $debug = [
225        'node_id' => $node_id,
226        'request' => $request->request->all(),
227        'body' => $body,
228        'instance' => $display_builder_instance,
229      ];
230
231      return $this->responseMessageError((string) $display_builder_instance->id(), $message, $debug);
235    $node = $display_builder_instance->getNode($node_id);
236
237    if (isset($body['source']['form_build_id'])) {
238      unset($body['source']['form_build_id'], $body['source']['form_token'], $body['source']['form_id']);
239    }
240
241    if (isset($body['form_build_id'])) {
241    if (isset($body['form_build_id'])) {
242      unset($body['form_build_id'], $body['form_token'], $body['form_id']);
243    }
244    $form_state = new FormState();
244    $form_state = new FormState();
245    // Default values are the existing values from the state.
246    $form_state->addBuildInfo('args', [
247      [
248        'island_id' => 'contextual_form',
249        'builder_id' => (string) $display_builder_instance->id(),
250        'instance' => $node,
251      ],
252      $display_builder_instance->getAvailableContexts(),
253    ]);
254    $form_state->setTemporaryValue('gathered_contexts', $display_builder_instance->getAvailableContexts());
255    // The body received corresponds to raw form values.
256    // We need to set them in the form state to properly
257    // take them into account.
258    $form_state->setValues($body);
259
260    $formClass = ContextualFormPanel::getFormClass();
261    $data = [];
262
263    try {
264      $values = $this->validateIslandForm($formClass, $form_state);
265      $data['source'] = $values;
267    catch (FormAjaxException $e) {
268      throw $e;
270    catch (\Exception $e) {
272        'node_id' => $node_id,
273        'request' => $request->request->all(),
274        'form' => $form_state->getValues(),
275        'body' => $body,
276        'instance' => $display_builder_instance,
277      ];
278
279      return $this->responseMessageError((string) $display_builder_instance->id(), $e->getMessage(), $debug);
282    $display_builder_instance->setSource($node_id, $node['source_id'], $data['source']);
283    $display_builder_instance->save();
284
285    $this->builder = $display_builder_instance;
286    $this->islandId = (string) $request->query->get('from', NULL);
287
288    return $this->dispatchDisplayBuilderEvent(
289      DisplayBuilderEvents::ON_UPDATE,
290      NULL,
291      $node_id,
292    );
293  }
ApiController->validateIslandForm
543  private function validateIslandForm(string $formClass, FormStateInterface $form_state): array {
544    /** @var \Drupal\Core\Form\FormBuilder $formBuilder */
545    $formBuilder = $this->formBuilder();
546
547    try {
548      $triggering_element = $form_state->getTriggeringElement();
549
550      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
550      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
550      if (!$triggering_element && !isset($form_state->getValues()['_triggering_element_name'])) {
552        $form_state->setTriggeringElement([
553          '#type' => 'submit',
554          '#limit_validation_errors' => FALSE,
555          '#value' => (string) $this->t('Submit'),
556        ]);
557      }
558      $form = $formBuilder->buildForm($formClass, $form_state);
558      $form = $formBuilder->buildForm($formClass, $form_state);
559      $formErrors = $form_state->getErrors();
560
561      if (!empty($formErrors)) {
562        $first_error = \reset($formErrors);
563
564        throw new FormValidationException((string) $first_error);
566      $formBuilder->validateForm($formClass, $form, $form_state);
567      $formErrors = $form_state->getErrors();
568
569      if (!empty($formErrors)) {
570        $first_error = \reset($formErrors);
571
572        throw new FormValidationException((string) $first_error);
572        throw new FormValidationException((string) $first_error);
575    catch (FormAjaxException $e) {
576      throw $e;
580    $values = $form_state->getValues();
581
582    // We clean the values from form API keys.
583    if (isset($values['form_build_id'])) {
584      unset($values['form_build_id'], $values['form_token'], $values['form_id']);
585    }
586
587    return $values;
587    return $values;
588  }