1<?php
2
3namespace Drupal\jsonapi\Context;
4
5use Drupal\Component\Utility\NestedArray;
6use Drupal\Core\Access\AccessResult;
7use Drupal\Core\Access\AccessResultInterface;
8use Drupal\Core\Access\AccessResultReasonInterface;
9use Drupal\Core\Cache\CacheableMetadata;
10use Drupal\Core\Entity\EntityFieldManagerInterface;
11use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
12use Drupal\Core\Entity\EntityTypeManagerInterface;
13use Drupal\Core\Entity\FieldableEntityInterface;
14use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
15use Drupal\Core\Extension\ModuleHandlerInterface;
16use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
17use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException;
18use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
19use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
20use Drupal\Core\TypedData\DataReferenceTargetDefinition;
21use Drupal\jsonapi\ResourceType\ResourceType;
22use Drupal\jsonapi\ResourceType\ResourceTypeRelationship;
23use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
24use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
25
26/**
27 * A service that evaluates external path expressions against Drupal fields.
28 *
29 * This class performs 3 essential functions, path resolution, path validation
30 * and path expansion.
31 *
32 * Path resolution:
33 * Path resolution refers to the ability to map a set of external field names to
34 * their internal counterparts. This is necessary because a resource type can
35 * provide aliases for its field names. For example, the resource type @code
36 * node--article @endcode might "alias" the internal field name @code
37 * uid @endcode to the external field name @code author @endcode. This permits
38 * an API consumer to request @code
39 * /jsonapi/node/article?include=author @endcode for a better developer
40 * experience.
41 *
42 * Path validation:
43 * Path validation refers to the ability to ensure that a requested path
44 * corresponds to a valid set of internal fields. For example, if an API
45 * consumer may send a @code GET @endcode request to @code
46 * /jsonapi/node/article?sort=author.field_first_name @endcode. The field
47 * resolver ensures that @code uid @endcode (which would have been resolved
48 * from @code author @endcode) exists on article nodes and that @code
49 * field_first_name @endcode exists on user entities. However, in the case of
50 * an @code include @endcode path, the field resolver would raise a client error
51 * because @code field_first_name @endcode is not an entity reference field,
52 * meaning it does not identify any related resources that can be included in a
53 * compound document.
54 *
55 * Path expansion:
56 * Path expansion refers to the ability to expand a path to an entity query
57 * compatible field expression. For example, a request URL might have a query
58 * string like @code ?filter[field_tags.name]=aviation @endcode, before
59 * constructing the appropriate entity query, the entity query system needs the
60 * path expression to be "expanded" into @code field_tags.entity.name @endcode.
61 * In some rare cases, the entity query system needs this to be expanded to
62 * @code field_tags.entity:taxonomy_term.name @endcode; the field resolver
63 * simply does this by default for every path.
64 *
65 * *Note:* path expansion is *not* performed for @code include @endcode paths.
66 *
67 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
68 *   may change at any time and could break any dependencies on it.
69 *
70 * @see https://www.drupal.org/project/drupal/issues/3032787
71 * @see jsonapi.api.php
72 */
73class FieldResolver {
74
75  /**
76   * The entity type manager.
77   *
78   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
79   */
80  protected $entityTypeManager;
81
82  /**
83   * The field manager.
84   *
85   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
86   */
87  protected $fieldManager;
88
89  /**
90   * The entity type bundle information service.
91   *
92   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
93   */
94  protected $entityTypeBundleInfo;
95
96  /**
97   * The JSON:API resource type repository service.
98   *
99   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
100   */
101  protected $resourceTypeRepository;
102
103  /**
104   * The module handler.
105   *
106   * @var \Drupal\Core\Extension\ModuleHandlerInterface
107   */
108  protected $moduleHandler;
109
110  /**
111   * Creates a FieldResolver instance.
112   *
113   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
114   *   The entity type manager.
115   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
116   *   The field manager.
117   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
118   *   The bundle info service.
119   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
120   *   The resource type repository.
121   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
122   *   The module handler.
123   */
124  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, ResourceTypeRepositoryInterface $resource_type_repository, ModuleHandlerInterface $module_handler) {
125    $this->entityTypeManager = $entity_type_manager;
126    $this->fieldManager = $field_manager;
127    $this->entityTypeBundleInfo = $entity_type_bundle_info;
128    $this->resourceTypeRepository = $resource_type_repository;
129    $this->moduleHandler = $module_handler;
130  }
131
132  /**
133   * Validates and resolves an include path into its internal possibilities.
134   *
135   * Each resource type may define its own external names for its internal
136   * field names. As a result, a single external include path may target
137   * multiple internal paths.
138   *
139   * This can happen when an entity reference field has different allowed entity
140   * types *per bundle* (as is possible with comment entities) or when
141   * different resource types share an external field name but resolve to
142   * different internal fields names.
143   *
144   * Example 1:
145   * An installation may have three comment types for three different entity
146   * types, two of which have a file field and one of which does not. In that
147   * case, a path like @code field_comments.entity_id.media @endcode might be
148   * resolved to both @code field_comments.entity_id.field_audio @endcode
149   * and @code field_comments.entity_id.field_image @endcode.
150   *
151   * Example 2:
152   * A path of @code field_author_profile.account @endcode might
153   * resolve to @code field_author_profile.uid @endcode and @code
154   * field_author_profile.field_user @endcode if @code
155   * field_author_profile @endcode can relate to two different JSON:API resource
156   * types (like `node--profile` and `node--migrated_profile`) which have the
157   * external field name @code account @endcode aliased to different internal
158   * field names.
159   *
160   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
161   *   The resource type for which the path should be validated.
162   * @param string[] $path_parts
163   *   The include path as an array of strings. For example, the include query
164   *   parameter string of @code field_tags.uid @endcode should be given
165   *   as @code ['field_tags', 'uid'] @endcode.
166   * @param int $depth
167   *   (internal) Used to track recursion depth in order to generate better
168   *   exception messages.
169   *
170   * @return string[]
171   *   The resolved internal include paths.
172   *
173   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
174   *   Thrown if the path contains invalid specifiers.
175   */
176  public static function resolveInternalIncludePath(ResourceType $resource_type, array $path_parts, $depth = 0) {
177    $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:include']);
178    if (empty($path_parts[0])) {
179      throw new CacheableBadRequestHttpException($cacheability, 'Empty include path.');
180    }
181    $public_field_name = $path_parts[0];
182    $internal_field_name = $resource_type->getInternalName($public_field_name);
183    $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($public_field_name);
184    if (empty($relatable_resource_types)) {
185      $message = "`$public_field_name` is not a valid relationship field name.";
186      if (!empty(($possible = implode(', ', array_keys($resource_type->getRelatableResourceTypes()))))) {
187        $message .= " Possible values: $possible.";
188      }
189      throw new CacheableBadRequestHttpException($cacheability, $message);
190    }
191    $remaining_parts = array_slice($path_parts, 1);
192    if (empty($remaining_parts)) {
193      return [[$internal_field_name]];
194    }
195    $exceptions = [];
196    $resolved = [];
197    foreach ($relatable_resource_types as $relatable_resource_type) {
198      try {
199        // Each resource type may resolve the path differently and may return
200        // multiple possible resolutions.
201        $resolved = array_merge($resolved, static::resolveInternalIncludePath($relatable_resource_type, $remaining_parts, $depth + 1));
202      }
203      catch (CacheableBadRequestHttpException $e) {
204        $exceptions[] = $e;
205      }
206    }
207    if (!empty($exceptions) && count($exceptions) === count($relatable_resource_types)) {
208      $previous_messages = implode(' ', array_unique(array_map(function (CacheableBadRequestHttpException $e) {
209        return $e->getMessage();
210      }, $exceptions)));
211      // Only add the full include path on the first level of recursion so that
212      // the invalid path phrase isn't repeated at every level.
213      throw new CacheableBadRequestHttpException($cacheability, $depth === 0
214        ? sprintf("`%s` is not a valid include path. $previous_messages", implode('.', $path_parts))
215        : $previous_messages
216      );
217    }
218    // Remove duplicates by converting to strings and then using array_unique().
219    $resolved_as_strings = array_map(function ($possibility) {
220      return implode('.', $possibility);
221    }, $resolved);
222    $resolved_as_strings = array_unique($resolved_as_strings);
223
224    // The resolved internal paths do not include the current field name because
225    // resolution happens in a recursive process. Convert back from strings.
226    return array_map(function ($possibility) use ($internal_field_name) {
227      return array_merge([$internal_field_name], explode('.', $possibility));
228    }, $resolved_as_strings);
229  }
230
231  /**
232   * Resolves external field expressions into entity query compatible paths.
233   *
234   * It is often required to reference data which may exist across a
235   * relationship. For example, you may want to sort a list of articles by
236   * a field on the article author's representative entity. Or you may wish
237   * to filter a list of content by the name of referenced taxonomy terms.
238   *
239   * In an effort to simplify the referenced paths and align them with the
240   * structure of JSON:API responses and the structure of the hypothetical
241   * "reference document" (see link), it is possible to alias field names and
242   * elide the "entity" keyword from them (this word is used by the entity query
243   * system to traverse entity references).
244   *
245   * This method takes this external field expression and attempts to resolve
246   * any aliases and/or abbreviations into a field expression that will be
247   * compatible with the entity query system.
248   *
249   * @link http://jsonapi.org/recommendations/#urls-reference-document
250   *
251   * Example:
252   *   'uid.field_first_name' -> 'uid.entity.field_first_name'.
253   *   'author.firstName' -> 'field_author.entity.field_first_name'
254   *
255   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
256   *   The JSON:API resource type from which to resolve the field name.
257   * @param string $external_field_name
258   *   The public field name to map to a Drupal field name.
259   * @param string $operator
260   *   (optional) The operator of the condition for which the path should be
261   *   resolved.
262   *
263   * @return string
264   *   The mapped field name.
265   *
266   * @throws \Drupal\Core\Http\Exception\CacheableBadRequestHttpException
267   */
268  public function resolveInternalEntityQueryPath($resource_type, $external_field_name, $operator = NULL) {
269    $function_args = func_get_args();
270    // @todo Remove this conditional block in drupal:9.0.0 and add a type hint
271    // to the first argument of this method.
272    // @see https://www.drupal.org/project/drupal/issues/3078045
273    if (count($function_args) === 3 && is_string($resource_type)) {
274      @trigger_error('Passing the entity type ID and bundle to ' . __METHOD__ . ' is deprecated in drupal:8.8.0 and will throw a fatal error in drupal:9.0.0. Pass a JSON:API resource type instead. See https://www.drupal.org/node/3078036', E_USER_DEPRECATED);
275      list($entity_type_id, $bundle, $external_field_name) = $function_args;
276      $resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle);
277    }
278    elseif (!$resource_type instanceof ResourceType) {
279      throw new \InvalidArgumentException("The first argument to " . __METHOD__ . " should be an instance of \Drupal\jsonapi\ResourceType\ResourceType, " . gettype($resource_type) . " given.");
280    }
281
282    $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:filter', 'url.query_args:sort']);
283    if (empty($external_field_name)) {
284      throw new CacheableBadRequestHttpException($cacheability, 'No field name was provided for the filter.');
285    }
286
287    // Turns 'uid.categories.name' into
288    // 'uid.entity.field_category.entity.name'. This may be too simple, but it
289    // works for the time being.
290    $parts = explode('.', $external_field_name);
291    $unresolved_path_parts = $parts;
292    $reference_breadcrumbs = [];
293    /* @var \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types */
294    $resource_types = [$resource_type];
295    // This complex expression is needed to handle the string, "0", which would
296    // otherwise be evaluated as FALSE.
297    while (!is_null(($part = array_shift($parts)))) {
298      if (!$this->isMemberFilterable($part, $resource_types)) {
299        throw new CacheableBadRequestHttpException($cacheability, sprintf(
300          'Invalid nested filtering. The field `%s`, given in the path `%s`, does not exist.',
301          $part,
302          $external_field_name
303        ));
304      }
305
306      $field_name = $this->getInternalName($part, $resource_types);
307
308      // If none of the resource types are traversable, assume that the
309      // remaining path parts are targeting field deltas and/or field
310      // properties.
311      if (!$this->resourceTypesAreTraversable($resource_types)) {
312        $reference_breadcrumbs[] = $field_name;
313        return $this->constructInternalPath($reference_breadcrumbs, $parts);
314      }
315
316      // Different resource types have different field definitions.
317      $candidate_definitions = $this->getFieldItemDefinitions($resource_types, $field_name);
318      assert(!empty($candidate_definitions));
319
320      // We have a valid field, so add it to the validated trail of path parts.
321      $reference_breadcrumbs[] = $field_name;
322
323      // Remove resource types which do not have a candidate definition.
324      $resource_types = array_filter($resource_types, function (ResourceType $resource_type) use ($candidate_definitions) {
325        return isset($candidate_definitions[$resource_type->getTypeName()]);
326      });
327
328      // Check access to execute a query for each field per resource type since
329      // field definitions are bundle-specific.
330      foreach ($resource_types as $resource_type) {
331        $field_access = $this->getFieldAccess($resource_type, $field_name);
332        $cacheability->addCacheableDependency($field_access);
333        if (!$field_access->isAllowed()) {
334          $message = sprintf('The current user is not authorized to filter by the `%s` field, given in the path `%s`.', $field_name, implode('.', $reference_breadcrumbs));
335          if ($field_access instanceof AccessResultReasonInterface && ($reason = $field_access->getReason()) && !empty($reason)) {
336            $message .= ' ' . $reason;
337          }
338          throw new CacheableAccessDeniedHttpException($cacheability, $message);
339        }
340      }
341
342      // Get all of the referenceable resource types.
343      $resource_types = $this->getRelatableResourceTypes($resource_types, $candidate_definitions);
344
345      $at_least_one_entity_reference_field = FALSE;
346      $candidate_property_names = array_unique(NestedArray::mergeDeepArray(array_map(function (FieldItemDataDefinitionInterface $definition) use (&$at_least_one_entity_reference_field) {
347        $property_definitions = $definition->getPropertyDefinitions();
348        return array_reduce(array_keys($property_definitions), function ($property_names, $property_name) use ($property_definitions, &$at_least_one_entity_reference_field) {
349          $property_definition = $property_definitions[$property_name];
350          $is_data_reference_definition = $property_definition instanceof DataReferenceTargetDefinition;
351          if (!$property_definition->isInternal()) {
352            // Entity reference fields are special: their reference property
353            // (usually `target_id`) is never exposed in the JSON:API
354            // representation. Hence it must also not be exposed in 400
355            // responses' error messages.
356            $property_names[] = $is_data_reference_definition ? 'id' : $property_name;
357          }
358          if ($is_data_reference_definition) {
359            $at_least_one_entity_reference_field = TRUE;
360          }
361          return $property_names;
362        }, []);
363      }, $candidate_definitions)));
364
365      // Determine if the specified field has one property or many in its
366      // JSON:API representation, or if it is an relationship (an entity
367      // reference field), in which case the `id` of the related resource must
368      // always be specified.
369      $property_specifier_needed = $at_least_one_entity_reference_field || count($candidate_property_names) > 1;
370
371      // If there are no remaining path parts, the process is finished unless
372      // the field has multiple properties, in which case one must be specified.
373      if (empty($parts)) {
374        // If the operator is asserting the presence or absence of a
375        // relationship entirely, it does not make sense to require a property
376        // specifier.
377        if ($property_specifier_needed && (!$at_least_one_entity_reference_field || !in_array($operator, ['IS NULL', 'IS NOT NULL'], TRUE))) {
378          $possible_specifiers = array_map(function ($specifier) use ($at_least_one_entity_reference_field) {
379            return $at_least_one_entity_reference_field && $specifier !== 'id' ? "meta.$specifier" : $specifier;
380          }, $candidate_property_names);
381          throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The field `%s`, given in the path `%s` is incomplete, it must end with one of the following specifiers: `%s`.', $part, $external_field_name, implode('`, `', $possible_specifiers)));
382        }
383        return $this->constructInternalPath($reference_breadcrumbs);
384      }
385
386      // If the next part is a delta, as in "body.0.value", then we add it to
387      // the breadcrumbs and remove it from the parts that still must be
388      // processed.
389      if (static::isDelta($parts[0])) {
390        $reference_breadcrumbs[] = array_shift($parts);
391      }
392
393      // If there are no remaining path parts, the process is finished.
394      if (empty($parts)) {
395        return $this->constructInternalPath($reference_breadcrumbs);
396      }
397
398      // JSON:API outputs entity reference field properties under a meta object
399      // on a relationship. If the filter specifies one of these properties, it
400      // must prefix the property name with `meta`. The only exception is if the
401      // next path part is the same as the name for the reference property
402      // (typically `entity`), this is permitted to disambiguate the case of a
403      // field name on the target entity which is the same a property name on
404      // the entity reference field.
405      if ($at_least_one_entity_reference_field && $parts[0] !== 'id') {
406        if ($parts[0] === 'meta') {
407          array_shift($parts);
408        }
409        elseif (in_array($parts[0], $candidate_property_names) && !static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) {
410          throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s` belongs to the meta object of a relationship and must be preceded by `meta`.', $parts[0], $external_field_name));
411        }
412      }
413
414      // Determine if the next part is not a property of $field_name.
415      if (!static::isCandidateDefinitionProperty($parts[0], $candidate_definitions) && !empty(static::getAllDataReferencePropertyNames($candidate_definitions))) {
416        // The next path part is neither a delta nor a field property, so it
417        // must be a field on a targeted resource type. We need to guess the
418        // intermediate reference property since one was not provided.
419        //
420        // For example, the path `uid.name` for a `node--article` resource type
421        // will be resolved into `uid.entity.name`.
422        $reference_breadcrumbs[] = static::getDataReferencePropertyName($candidate_definitions, $parts, $unresolved_path_parts);
423      }
424      else {
425        // If the property is not a reference property, then all
426        // remaining parts must be further property specifiers.
427        if (!static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) {
428          // If a field property is specified on a field with only one property
429          // defined, throw an error because in the JSON:API output, it does not
430          // exist. This is because JSON:API elides single-value properties;
431          // respecting it would leak this Drupalism out.
432          if (count($candidate_property_names) === 1) {
433            throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s`, does not exist. Filter by `%s`, not `%s` (the JSON:API module elides property names from single-property fields).', $parts[0], $external_field_name, substr($external_field_name, 0, strlen($external_field_name) - strlen($parts[0]) - 1), $external_field_name));
434          }
435          elseif (!in_array($parts[0], $candidate_property_names, TRUE)) {
436            throw new CacheableBadRequestHttpException($cacheability, sprintf('Invalid nested filtering. The property `%s`, given in the path `%s`, does not exist. Must be one of the following property names: `%s`.', $parts[0], $external_field_name, implode('`, `', $candidate_property_names)));
437          }
438          return $this->constructInternalPath($reference_breadcrumbs, $parts);
439        }
440        // The property is a reference, so add it to the breadcrumbs and
441        // continue resolving fields.
442        $reference_breadcrumbs[] = array_shift($parts);
443      }
444    }
445
446    // Reconstruct the full path to the final reference field.
447    return $this->constructInternalPath($reference_breadcrumbs);
448  }
449
450  /**
451   * Expands the internal path with the "entity" keyword.
452   *
453   * @param string[] $references
454   *   The resolved internal field names of all entity references.
455   * @param string[] $property_path
456   *   (optional) A sub-property path for the last field in the path.
457   *
458   * @return string
459   *   The expanded and imploded path.
460   */
461  protected function constructInternalPath(array $references, array $property_path = []) {
462    // Reconstruct the path parts that are referencing sub-properties.
463    $field_path = implode('.', $property_path);
464
465    // This rebuilds the path from the real, internal field names that have
466    // been traversed so far. It joins them with the "entity" keyword as
467    // required by the entity query system.
468    $entity_path = implode('.', $references);
469
470    // Reconstruct the full path to the final reference field.
471    return (empty($field_path)) ? $entity_path : $entity_path . '.' . $field_path;
472  }
473
474  /**
475   * Get all item definitions from a set of resources types by a field name.
476   *
477   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
478   *   The resource types on which the field might exist.
479   * @param string $field_name
480   *   The field for which to retrieve field item definitions.
481   *
482   * @return \Drupal\Core\TypedData\ComplexDataDefinitionInterface[]
483   *   The found field item definitions.
484   */
485  protected function getFieldItemDefinitions(array $resource_types, $field_name) {
486    return array_reduce($resource_types, function ($result, ResourceType $resource_type) use ($field_name) {
487      /* @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */
488      $entity_type = $resource_type->getEntityTypeId();
489      $bundle = $resource_type->getBundle();
490      $definitions = $this->fieldManager->getFieldDefinitions($entity_type, $bundle);
491      if (isset($definitions[$field_name])) {
492        $result[$resource_type->getTypeName()] = $definitions[$field_name]->getItemDefinition();
493      }
494      return $result;
495    }, []);
496  }
497
498  /**
499   * Resolves the UUID field name for a resource type.
500   *
501   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
502   *   The resource type for which to get the UUID field name.
503   *
504   * @return string
505   *   The resolved internal name.
506   */
507  protected function getIdFieldName(ResourceType $resource_type) {
508    $entity_type = $this->entityTypeManager->getDefinition($resource_type->getEntityTypeId());
509    return $entity_type->getKey('uuid');
510  }
511
512  /**
513   * Resolves the internal field name based on a collection of resource types.
514   *
515   * @param string $field_name
516   *   The external field name.
517   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
518   *   The resource types from which to get an internal name.
519   *
520   * @return string
521   *   The resolved internal name.
522   */
523  protected function getInternalName($field_name, array $resource_types) {
524    return array_reduce($resource_types, function ($carry, ResourceType $resource_type) use ($field_name) {
525      if ($carry != $field_name) {
526        // We already found the internal name.
527        return $carry;
528      }
529      return $field_name === 'id' ? $this->getIdFieldName($resource_type) : $resource_type->getInternalName($field_name);
530    }, $field_name);
531  }
532
533  /**
534   * Determines if the given field or member name is filterable.
535   *
536   * @param string $external_name
537   *   The external field or member name.
538   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
539   *   The resource types to test.
540   *
541   * @return bool
542   *   Whether the given field is present as a filterable member of the targeted
543   *   resource objects.
544   */
545  protected function isMemberFilterable($external_name, array $resource_types) {
546    return array_reduce($resource_types, function ($carry, ResourceType $resource_type) use ($external_name) {
547      // @todo: remove the next line and uncomment the following one in https://www.drupal.org/project/drupal/issues/3017047.
548      return $carry ?: $external_name === 'id' || $resource_type->isFieldEnabled($resource_type->getInternalName($external_name));
549      /*return $carry ?: in_array($external_name, ['id', 'type']) || $resource_type->isFieldEnabled($resource_type->getInternalName($external_name));*/
550    }, FALSE);
551  }
552
553  /**
554   * Get the referenceable ResourceTypes for a set of field definitions.
555   *
556   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
557   *   The resource types on which the reference field might exist.
558   * @param \Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface[] $definitions
559   *   The field item definitions of targeted fields, keyed by the resource
560   *   type name on which they reside.
561   *
562   * @return \Drupal\jsonapi\ResourceType\ResourceType[]
563   *   The referenceable target resource types.
564   */
565  protected function getRelatableResourceTypes(array $resource_types, array $definitions) {
566    $relatable_resource_types = [];
567    foreach ($resource_types as $resource_type) {
568      $definition = $definitions[$resource_type->getTypeName()];
569      $resource_type_field = $resource_type->getFieldByInternalName($definition->getFieldDefinition()->getName());
570      if ($resource_type_field instanceof ResourceTypeRelationship) {
571        foreach ($resource_type_field->getRelatableResourceTypes() as $relatable_resource_type) {
572          $relatable_resource_types[$relatable_resource_type->getTypeName()] = $relatable_resource_type;
573        }
574      }
575    }
576    return $relatable_resource_types;
577  }
578
579  /**
580   * Whether the given resources can be traversed to other resources.
581   *
582   * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types
583   *   The resources types to evaluate.
584   *
585   * @return bool
586   *   TRUE if any one of the given resource types is traversable.
587   *
588   * @todo This class shouldn't be aware of entity types and their definitions.
589   * Whether a resource can have relationships to other resources is information
590   * we ought to be able to discover on the ResourceType. However, we cannot
591   * reliably determine this information with existing APIs. Entities may be
592   * backed by various storages that are unable to perform queries across
593   * references and certain storages may not be able to store references at all.
594   */
595  protected function resourceTypesAreTraversable(array $resource_types) {
596    foreach ($resource_types as $resource_type) {
597      $entity_type_definition = $this->entityTypeManager->getDefinition($resource_type->getEntityTypeId());
598      if ($entity_type_definition->entityClassImplements(FieldableEntityInterface::class)) {
599        return TRUE;
600      }
601    }
602    return FALSE;
603  }
604
605  /**
606   * Gets all unique reference property names from the given field definitions.
607   *
608   * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions
609   *   A list of targeted field item definitions specified by the path.
610   *
611   * @return string[]
612   *   The reference property names, if any.
613   */
614  protected static function getAllDataReferencePropertyNames(array $candidate_definitions) {
615    $reference_property_names = array_reduce($candidate_definitions, function (array $reference_property_names, ComplexDataDefinitionInterface $definition) {
616      $property_definitions = $definition->getPropertyDefinitions();
617      foreach ($property_definitions as $property_name => $property_definition) {
618        if ($property_definition instanceof DataReferenceDefinitionInterface) {
619          $target_definition = $property_definition->getTargetDefinition();
620          assert($target_definition instanceof EntityDataDefinitionInterface, 'Entity reference fields should only be able to reference entities.');
621          $reference_property_names[] = $property_name . ':' . $target_definition->getEntityTypeId();
622        }
623      }
624      return $reference_property_names;
625    }, []);
626    return array_unique($reference_property_names);
627  }
628
629  /**
630   * Determines the reference property name for the remaining unresolved parts.
631   *
632   * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions
633   *   A list of targeted field item definitions specified by the path.
634   * @param string[] $remaining_parts
635   *   The remaining path parts.
636   * @param string[] $unresolved_path_parts
637   *   The unresolved path parts.
638   *
639   * @return string
640   *   The reference name.
641   */
642  protected static function getDataReferencePropertyName(array $candidate_definitions, array $remaining_parts, array $unresolved_path_parts) {
643    $unique_reference_names = static::getAllDataReferencePropertyNames($candidate_definitions);
644    if (count($unique_reference_names) > 1) {
645      $choices = array_map(function ($reference_name) use ($unresolved_path_parts, $remaining_parts) {
646        $prior_parts = array_slice($unresolved_path_parts, 0, count($unresolved_path_parts) - count($remaining_parts));
647        return implode('.', array_merge($prior_parts, [$reference_name], $remaining_parts));
648      }, $unique_reference_names);
649      // @todo Add test coverage for this in https://www.drupal.org/project/drupal/issues/2971281
650      $message = sprintf('Ambiguous path. Try one of the following: %s, in place of the given path: %s', implode(', ', $choices), implode('.', $unresolved_path_parts));
651      $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:filter', 'url.query_args:sort']);
652      throw new CacheableBadRequestHttpException($cacheability, $message);
653    }
654    return $unique_reference_names[0];
655  }
656
657  /**
658   * Determines if a path part targets a specific field delta.
659   *
660   * @param string $part
661   *   The path part.
662   *
663   * @return bool
664   *   TRUE if the part is an integer, FALSE otherwise.
665   */
666  protected static function isDelta($part) {
667    return (bool) preg_match('/^[0-9]+$/', $part);
668  }
669
670  /**
671   * Determines if a path part targets a field property, not a subsequent field.
672   *
673   * @param string $part
674   *   The path part.
675   * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions
676   *   A list of targeted field item definitions which are specified by the
677   *   path.
678   *
679   * @return bool
680   *   TRUE if the part is a property of one of the candidate definitions, FALSE
681   *   otherwise.
682   */
683  protected static function isCandidateDefinitionProperty($part, array $candidate_definitions) {
684    $part = static::getPathPartPropertyName($part);
685    foreach ($candidate_definitions as $definition) {
686      if ($definition->getPropertyDefinition($part)) {
687        return TRUE;
688      }
689    }
690    return FALSE;
691  }
692
693  /**
694   * Determines if a path part targets a reference property.
695   *
696   * @param string $part
697   *   The path part.
698   * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions
699   *   A list of targeted field item definitions which are specified by the
700   *   path.
701   *
702   * @return bool
703   *   TRUE if the part is a property of one of the candidate definitions, FALSE
704   *   otherwise.
705   */
706  protected static function isCandidateDefinitionReferenceProperty($part, array $candidate_definitions) {
707    $part = static::getPathPartPropertyName($part);
708    foreach ($candidate_definitions as $definition) {
709      $property = $definition->getPropertyDefinition($part);
710      if ($property && $property instanceof DataReferenceDefinitionInterface) {
711        return TRUE;
712      }
713    }
714    return FALSE;
715  }
716
717  /**
718   * Gets the property name from an entity typed or untyped path part.
719   *
720   * A path part may contain an entity type specifier like `entity:node`. This
721   * extracts the actual property name. If an entity type is not specified, then
722   * the path part is simply returned. For example, both `foo` and `foo:bar`
723   * will return `foo`.
724   *
725   * @param string $part
726   *   A path part.
727   *
728   * @return string
729   *   The property name from a path part.
730   */
731  protected static function getPathPartPropertyName($part) {
732    return strpos($part, ':') !== FALSE ? explode(':', $part)[0] : $part;
733  }
734
735  /**
736   * Gets the field access result for the 'view' operation.
737   *
738   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
739   *   The JSON:API resource type on which the field exists.
740   * @param string $internal_field_name
741   *   The field name for which access should be checked.
742   *
743   * @return \Drupal\Core\Access\AccessResultInterface
744   *   The 'view' access result.
745   */
746  protected function getFieldAccess(ResourceType $resource_type, $internal_field_name) {
747    $definitions = $this->fieldManager->getFieldDefinitions($resource_type->getEntityTypeId(), $resource_type->getBundle());
748    assert(isset($definitions[$internal_field_name]), 'The field name should have already been validated.');
749    $field_definition = $definitions[$internal_field_name];
750    $filter_access_results = $this->moduleHandler->invokeAll('jsonapi_entity_field_filter_access', [$field_definition, \Drupal::currentUser()]);
751    $filter_access_result = array_reduce($filter_access_results, function (AccessResultInterface $combined_result, AccessResultInterface $result) {
752      return $combined_result->orIf($result);
753    }, AccessResult::neutral());
754    if (!$filter_access_result->isNeutral()) {
755      return $filter_access_result;
756    }
757    $entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($resource_type->getEntityTypeId());
758    $field_access = $entity_access_control_handler->fieldAccess('view', $field_definition, NULL, NULL, TRUE);
759    return $filter_access_result->orIf($field_access);
760  }
761
762}
763