Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
94.79% covered (success)
94.79%
91 / 96
81.13% covered (warning)
81.13%
43 / 53
1.95% covered (danger)
1.95%
12 / 616
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiContextualMenuController
94.79% covered (success)
94.79%
91 / 96
81.13% covered (warning)
81.13%
43 / 53
1.95% covered (danger)
1.95%
12 / 616
71.43% covered (warning)
71.43%
5 / 7
714.22
0.00% covered (danger)
0.00%
0 / 1
 paste
100.00% covered (success)
100.00%
24 / 24
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%
23 / 23
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
 pasteStyles
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 deleteStyles
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
 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\display_builder\Event\DisplayBuilderEvents;
8use Drupal\display_builder\InstanceInterface;
9use Symfony\Component\HttpFoundation\Request;
10
11/**
12 * Returns responses for the Display Builder contextual menu routes.
13 */
14class ApiContextualMenuController extends ApiControllerBase implements ApiContextualMenuControllerInterface {
15
16  /**
17   * {@inheritdoc}
18   */
19  public function paste(Request $request, InstanceInterface $display_builder_instance): array {
20    $node_id = (string) $request->request->get('node_id', '');
21    $parent_id = (string) $request->request->get('parent_id', '__root__');
22    $slot_id = (string) $request->request->get('slot_id', '');
23    $slot_position = (string) $request->request->get('slot_position', '0');
24
25    $this->builder = $display_builder_instance;
26    $dataToCopy = $display_builder_instance->getNode($node_id);
27
28    // Keep flag for move or attach to root.
29    $is_paste_root = FALSE;
30    $new_node_id = NULL;
31
32    if (isset($dataToCopy['source_id'], $dataToCopy['source'])) {
33      $source_id = $dataToCopy['source_id'];
34      // Use reference to ensure modifications by recursiveRefreshNodeId
35      // persist.
36      $data = &$dataToCopy['source'];
37
38      // Refresh nested node_ids.
39      self::recursiveRefreshNodeId($data);
40
41      // If no parent we are on root.
42      // @todo for duplicate and not parent root seems not detected and copy is inside the slot.
43      if ($parent_id === '__root__') {
44        $is_paste_root = TRUE;
45        $new_node_id = $display_builder_instance->attachToRoot(0, $source_id, $data, $dataToCopy['third_party_settings'] ?? []);
46      }
47      else {
48        $new_node_id = $display_builder_instance->attachToSlot($parent_id, $slot_id, (int) $slot_position, $source_id, $data, $dataToCopy['third_party_settings'] ?? []);
49      }
50    }
51    $display_builder_instance->save();
52
53    $this->builder = $display_builder_instance;
54
55    return $this->dispatchDisplayBuilderEvent(
56      $is_paste_root ? DisplayBuilderEvents::ON_ATTACH_TO_ROOT : DisplayBuilderEvents::ON_ATTACH_TO_SLOT,
57      NULL,
58      $new_node_id,
59      $is_paste_root ? NULL : $parent_id,
60    );
61  }
62
63  /**
64   * {@inheritdoc}
65   */
66  public function delete(Request $request, InstanceInterface $display_builder_instance): array {
67    $node_id = (string) $request->request->get('node_id', '');
68    $parent_id = $display_builder_instance->getParentId($node_id);
69    $display_builder_instance->remove($node_id);
70    $this->builder = $display_builder_instance;
71
72    return $this->dispatchDisplayBuilderEvent(
73      DisplayBuilderEvents::ON_DELETE,
74      NULL,
75      $node_id,
76      $parent_id
77    );
78  }
79
80  /**
81   * {@inheritdoc}
82   */
83  public function saveAsPreset(Request $request, InstanceInterface $display_builder_instance): array {
84    $node_id = (string) $request->request->get('node_id', '');
85    $label = (string) $this->t('New preset');
86    $data = $display_builder_instance->getNode($node_id);
87    self::cleanPreset($data);
88
89    $preset_storage = $this->entityTypeManager()->getStorage('pattern_preset');
90    $label = $request->headers->get('hx-prompt', $label) ?: $label;
91    // In HTTP headers, only ASCII is guaranteed to work but historically,
92    // HTTP has allowed header values with the ISO-8859-1 charset.
93    $label = \mb_convert_encoding($label, 'UTF-8', 'ISO-8859-1');
94
95    // Build a valid config-entity machine name from the label. Config IDs
96    // must match [a-z0-9_] and must not start with a digit.
97    $base_id = 'preset_' . \preg_replace('/[^a-z0-9_]+/', '_', \mb_strtolower($label));
98    $base_id = \trim($base_id, '_');
99    $id = $base_id;
100    $suffix = 1;
101
102    while ($preset_storage->load($id) !== NULL) {
103      $id = $base_id . '_' . $suffix++;
104    }
105
106    $preset = $preset_storage->create([
107      'id' => $id,
108      'label' => $label,
109      'status' => TRUE,
110      'description' => '',
111      'sources' => $data,
112    ]);
113    $preset->save();
114
115    $this->builder = $display_builder_instance;
116
117    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_PRESET_SAVE);
118  }
119
120  /**
121   * {@inheritdoc}
122   */
123  public function pasteStyles(Request $request, InstanceInterface $display_builder_instance): array {
124    $node_id = (string) $request->request->get('node_id', '');
125    $source_node_id = (string) $request->request->get('source_node_id', '');
126    $mode = (string) $request->request->get('mode', 'replace');
127
128    $default_styles = ['selected' => [], 'extra' => ''];
129    $source = $display_builder_instance->getNode($source_node_id);
130    $styles = $source['third_party_settings']['styles'] ?? $default_styles;
131
132    if ($mode === 'merge') {
133      $target = $display_builder_instance->getNode($node_id);
134      $current = $target['third_party_settings']['styles'] ?? $default_styles;
135      $styles = [
136        // Later array wins on overlapping keys: the copied styles override
137        // the target's own selection for any style category they share,
138        // while categories only the target had are preserved.
139        'selected' => \array_merge($current['selected'] ?? [], $styles['selected'] ?? []),
140        'extra' => \trim(\trim((string) ($current['extra'] ?? '')) . ' ' . \trim((string) ($styles['extra'] ?? ''))),
141      ];
142    }
143
144    $display_builder_instance->setThirdPartySettings($node_id, 'styles', $styles);
145    $display_builder_instance->save();
146
147    $this->builder = $display_builder_instance;
148
149    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_UPDATE, NULL, $node_id);
150  }
151
152  /**
153   * {@inheritdoc}
154   */
155  public function deleteStyles(Request $request, InstanceInterface $display_builder_instance): array {
156    $node_id = (string) $request->request->get('node_id', '');
157    $display_builder_instance->setThirdPartySettings($node_id, 'styles', ['selected' => [], 'extra' => '']);
158    $display_builder_instance->save();
159
160    $this->builder = $display_builder_instance;
161
162    return $this->dispatchDisplayBuilderEvent(DisplayBuilderEvents::ON_UPDATE, NULL, $node_id);
163  }
164
165  /**
166   * Recursively regenerate the node_id key.
167   *
168   * @param array $array
169   *   The array reference.
170   */
171  private static function recursiveRefreshNodeId(array &$array): void {
172    if (isset($array['node_id'])) {
173      $array['node_id'] = \bin2hex(\random_bytes(8));
174    }
175
176    foreach ($array as &$value) {
177      if (\is_array($value)) {
178        self::recursiveRefreshNodeId($value);
179      }
180    }
181  }
182
183  /**
184   * Recursively clean the node data for export or preset saving.
185   *
186   * Unset node_id and remove empty values.
187   *
188   * @param array $array
189   *   The array reference.
190   */
191  private static function cleanPreset(array &$array): void {
192    unset($array['node_id']);
193
194    foreach ($array as $key => &$value) {
195      if (\is_array($value)) {
196        self::cleanPreset($value);
197
198        // Remove empty values to reduce size and noise in the exported preset.
199        if (isset($value['source_id'], $value['source']['value']) && $value['source']['value'] === '') {
200          unset($array[$key]);
201        }
202      }
203
204      if ($key === 'extra' && empty($value)) {
205        unset($array[$key]);
206      }
207
208      if ($key === 'third_party_settings' && empty($value)) {
209        unset($array[$key]);
210      }
211
212      if ($key === 'variant_id' && $value === NULL) {
213        unset($array[$key]);
214      }
215    }
216  }
217
218}