Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
69.29% covered (warning)
69.29%
88 / 127
70.69% covered (warning)
70.69%
41 / 58
18.29% covered (danger)
18.29%
15 / 82
38.89% covered (danger)
38.89%
7 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplayBuildablePluginBase
69.29% covered (warning)
69.29%
88 / 127
70.69% covered (warning)
70.69%
41 / 58
18.29% covered (danger)
18.29%
15 / 82
38.89% covered (danger)
38.89%
7 / 18
783.77
0.00% covered (danger)
0.00%
0 / 1
 create
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
 getPrefix
0.00% covered (danger)
0.00%
0 / 3
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
 label
0.00% covered (danger)
0.00%
0 / 2
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
 initInstanceIfMissing
0.00% covered (danger)
0.00%
0 / 4
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
 getInstance
0.00% covered (danger)
0.00%
0 / 5
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
 getProfile
0.00% covered (danger)
0.00%
0 / 1
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
 getBuilderUrl
0.00% covered (danger)
0.00%
0 / 1
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
 buildInstanceForm
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
15 / 15
10.53% covered (danger)
10.53%
4 / 38
100.00% covered (success)
100.00%
1 / 1
53.84
 getAllowedProfiles
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
6 / 6
25.00% covered (danger)
25.00%
1 / 4
100.00% covered (success)
100.00%
1 / 1
6.80
 isAllowed
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getAvailableContexts
0.00% covered (danger)
0.00%
0 / 1
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
 createDisplayBuilderInstance
0.00% covered (danger)
0.00%
0 / 18
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
 getInitializationMessage
0.00% covered (danger)
0.00%
0 / 1
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
 getInitialSources
0.00% covered (danger)
0.00%
0 / 1
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
 buildSelect
92.31% covered (success)
92.31%
24 / 26
75.00% covered (warning)
75.00%
9 / 12
15.00% covered (danger)
15.00%
3 / 20
0.00% covered (danger)
0.00%
0 / 1
28.11
 buildLink
100.00% covered (success)
100.00%
15 / 15
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
 buildDisabledSelect
100.00% covered (success)
100.00%
9 / 9
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
 isCurrentUserAllowedToAdministrate
100.00% covered (success)
100.00%
1 / 1
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;
6
7use Drupal\Core\Entity\EntityInterface;
8use Drupal\Core\Entity\EntityTypeManagerInterface;
9use Drupal\Core\Extension\ModuleHandlerInterface;
10use Drupal\Core\Plugin\ConfigurablePluginBase;
11use Drupal\Core\Session\AccountInterface;
12use Drupal\Core\Session\AccountProxyInterface;
13use Drupal\Core\StringTranslation\TranslatableMarkup;
14use Drupal\Core\Url;
15use Drupal\display_builder\Attribute\DisplayBuildable;
16use Drupal\display_builder\Entity\Instance;
17use Drupal\display_builder\Entity\ProfileInterface;
18use Symfony\Component\DependencyInjection\ContainerInterface;
19
20/**
21 * Base class for display_buildable plugins.
22 */
23abstract class DisplayBuildablePluginBase extends ConfigurablePluginBase implements DisplayBuildableInterface {
24
25  /**
26   * The entity type manager.
27   */
28  protected EntityTypeManagerInterface $entityTypeManager;
29
30  /**
31   * Current user.
32   */
33  protected AccountProxyInterface $currentUser;
34
35  /**
36   * Module handler.
37   */
38  protected ModuleHandlerInterface $moduleHandler;
39
40  /**
41   * A tiny hint to remember where the initial data comes from.
42   *
43   * See ::getInitialSources() and ::getInitializationMessage().
44   */
45  protected string $initialDataSource = '';
46
47  /**
48   * {@inheritdoc}
49   */
50  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
51    $instance = new static($configuration, $plugin_id, $plugin_definition);
52    $instance->entityTypeManager = $container->get('entity_type.manager');
53    $instance->currentUser = $container->get('current_user');
54    $instance->moduleHandler = $container->get('module_handler');
55
56    return $instance;
57  }
58
59  /**
60   * {@inheritdoc}
61   */
62  public static function getPrefix(): string {
63    $reflection = new \ReflectionClass(static::class);
64    $attribute = $reflection->getAttributes(DisplayBuildable::class);
65
66    return $attribute[0]->newInstance()->instance_prefix;
67  }
68
69  /**
70   * {@inheritdoc}
71   */
72  public function label(): string {
73    /** @var array $definition */
74    $definition = $this->pluginDefinition;
75
76    // Cast the label to a string since it is a TranslatableMarkup object.
77    return (string) $definition['label'];
78  }
79
80  /**
81   * {@inheritdoc}
82   */
83  public function initInstanceIfMissing(): void {
84    /** @var \Drupal\display_builder\InstanceInterface $instance */
85    $instance = $this->getInstance();
86
87    if (!$instance) {
88      $instance = $this->createDisplayBuilderInstance();
89      $instance->save();
90    }
91  }
92
93  /**
94   * {@inheritdoc}
95   */
96  public function getInstance(): ?InstanceInterface {
97    if ($this->getInstanceId() === NULL) {
98      return NULL;
99    }
100
101    $storage = $this->entityTypeManager->getStorage('display_builder_instance');
102    /** @var \Drupal\display_builder\InstanceInterface|null $instance */
103    $instance = $storage->load($this->getInstanceId());
104
105    return $instance;
106  }
107
108  /**
109   * {@inheritdoc}
110   */
111  public function getProfile(): ?ProfileInterface {
112    // Plugins will override this method.
113    return NULL;
114  }
115
116  /**
117   * {@inheritdoc}
118   */
119  public function getBuilderUrl(): Url {
120    // Plugins will override this method.
121    return Url::fromRoute('<front>');
122  }
123
124  /**
125   * {@inheritdoc}
126   */
127  public function buildInstanceForm(bool $mandatory = TRUE, ?TranslatableMarkup $title = NULL, bool $link = TRUE): array {
128    $profile = $this->getProfile();
129    $allowed = $this->isAllowed();
130
131    if (!$allowed && !$profile) {
132      return [
133        self::PROFILE_PROPERTY => [
134          '#markup' => $this->t('You are not allowed to use Display Builder.'),
135        ],
136      ];
137    }
138
139    if (!$allowed && $profile) {
140      return [
141        self::PROFILE_PROPERTY => $this->buildDisabledSelect($profile),
142      ];
143    }
144
145    $form = [
146      self::PROFILE_PROPERTY => $this->buildSelect($profile, $mandatory, $title),
147    ];
148
149    // Add the builder link to edit.
150    if ($this->getInstanceId() && $profile && $link) {
151      $form['link'] = $this->buildLink();
152    }
153
154    return $form;
155  }
156
157  /**
158   * {@inheritdoc}
159   */
160  public function getAllowedProfiles(?AccountInterface $account = NULL): array {
161    $account ??= $this->currentUser;
162    $options = [];
163    $storage = $this->entityTypeManager->getStorage('display_builder_profile');
164    $entity_ids = $storage->getQuery()->accessCheck(TRUE)->sort('weight', 'ASC')->execute();
165    /** @var \Drupal\display_builder\Entity\ProfileInterface[] $display_builders */
166    $display_builders = $storage->loadMultiple($entity_ids);
167
168    // Entity query doesn't execute access control handlers for config
169    // entities. So we need to do an extra check here.
170    foreach ($display_builders as $entity_id => $entity) {
171      // We don't execute $entity->access() to not catch admin permission
172      // 'administer display builder profile'.
173      // @see ProfileAccessControlHandler.
174      // Administrators can use any profile, but it is better to only propose
175      // them the ones related to their permissions.
176      if ($account->hasPermission($entity->getPermissionName())) {
177        $options[$entity_id] = $entity->label();
178      }
179    }
180
181    return $options;
182  }
183
184  /**
185   * {@inheritdoc}
186   */
187  public function isAllowed(?AccountInterface $account = NULL): bool {
188    $options = $this->getAllowedProfiles($account);
189
190    if (empty($options)) {
191      return FALSE;
192    }
193    $profile = $this->getProfile();
194
195    if (!$profile) {
196      return TRUE;
197    }
198
199    return isset($options[(string) $profile->id()]);
200  }
201
202  /**
203   * {@inheritdoc}
204   */
205  public function getAvailableContexts() {
206    return $this->getRuntimeContexts([]);
207  }
208
209  /**
210   * Create a display builder instance.
211   *
212   * @return \Drupal\Core\Entity\EntityInterface
213   *   The entity.
214   */
215  protected function createDisplayBuilderInstance(): EntityInterface {
216    $data = $this->getInitialSources();
217    $tree = new SourceTree($data);
218    $data = $tree->getTree();
219    $data = [
220      'id' => $this->getInstanceId(),
221      'buildable' => [
222        'plugin_id' => $this->getPluginId(),
223        'configuration' => $this->getConfiguration(),
224      ],
225      'sources' => $data,
226      'hash' => Instance::getUniqId($data),
227      'revision_log_message' => $this->getInitializationMessage(),
228      'revision_created' => \time(),
229      'revision_user' => (int) $this->currentUser->id(),
230    ];
231
232    $storage = $this->entityTypeManager->getStorage('display_builder_instance');
233    /** @var \Drupal\display_builder\InstanceInterface $instance */
234    $instance = $storage->create($data);
235
236    return $instance;
237  }
238
239  /**
240   * Get the message to put in the first log step.
241   *
242   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
243   *   The log message.
244   */
245  protected function getInitializationMessage(): TranslatableMarkup {
246    return $this->t('Initialize display');
247  }
248
249  /**
250   * Initialize sources for this implementation.
251   *
252   * @return array
253   *   The data.
254   */
255  protected function getInitialSources(): array {
256    return $this->getSources();
257  }
258
259  /**
260   * Build profile select when user is allowed to select one.
261   *
262   * @param \Drupal\display_builder\Entity\ProfileInterface|null $profile
263   *   Display Builder profile (or not)
264   * @param bool $mandatory
265   *   (Optional) Is it mandatory to use Display Builder? (for example, in
266   *   Page Layouts or in Entity View display Overrides). If not mandatory,
267   *   the Display Builder is activated only if a Display Builder config entity
268   *   is selected.
269   * @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $title
270   *   (Optional) The Select title, default to 'Profile'.
271   *
272   * @return array
273   *   A renderable form array.
274   */
275  protected function buildSelect(?ProfileInterface $profile, bool $mandatory = TRUE, ?TranslatableMarkup $title = NULL): array {
276    $select = [
277      '#type' => 'select',
278      '#title' => $title ?? $this->t('Profile'),
279      '#description' => $this->t('The profile defines the features available in the builder.'),
280      '#options' => $this->getAllowedProfiles(),
281    ];
282
283    if ($profile) {
284      $select['#default_value'] = (string) $profile->id();
285    }
286    elseif (isset($select['#options']['default']) && $mandatory) {
287      $select['#default_value'] = 'default';
288    }
289
290    if ($mandatory) {
291      $select['#required'] = TRUE;
292    }
293    else {
294      $select['#empty_option'] = $this->t('- Disabled -');
295    }
296
297    // Add admin information to link the profiles.
298    if ($this->isCurrentUserAllowedToAdministrate()) {
299      $select['#description'] = [
300        [
301          '#markup' => $select['#description'] . '<br>',
302        ],
303        [
304          '#type' => 'link',
305          '#title' => $this->t('Add and configure display builder profiles'),
306          '#url' => Url::fromRoute('entity.display_builder_profile.collection'),
307          '#suffix' => '.',
308        ],
309      ];
310    }
311
312    return $select;
313  }
314
315  /**
316   * Build link to Display Builder.
317   *
318   * @return array
319   *   A renderable array.
320   */
321  protected function buildLink(): array {
322    return [
323      '#type' => 'html_tag',
324      '#tag' => 'p',
325      '#attributes' => [
326        'class' => ['form-item__description'],
327      ],
328      'content' => [
329        '#type' => 'link',
330        '#title' => $this->t('Build the display'),
331        '#url' => $this->getBuilderUrl(),
332        '#attributes' => [
333          'class' => ['button', 'button--small'],
334        ],
335      ],
336    ];
337  }
338
339  /**
340   * Build disabled profile select when user is not allowed to select one.
341   *
342   * @param \Drupal\display_builder\Entity\ProfileInterface $profile
343   *   Display Builder profile (or not)
344   *
345   * @return array
346   *   A renderable form array.
347   */
348  protected function buildDisabledSelect(ProfileInterface $profile): array {
349    return [
350      '#type' => 'select',
351      '#title' => $this->t('Profile'),
352      '#description' => $this->t('You are not allowed to use Display Builder here.'),
353      '#options' => [
354        (string) $profile->id() => $profile->label(),
355      ],
356      '#disabled' => TRUE,
357    ];
358  }
359
360  /**
361   * Is the current user allowed to use administrate display builder profiles?
362   *
363   * @return bool
364   *   Allowed or not.
365   */
366  protected function isCurrentUserAllowedToAdministrate(): bool {
367    return $this->moduleHandler->moduleExists('display_builder_ui') && $this->currentUser->hasPermission('administer display builder profile');
368  }
369
370}