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 }