Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AccessControlHandler
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 6
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 createInstance
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 loadCurrentPageLayout
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 checkAccess
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 getAccessResult
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 mergeCacheabilityFromConditions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_page_layout;
6
7use Drupal\Component\Plugin\Exception\ContextException;
8use Drupal\Component\Plugin\Exception\MissingValueContextException;
9use Drupal\Core\Access\AccessResult;
10use Drupal\Core\Access\AccessResultInterface;
11use Drupal\Core\Cache\Cache;
12use Drupal\Core\Cache\CacheableDependencyInterface;
13use Drupal\Core\Condition\ConditionAccessResolverTrait;
14use Drupal\Core\Entity\EntityAccessControlHandler;
15use Drupal\Core\Entity\EntityHandlerInterface;
16use Drupal\Core\Entity\EntityInterface;
17use Drupal\Core\Entity\EntityTypeInterface;
18use Drupal\Core\Entity\EntityTypeManagerInterface;
19use Drupal\Core\Plugin\Context\ContextHandlerInterface;
20use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
21use Drupal\Core\Plugin\ContextAwarePluginInterface;
22use Drupal\Core\Session\AccountInterface;
23use Symfony\Component\DependencyInjection\ContainerInterface;
24
25/**
26 * Defines the access control handler for the page layout entity type.
27 *
28 * @see \Drupal\display_builder_page_layout\Entity\PageLayout
29 */
30class AccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
31
32  use ConditionAccessResolverTrait;
33
34  /**
35   * The plugin context handler.
36   *
37   * @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
38   */
39  protected $contextHandler;
40
41  /**
42   * The context manager service.
43   *
44   * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
45   */
46  protected $contextRepository;
47
48  /**
49   * The entity type manager.
50   */
51  protected EntityTypeManagerInterface $entityTypeManager;
52
53  /**
54   * {@inheritdoc}
55   */
56  public function __construct(
57    EntityTypeInterface $entity_type,
58    ContextHandlerInterface $context_handler,
59    ContextRepositoryInterface $context_repository,
60    EntityTypeManagerInterface $entity_type_manager,
61  ) {
62    parent::__construct($entity_type);
63    $this->contextHandler = $context_handler;
64    $this->contextRepository = $context_repository;
65    $this->entityTypeManager = $entity_type_manager;
66  }
67
68  /**
69   * {@inheritdoc}
70   */
71  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type): AccessControlHandler {
72    return new static(
73      $entity_type,
74      $container->get('context.handler'),
75      $container->get('context.repository'),
76      $container->get('entity_type.manager'),
77    );
78  }
79
80  /**
81   * Load the first suitable Page Layout entity.
82   *
83   * A 'suitable' Page Layout is an entity which is enabled, meeting the right
84   * conditions, and with non-empty sources.
85   *
86   * @return ?PageLayoutInterface
87   *   A Page Layout entity.
88   */
89  public function loadCurrentPageLayout(): ?PageLayoutInterface {
90    $storage = $this->entityTypeManager->getStorage('page_layout');
91    $entity_ids = $storage->getQuery()->accessCheck(TRUE)->sort('weight', 'ASC')->execute();
92    /** @var \Drupal\display_builder_page_layout\PageLayoutInterface[] $page_layouts */
93    $page_layouts = $storage->loadMultiple($entity_ids);
94
95    foreach ($page_layouts as $page_layout) {
96      if ($this->access($page_layout, 'view')) {
97        return $page_layout;
98      }
99    }
100
101    // If no suitable Page Layout found, PageVariantSubscriber will load the
102    // usual Block Layout with theme's page.html.twig.
103    return NULL;
104  }
105
106  /**
107   * {@inheritdoc}
108   */
109  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account): AccessResultInterface {
110    /** @var \Drupal\display_builder_page_layout\PageLayoutInterface $entity */
111    if ($operation !== 'view') {
112      return parent::checkAccess($entity, $operation, $account);
113    }
114
115    // Don't grant access to disabled page layouts.
116    if (!$entity->status() || empty($entity->getSources())) {
117      return AccessResult::forbidden()->addCacheableDependency($entity);
118    }
119
120    $conditions = [];
121    $missing_context = FALSE;
122    $missing_value = FALSE;
123
124    foreach ($entity->getConditions() as $condition_id => $condition) {
125      if ($condition instanceof ContextAwarePluginInterface) {
126        try {
127          $contexts = $this->contextRepository->getRuntimeContexts(\array_values($condition->getContextMapping()));
128          $this->contextHandler->applyContextMapping($condition, $contexts);
129        }
130        catch (MissingValueContextException) {
131          $missing_value = TRUE;
132        }
133        catch (ContextException) {
134          $missing_context = TRUE;
135        }
136      }
137      $conditions[$condition_id] = $condition;
138    }
139    $access = $this->getAccessResult($missing_value, $missing_context, $conditions);
140    $this->mergeCacheabilityFromConditions($access, $conditions);
141
142    // Ensure that access is evaluated again when the entity changes.
143    return $access->addCacheableDependency($entity);
144  }
145
146  /**
147   * Get access result.
148   *
149   * @param bool $missing_value
150   *   Whether any of the contexts are missing a value.
151   * @param bool $missing_context
152   *   Whether any of the contexts are missing.
153   * @param \Drupal\Core\Condition\ConditionInterface[] $conditions
154   *   List of visibility conditions.
155   *
156   * @return \Drupal\Core\Access\AccessResult
157   *   The access result.
158   */
159  protected function getAccessResult(bool $missing_value, bool $missing_context, array $conditions): AccessResult {
160    if ($missing_context) {
161      // If any context is missing then we might be missing cacheable
162      // metadata, and don't know based on what conditions the layout is
163      // accessible or not. Make sure the result cannot be cached.
164      $access = AccessResult::forbidden()->setCacheMaxAge(0);
165    }
166    elseif ($missing_value) {
167      // The contexts exist but have no value. Deny access without
168      // disabling caching. For example the node type condition will have a
169      // missing context on any non-node route like the frontpage.
170      $access = AccessResult::forbidden();
171    }
172    elseif ($this->resolveConditions($conditions, 'and') !== FALSE) {
173      $access = AccessResult::allowed();
174    }
175    else {
176      $reason = \count($conditions) > 1
177      ? "One of the conditions ('%s') denied access."
178      : "The  condition '%s' denied access.";
179      $access = AccessResult::forbidden(\sprintf($reason, \implode("', '", \array_keys($conditions))));
180    }
181
182    return $access;
183  }
184
185  /**
186   * Merges cacheable metadata from conditions onto the access result object.
187   *
188   * @param \Drupal\Core\Access\AccessResult $access
189   *   The access result object.
190   * @param \Drupal\Core\Condition\ConditionInterface[] $conditions
191   *   List of visibility conditions.
192   */
193  protected function mergeCacheabilityFromConditions(AccessResult $access, array $conditions): void {
194    foreach ($conditions as $condition) {
195      if ($condition instanceof CacheableDependencyInterface) {
196        $access->addCacheTags($condition->getCacheTags());
197        $access->addCacheContexts($condition->getCacheContexts());
198        $access->setCacheMaxAge(Cache::mergeMaxAges($access->getCacheMaxAge(), $condition->getCacheMaxAge()));
199      }
200    }
201  }
202
203}