Mercurial > hg > cmmr2012-drupal-site
comparison core/modules/jsonapi/src/Access/TemporaryQueryGuard.php @ 5:12f9dff5fda9 tip
Update to Drupal core 8.7.1
author | Chris Cannam |
---|---|
date | Thu, 09 May 2019 15:34:47 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
4:a9cd425dd02b | 5:12f9dff5fda9 |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\jsonapi\Access; | |
4 | |
5 use Drupal\Core\Access\AccessResult; | |
6 use Drupal\Core\Cache\CacheableMetadata; | |
7 use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; | |
8 use Drupal\Core\Entity\EntityFieldManagerInterface; | |
9 use Drupal\Core\Entity\EntityTypeInterface; | |
10 use Drupal\Core\Entity\Query\QueryInterface; | |
11 use Drupal\Core\Extension\ModuleHandlerInterface; | |
12 use Drupal\Core\Field\FieldStorageDefinitionInterface; | |
13 use Drupal\Core\Session\AccountInterface; | |
14 use Drupal\Core\TypedData\DataReferenceDefinitionInterface; | |
15 use Drupal\jsonapi\Query\EntityCondition; | |
16 use Drupal\jsonapi\Query\EntityConditionGroup; | |
17 use Drupal\jsonapi\Query\Filter; | |
18 use Drupal\workspaces\WorkspaceInterface; | |
19 | |
20 /** | |
21 * Adds sufficient access control to collection queries. | |
22 * | |
23 * This class will be removed when new Drupal core APIs have been put in place | |
24 * to make it obsolete. | |
25 * | |
26 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class | |
27 * may change at any time and could break any dependencies on it. | |
28 * | |
29 * @todo These additional security measures should eventually reside in the | |
30 * Entity API subsystem but were introduced here to address a security | |
31 * vulnerability. The following two issues should obsolesce this class: | |
32 * | |
33 * @see https://www.drupal.org/project/drupal/issues/2809177 | |
34 * @see https://www.drupal.org/project/drupal/issues/777578 | |
35 * | |
36 * @see https://www.drupal.org/project/jsonapi/issues/3032787 | |
37 * @see jsonapi.api.php | |
38 */ | |
39 class TemporaryQueryGuard { | |
40 | |
41 /** | |
42 * The entity field manager. | |
43 * | |
44 * @var \Drupal\Core\Entity\EntityFieldManagerInterface | |
45 */ | |
46 protected static $fieldManager; | |
47 | |
48 /** | |
49 * The module handler. | |
50 * | |
51 * @var \Drupal\Core\Extension\ModuleHandlerInterface | |
52 */ | |
53 protected static $moduleHandler; | |
54 | |
55 /** | |
56 * Sets the entity field manager. | |
57 * | |
58 * This must be called before calling ::applyAccessControls(). | |
59 * | |
60 * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager | |
61 * The entity field manager. | |
62 */ | |
63 public static function setFieldManager(EntityFieldManagerInterface $field_manager) { | |
64 static::$fieldManager = $field_manager; | |
65 } | |
66 | |
67 /** | |
68 * Sets the module handler. | |
69 * | |
70 * This must be called before calling ::applyAccessControls(). | |
71 * | |
72 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler | |
73 * The module handler. | |
74 */ | |
75 public static function setModuleHandler(ModuleHandlerInterface $module_handler) { | |
76 static::$moduleHandler = $module_handler; | |
77 } | |
78 | |
79 /** | |
80 * Applies access controls to an entity query. | |
81 * | |
82 * @param \Drupal\jsonapi\Query\Filter $filter | |
83 * The filters applicable to the query. | |
84 * @param \Drupal\Core\Entity\Query\QueryInterface $query | |
85 * The query to which access controls should be applied. | |
86 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability | |
87 * Collects cacheability for the query. | |
88 */ | |
89 public static function applyAccessControls(Filter $filter, QueryInterface $query, CacheableMetadata $cacheability) { | |
90 assert(static::$fieldManager !== NULL); | |
91 assert(static::$moduleHandler !== NULL); | |
92 $filtered_fields = static::collectFilteredFields($filter->root()); | |
93 $field_specifiers = array_map(function ($field) { | |
94 return explode('.', $field); | |
95 }, $filtered_fields); | |
96 static::secureQuery($query, $query->getEntityTypeId(), static::buildTree($field_specifiers), $cacheability); | |
97 } | |
98 | |
99 /** | |
100 * Applies tags, metadata and conditions to secure an entity query. | |
101 * | |
102 * @param \Drupal\Core\Entity\Query\QueryInterface $query | |
103 * The query to be secured. | |
104 * @param string $entity_type_id | |
105 * An entity type ID. | |
106 * @param array $tree | |
107 * A tree of field specifiers in an entity query condition. The tree is a | |
108 * multi-dimensional array where the keys are field specifiers and the | |
109 * values are multi-dimensional array of the same form, containing only | |
110 * subsequent specifiers. @see ::buildTree(). | |
111 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability | |
112 * Collects cacheability for the query. | |
113 * @param string|null $field_prefix | |
114 * Internal use only. Contains a string representation of the previously | |
115 * visited field specifiers. | |
116 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition | |
117 * Internal use only. The current field storage definition, if known. | |
118 * | |
119 * @see \Drupal\Core\Database\Query\AlterableInterface::addTag() | |
120 * @see \Drupal\Core\Database\Query\AlterableInterface::addMetaData() | |
121 * @see \Drupal\Core\Database\Query\ConditionInterface | |
122 */ | |
123 protected static function secureQuery(QueryInterface $query, $entity_type_id, array $tree, CacheableMetadata $cacheability, $field_prefix = NULL, FieldStorageDefinitionInterface $field_storage_definition = NULL) { | |
124 $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); | |
125 // Config entity types are not fieldable, therefore they do not have field | |
126 // access restrictions, nor entity references to other entity types. | |
127 if ($entity_type instanceof ConfigEntityTypeInterface) { | |
128 return; | |
129 } | |
130 foreach ($tree as $specifier => $children) { | |
131 // The field path reconstructs the entity condition fields. | |
132 // E.g. `uid.0` would become `uid.0.name` if $specifier === 'name'. | |
133 $child_prefix = (is_null($field_prefix)) ? $specifier : "$field_prefix.$specifier"; | |
134 if (is_null($field_storage_definition)) { | |
135 // When the field storage definition is NULL, this specifier is the | |
136 // first specifier in an entity query field path or the previous | |
137 // specifier was a data reference that has been traversed. In both | |
138 // cases, the specifier must be a field name. | |
139 $field_storage_definitions = static::$fieldManager->getFieldStorageDefinitions($entity_type_id); | |
140 static::secureQuery($query, $entity_type_id, $children, $cacheability, $child_prefix, $field_storage_definitions[$specifier]); | |
141 // When $field_prefix is NULL, this must be the first specifier in the | |
142 // entity query field path and a condition for the query's base entity | |
143 // type must be applied. | |
144 if (is_null($field_prefix)) { | |
145 static::applyAccessConditions($query, $entity_type_id, NULL, $cacheability); | |
146 } | |
147 } | |
148 else { | |
149 // When the specifier is an entity reference, it can contain an entity | |
150 // type specifier, like so: `entity:node`. This extracts the `entity` | |
151 // portion. JSON:API will have already validated that the property | |
152 // exists. | |
153 $split_specifier = explode(':', $specifier, 2); | |
154 list($property_name, $target_entity_type_id) = array_merge($split_specifier, count($split_specifier) === 2 ? [] : [NULL]); | |
155 // The specifier is either a field property or a delta. If it is a data | |
156 // reference or a delta, then it needs to be traversed to the next | |
157 // specifier. However, if the specific is a simple field property, i.e. | |
158 // it is neither a data reference nor a delta, then there is no need to | |
159 // evaluate the remaining specifiers. | |
160 $property_definition = $field_storage_definition->getPropertyDefinition($property_name); | |
161 if ($property_definition instanceof DataReferenceDefinitionInterface) { | |
162 // Because the filter is following an entity reference, ensure | |
163 // access is respected on those targeted entities. | |
164 // Examples: | |
165 // - node_query_node_access_alter() | |
166 $target_entity_type_id = $target_entity_type_id ?: $field_storage_definition->getSetting('target_type'); | |
167 $query->addTag("{$target_entity_type_id}_access"); | |
168 static::applyAccessConditions($query, $target_entity_type_id, $child_prefix, $cacheability); | |
169 // Keep descending the tree. | |
170 static::secureQuery($query, $target_entity_type_id, $children, $cacheability, $child_prefix); | |
171 } | |
172 elseif (is_null($property_definition)) { | |
173 assert(is_numeric($property_name), 'The specifier is not a property name, it must be a delta.'); | |
174 // Keep descending the tree. | |
175 static::secureQuery($query, $entity_type_id, $children, $cacheability, $child_prefix, $field_storage_definition); | |
176 } | |
177 } | |
178 } | |
179 } | |
180 | |
181 /** | |
182 * Applies access conditions to ensure 'view' access is respected. | |
183 * | |
184 * Since the given entity type might not be the base entity type of the query, | |
185 * the field prefix should be applied to ensure that the conditions are | |
186 * applied to the right subset of entities in the query. | |
187 * | |
188 * @param \Drupal\Core\Entity\Query\QueryInterface $query | |
189 * The query to which access conditions should be applied. | |
190 * @param string $entity_type_id | |
191 * The entity type for which to access conditions should be applied. | |
192 * @param string|null $field_prefix | |
193 * A prefix to add before any query condition fields. NULL if no prefix | |
194 * should be added. | |
195 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability | |
196 * Collects cacheability for the query. | |
197 */ | |
198 protected static function applyAccessConditions(QueryInterface $query, $entity_type_id, $field_prefix, CacheableMetadata $cacheability) { | |
199 $access_condition = static::getAccessCondition($entity_type_id, $cacheability); | |
200 if ($access_condition) { | |
201 $prefixed_condition = !is_null($field_prefix) | |
202 ? static::addConditionFieldPrefix($access_condition, $field_prefix) | |
203 : $access_condition; | |
204 $filter = new Filter($prefixed_condition); | |
205 $query->condition($filter->queryCondition($query)); | |
206 } | |
207 } | |
208 | |
209 /** | |
210 * Prefixes all fields in an EntityConditionGroup. | |
211 */ | |
212 protected static function addConditionFieldPrefix(EntityConditionGroup $group, $field_prefix) { | |
213 $prefixed = []; | |
214 foreach ($group->members() as $member) { | |
215 if ($member instanceof EntityConditionGroup) { | |
216 $prefixed[] = static::addConditionFieldPrefix($member, $field_prefix); | |
217 } | |
218 else { | |
219 $field = !empty($field_prefix) ? "{$field_prefix}." . $member->field() : $member->field(); | |
220 $prefixed[] = new EntityCondition($field, $member->value(), $member->operator()); | |
221 } | |
222 } | |
223 return new EntityConditionGroup($group->conjunction(), $prefixed); | |
224 } | |
225 | |
226 /** | |
227 * Gets an EntityConditionGroup that filters out inaccessible entities. | |
228 * | |
229 * @param string $entity_type_id | |
230 * The entity type ID for which to get an EntityConditionGroup. | |
231 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability | |
232 * Collects cacheability for the query. | |
233 * | |
234 * @return \Drupal\jsonapi\Query\EntityConditionGroup|null | |
235 * An EntityConditionGroup or NULL if no conditions need to be applied to | |
236 * secure an entity query. | |
237 */ | |
238 protected static function getAccessCondition($entity_type_id, CacheableMetadata $cacheability) { | |
239 $current_user = \Drupal::currentUser(); | |
240 $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); | |
241 | |
242 // Get the condition that handles generic restrictions, such as published | |
243 // and owner. | |
244 $generic_condition = static::getAccessConditionForKnownSubsets($entity_type, $current_user, $cacheability); | |
245 | |
246 // Some entity types require additional conditions. We don't know what | |
247 // contrib entity types require, so they are responsible for implementing | |
248 // hook_query_ENTITY_TYPE_access_alter(). Some core entity types have | |
249 // logic in their access control handler that isn't mirrored in | |
250 // hook_query_ENTITY_TYPE_access_alter(), so we duplicate that here until | |
251 // that's resolved. | |
252 $specific_condition = NULL; | |
253 switch ($entity_type_id) { | |
254 case 'block_content': | |
255 // Allow access only to reusable blocks. | |
256 // @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess() | |
257 if (isset(static::$fieldManager->getBaseFieldDefinitions($entity_type_id)['reusable'])) { | |
258 $specific_condition = new EntityCondition('reusable', 1); | |
259 $cacheability->addCacheTags($entity_type->getListCacheTags()); | |
260 } | |
261 break; | |
262 | |
263 case 'comment': | |
264 // @see \Drupal\comment\CommentAccessControlHandler::checkAccess() | |
265 $specific_condition = static::getCommentAccessCondition($entity_type, $current_user, $cacheability); | |
266 break; | |
267 | |
268 case 'entity_test': | |
269 // This case is only necessary for testing comment access controls. | |
270 // @see \Drupal\jsonapi\Tests\Functional\CommentTest::testCollectionFilterAccess() | |
271 $blacklist = \Drupal::state()->get('jsonapi__entity_test_filter_access_blacklist', []); | |
272 $cacheability->addCacheTags(['state:jsonapi__entity_test_filter_access_blacklist']); | |
273 $specific_conditions = []; | |
274 foreach ($blacklist as $id) { | |
275 $specific_conditions[] = new EntityCondition('id', $id, '<>'); | |
276 } | |
277 if ($specific_conditions) { | |
278 $specific_condition = new EntityConditionGroup('AND', $specific_conditions); | |
279 } | |
280 break; | |
281 | |
282 case 'file': | |
283 // Allow access only to public files and files uploaded by the current | |
284 // user. | |
285 // @see \Drupal\file\FileAccessControlHandler::checkAccess() | |
286 $specific_condition = new EntityConditionGroup('OR', [ | |
287 new EntityCondition('uri', 'public://', 'STARTS_WITH'), | |
288 new EntityCondition('uid', $current_user->id()), | |
289 ]); | |
290 $cacheability->addCacheTags($entity_type->getListCacheTags()); | |
291 break; | |
292 | |
293 case 'shortcut': | |
294 // Unless the user can administer shortcuts, allow access only to the | |
295 // user's currently displayed shortcut set. | |
296 // @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess() | |
297 if (!$current_user->hasPermission('administer shortcuts')) { | |
298 $specific_condition = new EntityCondition('shortcut_set', shortcut_current_displayed_set()->id()); | |
299 $cacheability->addCacheContexts(['user']); | |
300 $cacheability->addCacheTags($entity_type->getListCacheTags()); | |
301 } | |
302 break; | |
303 | |
304 case 'user': | |
305 // Disallow querying values of the anonymous user. | |
306 // @see \Drupal\user\UserAccessControlHandler::checkAccess() | |
307 $specific_condition = new EntityCondition('uid', '0', '!='); | |
308 break; | |
309 | |
310 case 'workspace': | |
311 // The default workspace is always viewable, no matter what, so if | |
312 // the generic condition prevents that, add an OR. | |
313 // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess() | |
314 if ($generic_condition) { | |
315 $specific_condition = new EntityConditionGroup('OR', [ | |
316 $generic_condition, | |
317 new EntityCondition('id', WorkspaceInterface::DEFAULT_WORKSPACE), | |
318 ]); | |
319 // The generic condition is now part of the specific condition. | |
320 $generic_condition = NULL; | |
321 } | |
322 break; | |
323 } | |
324 | |
325 // Return a combined condition. | |
326 if ($generic_condition && $specific_condition) { | |
327 return new EntityConditionGroup('AND', [$generic_condition, $specific_condition]); | |
328 } | |
329 elseif ($generic_condition) { | |
330 return $generic_condition instanceof EntityConditionGroup ? $generic_condition : new EntityConditionGroup('AND', [$generic_condition]); | |
331 } | |
332 elseif ($specific_condition) { | |
333 return $specific_condition instanceof EntityConditionGroup ? $specific_condition : new EntityConditionGroup('AND', [$specific_condition]); | |
334 } | |
335 | |
336 return NULL; | |
337 } | |
338 | |
339 /** | |
340 * Gets an access condition for the allowed JSONAPI_FILTER_AMONG_* subsets. | |
341 * | |
342 * If access is allowed for the JSONAPI_FILTER_AMONG_ALL subset, then no | |
343 * conditions are returned. Otherwise, if access is allowed for | |
344 * JSONAPI_FILTER_AMONG_PUBLISHED, JSONAPI_FILTER_AMONG_ENABLED, or | |
345 * JSONAPI_FILTER_AMONG_OWN, then a condition group is returned for the union | |
346 * of allowed subsets. If no subsets are allowed, then static::alwaysFalse() | |
347 * is returned. | |
348 * | |
349 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type | |
350 * The entity type for which to check filter access. | |
351 * @param \Drupal\Core\Session\AccountInterface $account | |
352 * The account for which to check access. | |
353 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability | |
354 * Collects cacheability for the query. | |
355 * | |
356 * @return \Drupal\jsonapi\Query\EntityConditionGroup|null | |
357 * An EntityConditionGroup or NULL if no conditions need to be applied to | |
358 * secure an entity query. | |
359 */ | |
360 protected static function getAccessConditionForKnownSubsets(EntityTypeInterface $entity_type, AccountInterface $account, CacheableMetadata $cacheability) { | |
361 // Get the combined access results for each JSONAPI_FILTER_AMONG_* subset. | |
362 $access_results = static::getAccessResultsFromEntityFilterHook($entity_type, $account); | |
363 | |
364 // No conditions are needed if access is allowed for all entities. | |
365 $cacheability->addCacheableDependency($access_results[JSONAPI_FILTER_AMONG_ALL]); | |
366 if ($access_results[JSONAPI_FILTER_AMONG_ALL]->isAllowed()) { | |
367 return NULL; | |
368 } | |
369 | |
370 // If filtering is not allowed across all entities, but is allowed for | |
371 // certain subsets, then add conditions that reflect those subsets. These | |
372 // will be grouped in an OR to reflect that access may be granted to | |
373 // more than one subset. If no conditions are added below, then | |
374 // static::alwaysFalse() is returned. | |
375 $conditions = []; | |
376 | |
377 // The "published" subset. | |
378 $published_field_name = $entity_type->getKey('published'); | |
379 if ($published_field_name) { | |
380 $access_result = $access_results[JSONAPI_FILTER_AMONG_PUBLISHED]; | |
381 $cacheability->addCacheableDependency($access_result); | |
382 if ($access_result->isAllowed()) { | |
383 $conditions[] = new EntityCondition($published_field_name, 1); | |
384 $cacheability->addCacheTags($entity_type->getListCacheTags()); | |
385 } | |
386 } | |
387 | |
388 // The "enabled" subset. | |
389 // @todo Remove ternary when the 'status' key is added to the User entity type. | |
390 $status_field_name = $entity_type->id() === 'user' ? 'status' : $entity_type->getKey('status'); | |
391 if ($status_field_name) { | |
392 $access_result = $access_results[JSONAPI_FILTER_AMONG_ENABLED]; | |
393 $cacheability->addCacheableDependency($access_result); | |
394 if ($access_result->isAllowed()) { | |
395 $conditions[] = new EntityCondition($status_field_name, 1); | |
396 $cacheability->addCacheTags($entity_type->getListCacheTags()); | |
397 } | |
398 } | |
399 | |
400 // The "owner" subset. | |
401 // @todo Remove ternary when the 'uid' key is added to the User entity type. | |
402 $owner_field_name = $entity_type->id() === 'user' ? 'uid' : $entity_type->getKey('owner'); | |
403 if ($owner_field_name) { | |
404 $access_result = $access_results[JSONAPI_FILTER_AMONG_OWN]; | |
405 $cacheability->addCacheableDependency($access_result); | |
406 if ($access_result->isAllowed()) { | |
407 $cacheability->addCacheContexts(['user']); | |
408 if ($account->isAuthenticated()) { | |
409 $conditions[] = new EntityCondition($owner_field_name, $account->id()); | |
410 $cacheability->addCacheTags($entity_type->getListCacheTags()); | |
411 } | |
412 } | |
413 } | |
414 | |
415 // If no conditions were added above, then access wasn't granted to any | |
416 // subset, so return alwaysFalse(). | |
417 if (empty($conditions)) { | |
418 return static::alwaysFalse($entity_type); | |
419 } | |
420 | |
421 // If more than one condition was added above, then access was granted to | |
422 // more than one subset, so combine them with an OR. | |
423 if (count($conditions) > 1) { | |
424 return new EntityConditionGroup('OR', $conditions); | |
425 } | |
426 | |
427 // Otherwise return the single condition. | |
428 return $conditions[0]; | |
429 } | |
430 | |
431 /** | |
432 * Gets the combined access result for each JSONAPI_FILTER_AMONG_* subset. | |
433 * | |
434 * This invokes hook_jsonapi_entity_filter_access() and | |
435 * hook_jsonapi_ENTITY_TYPE_filter_access() and combines the results from all | |
436 * of the modules into a single set of results. | |
437 * | |
438 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type | |
439 * The entity type for which to check filter access. | |
440 * @param \Drupal\Core\Session\AccountInterface $account | |
441 * The account for which to check access. | |
442 * | |
443 * @return \Drupal\Core\Access\AccessResultInterface[] | |
444 * The array of access results, keyed by subset. See | |
445 * hook_jsonapi_entity_filter_access() for details. | |
446 */ | |
447 protected static function getAccessResultsFromEntityFilterHook(EntityTypeInterface $entity_type, AccountInterface $account) { | |
448 /* @var \Drupal\Core\Access\AccessResultInterface[] $combined_access_results */ | |
449 $combined_access_results = [ | |
450 JSONAPI_FILTER_AMONG_ALL => AccessResult::neutral(), | |
451 JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::neutral(), | |
452 JSONAPI_FILTER_AMONG_ENABLED => AccessResult::neutral(), | |
453 JSONAPI_FILTER_AMONG_OWN => AccessResult::neutral(), | |
454 ]; | |
455 | |
456 // Invoke hook_jsonapi_entity_filter_access() and | |
457 // hook_jsonapi_ENTITY_TYPE_filter_access() for each module and merge its | |
458 // results with the combined results. | |
459 foreach (['jsonapi_entity_filter_access', 'jsonapi_' . $entity_type->id() . '_filter_access'] as $hook) { | |
460 foreach (static::$moduleHandler->getImplementations($hook) as $module) { | |
461 $module_access_results = static::$moduleHandler->invoke($module, $hook, [$entity_type, $account]); | |
462 if ($module_access_results) { | |
463 foreach ($module_access_results as $subset => $access_result) { | |
464 $combined_access_results[$subset] = $combined_access_results[$subset]->orIf($access_result); | |
465 } | |
466 } | |
467 } | |
468 } | |
469 | |
470 return $combined_access_results; | |
471 } | |
472 | |
473 /** | |
474 * Gets an access condition for a comment entity. | |
475 * | |
476 * Unlike all other core entity types, Comment entities' access control | |
477 * depends on access to a referenced entity. More challenging yet, that entity | |
478 * reference field may target different entity types depending on the comment | |
479 * bundle. This makes the query access conditions sufficiently complex to | |
480 * merit a dedicated method. | |
481 * | |
482 * @param \Drupal\Core\Entity\EntityTypeInterface $comment_entity_type | |
483 * The comment entity type object. | |
484 * @param \Drupal\Core\Session\AccountInterface $current_user | |
485 * The current user. | |
486 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability | |
487 * Collects cacheability for the query. | |
488 * @param int $depth | |
489 * Internal use only. The recursion depth. It is possible to have comments | |
490 * on comments, but since comment access is dependent on access to the | |
491 * entity on which they live, this method can recurse endlessly. | |
492 * | |
493 * @return \Drupal\jsonapi\Query\EntityConditionGroup|null | |
494 * An EntityConditionGroup or NULL if no conditions need to be applied to | |
495 * secure an entity query. | |
496 */ | |
497 protected static function getCommentAccessCondition(EntityTypeInterface $comment_entity_type, AccountInterface $current_user, CacheableMetadata $cacheability, $depth = 1) { | |
498 // If a comment is assigned to another entity or author the cache needs to | |
499 // be invalidated. | |
500 $cacheability->addCacheTags($comment_entity_type->getListCacheTags()); | |
501 // Constructs a big EntityConditionGroup which will filter comments based on | |
502 // the current user's access to the entities on which each comment lives. | |
503 // This is especially complex because comments of different bundles can | |
504 // live on entities of different entity types. | |
505 $comment_entity_type_id = $comment_entity_type->id(); | |
506 $field_map = static::$fieldManager->getFieldMapByFieldType('entity_reference'); | |
507 assert(isset($field_map[$comment_entity_type_id]['entity_id']['bundles']), 'Every comment has an `entity_id` field.'); | |
508 $bundle_ids_by_target_entity_type_id = []; | |
509 foreach ($field_map[$comment_entity_type_id]['entity_id']['bundles'] as $bundle_id) { | |
510 $field_definitions = static::$fieldManager->getFieldDefinitions($comment_entity_type_id, $bundle_id); | |
511 $commented_entity_field_definition = $field_definitions['entity_id']; | |
512 // Each commented entity field definition has a setting which indicates | |
513 // the entity type of the commented entity reference field. This differs | |
514 // per bundle. | |
515 $target_entity_type_id = $commented_entity_field_definition->getSetting('target_type'); | |
516 $bundle_ids_by_target_entity_type_id[$target_entity_type_id][] = $bundle_id; | |
517 } | |
518 $bundle_specific_access_conditions = []; | |
519 foreach ($bundle_ids_by_target_entity_type_id as $target_entity_type_id => $bundle_ids) { | |
520 // Construct a field specifier prefix which targets the commented entity. | |
521 $condition_field_prefix = "entity_id.entity:$target_entity_type_id"; | |
522 // Ensure that for each possible commented entity type (which varies per | |
523 // bundle), a condition is created that restricts access based on access | |
524 // to the commented entity. | |
525 $bundle_condition = new EntityCondition($comment_entity_type->getKey('bundle'), $bundle_ids, 'IN'); | |
526 // Comments on comments can create an infinite recursion! If the target | |
527 // entity type ID is comment, we need special behavior. | |
528 if ($target_entity_type_id === $comment_entity_type_id) { | |
529 $nested_comment_condition = $depth <= 3 | |
530 ? static::getCommentAccessCondition($comment_entity_type, $current_user, $cacheability, $depth + 1) | |
531 : static::alwaysFalse($comment_entity_type); | |
532 $prefixed_comment_condition = static::addConditionFieldPrefix($nested_comment_condition, $condition_field_prefix); | |
533 $bundle_specific_access_conditions[$target_entity_type_id] = new EntityConditionGroup('AND', [$bundle_condition, $prefixed_comment_condition]); | |
534 } | |
535 else { | |
536 $target_condition = static::getAccessCondition($target_entity_type_id, $cacheability); | |
537 $bundle_specific_access_conditions[$target_entity_type_id] = !is_null($target_condition) | |
538 ? new EntityConditionGroup('AND', [ | |
539 $bundle_condition, | |
540 static::addConditionFieldPrefix($target_condition, $condition_field_prefix), | |
541 ]) | |
542 : $bundle_condition; | |
543 } | |
544 } | |
545 | |
546 // This condition ensures that the user is only permitted to see the | |
547 // comments for which the user is also able to view the entity on which each | |
548 // comment lives. | |
549 $commented_entity_condition = new EntityConditionGroup('OR', array_values($bundle_specific_access_conditions)); | |
550 return $commented_entity_condition; | |
551 } | |
552 | |
553 /** | |
554 * Gets an always FALSE entity condition group for the given entity type. | |
555 * | |
556 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type | |
557 * The entity type for which to construct an impossible condition. | |
558 * | |
559 * @return \Drupal\jsonapi\Query\EntityConditionGroup | |
560 * An EntityConditionGroup which cannot evaluate to TRUE. | |
561 */ | |
562 protected static function alwaysFalse(EntityTypeInterface $entity_type) { | |
563 return new EntityConditionGroup('AND', [ | |
564 new EntityCondition($entity_type->getKey('id'), 1, '<'), | |
565 new EntityCondition($entity_type->getKey('id'), 1, '>'), | |
566 ]); | |
567 } | |
568 | |
569 /** | |
570 * Recursively collects all entity query condition fields. | |
571 * | |
572 * Entity conditions can be nested within AND and OR groups. This recursively | |
573 * finds all unique fields in an entity query condition. | |
574 * | |
575 * @param \Drupal\jsonapi\Query\EntityConditionGroup $group | |
576 * The root entity condition group. | |
577 * @param array $fields | |
578 * Internal use only. | |
579 * | |
580 * @return array | |
581 * An array of entity query condition field names. | |
582 */ | |
583 protected static function collectFilteredFields(EntityConditionGroup $group, array $fields = []) { | |
584 foreach ($group->members() as $member) { | |
585 if ($member instanceof EntityConditionGroup) { | |
586 $fields = static::collectFilteredFields($member, $fields); | |
587 } | |
588 else { | |
589 $fields[] = $member->field(); | |
590 } | |
591 } | |
592 return array_unique($fields); | |
593 } | |
594 | |
595 /** | |
596 * Copied from \Drupal\jsonapi\IncludeResolver. | |
597 * | |
598 * @see \Drupal\jsonapi\IncludeResolver::buildTree() | |
599 */ | |
600 protected static function buildTree(array $paths) { | |
601 $merged = []; | |
602 foreach ($paths as $parts) { | |
603 // This complex expression is needed to handle the string, "0", which | |
604 // would be evaluated as FALSE. | |
605 if (!is_null(($field_name = array_shift($parts)))) { | |
606 $previous = isset($merged[$field_name]) ? $merged[$field_name] : []; | |
607 $merged[$field_name] = array_merge($previous, [$parts]); | |
608 } | |
609 } | |
610 return !empty($merged) ? array_map([static::class, __FUNCTION__], $merged) : $merged; | |
611 } | |
612 | |
613 } |