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