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}