Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 61
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
EntityViewOverridesController
0.00% covered (danger)
0.00%
0 / 61
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 8
306
0.00% covered (danger)
0.00%
0 / 1
 title
0.00% covered (danger)
0.00%
0 / 7
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 getBuilder
0.00% covered (danger)
0.00%
0 / 8
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 getFirstBuilder
0.00% covered (danger)
0.00%
0 / 10
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
6
 checkAccess
0.00% covered (danger)
0.00%
0 / 7
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
6
 checkFirstBuilderAccess
0.00% covered (danger)
0.00%
0 / 6
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
6
 getFirstOverridableViewMode
0.00% covered (danger)
0.00%
0 / 7
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
12
 getEntityViewDisplay
0.00% covered (danger)
0.00%
0 / 4
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
2
 overrideBuilderAccessResult
0.00% covered (danger)
0.00%
0 / 12
n/a
0 / 0
n/a
0 / 0
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder_entity_view\Controller;
6
7use Drupal\Core\Access\AccessResult;
8use Drupal\Core\Access\AccessResultInterface;
9use Drupal\Core\Cache\CacheableMetadata;
10use Drupal\Core\Entity\Display\EntityDisplayInterface;
11use Drupal\Core\Entity\EntityInterface;
12use Drupal\Core\Routing\RouteMatchInterface;
13use Drupal\Core\Routing\TrustedRedirectResponse;
14use Drupal\Core\Session\AccountInterface;
15use Drupal\Core\StringTranslation\TranslatableMarkup;
16use Drupal\Core\Url;
17use Drupal\display_builder\Controller\IntegrationControllerBase;
18use Drupal\display_builder_entity_view\Entity\DisplayBuilderOverridableInterface;
19use Drupal\display_builder_entity_view\Entity\EntityViewDisplay;
20use Symfony\Component\HttpFoundation\Response;
21use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
22
23/**
24 * Defines a controller to get access the Display Builder admin UI.
25 *
26 * @internal
27 *   Controller classes are internal.
28 */
29final class EntityViewOverridesController extends IntegrationControllerBase {
30
31  /**
32   * Provides a generic title callback for a display used in entities.
33   *
34   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
35   *   The route match object.
36   *
37   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
38   *   The title for the display page.
39   */
40  public function title(RouteMatchInterface $route_match): TranslatableMarkup {
41    $entity_type = $route_match->getParameter('entity_type_id');
42    $entity = $route_match->getParameter($entity_type);
43    $param = [
44      '@title' => $entity->label(),
45      // view_mode_name route parameter is never supposed to be NULL but Drupal
46      // trigger a warning if we omit to check this.
47      '@view_mode_name' => $route_match->getParameter('view_mode_name') ?? '',
48    ];
49
50    return $this->t('Display builder for @title, @view_mode_name', $param);
51  }
52
53  /**
54   * Renders the Layout UI for override entities.
55   *
56   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
57   *   The route match object.
58   *
59   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
60   *   If the entity view display is not found or not overridable.
61   *
62   * @return array
63   *   A render array containing the display builder.
64   */
65  public function getBuilder(RouteMatchInterface $route_match): array {
66    $entity_type_id = $route_match->getParameter('entity_type_id');
67    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
68    $entity = $route_match->getParameter($entity_type_id);
69
70    $view_mode = $route_match->getParameter('view_mode_name');
71
72    /** @var \Drupal\display_builder_entity_view\Entity\DisplayBuilderEntityDisplayInterface $entity_display */
73    $entity_display = $this->getEntityViewDisplay($entity_type_id, $entity->bundle(), $view_mode);
74    \assert($entity_display instanceof DisplayBuilderOverridableInterface);
75    /** @var \Drupal\Core\Field\FieldItemListInterface $with_display_builder */
76    $with_display_builder = $entity->get($entity_display->getDisplayBuilderOverrideField());
77    /** @var \Drupal\display_builder\DisplayBuildableInterface $buildable */
78    $buildable = $this->displayBuildableManager->createInstance('entity_view_override', ['field' => $with_display_builder]);
79
80    return $this->renderBuilder($buildable);
81  }
82
83  /**
84   * Redirects to the default route of a specified component.
85   *
86   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
87   *   The route match object.
88   *
89   * @return \Symfony\Component\HttpFoundation\Response
90   *   A response object that redirects to the first available builder.
91   */
92  public function getFirstBuilder(RouteMatchInterface $route_match): Response {
93    $entity_type_id = $route_match->getParameter('entity_type_id');
94    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
95    $entity = $route_match->getParameter($entity_type_id);
96    $account = $this->currentUser();
97    $view_mode = $this->getFirstOverridableViewMode($entity, $account);
98
99    if ($view_mode) {
100      $route = \sprintf('entity.%s.display_builder.%s', $entity_type_id, $view_mode);
101      $url = Url::fromRoute($route, [$entity_type_id => $entity->id()]);
102      $response = new TrustedRedirectResponse($url->toString());
103
104      return $response->addCacheableDependency((new CacheableMetadata())->setCacheMaxAge(0));
105    }
106
107    throw new AccessDeniedHttpException();
108  }
109
110  /**
111   * Access callback to ensure display tab belongs to current bundle.
112   *
113   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
114   *   The route match object.
115   * @param \Drupal\Core\Session\AccountInterface $account
116   *   The user account.
117   *
118   * @return \Drupal\Core\Access\AccessResultInterface
119   *   The access result.
120   */
121  public function checkAccess(RouteMatchInterface $route_match, AccountInterface $account): AccessResultInterface {
122    $route = $route_match->getRouteObject();
123
124    if (!$route) {
125      return AccessResult::forbidden()->addCacheContexts(['route']);
126    }
127    $entity_type_id = $route->getDefault('entity_type_id');
128    $entity = $route_match->getParameter($entity_type_id);
129    $view_mode_name = $route->getDefault('view_mode_name');
130
131    return $this->overrideBuilderAccessResult($account, $entity, $view_mode_name);
132  }
133
134  /**
135   * Access callback for the main display.
136   *
137   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
138   *   The route match object.
139   * @param \Drupal\Core\Session\AccountInterface $account
140   *   The user account.
141   *
142   * @return \Drupal\Core\Access\AccessResultInterface
143   *   The access result.
144   */
145  public function checkFirstBuilderAccess(RouteMatchInterface $route_match, AccountInterface $account): AccessResultInterface {
146    $entity_type_id = $route_match->getParameter('entity_type_id');
147    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
148    $entity = $route_match->getParameter($entity_type_id);
149    $view_mode = $this->getFirstOverridableViewMode($entity, $account);
150
151    if ($view_mode) {
152      return AccessResult::allowed()->addCacheContexts(['route']);
153    }
154
155    return AccessResult::forbidden()->addCacheContexts(['route']);
156  }
157
158  /**
159   * Get the first overridable view mode of an entity.
160   *
161   * @param \Drupal\Core\Entity\EntityInterface $entity
162   *   The entity to check.
163   * @param \Drupal\Core\Session\AccountInterface $account
164   *   The user account.
165   *
166   * @return string|null
167   *   The first overridable view mode name, or NULL if none is found.
168   */
169  protected function getFirstOverridableViewMode(EntityInterface $entity, AccountInterface $account): ?string {
170    $display_infos = EntityViewDisplay::getDisplayInfos($this->entityTypeManager());
171    $view_modes = $display_infos[$entity->getEntityTypeId()]['bundles'][$entity->bundle()] ?? [];
172
173    foreach (\array_keys($view_modes) as $view_mode) {
174      $access = $this->overrideBuilderAccessResult($account, $entity, (string) $view_mode);
175
176      if ($access->isAllowed()) {
177        return (string) $view_mode;
178      }
179    }
180
181    return NULL;
182  }
183
184  /**
185   * Get entity view display entity.
186   *
187   * @param string $entity_type_id
188   *   Entity type ID.
189   * @param string $bundle
190   *   Fieldable entity's bundle.
191   * @param string $view_mode
192   *   View mode of the display.
193   *
194   * @return \Drupal\Core\Entity\Display\EntityDisplayInterface|null
195   *   The corresponding entity view display.
196   */
197  protected function getEntityViewDisplay(string $entity_type_id, string $bundle, string $view_mode): ?EntityDisplayInterface {
198    $display_id = \sprintf('%s.%s.%s', $entity_type_id, $bundle, $view_mode);
199    /** @var \Drupal\Core\Entity\Display\EntityDisplayInterface|null $display */
200    $display = $this->entityTypeManager()->getStorage('entity_view_display')
201      ->load($display_id);
202
203    return $display;
204  }
205
206  /**
207   * Returns AccessResult for given entity and view mode.
208   *
209   * Any user with both the permission to edit the content and the one to use
210   * the display builder profile can override the display.
211   * The permission to edit the content is already checked with _entity_access
212   * in DisplayBuilderRoutes so we just need to check the profile permission.
213   *
214   * @param \Drupal\Core\Session\AccountInterface $account
215   *   The user account.
216   * @param \Drupal\Core\Entity\EntityInterface $entity
217   *   The entity to check access for.
218   * @param string|null $view_mode_name
219   *   The view mode name, or NULL if not applicable.
220   *
221   * @return \Drupal\Core\Access\AccessResultInterface
222   *   The access result.
223   */
224  protected function overrideBuilderAccessResult(AccountInterface $account, EntityInterface $entity, ?string $view_mode_name): AccessResultInterface {
225    $forbidden = AccessResult::forbidden()->addCacheContexts(['route']);
226
227    if ($view_mode_name === NULL) {
228      return $forbidden;
229    }
230
231    $display = self::getEntityViewDisplay($entity->getEntityTypeId(), $entity->bundle(), $view_mode_name);
232
233    if (!$display instanceof DisplayBuilderOverridableInterface) {
234      return $forbidden;
235    }
236
237    if (!$display->isDisplayBuilderOverridable()) {
238      return $forbidden;
239    }
240
241    $permission = $display->getDisplayBuilderOverrideProfile()->getPermissionName();
242
243    // This is the expected check.
244    if (!$account->hasPermission($permission)) {
245      return $forbidden;
246    }
247
248    return AccessResult::allowed()->addCacheableDependency($display)->addCacheContexts(['route']);
249  }
250
251}

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.