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}