Chris@18: root()); Chris@18: $field_specifiers = array_map(function ($field) { Chris@18: return explode('.', $field); Chris@18: }, $filtered_fields); Chris@18: static::secureQuery($query, $query->getEntityTypeId(), static::buildTree($field_specifiers), $cacheability); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Applies tags, metadata and conditions to secure an entity query. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\Query\QueryInterface $query Chris@18: * The query to be secured. Chris@18: * @param string $entity_type_id Chris@18: * An entity type ID. Chris@18: * @param array $tree Chris@18: * A tree of field specifiers in an entity query condition. The tree is a Chris@18: * multi-dimensional array where the keys are field specifiers and the Chris@18: * values are multi-dimensional array of the same form, containing only Chris@18: * subsequent specifiers. @see ::buildTree(). Chris@18: * @param \Drupal\Core\Cache\CacheableMetadata $cacheability Chris@18: * Collects cacheability for the query. Chris@18: * @param string|null $field_prefix Chris@18: * Internal use only. Contains a string representation of the previously Chris@18: * visited field specifiers. Chris@18: * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition Chris@18: * Internal use only. The current field storage definition, if known. Chris@18: * Chris@18: * @see \Drupal\Core\Database\Query\AlterableInterface::addTag() Chris@18: * @see \Drupal\Core\Database\Query\AlterableInterface::addMetaData() Chris@18: * @see \Drupal\Core\Database\Query\ConditionInterface Chris@18: */ Chris@18: protected static function secureQuery(QueryInterface $query, $entity_type_id, array $tree, CacheableMetadata $cacheability, $field_prefix = NULL, FieldStorageDefinitionInterface $field_storage_definition = NULL) { Chris@18: $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); Chris@18: // Config entity types are not fieldable, therefore they do not have field Chris@18: // access restrictions, nor entity references to other entity types. Chris@18: if ($entity_type instanceof ConfigEntityTypeInterface) { Chris@18: return; Chris@18: } Chris@18: foreach ($tree as $specifier => $children) { Chris@18: // The field path reconstructs the entity condition fields. Chris@18: // E.g. `uid.0` would become `uid.0.name` if $specifier === 'name'. Chris@18: $child_prefix = (is_null($field_prefix)) ? $specifier : "$field_prefix.$specifier"; Chris@18: if (is_null($field_storage_definition)) { Chris@18: // When the field storage definition is NULL, this specifier is the Chris@18: // first specifier in an entity query field path or the previous Chris@18: // specifier was a data reference that has been traversed. In both Chris@18: // cases, the specifier must be a field name. Chris@18: $field_storage_definitions = static::$fieldManager->getFieldStorageDefinitions($entity_type_id); Chris@18: static::secureQuery($query, $entity_type_id, $children, $cacheability, $child_prefix, $field_storage_definitions[$specifier]); Chris@18: // When $field_prefix is NULL, this must be the first specifier in the Chris@18: // entity query field path and a condition for the query's base entity Chris@18: // type must be applied. Chris@18: if (is_null($field_prefix)) { Chris@18: static::applyAccessConditions($query, $entity_type_id, NULL, $cacheability); Chris@18: } Chris@18: } Chris@18: else { Chris@18: // When the specifier is an entity reference, it can contain an entity Chris@18: // type specifier, like so: `entity:node`. This extracts the `entity` Chris@18: // portion. JSON:API will have already validated that the property Chris@18: // exists. Chris@18: $split_specifier = explode(':', $specifier, 2); Chris@18: list($property_name, $target_entity_type_id) = array_merge($split_specifier, count($split_specifier) === 2 ? [] : [NULL]); Chris@18: // The specifier is either a field property or a delta. If it is a data Chris@18: // reference or a delta, then it needs to be traversed to the next Chris@18: // specifier. However, if the specific is a simple field property, i.e. Chris@18: // it is neither a data reference nor a delta, then there is no need to Chris@18: // evaluate the remaining specifiers. Chris@18: $property_definition = $field_storage_definition->getPropertyDefinition($property_name); Chris@18: if ($property_definition instanceof DataReferenceDefinitionInterface) { Chris@18: // Because the filter is following an entity reference, ensure Chris@18: // access is respected on those targeted entities. Chris@18: // Examples: Chris@18: // - node_query_node_access_alter() Chris@18: $target_entity_type_id = $target_entity_type_id ?: $field_storage_definition->getSetting('target_type'); Chris@18: $query->addTag("{$target_entity_type_id}_access"); Chris@18: static::applyAccessConditions($query, $target_entity_type_id, $child_prefix, $cacheability); Chris@18: // Keep descending the tree. Chris@18: static::secureQuery($query, $target_entity_type_id, $children, $cacheability, $child_prefix); Chris@18: } Chris@18: elseif (is_null($property_definition)) { Chris@18: assert(is_numeric($property_name), 'The specifier is not a property name, it must be a delta.'); Chris@18: // Keep descending the tree. Chris@18: static::secureQuery($query, $entity_type_id, $children, $cacheability, $child_prefix, $field_storage_definition); Chris@18: } Chris@18: } Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * Applies access conditions to ensure 'view' access is respected. Chris@18: * Chris@18: * Since the given entity type might not be the base entity type of the query, Chris@18: * the field prefix should be applied to ensure that the conditions are Chris@18: * applied to the right subset of entities in the query. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\Query\QueryInterface $query Chris@18: * The query to which access conditions should be applied. Chris@18: * @param string $entity_type_id Chris@18: * The entity type for which to access conditions should be applied. Chris@18: * @param string|null $field_prefix Chris@18: * A prefix to add before any query condition fields. NULL if no prefix Chris@18: * should be added. Chris@18: * @param \Drupal\Core\Cache\CacheableMetadata $cacheability Chris@18: * Collects cacheability for the query. Chris@18: */ Chris@18: protected static function applyAccessConditions(QueryInterface $query, $entity_type_id, $field_prefix, CacheableMetadata $cacheability) { Chris@18: $access_condition = static::getAccessCondition($entity_type_id, $cacheability); Chris@18: if ($access_condition) { Chris@18: $prefixed_condition = !is_null($field_prefix) Chris@18: ? static::addConditionFieldPrefix($access_condition, $field_prefix) Chris@18: : $access_condition; Chris@18: $filter = new Filter($prefixed_condition); Chris@18: $query->condition($filter->queryCondition($query)); Chris@18: } Chris@18: } Chris@18: Chris@18: /** Chris@18: * Prefixes all fields in an EntityConditionGroup. Chris@18: */ Chris@18: protected static function addConditionFieldPrefix(EntityConditionGroup $group, $field_prefix) { Chris@18: $prefixed = []; Chris@18: foreach ($group->members() as $member) { Chris@18: if ($member instanceof EntityConditionGroup) { Chris@18: $prefixed[] = static::addConditionFieldPrefix($member, $field_prefix); Chris@18: } Chris@18: else { Chris@18: $field = !empty($field_prefix) ? "{$field_prefix}." . $member->field() : $member->field(); Chris@18: $prefixed[] = new EntityCondition($field, $member->value(), $member->operator()); Chris@18: } Chris@18: } Chris@18: return new EntityConditionGroup($group->conjunction(), $prefixed); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets an EntityConditionGroup that filters out inaccessible entities. Chris@18: * Chris@18: * @param string $entity_type_id Chris@18: * The entity type ID for which to get an EntityConditionGroup. Chris@18: * @param \Drupal\Core\Cache\CacheableMetadata $cacheability Chris@18: * Collects cacheability for the query. Chris@18: * Chris@18: * @return \Drupal\jsonapi\Query\EntityConditionGroup|null Chris@18: * An EntityConditionGroup or NULL if no conditions need to be applied to Chris@18: * secure an entity query. Chris@18: */ Chris@18: protected static function getAccessCondition($entity_type_id, CacheableMetadata $cacheability) { Chris@18: $current_user = \Drupal::currentUser(); Chris@18: $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); Chris@18: Chris@18: // Get the condition that handles generic restrictions, such as published Chris@18: // and owner. Chris@18: $generic_condition = static::getAccessConditionForKnownSubsets($entity_type, $current_user, $cacheability); Chris@18: Chris@18: // Some entity types require additional conditions. We don't know what Chris@18: // contrib entity types require, so they are responsible for implementing Chris@18: // hook_query_ENTITY_TYPE_access_alter(). Some core entity types have Chris@18: // logic in their access control handler that isn't mirrored in Chris@18: // hook_query_ENTITY_TYPE_access_alter(), so we duplicate that here until Chris@18: // that's resolved. Chris@18: $specific_condition = NULL; Chris@18: switch ($entity_type_id) { Chris@18: case 'block_content': Chris@18: // Allow access only to reusable blocks. Chris@18: // @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess() Chris@18: if (isset(static::$fieldManager->getBaseFieldDefinitions($entity_type_id)['reusable'])) { Chris@18: $specific_condition = new EntityCondition('reusable', 1); Chris@18: $cacheability->addCacheTags($entity_type->getListCacheTags()); Chris@18: } Chris@18: break; Chris@18: Chris@18: case 'comment': Chris@18: // @see \Drupal\comment\CommentAccessControlHandler::checkAccess() Chris@18: $specific_condition = static::getCommentAccessCondition($entity_type, $current_user, $cacheability); Chris@18: break; Chris@18: Chris@18: case 'entity_test': Chris@18: // This case is only necessary for testing comment access controls. Chris@18: // @see \Drupal\jsonapi\Tests\Functional\CommentTest::testCollectionFilterAccess() Chris@18: $blacklist = \Drupal::state()->get('jsonapi__entity_test_filter_access_blacklist', []); Chris@18: $cacheability->addCacheTags(['state:jsonapi__entity_test_filter_access_blacklist']); Chris@18: $specific_conditions = []; Chris@18: foreach ($blacklist as $id) { Chris@18: $specific_conditions[] = new EntityCondition('id', $id, '<>'); Chris@18: } Chris@18: if ($specific_conditions) { Chris@18: $specific_condition = new EntityConditionGroup('AND', $specific_conditions); Chris@18: } Chris@18: break; Chris@18: Chris@18: case 'file': Chris@18: // Allow access only to public files and files uploaded by the current Chris@18: // user. Chris@18: // @see \Drupal\file\FileAccessControlHandler::checkAccess() Chris@18: $specific_condition = new EntityConditionGroup('OR', [ Chris@18: new EntityCondition('uri', 'public://', 'STARTS_WITH'), Chris@18: new EntityCondition('uid', $current_user->id()), Chris@18: ]); Chris@18: $cacheability->addCacheTags($entity_type->getListCacheTags()); Chris@18: break; Chris@18: Chris@18: case 'shortcut': Chris@18: // Unless the user can administer shortcuts, allow access only to the Chris@18: // user's currently displayed shortcut set. Chris@18: // @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess() Chris@18: if (!$current_user->hasPermission('administer shortcuts')) { Chris@18: $specific_condition = new EntityCondition('shortcut_set', shortcut_current_displayed_set()->id()); Chris@18: $cacheability->addCacheContexts(['user']); Chris@18: $cacheability->addCacheTags($entity_type->getListCacheTags()); Chris@18: } Chris@18: break; Chris@18: Chris@18: case 'user': Chris@18: // Disallow querying values of the anonymous user. Chris@18: // @see \Drupal\user\UserAccessControlHandler::checkAccess() Chris@18: $specific_condition = new EntityCondition('uid', '0', '!='); Chris@18: break; Chris@18: Chris@18: case 'workspace': Chris@18: // The default workspace is always viewable, no matter what, so if Chris@18: // the generic condition prevents that, add an OR. Chris@18: // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess() Chris@18: if ($generic_condition) { Chris@18: $specific_condition = new EntityConditionGroup('OR', [ Chris@18: $generic_condition, Chris@18: new EntityCondition('id', WorkspaceInterface::DEFAULT_WORKSPACE), Chris@18: ]); Chris@18: // The generic condition is now part of the specific condition. Chris@18: $generic_condition = NULL; Chris@18: } Chris@18: break; Chris@18: } Chris@18: Chris@18: // Return a combined condition. Chris@18: if ($generic_condition && $specific_condition) { Chris@18: return new EntityConditionGroup('AND', [$generic_condition, $specific_condition]); Chris@18: } Chris@18: elseif ($generic_condition) { Chris@18: return $generic_condition instanceof EntityConditionGroup ? $generic_condition : new EntityConditionGroup('AND', [$generic_condition]); Chris@18: } Chris@18: elseif ($specific_condition) { Chris@18: return $specific_condition instanceof EntityConditionGroup ? $specific_condition : new EntityConditionGroup('AND', [$specific_condition]); Chris@18: } Chris@18: Chris@18: return NULL; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets an access condition for the allowed JSONAPI_FILTER_AMONG_* subsets. Chris@18: * Chris@18: * If access is allowed for the JSONAPI_FILTER_AMONG_ALL subset, then no Chris@18: * conditions are returned. Otherwise, if access is allowed for Chris@18: * JSONAPI_FILTER_AMONG_PUBLISHED, JSONAPI_FILTER_AMONG_ENABLED, or Chris@18: * JSONAPI_FILTER_AMONG_OWN, then a condition group is returned for the union Chris@18: * of allowed subsets. If no subsets are allowed, then static::alwaysFalse() Chris@18: * is returned. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type Chris@18: * The entity type for which to check filter access. Chris@18: * @param \Drupal\Core\Session\AccountInterface $account Chris@18: * The account for which to check access. Chris@18: * @param \Drupal\Core\Cache\CacheableMetadata $cacheability Chris@18: * Collects cacheability for the query. Chris@18: * Chris@18: * @return \Drupal\jsonapi\Query\EntityConditionGroup|null Chris@18: * An EntityConditionGroup or NULL if no conditions need to be applied to Chris@18: * secure an entity query. Chris@18: */ Chris@18: protected static function getAccessConditionForKnownSubsets(EntityTypeInterface $entity_type, AccountInterface $account, CacheableMetadata $cacheability) { Chris@18: // Get the combined access results for each JSONAPI_FILTER_AMONG_* subset. Chris@18: $access_results = static::getAccessResultsFromEntityFilterHook($entity_type, $account); Chris@18: Chris@18: // No conditions are needed if access is allowed for all entities. Chris@18: $cacheability->addCacheableDependency($access_results[JSONAPI_FILTER_AMONG_ALL]); Chris@18: if ($access_results[JSONAPI_FILTER_AMONG_ALL]->isAllowed()) { Chris@18: return NULL; Chris@18: } Chris@18: Chris@18: // If filtering is not allowed across all entities, but is allowed for Chris@18: // certain subsets, then add conditions that reflect those subsets. These Chris@18: // will be grouped in an OR to reflect that access may be granted to Chris@18: // more than one subset. If no conditions are added below, then Chris@18: // static::alwaysFalse() is returned. Chris@18: $conditions = []; Chris@18: Chris@18: // The "published" subset. Chris@18: $published_field_name = $entity_type->getKey('published'); Chris@18: if ($published_field_name) { Chris@18: $access_result = $access_results[JSONAPI_FILTER_AMONG_PUBLISHED]; Chris@18: $cacheability->addCacheableDependency($access_result); Chris@18: if ($access_result->isAllowed()) { Chris@18: $conditions[] = new EntityCondition($published_field_name, 1); Chris@18: $cacheability->addCacheTags($entity_type->getListCacheTags()); Chris@18: } Chris@18: } Chris@18: Chris@18: // The "enabled" subset. Chris@18: // @todo Remove ternary when the 'status' key is added to the User entity type. Chris@18: $status_field_name = $entity_type->id() === 'user' ? 'status' : $entity_type->getKey('status'); Chris@18: if ($status_field_name) { Chris@18: $access_result = $access_results[JSONAPI_FILTER_AMONG_ENABLED]; Chris@18: $cacheability->addCacheableDependency($access_result); Chris@18: if ($access_result->isAllowed()) { Chris@18: $conditions[] = new EntityCondition($status_field_name, 1); Chris@18: $cacheability->addCacheTags($entity_type->getListCacheTags()); Chris@18: } Chris@18: } Chris@18: Chris@18: // The "owner" subset. Chris@18: // @todo Remove ternary when the 'uid' key is added to the User entity type. Chris@18: $owner_field_name = $entity_type->id() === 'user' ? 'uid' : $entity_type->getKey('owner'); Chris@18: if ($owner_field_name) { Chris@18: $access_result = $access_results[JSONAPI_FILTER_AMONG_OWN]; Chris@18: $cacheability->addCacheableDependency($access_result); Chris@18: if ($access_result->isAllowed()) { Chris@18: $cacheability->addCacheContexts(['user']); Chris@18: if ($account->isAuthenticated()) { Chris@18: $conditions[] = new EntityCondition($owner_field_name, $account->id()); Chris@18: $cacheability->addCacheTags($entity_type->getListCacheTags()); Chris@18: } Chris@18: } Chris@18: } Chris@18: Chris@18: // If no conditions were added above, then access wasn't granted to any Chris@18: // subset, so return alwaysFalse(). Chris@18: if (empty($conditions)) { Chris@18: return static::alwaysFalse($entity_type); Chris@18: } Chris@18: Chris@18: // If more than one condition was added above, then access was granted to Chris@18: // more than one subset, so combine them with an OR. Chris@18: if (count($conditions) > 1) { Chris@18: return new EntityConditionGroup('OR', $conditions); Chris@18: } Chris@18: Chris@18: // Otherwise return the single condition. Chris@18: return $conditions[0]; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets the combined access result for each JSONAPI_FILTER_AMONG_* subset. Chris@18: * Chris@18: * This invokes hook_jsonapi_entity_filter_access() and Chris@18: * hook_jsonapi_ENTITY_TYPE_filter_access() and combines the results from all Chris@18: * of the modules into a single set of results. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type Chris@18: * The entity type for which to check filter access. Chris@18: * @param \Drupal\Core\Session\AccountInterface $account Chris@18: * The account for which to check access. Chris@18: * Chris@18: * @return \Drupal\Core\Access\AccessResultInterface[] Chris@18: * The array of access results, keyed by subset. See Chris@18: * hook_jsonapi_entity_filter_access() for details. Chris@18: */ Chris@18: protected static function getAccessResultsFromEntityFilterHook(EntityTypeInterface $entity_type, AccountInterface $account) { Chris@18: /* @var \Drupal\Core\Access\AccessResultInterface[] $combined_access_results */ Chris@18: $combined_access_results = [ Chris@18: JSONAPI_FILTER_AMONG_ALL => AccessResult::neutral(), Chris@18: JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::neutral(), Chris@18: JSONAPI_FILTER_AMONG_ENABLED => AccessResult::neutral(), Chris@18: JSONAPI_FILTER_AMONG_OWN => AccessResult::neutral(), Chris@18: ]; Chris@18: Chris@18: // Invoke hook_jsonapi_entity_filter_access() and Chris@18: // hook_jsonapi_ENTITY_TYPE_filter_access() for each module and merge its Chris@18: // results with the combined results. Chris@18: foreach (['jsonapi_entity_filter_access', 'jsonapi_' . $entity_type->id() . '_filter_access'] as $hook) { Chris@18: foreach (static::$moduleHandler->getImplementations($hook) as $module) { Chris@18: $module_access_results = static::$moduleHandler->invoke($module, $hook, [$entity_type, $account]); Chris@18: if ($module_access_results) { Chris@18: foreach ($module_access_results as $subset => $access_result) { Chris@18: $combined_access_results[$subset] = $combined_access_results[$subset]->orIf($access_result); Chris@18: } Chris@18: } Chris@18: } Chris@18: } Chris@18: Chris@18: return $combined_access_results; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets an access condition for a comment entity. Chris@18: * Chris@18: * Unlike all other core entity types, Comment entities' access control Chris@18: * depends on access to a referenced entity. More challenging yet, that entity Chris@18: * reference field may target different entity types depending on the comment Chris@18: * bundle. This makes the query access conditions sufficiently complex to Chris@18: * merit a dedicated method. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\EntityTypeInterface $comment_entity_type Chris@18: * The comment entity type object. Chris@18: * @param \Drupal\Core\Session\AccountInterface $current_user Chris@18: * The current user. Chris@18: * @param \Drupal\Core\Cache\CacheableMetadata $cacheability Chris@18: * Collects cacheability for the query. Chris@18: * @param int $depth Chris@18: * Internal use only. The recursion depth. It is possible to have comments Chris@18: * on comments, but since comment access is dependent on access to the Chris@18: * entity on which they live, this method can recurse endlessly. Chris@18: * Chris@18: * @return \Drupal\jsonapi\Query\EntityConditionGroup|null Chris@18: * An EntityConditionGroup or NULL if no conditions need to be applied to Chris@18: * secure an entity query. Chris@18: */ Chris@18: protected static function getCommentAccessCondition(EntityTypeInterface $comment_entity_type, AccountInterface $current_user, CacheableMetadata $cacheability, $depth = 1) { Chris@18: // If a comment is assigned to another entity or author the cache needs to Chris@18: // be invalidated. Chris@18: $cacheability->addCacheTags($comment_entity_type->getListCacheTags()); Chris@18: // Constructs a big EntityConditionGroup which will filter comments based on Chris@18: // the current user's access to the entities on which each comment lives. Chris@18: // This is especially complex because comments of different bundles can Chris@18: // live on entities of different entity types. Chris@18: $comment_entity_type_id = $comment_entity_type->id(); Chris@18: $field_map = static::$fieldManager->getFieldMapByFieldType('entity_reference'); Chris@18: assert(isset($field_map[$comment_entity_type_id]['entity_id']['bundles']), 'Every comment has an `entity_id` field.'); Chris@18: $bundle_ids_by_target_entity_type_id = []; Chris@18: foreach ($field_map[$comment_entity_type_id]['entity_id']['bundles'] as $bundle_id) { Chris@18: $field_definitions = static::$fieldManager->getFieldDefinitions($comment_entity_type_id, $bundle_id); Chris@18: $commented_entity_field_definition = $field_definitions['entity_id']; Chris@18: // Each commented entity field definition has a setting which indicates Chris@18: // the entity type of the commented entity reference field. This differs Chris@18: // per bundle. Chris@18: $target_entity_type_id = $commented_entity_field_definition->getSetting('target_type'); Chris@18: $bundle_ids_by_target_entity_type_id[$target_entity_type_id][] = $bundle_id; Chris@18: } Chris@18: $bundle_specific_access_conditions = []; Chris@18: foreach ($bundle_ids_by_target_entity_type_id as $target_entity_type_id => $bundle_ids) { Chris@18: // Construct a field specifier prefix which targets the commented entity. Chris@18: $condition_field_prefix = "entity_id.entity:$target_entity_type_id"; Chris@18: // Ensure that for each possible commented entity type (which varies per Chris@18: // bundle), a condition is created that restricts access based on access Chris@18: // to the commented entity. Chris@18: $bundle_condition = new EntityCondition($comment_entity_type->getKey('bundle'), $bundle_ids, 'IN'); Chris@18: // Comments on comments can create an infinite recursion! If the target Chris@18: // entity type ID is comment, we need special behavior. Chris@18: if ($target_entity_type_id === $comment_entity_type_id) { Chris@18: $nested_comment_condition = $depth <= 3 Chris@18: ? static::getCommentAccessCondition($comment_entity_type, $current_user, $cacheability, $depth + 1) Chris@18: : static::alwaysFalse($comment_entity_type); Chris@18: $prefixed_comment_condition = static::addConditionFieldPrefix($nested_comment_condition, $condition_field_prefix); Chris@18: $bundle_specific_access_conditions[$target_entity_type_id] = new EntityConditionGroup('AND', [$bundle_condition, $prefixed_comment_condition]); Chris@18: } Chris@18: else { Chris@18: $target_condition = static::getAccessCondition($target_entity_type_id, $cacheability); Chris@18: $bundle_specific_access_conditions[$target_entity_type_id] = !is_null($target_condition) Chris@18: ? new EntityConditionGroup('AND', [ Chris@18: $bundle_condition, Chris@18: static::addConditionFieldPrefix($target_condition, $condition_field_prefix), Chris@18: ]) Chris@18: : $bundle_condition; Chris@18: } Chris@18: } Chris@18: Chris@18: // This condition ensures that the user is only permitted to see the Chris@18: // comments for which the user is also able to view the entity on which each Chris@18: // comment lives. Chris@18: $commented_entity_condition = new EntityConditionGroup('OR', array_values($bundle_specific_access_conditions)); Chris@18: return $commented_entity_condition; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets an always FALSE entity condition group for the given entity type. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type Chris@18: * The entity type for which to construct an impossible condition. Chris@18: * Chris@18: * @return \Drupal\jsonapi\Query\EntityConditionGroup Chris@18: * An EntityConditionGroup which cannot evaluate to TRUE. Chris@18: */ Chris@18: protected static function alwaysFalse(EntityTypeInterface $entity_type) { Chris@18: return new EntityConditionGroup('AND', [ Chris@18: new EntityCondition($entity_type->getKey('id'), 1, '<'), Chris@18: new EntityCondition($entity_type->getKey('id'), 1, '>'), Chris@18: ]); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Recursively collects all entity query condition fields. Chris@18: * Chris@18: * Entity conditions can be nested within AND and OR groups. This recursively Chris@18: * finds all unique fields in an entity query condition. Chris@18: * Chris@18: * @param \Drupal\jsonapi\Query\EntityConditionGroup $group Chris@18: * The root entity condition group. Chris@18: * @param array $fields Chris@18: * Internal use only. Chris@18: * Chris@18: * @return array Chris@18: * An array of entity query condition field names. Chris@18: */ Chris@18: protected static function collectFilteredFields(EntityConditionGroup $group, array $fields = []) { Chris@18: foreach ($group->members() as $member) { Chris@18: if ($member instanceof EntityConditionGroup) { Chris@18: $fields = static::collectFilteredFields($member, $fields); Chris@18: } Chris@18: else { Chris@18: $fields[] = $member->field(); Chris@18: } Chris@18: } Chris@18: return array_unique($fields); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Copied from \Drupal\jsonapi\IncludeResolver. Chris@18: * Chris@18: * @see \Drupal\jsonapi\IncludeResolver::buildTree() Chris@18: */ Chris@18: protected static function buildTree(array $paths) { Chris@18: $merged = []; Chris@18: foreach ($paths as $parts) { Chris@18: // This complex expression is needed to handle the string, "0", which Chris@18: // would be evaluated as FALSE. Chris@18: if (!is_null(($field_name = array_shift($parts)))) { Chris@18: $previous = isset($merged[$field_name]) ? $merged[$field_name] : []; Chris@18: $merged[$field_name] = array_merge($previous, [$parts]); Chris@18: } Chris@18: } Chris@18: return !empty($merged) ? array_map([static::class, __FUNCTION__], $merged) : $merged; Chris@18: } Chris@18: Chris@18: }