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}