1<?php
2
3namespace Drupal\layout_builder\Plugin\SectionStorage;
4
5use Drupal\Core\Access\AccessResult;
6use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
7use Drupal\Core\Entity\EntityFieldManagerInterface;
8use Drupal\Core\Entity\EntityRepositoryInterface;
9use Drupal\Core\Entity\EntityTypeInterface;
10use Drupal\Core\Entity\EntityTypeManagerInterface;
11use Drupal\Core\Entity\FieldableEntityInterface;
12use Drupal\Core\Entity\TranslatableInterface;
13use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
14use Drupal\Core\Plugin\Context\Context;
15use Drupal\Core\Plugin\Context\ContextDefinition;
16use Drupal\Core\Plugin\Context\EntityContext;
17use Drupal\Core\Session\AccountInterface;
18use Drupal\Core\Url;
19use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
20use Drupal\layout_builder\OverridesSectionStorageInterface;
21use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
22use Symfony\Component\DependencyInjection\ContainerInterface;
23use Symfony\Component\Routing\RouteCollection;
24
25/**
26 * Defines the 'overrides' section storage type.
27 *
28 * OverridesSectionStorage uses a negative weight because:
29 * - It must be picked before
30 *   \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage.
31 * - The default weight is 0, so custom implementations will not take
32 *   precedence unless otherwise specified.
33 *
34 * @SectionStorage(
35 *   id = "overrides",
36 *   weight = -20,
37 *   handles_permission_check = TRUE,
38 *   context_definitions = {
39 *     "entity" = @ContextDefinition("entity", constraints = {
40 *       "EntityHasField" = \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::FIELD_NAME,
41 *     }),
42 *     "view_mode" = @ContextDefinition("string", default_value = "default"),
43 *   }
44 * )
45 *
46 * @internal
47 *   Plugin classes are internal.
48 */
49class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
50
51  /**
52   * The field name used by this storage.
53   *
54   * @var string
55   */
56  const FIELD_NAME = 'layout_builder__layout';
57
58  /**
59   * The entity type manager.
60   *
61   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
62   */
63  protected $entityTypeManager;
64
65  /**
66   * The entity field manager.
67   *
68   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
69   */
70  protected $entityFieldManager;
71
72  /**
73   * The section storage manager.
74   *
75   * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
76   */
77  protected $sectionStorageManager;
78
79  /**
80   * The entity repository.
81   *
82   * @var \Drupal\Core\Entity\EntityRepositoryInterface
83   */
84  protected $entityRepository;
85
86  /**
87   * The current user.
88   *
89   * @var \Drupal\Core\Session\AccountInterface
90   */
91  protected $currentUser;
92
93  /**
94   * {@inheritdoc}
95   */
96  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, SectionStorageManagerInterface $section_storage_manager, EntityRepositoryInterface $entity_repository, AccountInterface $current_user = NULL) {
97    parent::__construct($configuration, $plugin_id, $plugin_definition);
98
99    $this->entityTypeManager = $entity_type_manager;
100    $this->entityFieldManager = $entity_field_manager;
101    $this->sectionStorageManager = $section_storage_manager;
102    $this->entityRepository = $entity_repository;
103    if (!$current_user) {
104      @trigger_error('The current_user service must be passed to OverridesSectionStorage::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
105      $current_user = \Drupal::currentUser();
106    }
107    $this->currentUser = $current_user;
108  }
109
110  /**
111   * {@inheritdoc}
112   */
113  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
114    return new static(
115      $configuration,
116      $plugin_id,
117      $plugin_definition,
118      $container->get('entity_type.manager'),
119      $container->get('entity_field.manager'),
120      $container->get('plugin.manager.layout_builder.section_storage'),
121      $container->get('entity.repository'),
122      $container->get('current_user')
123    );
124  }
125
126  /**
127   * {@inheritdoc}
128   */
129  protected function getSectionList() {
130    return $this->getEntity()->get(static::FIELD_NAME);
131  }
132
133  /**
134   * Gets the entity storing the overrides.
135   *
136   * @return \Drupal\Core\Entity\FieldableEntityInterface
137   *   The entity storing the overrides.
138   */
139  protected function getEntity() {
140    return $this->getContextValue('entity');
141  }
142
143  /**
144   * {@inheritdoc}
145   */
146  public function getStorageId() {
147    $entity = $this->getEntity();
148    return $entity->getEntityTypeId() . '.' . $entity->id();
149  }
150
151  /**
152   * {@inheritdoc}
153   */
154  public function getTempstoreKey() {
155    $key = parent::getTempstoreKey();
156    $key .= '.' . $this->getContextValue('view_mode');
157
158    $entity = $this->getEntity();
159    // @todo Allow entities to provide this contextual information in
160    //   https://www.drupal.org/project/drupal/issues/3026957.
161    if ($entity instanceof TranslatableInterface) {
162      $key .= '.' . $entity->language()->getId();
163    }
164    return $key;
165  }
166
167  /**
168   * {@inheritdoc}
169   */
170  public function extractIdFromRoute($value, $definition, $name, array $defaults) {
171    @trigger_error('\Drupal\layout_builder\SectionStorageInterface::extractIdFromRoute() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute() should be used instead. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED);
172    if (strpos($value, '.') !== FALSE) {
173      return $value;
174    }
175
176    if (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) {
177      $entity_type_id = $defaults['entity_type_id'];
178      $entity_id = $defaults[$entity_type_id];
179      return $entity_type_id . '.' . $entity_id;
180    }
181  }
182
183  /**
184   * {@inheritdoc}
185   */
186  public function getSectionListFromId($id) {
187    @trigger_error('\Drupal\layout_builder\SectionStorageInterface::getSectionListFromId() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. The section list should be derived from context. See https://www.drupal.org/node/3016262.', E_USER_DEPRECATED);
188    if (strpos($id, '.') !== FALSE) {
189      list($entity_type_id, $entity_id) = explode('.', $id, 2);
190      $entity = $this->entityRepository->getActive($entity_type_id, $entity_id);
191      if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) {
192        return $entity->get(static::FIELD_NAME);
193      }
194    }
195    throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
196  }
197
198  /**
199   * {@inheritdoc}
200   */
201  public function deriveContextsFromRoute($value, $definition, $name, array $defaults) {
202    $contexts = [];
203
204    if ($entity = $this->extractEntityFromRoute($value, $defaults)) {
205      $contexts['entity'] = EntityContext::fromEntity($entity);
206      // @todo Expand to work for all view modes in
207      //   https://www.drupal.org/node/2907413.
208      $view_mode = 'full';
209      // Retrieve the actual view mode from the returned view display as the
210      // requested view mode may not exist and a fallback will be used.
211      $view_mode = LayoutBuilderEntityViewDisplay::collectRenderDisplay($entity, $view_mode)->getMode();
212      $contexts['view_mode'] = new Context(new ContextDefinition('string'), $view_mode);
213    }
214    return $contexts;
215  }
216
217  /**
218   * Extracts an entity from the route values.
219   *
220   * @param mixed $value
221   *   The raw value from the route.
222   * @param array $defaults
223   *   The route defaults array.
224   *
225   * @return \Drupal\Core\Entity\EntityInterface|null
226   *   The entity for the route, or NULL if none exist.
227   *
228   * @see \Drupal\layout_builder\SectionStorageInterface::deriveContextsFromRoute()
229   * @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert()
230   */
231  private function extractEntityFromRoute($value, array $defaults) {
232    if (strpos($value, '.') !== FALSE) {
233      list($entity_type_id, $entity_id) = explode('.', $value, 2);
234    }
235    elseif (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) {
236      $entity_type_id = $defaults['entity_type_id'];
237      $entity_id = $defaults[$entity_type_id];
238    }
239    else {
240      return NULL;
241    }
242
243    $entity = $this->entityRepository->getActive($entity_type_id, $entity_id);
244    if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) {
245      return $entity;
246    }
247  }
248
249  /**
250   * {@inheritdoc}
251   */
252  public function buildRoutes(RouteCollection $collection) {
253    foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
254      // If the canonical route does not exist, do not provide any Layout
255      // Builder UI routes for this entity type.
256      if (!$collection->get("entity.$entity_type_id.canonical")) {
257        continue;
258      }
259
260      $defaults = [];
261      $defaults['entity_type_id'] = $entity_type_id;
262
263      // Retrieve the requirements from the canonical route.
264      $requirements = $collection->get("entity.$entity_type_id.canonical")->getRequirements();
265
266      $options = [];
267      // Ensure that upcasting is run in the correct order.
268      $options['parameters']['section_storage'] = [];
269      $options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id;
270
271      $template = $entity_type->getLinkTemplate('canonical') . '/layout';
272      $this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $template, $defaults, $requirements, $options, $entity_type_id, $entity_type_id);
273    }
274  }
275
276  /**
277   * {@inheritdoc}
278   */
279  public function buildLocalTasks($base_plugin_definition) {
280    $local_tasks = [];
281    foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
282      $local_tasks["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
283        'route_name' => "layout_builder.overrides.$entity_type_id.view",
284        'weight' => 15,
285        'title' => $this->t('Layout'),
286        'base_route' => "entity.$entity_type_id.canonical",
287        'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
288      ];
289    }
290    return $local_tasks;
291  }
292
293  /**
294   * Determines if this entity type's ID is stored as an integer.
295   *
296   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
297   *   An entity type.
298   *
299   * @return bool
300   *   TRUE if this entity type's ID key is always an integer, FALSE otherwise.
301   */
302  protected function hasIntegerId(EntityTypeInterface $entity_type) {
303    $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
304    return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer';
305  }
306
307  /**
308   * Returns an array of relevant entity types.
309   *
310   * @return \Drupal\Core\Entity\EntityTypeInterface[]
311   *   An array of entity types.
312   */
313  protected function getEntityTypes() {
314    return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
315      return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasHandlerClass('form', 'layout_builder') && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
316    });
317  }
318
319  /**
320   * {@inheritdoc}
321   */
322  public function getDefaultSectionStorage() {
323    $display = LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), $this->getContextValue('view_mode'));
324    return $this->sectionStorageManager->load('defaults', ['display' => EntityContext::fromEntity($display)]);
325  }
326
327  /**
328   * {@inheritdoc}
329   */
330  public function getRedirectUrl() {
331    return $this->getEntity()->toUrl('canonical');
332  }
333
334  /**
335   * {@inheritdoc}
336   */
337  public function getLayoutBuilderUrl($rel = 'view') {
338    $entity = $this->getEntity();
339    $route_parameters[$entity->getEntityTypeId()] = $entity->id();
340    return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.$rel", $route_parameters);
341  }
342
343  /**
344   * {@inheritdoc}
345   */
346  public function getContextsDuringPreview() {
347    $contexts = parent::getContextsDuringPreview();
348
349    // @todo Remove this in https://www.drupal.org/node/3018782.
350    if (isset($contexts['entity'])) {
351      $contexts['layout_builder.entity'] = $contexts['entity'];
352      unset($contexts['entity']);
353    }
354    return $contexts;
355  }
356
357  /**
358   * {@inheritdoc}
359   */
360  public function label() {
361    return $this->getEntity()->label();
362  }
363
364  /**
365   * {@inheritdoc}
366   */
367  public function save() {
368    return $this->getEntity()->save();
369  }
370
371  /**
372   * {@inheritdoc}
373   */
374  public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
375    if ($account === NULL) {
376      $account = $this->currentUser;
377    }
378
379    $entity = $this->getEntity();
380
381    // Create an access result that will allow access to the layout if one of
382    // these conditions applies:
383    // 1. The user can configure any layouts.
384    $any_access = AccessResult::allowedIfHasPermission($account, 'configure any layout');
385    // 2. The user can configure layouts on all items of the bundle type.
386    $bundle_access = AccessResult::allowedIfHasPermission($account, "configure all {$entity->bundle()} {$entity->getEntityTypeId()} layout overrides");
387    // 3. The user can configure layouts items of this bundle type they can edit
388    //    AND the user has access to edit this entity.
389    $edit_only_bundle_access = AccessResult::allowedIfHasPermission($account, "configure editable {$entity->bundle()} {$entity->getEntityTypeId()} layout overrides");
390    $edit_only_bundle_access = $edit_only_bundle_access->andIf($entity->access('update', $account, TRUE));
391
392    $result = $any_access
393      ->orIf($bundle_access)
394      ->orIf($edit_only_bundle_access);
395
396    // Access also depends on the default being enabled.
397    $result = $result->andIf($this->getDefaultSectionStorage()->access($operation, $account, TRUE));
398    $result = $this->handleTranslationAccess($result, $operation, $account);
399    return $return_as_object ? $result : $result->isAllowed();
400  }
401
402  /**
403   * Handles access checks related to translations.
404   *
405   * @param \Drupal\Core\Access\AccessResult $result
406   *   The access result.
407   * @param string $operation
408   *   The operation to be performed.
409   * @param \Drupal\Core\Session\AccountInterface $account
410   *   The user for which to check access.
411   *
412   * @return \Drupal\Core\Access\AccessResultInterface
413   *   The access result.
414   */
415  protected function handleTranslationAccess(AccessResult $result, $operation, AccountInterface $account) {
416    $entity = $this->getEntity();
417    // Access is always denied on non-default translations.
418    return $result->andIf(AccessResult::allowedIf(!($entity instanceof TranslatableInterface && !$entity->isDefaultTranslation())))->addCacheableDependency($entity);
419  }
420
421  /**
422   * {@inheritdoc}
423   */
424  public function isApplicable(RefinableCacheableDependencyInterface $cacheability) {
425    $default_section_storage = $this->getDefaultSectionStorage();
426    $cacheability->addCacheableDependency($default_section_storage)->addCacheableDependency($this);
427    // Check that overrides are enabled and have at least one section.
428    return $default_section_storage->isOverridable() && $this->isOverridden();
429  }
430
431  /**
432   * {@inheritdoc}
433   */
434  public function isOverridden() {
435    // If there are any sections at all, including a blank one, this section
436    // storage has been overridden. Do not use count() as it does not include
437    // blank sections.
438    return !empty($this->getSections());
439  }
440
441}
442