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}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

AccessControlHandler->__construct
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  }
AccessControlHandler->checkAccess
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);
116    if (!$account->hasPermission('administer page_layout')) {
117      return AccessResult::forbidden()->cachePerPermissions();
121    if (!$entity->status() || empty($entity->getSources())) {
121    if (!$entity->status() || empty($entity->getSources())) {
122      return AccessResult::forbidden()->addCacheableDependency($entity);
125    $conditions = [];
126    $missing_context = FALSE;
127    $missing_value = FALSE;
128
129    foreach ($entity->getConditions() as $condition_id => $condition) {
129    foreach ($entity->getConditions() as $condition_id => $condition) {
129    foreach ($entity->getConditions() as $condition_id => $condition) {
130      if ($condition instanceof ContextAwarePluginInterface) {
132          $contexts = $this->contextRepository->getRuntimeContexts(\array_values($condition->getContextMapping()));
133          $this->contextHandler->applyContextMapping($condition, $contexts);
135        catch (MissingValueContextException) {
136          $missing_value = TRUE;
138        catch (ContextException) {
139          $missing_context = TRUE;
140        }
141      }
142      $conditions[$condition_id] = $condition;
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;
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);
AccessControlHandler->createInstance
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'),
AccessControlHandler->getAccessResult
164  protected function getAccessResult(bool $missing_value, bool $missing_context, array $conditions): AccessResult {
165    if ($missing_context) {
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);
171    elseif ($missing_value) {
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();
177    elseif ($this->resolveConditions($conditions, 'and') !== FALSE) {
177    elseif ($this->resolveConditions($conditions, 'and') !== FALSE) {
178      $access = AccessResult::allowed();
181      $reason = \count($conditions) > 1
182      ? "One of the conditions ('%s') denied access."
181      $reason = \count($conditions) > 1
182      ? "One of the conditions ('%s') denied access."
183      : "The  condition '%s' denied access.";
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;
187    return $access;
AccessControlHandler->loadCurrentPageLayout
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) {
95    foreach ($page_layouts as $page_layout) {
96      if ($this->access($page_layout, 'view')) {
97        return $page_layout;
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;
AccessControlHandler->mergeCacheabilityFromConditions
198  protected function mergeCacheabilityFromConditions(AccessResult $access, array $conditions): void {
199    foreach ($conditions as $condition) {
199    foreach ($conditions as $condition) {
200      if ($condition instanceof CacheableDependencyInterface) {
199    foreach ($conditions as $condition) {
200      if ($condition instanceof CacheableDependencyInterface) {
201        $access->addCacheTags($condition->getCacheTags());
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  }