Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 54 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
EntityViewOverridesController | |
0.00% |
0 / 54 |
|
0.00% |
0 / 7 |
272 | |
0.00% |
0 / 1 |
getBuilder | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getFirstBuilder | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
checkAccess | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
checkFirstBuilderAccess | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getFirstOverridableViewMode | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getEntityViewDisplay | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
overrideBuilderAccessResult | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Drupal\display_builder_entity_view\Controller; |
6 | |
7 | use Drupal\Core\Access\AccessResult; |
8 | use Drupal\Core\Access\AccessResultInterface; |
9 | use Drupal\Core\Cache\CacheableMetadata; |
10 | use Drupal\Core\Entity\EntityInterface; |
11 | use Drupal\Core\Routing\RouteMatchInterface; |
12 | use Drupal\Core\Routing\TrustedRedirectResponse; |
13 | use Drupal\Core\Session\AccountInterface; |
14 | use Drupal\Core\Url; |
15 | use Drupal\display_builder\Controller\IntegrationControllerBase; |
16 | use Drupal\display_builder\DisplayBuildableInterface; |
17 | use Drupal\display_builder_entity_view\Entity\DisplayBuilderOverridableInterface; |
18 | use Drupal\display_builder_entity_view\Entity\EntityViewDisplay; |
19 | use Symfony\Component\HttpFoundation\Response; |
20 | use 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 | */ |
28 | final 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 | } |