"Fossies" - the Fresh Open Source Software Archive

Member "drupal-8.9.10/core/modules/jsonapi/src/Access/TemporaryQueryGuard.php" (26 Nov 2020, 27683 Bytes) of package /linux/www/drupal-8.9.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "TemporaryQueryGuard.php" see the Fossies "Dox" file reference documentation.

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