Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 60
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 / 60
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 / 7
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\EntityInterface;
11use Drupal\Core\Routing\RouteMatchInterface;
12use Drupal\Core\Routing\TrustedRedirectResponse;
13use Drupal\Core\Session\AccountInterface;
14use Drupal\Core\StringTranslation\TranslatableMarkup;
15use Drupal\Core\Url;
16use Drupal\display_builder\Controller\IntegrationControllerBase;
17use Drupal\display_builder\DisplayBuildableInterface;
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\display_builder\DisplayBuildableInterface $with_display_builder */
76    $with_display_builder = $entity->get($entity_display->getDisplayBuilderOverrideField());
77
78    return $this->renderBuilder($with_display_builder);
79  }
80
81  /**
82   * Redirects to the default route of a specified component.
83   *
84   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
85   *   The route match object.
86   *
87   * @return \Symfony\Component\HttpFoundation\Response
88   *   A response object that redirects to the first available builder.
89   */
90  public function getFirstBuilder(RouteMatchInterface $route_match): Response {
91    $entity_type_id = $route_match->getParameter('entity_type_id');
92    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
93    $entity = $route_match->getParameter($entity_type_id);
94    $account = $this->currentUser();
95    $view_mode = $this->getFirstOverridableViewMode($entity, $account);
96
97    if ($view_mode) {
98      $route = "entity.{$entity_type_id}.display_builder.{$view_mode}";
99      $url = Url::fromRoute($route, [$entity_type_id => $entity->id()]);
100      $response = new TrustedRedirectResponse($url->toString());
101
102      return $response->addCacheableDependency((new CacheableMetadata())->setCacheMaxAge(0));
103    }
104
105    throw new AccessDeniedHttpException();
106  }
107
108  /**
109   * Access callback to ensure display tab belongs to current bundle.
110   *
111   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
112   *   The route match object.
113   * @param \Drupal\Core\Session\AccountInterface $account
114   *   The user account.
115   *
116   * @return \Drupal\Core\Access\AccessResultInterface
117   *   The access result.
118   */
119  public function checkAccess(RouteMatchInterface $route_match, AccountInterface $account): AccessResultInterface {
120    $route = $route_match->getRouteObject();
121
122    if (!$route) {
123      return AccessResult::forbidden()->addCacheContexts(['route']);
124    }
125    $entity_type_id = $route->getDefault('entity_type_id');
126    $entity = $route_match->getParameter($entity_type_id);
127    $view_mode_name = $route->getDefault('view_mode_name');
128
129    return $this->overrideBuilderAccessResult($account, $entity, $view_mode_name);
130  }
131
132  /**
133   * Access callback for the main display.
134   *
135   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
136   *   The route match object.
137   * @param \Drupal\Core\Session\AccountInterface $account
138   *   The user account.
139   *
140   * @return \Drupal\Core\Access\AccessResultInterface
141   *   The access result.
142   */
143  public function checkFirstBuilderAccess(RouteMatchInterface $route_match, AccountInterface $account): AccessResultInterface {
144    $entity_type_id = $route_match->getParameter('entity_type_id');
145    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
146    $entity = $route_match->getParameter($entity_type_id);
147    $view_mode = $this->getFirstOverridableViewMode($entity, $account);
148
149    if ($view_mode) {
150      return AccessResult::allowed()->addCacheContexts(['route']);
151    }
152
153    return AccessResult::forbidden()->addCacheContexts(['route']);
154  }
155
156  /**
157   * Get the first overridable view mode of an entity.
158   *
159   * @param \Drupal\Core\Entity\EntityInterface $entity
160   *   The entity to check.
161   * @param \Drupal\Core\Session\AccountInterface $account
162   *   The user account.
163   *
164   * @return string|null
165   *   The first overridable view mode name, or NULL if none is found.
166   */
167  protected function getFirstOverridableViewMode(EntityInterface $entity, AccountInterface $account): ?string {
168    $display_infos = EntityViewDisplay::getDisplayInfos($this->entityTypeManager());
169    $view_modes = $display_infos[$entity->getEntityTypeId()]['bundles'][$entity->bundle()] ?? [];
170
171    foreach (\array_keys($view_modes) as $view_mode) {
172      $access = $this->overrideBuilderAccessResult($account, $entity, (string) $view_mode);
173
174      if ($access->isAllowed()) {
175        return (string) $view_mode;
176      }
177    }
178
179    return NULL;
180  }
181
182  /**
183   * Get entity view display entity.
184   *
185   * @param string $entity_type_id
186   *   Entity type ID.
187   * @param string $bundle
188   *   Fieldable entity's bundle.
189   * @param string $view_mode
190   *   View mode of the display.
191   *
192   * @return \Drupal\display_builder\DisplayBuildableInterface|null
193   *   The corresponding entity view display.
194   */
195  protected function getEntityViewDisplay(string $entity_type_id, string $bundle, string $view_mode): ?DisplayBuildableInterface {
196    $display_id = \sprintf('%s.%s.%s', $entity_type_id, $bundle, $view_mode);
197
198    /** @var \Drupal\display_builder\DisplayBuildableInterface|null $display */
199    $display = $this->entityTypeManager()->getStorage('entity_view_display')
200      ->load($display_id);
201
202    return $display;
203  }
204
205  /**
206   * Returns AccessResult for given entity and view mode.
207   *
208   * Any user with both the permission to edit the content and the one to use
209   * the display builder profile can override the display.
210   * The permission to edit the content is already checked with _entity_access
211   * in DisplayBuilderRoutes so we just need to check the profile permission.
212   *
213   * @param \Drupal\Core\Session\AccountInterface $account
214   *   The user account.
215   * @param \Drupal\Core\Entity\EntityInterface $entity
216   *   The entity to check access for.
217   * @param string|null $view_mode_name
218   *   The view mode name, or NULL if not applicable.
219   *
220   * @return \Drupal\Core\Access\AccessResultInterface
221   *   The access result.
222   */
223  protected function overrideBuilderAccessResult(AccountInterface $account, EntityInterface $entity, ?string $view_mode_name): AccessResultInterface {
224    $forbidden = AccessResult::forbidden()->addCacheContexts(['route']);
225
226    if ($view_mode_name === NULL) {
227      return $forbidden;
228    }
229
230    $display = self::getEntityViewDisplay($entity->getEntityTypeId(), $entity->bundle(), $view_mode_name);
231
232    if (!$display instanceof DisplayBuilderOverridableInterface) {
233      return $forbidden;
234    }
235
236    if (!$display->isDisplayBuilderOverridable()) {
237      return $forbidden;
238    }
239
240    $permission = $display->getDisplayBuilderOverrideProfile()->getPermissionName();
241
242    // This is the expected check.
243    if (!$account->hasPermission($permission)) {
244      return $forbidden;
245    }
246
247    return AccessResult::allowed()->addCacheableDependency($display)->addCacheContexts(['route']);
248  }
249
250}

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.