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

Paths

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