Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 29
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 / 54
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 6
506
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
0.00% covered (danger)
0.00%
0 / 1
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
0.00% covered (danger)
0.00%
0 / 1
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 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 checkAccess
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
90
 getAccessResult
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 5
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 / 5
0.00% covered (danger)
0.00%
0 / 4
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    // Require permission to administer page layouts.
116    if (!$account->hasPermission('administer page_layout')) {
117      return AccessResult::forbidden()->cachePerPermissions();
118    }
119
120    // Don't grant access to disabled page layouts.
121    if (!$entity->status() || empty($entity->getSources())) {
122      return AccessResult::forbidden()->addCacheableDependency($entity);
123    }
124
125    $conditions = [];
126    $missing_context = FALSE;
127    $missing_value = FALSE;
128
129    foreach ($entity->getConditions() as $condition_id => $condition) {
130      if ($condition instanceof ContextAwarePluginInterface) {
131        try {
132          $contexts = $this->contextRepository->getRuntimeContexts(\array_values($condition->getContextMapping()));
133          $this->contextHandler->applyContextMapping($condition, $contexts);
134        }
135        catch (MissingValueContextException) {
136          $missing_value = TRUE;
137        }
138        catch (ContextException) {
139          $missing_context = TRUE;
140        }
141      }
142      $conditions[$condition_id] = $condition;
143    }
144    $access = $this->getAccessResult($missing_value, $missing_context, $conditions);
145    $this->mergeCacheabilityFromConditions($access, $conditions);
146
147    // Ensure that access is evaluated again when the entity changes.
148    return $access->addCacheableDependency($entity);
149  }
150
151  /**
152   * Get access result.
153   *
154   * @param bool $missing_value
155   *   Whether any of the contexts are missing a value.
156   * @param bool $missing_context
157   *   Whether any of the contexts are missing.
158   * @param \Drupal\Core\Condition\ConditionInterface[] $conditions
159   *   List of visibility conditions.
160   *
161   * @return \Drupal\Core\Access\AccessResult
162   *   The access result.
163   */
164  protected function getAccessResult(bool $missing_value, bool $missing_context, array $conditions): AccessResult {
165    if ($missing_context) {
166      // If any context is missing then we might be missing cacheable
167      // metadata, and don't know based on what conditions the layout is
168      // accessible or not. Make sure the result cannot be cached.
169      $access = AccessResult::forbidden()->setCacheMaxAge(0);
170    }
171    elseif ($missing_value) {
172      // The contexts exist but have no value. Deny access without
173      // disabling caching. For example the node type condition will have a
174      // missing context on any non-node route like the frontpage.
175      $access = AccessResult::forbidden();
176    }
177    elseif ($this->resolveConditions($conditions, 'and') !== FALSE) {
178      $access = AccessResult::allowed();
179    }
180    else {
181      $reason = \count($conditions) > 1
182      ? "One of the conditions ('%s') denied access."
183      : "The  condition '%s' denied access.";
184      $access = AccessResult::forbidden(\sprintf($reason, \implode("', '", \array_keys($conditions))));
185    }
186
187    return $access;
188  }
189
190  /**
191   * Merges cacheable metadata from conditions onto the access result object.
192   *
193   * @param \Drupal\Core\Access\AccessResult $access
194   *   The access result object.
195   * @param \Drupal\Core\Condition\ConditionInterface[] $conditions
196   *   List of visibility conditions.
197   */
198  protected function mergeCacheabilityFromConditions(AccessResult $access, array $conditions): void {
199    foreach ($conditions as $condition) {
200      if ($condition instanceof CacheableDependencyInterface) {
201        $access->addCacheTags($condition->getCacheTags());
202        $access->addCacheContexts($condition->getCacheContexts());
203        $access->setCacheMaxAge(Cache::mergeMaxAges($access->getCacheMaxAge(), $condition->getCacheMaxAge()));
204      }
205    }
206  }
207
208}