jsonapi-8.x-2.x-dev/src/Context/FieldResolver.php
src/Context/FieldResolver.php
<?php namespace Drupal\jsonapi\Context; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Access\AccessResultReasonInterface; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface; use Drupal\Core\Http\Exception\CacheableAccessDeniedHttpException; use Drupal\Core\TypedData\ComplexDataDefinitionInterface; use Drupal\Core\TypedData\DataReferenceDefinitionInterface; use Drupal\Core\TypedData\DataReferenceTargetDefinition; use Drupal\jsonapi\ResourceType\ResourceType; use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface; use Drupal\Core\Http\Exception\CacheableBadRequestHttpException; /** * A service that evaluates external path expressions against Drupal fields. * * This class performs 3 essential functions, path resolution, path validation * and path expansion. * * Path resolution: * Path resolution refers to the ability to map a set of external field names to * their internal counterparts. This is necessary because a resource type can * provide aliases for its field names. For example, the resource type @code * node--article @endcode might "alias" the internal field name @code * uid @endcode to the external field name @code author @endcode. This permits * an API consumer to request @code * /jsonapi/node/article?include=author @endcode for a better developer * experience. * * Path validation: * Path validation refers to the ability to ensure that a requested path * corresponds to a valid set of internal fields. For example, if an API * consumer may send a @code GET @endcode request to @code * /jsonapi/node/article?sort=author.field_first_name @endcode. The field * resolver ensures that @code uid @endcode (which would have been resolved * from @code author @endcode) exists on article nodes and that @code * field_first_name @endcode exists on user entities. However, in the case of * an @code include @endcode path, the field resolver would raise a client error * because @code field_first_name @endcode is not an entity reference field, * meaning it does not identify any related resources that can be included in a * compound document. * * Path expansion: * Path expansion refers to the ability to expand a path to an entity query * compatible field expression. For example, a request URL might have a query * string like @code ?filter[field_tags.name]=aviation @endcode, before * constructing the appropriate entity query, the entity query system needs the * path expression to be "expanded" into @code field_tags.entity.name @endcode. * In some rare cases, the entity query system needs this to be expanded to * @code field_tags.entity:taxonomy_term.name @endcode; the field resolver * simply does this by default for every path. * * *Note:* path expansion is *not* performed for @code include @endcode paths. * * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class * may change at any time and could break any dependencies on it. * * @see https://www.drupal.org/project/jsonapi/issues/3032787 * @see jsonapi.api.php */ class FieldResolver { /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * The field manager. * * @var \Drupal\Core\Entity\EntityFieldManagerInterface */ protected $fieldManager; /** * The entity type bundle information service. * * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface */ protected $entityTypeBundleInfo; /** * The JSON:API resource type repository service. * * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface */ protected $resourceTypeRepository; /** * The module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */ protected $moduleHandler; /** * Creates a FieldResolver instance. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager * The field manager. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info * The bundle info service. * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository * The resource type repository. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, ResourceTypeRepositoryInterface $resource_type_repository, ModuleHandlerInterface $module_handler) { $this->entityTypeManager = $entity_type_manager; $this->fieldManager = $field_manager; $this->entityTypeBundleInfo = $entity_type_bundle_info; $this->resourceTypeRepository = $resource_type_repository; $this->moduleHandler = $module_handler; } /** * Validates and resolves an include path into its internal possibilities. * * Each resource type may define its own external names for its internal * field names. As a result, a single external include path may target * multiple internal paths. * * This can happen when an entity reference field has different allowed entity * types *per bundle* (as is possible with comment entities) or when * different resource types share an external field name but resolve to * different internal fields names. * * Example 1: * An installation may have three comment types for three different entity * types, two of which have a file field and one of which does not. In that * case, a path like @code field_comments.entity_id.media @endcode might be * resolved to both @code field_comments.entity_id.field_audio @endcode * and @code field_comments.entity_id.field_image @endcode. * * Example 2: * A path of @code field_author_profile.account @endcode might * resolve to @code field_author_profile.uid @endcode and @code * field_author_profile.field_user @endcode if @code * field_author_profile @endcode can relate to two different JSON:API resource * types (like `node--profile` and `node--migrated_profile`) which have the * external field name @code account @endcode aliased to different internal * field names. * * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type * The resource type for which the path should be validated. * @param string[] $path_parts * The include path as an array of strings. For example, the include query * parameter string of @code field_tags.uid @endcode should be given * as @code ['field_tags', 'uid'] @endcode. * @param int $depth * (internal) Used to track recursion depth in order to generate better * exception messages. * * @return string[] * The resolved internal include paths. * * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException * Thrown if the path contains invalid specifiers. */ public static function resolveInternalIncludePath(ResourceType $resource_type, array $path_parts, $depth = 0) { $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:include']); if (empty($path_parts[0])) { throw new CacheableBadRequestHttpException($cacheability, 'Empty include path.'); } $public_field_name = $path_parts[0]; $internal_field_name = $resource_type->getInternalName($public_field_name); $relatable_resource_types = $resource_type->getRelatableResourceTypesByField($public_field_name); if (empty($relatable_resource_types)) { $message = "`$public_field_name` is not a valid relationship field name."; if (!empty(($possible = implode(', ', array_keys($resource_type->getRelatableResourceTypes()))))) { $message .= " Possible values: $possible."; } throw new CacheableBadRequestHttpException($cacheability, $message); } $remaining_parts = array_slice($path_parts, 1); if (empty($remaining_parts)) { return [[$internal_field_name]]; } $exceptions = []; $resolved = []; foreach ($relatable_resource_types as $relatable_resource_type) { try { // Each resource type may resolve the path differently and may return // multiple possible resolutions. $resolved = array_merge($resolved, static::resolveInternalIncludePath($relatable_resource_type, $remaining_parts, $depth + 1)); } catch (CacheableBadRequestHttpException $e) { $exceptions[] = $e; } } if (!empty($exceptions) && count($exceptions) === count($relatable_resource_types)) { $previous_messages = implode(' ', array_unique(array_map(function (CacheableBadRequestHttpException $e) { return $e->getMessage(); }, $exceptions))); // Only add the full include path on the first level of recursion so that // the invalid path phrase isn't repeated at every level. throw new CacheableBadRequestHttpException($cacheability, $depth === 0 ? sprintf("`%s` is not a valid include path. $previous_messages", implode('.', $path_parts)) : $previous_messages ); } // Remove duplicates by converting to strings and then using array_unique(). $resolved_as_strings = array_map(function ($possibility) { return implode('.', $possibility); }, $resolved); $resolved_as_strings = array_unique($resolved_as_strings); // The resolved internal paths do not include the current field name because // resolution happens in a recursive process. Convert back from strings. return array_map(function ($possibility) use ($internal_field_name) { return array_merge([$internal_field_name], explode('.', $possibility)); }, $resolved_as_strings); } /** * Resolves external field expressions into entity query compatible paths. * * It is often required to reference data which may exist across a * relationship. For example, you may want to sort a list of articles by * a field on the article author's representative entity. Or you may wish * to filter a list of content by the name of referenced taxonomy terms. * * In an effort to simplify the referenced paths and align them with the * structure of JSON:API responses and the structure of the hypothetical * "reference document" (see link), it is possible to alias field names and * elide the "entity" keyword from them (this word is used by the entity query * system to traverse entity references). * * This method takes this external field expression and and attempts to * resolve any aliases and/or abbreviations into a field expression that will * be compatible with the entity query system. * * @link http://jsonapi.org/recommendations/#urls-reference-document * * Example: * 'uid.field_first_name' -> 'uid.entity.field_first_name'. * 'author.firstName' -> 'field_author.entity.field_first_name' * * @param string $entity_type_id * The type of the entity for which to resolve the field name. * @param string $bundle * The bundle of the entity for which to resolve the field name. * @param string $external_field_name * The public field name to map to a Drupal field name. * * @return string * The mapped field name. * * @throws \Drupal\Core\Http\Exception\CacheableBadRequestHttpException */ public function resolveInternalEntityQueryPath($entity_type_id, $bundle, $external_field_name) { $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:filter', 'url.query_args:sort']); $resource_type = $this->resourceTypeRepository->get($entity_type_id, $bundle); if (empty($external_field_name)) { throw new CacheableBadRequestHttpException($cacheability, 'No field name was provided for the filter.'); } // Turns 'uid.categories.name' into // 'uid.entity.field_category.entity.name'. This may be too simple, but it // works for the time being. $parts = explode('.', $external_field_name); $unresolved_path_parts = $parts; $reference_breadcrumbs = []; /* @var \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types */ $resource_types = [$resource_type]; // This complex expression is needed to handle the string, "0", which would // otherwise be evaluated as FALSE. while (!is_null(($part = array_shift($parts)))) { if (!$this->isMemberFilterable($part, $resource_types)) { throw new CacheableBadRequestHttpException($cacheability, sprintf( 'Invalid nested filtering. The field `%s`, given in the path `%s`, does not exist.', $part, $external_field_name )); } $field_name = $this->getInternalName($part, $resource_types); // If none of the resource types are traversable, assume that the // remaining path parts are targeting field deltas and/or field // properties. if (!$this->resourceTypesAreTraversable($resource_types)) { $reference_breadcrumbs[] = $field_name; return $this->constructInternalPath($reference_breadcrumbs, $parts); } // Different resource types have different field definitions. $candidate_definitions = $this->getFieldItemDefinitions($resource_types, $field_name); assert(!empty($candidate_definitions)); // We have a valid field, so add it to the validated trail of path parts. $reference_breadcrumbs[] = $field_name; // Remove resource types which do not have a candidate definition. $resource_types = array_filter($resource_types, function (ResourceType $resource_type) use ($candidate_definitions) { return isset($candidate_definitions[$resource_type->getTypeName()]); }); // Check access to execute a query for each field per resource type since // field definitions are bundle-specific. foreach ($resource_types as $resource_type) { $field_access = $this->getFieldAccess($resource_type, $field_name); $cacheability->addCacheableDependency($field_access); if (!$field_access->isAllowed()) { $message = sprintf('The current user is not authorized to filter by the `%s` field, given in the path `%s`.', $field_name, implode('.', $reference_breadcrumbs)); if ($field_access instanceof AccessResultReasonInterface && ($reason = $field_access->getReason()) && !empty($reason)) { $message .= ' ' . $reason; } throw new CacheableAccessDeniedHttpException($cacheability, $message); } } // Get all of the referenceable resource types. $resource_types = $this->getReferenceableResourceTypes($candidate_definitions); $at_least_one_entity_reference_field = FALSE; $candidate_property_names = array_unique(NestedArray::mergeDeepArray(array_map(function (FieldItemDataDefinitionInterface $definition) use (&$at_least_one_entity_reference_field) { $property_definitions = $definition->getPropertyDefinitions(); return array_reduce(array_keys($property_definitions), function ($property_names, $property_name) use ($property_definitions, &$at_least_one_entity_reference_field) { $property_definition = $property_definitions[$property_name]; $is_data_reference_definition = $property_definition instanceof DataReferenceTargetDefinition; if (!$property_definition->isInternal()) { // Entity reference fields are special: their reference property // (usually `target_id`) is never exposed in the JSON:API // representation. Hence it must also not be exposed in 400 // responses' error messages. $property_names[] = $is_data_reference_definition ? 'id' : $property_name; } if ($is_data_reference_definition) { $at_least_one_entity_reference_field = TRUE; } return $property_names; }, []); }, $candidate_definitions))); // Determine if the specified field has one property or many in its // JSON:API representation, or if it is an relationship (an entity // reference field), in which case the `id` of the related resource must // always be specified. $property_specifier_needed = $at_least_one_entity_reference_field || count($candidate_property_names) > 1; // If there are no remaining path parts, the process is finished unless // the field has multiple properties, in which case one must be specified. if (empty($parts)) { if ($property_specifier_needed) { $possible_specifiers = array_map(function ($specifier) use ($at_least_one_entity_reference_field) { return $at_least_one_entity_reference_field && $specifier !== 'id' ? "meta.$specifier" : $specifier; }, $candidate_property_names); 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))); } return $this->constructInternalPath($reference_breadcrumbs); } // If the next part is a delta, as in "body.0.value", then we add it to // the breadcrumbs and remove it from the parts that still must be // processed. if (static::isDelta($parts[0])) { $reference_breadcrumbs[] = array_shift($parts); } // If there are no remaining path parts, the process is finished. if (empty($parts)) { return $this->constructInternalPath($reference_breadcrumbs); } // JSON:API outputs entity reference field properties under a meta object // on a relationship. If the filter specifies one of these properties, it // must prefix the property name with `meta`. The only exception is if the // next path part is the same as the name for the reference property // (typically `entity`), this is permitted to disambiguate the case of a // field name on the target entity which is the same a property name on // the entity reference field. if ($at_least_one_entity_reference_field && $parts[0] !== 'id') { if ($parts[0] === 'meta') { array_shift($parts); } elseif (in_array($parts[0], $candidate_property_names) && !static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) { 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)); } } // Determine if the next part is not a property of $field_name. if (!static::isCandidateDefinitionProperty($parts[0], $candidate_definitions) && !empty(static::getAllDataReferencePropertyNames($candidate_definitions))) { // The next path part is neither a delta nor a field property, so it // must be a field on a targeted resource type. We need to guess the // intermediate reference property since one was not provided. // // For example, the path `uid.name` for a `node--article` resource type // will be resolved into `uid.entity.name`. $reference_breadcrumbs[] = static::getDataReferencePropertyName($candidate_definitions, $parts, $unresolved_path_parts); } else { // If the property is not a reference property, then all // remaining parts must be further property specifiers. if (!static::isCandidateDefinitionReferenceProperty($parts[0], $candidate_definitions)) { // If a field property is specified on a field with only one property // defined, throw an error because in the JSON:API output, it does not // exist. This is because JSON:API elides single-value properties; // respecting it would leak this Drupalism out. if (count($candidate_property_names) === 1) { 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)); } elseif (!in_array($parts[0], $candidate_property_names, TRUE)) { 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))); } return $this->constructInternalPath($reference_breadcrumbs, $parts); } // The property is a reference, so add it to the breadcrumbs and // continue resolving fields. $reference_breadcrumbs[] = array_shift($parts); } } // Reconstruct the full path to the final reference field. return $this->constructInternalPath($reference_breadcrumbs); } /** * Expands the internal path with the "entity" keyword. * * @param string[] $references * The resolved internal field names of all entity references. * @param string[] $property_path * (optional) A sub-property path for the last field in the path. * * @return string * The expanded and imploded path. */ protected function constructInternalPath(array $references, array $property_path = []) { // Reconstruct the path parts that are referencing sub-properties. $field_path = implode('.', $property_path); // This rebuilds the path from the real, internal field names that have // been traversed so far. It joins them with the "entity" keyword as // required by the entity query system. $entity_path = implode('.', $references); // Reconstruct the full path to the final reference field. return (empty($field_path)) ? $entity_path : $entity_path . '.' . $field_path; } /** * Get all item definitions from a set of resources types by a field name. * * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types * The resource types on which the field might exist. * @param string $field_name * The field for which to retrieve field item definitions. * * @return \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] * The found field item definitions. */ protected function getFieldItemDefinitions(array $resource_types, $field_name) { return array_reduce($resource_types, function ($result, ResourceType $resource_type) use ($field_name) { /* @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */ $entity_type = $resource_type->getEntityTypeId(); $bundle = $resource_type->getBundle(); $definitions = $this->fieldManager->getFieldDefinitions($entity_type, $bundle); if (isset($definitions[$field_name])) { $result[$resource_type->getTypeName()] = $definitions[$field_name]->getItemDefinition(); } return $result; }, []); } /** * Resolves the UUID field name for a resource type. * * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type * The resource type for which to get the UUID field name. * * @return string * The resolved internal name. */ protected function getIdFieldName(ResourceType $resource_type) { $entity_type = $this->entityTypeManager->getDefinition($resource_type->getEntityTypeId()); return $entity_type->getKey('uuid'); } /** * Resolves the internal field name based on a collection of resource types. * * @param string $field_name * The external field name. * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types * The resource types from which to get an internal name. * * @return string * The resolved internal name. */ protected function getInternalName($field_name, array $resource_types) { return array_reduce($resource_types, function ($carry, ResourceType $resource_type) use ($field_name) { if ($carry != $field_name) { // We already found the internal name. return $carry; } return $field_name === 'id' ? $this->getIdFieldName($resource_type) : $resource_type->getInternalName($field_name); }, $field_name); } /** * Determines if the given field or member name is filterable. * * @param string $external_name * The external field or member name. * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types * The resource types to test. * * @return bool * Whether the given field is present as a filterable member of the targeted * resource objects. */ protected function isMemberFilterable($external_name, array $resource_types) { return array_reduce($resource_types, function ($carry, ResourceType $resource_type) use ($external_name) { // @todo: remove the next line and uncomment the following one in https://www.drupal.org/project/jsonapi/issues/3017047. return $carry ?: $external_name === 'id' || $resource_type->isFieldEnabled($resource_type->getInternalName($external_name)); /*return $carry ?: in_array($external_name, ['id', 'type']) || $resource_type->isFieldEnabled($resource_type->getInternalName($external_name));*/ }, FALSE); } /** * Get the referenceable ResourceTypes for a set of field definitions. * * @param \Drupal\Core\Field\FieldDefinitionInterface[] $definitions * The resource types on which the reference field might exist. * * @return \Drupal\jsonapi\ResourceType\ResourceType[] * The referenceable target resource types. */ protected function getReferenceableResourceTypes(array $definitions) { return array_reduce($definitions, function ($result, $definition) { $resource_types = array_filter( $this->collectResourceTypesForReference($definition) ); $type_names = array_map(function ($resource_type) { /* @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */ return $resource_type->getTypeName(); }, $resource_types); return array_merge($result, array_combine($type_names, $resource_types)); }, []); } /** * Build a list of resource types depending on which bundles are referenced. * * @param \Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface $item_definition * The reference definition. * * @return \Drupal\jsonapi\ResourceType\ResourceType[] * The list of resource types. */ protected function collectResourceTypesForReference(FieldItemDataDefinitionInterface $item_definition) { $main_property_definition = $item_definition->getPropertyDefinition( $item_definition->getMainPropertyName() ); // Check if the field is a flavor of an Entity Reference field. if (!$main_property_definition instanceof DataReferenceTargetDefinition) { return []; } $entity_type_id = $item_definition->getSetting('target_type'); $handler_settings = $item_definition->getSetting('handler_settings'); $has_target_bundles = isset($handler_settings['target_bundles']) && !empty($handler_settings['target_bundles']); $target_bundles = $has_target_bundles ? $handler_settings['target_bundles'] : $this->getAllBundlesForEntityType($entity_type_id); return array_map(function ($bundle) use ($entity_type_id) { return $this->resourceTypeRepository->get($entity_type_id, $bundle); }, $target_bundles); } /** * Whether the given resources can be traversed to other resources. * * @param \Drupal\jsonapi\ResourceType\ResourceType[] $resource_types * The resources types to evaluate. * * @return bool * TRUE if any one of the given resource types is traversable. * * @todo This class shouldn't be aware of entity types and their definitions. * Whether a resource can have relationships to other resources is information * we ought to be able to discover on the ResourceType. However, we cannot * reliably determine this information with existing APIs. Entities may be * backed by various storages that are unable to perform queries across * references and certain storages may not be able to store references at all. */ protected function resourceTypesAreTraversable(array $resource_types) { foreach ($resource_types as $resource_type) { $entity_type_definition = $this->entityTypeManager->getDefinition($resource_type->getEntityTypeId()); if ($entity_type_definition->entityClassImplements(FieldableEntityInterface::class)) { return TRUE; } } return FALSE; } /** * Gets all bundle IDs for a given entity type. * * @param string $entity_type_id * The entity type for which to get bundles. * * @return string[] * The bundle IDs. */ protected function getAllBundlesForEntityType($entity_type_id) { return array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)); } /** * Gets all unique reference property names from the given field definitions. * * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions * A list of targeted field item definitions specified by the path. * * @return string[] * The reference property names, if any. */ protected static function getAllDataReferencePropertyNames(array $candidate_definitions) { $reference_property_names = array_reduce($candidate_definitions, function (array $reference_property_names, ComplexDataDefinitionInterface $definition) { $property_definitions = $definition->getPropertyDefinitions(); foreach ($property_definitions as $property_name => $property_definition) { if ($property_definition instanceof DataReferenceDefinitionInterface) { $target_definition = $property_definition->getTargetDefinition(); assert($target_definition instanceof EntityDataDefinitionInterface, 'Entity reference fields should only be able to reference entities.'); $reference_property_names[] = $property_name . ':' . $target_definition->getEntityTypeId(); } } return $reference_property_names; }, []); return array_unique($reference_property_names); } /** * Determines the reference property name for the remaining unresolved parts. * * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions * A list of targeted field item definitions specified by the path. * @param string[] $remaining_parts * The remaining path parts. * @param string[] $unresolved_path_parts * The unresolved path parts. * * @return string * The reference name. */ protected static function getDataReferencePropertyName(array $candidate_definitions, array $remaining_parts, array $unresolved_path_parts) { $unique_reference_names = static::getAllDataReferencePropertyNames($candidate_definitions); if (count($unique_reference_names) > 1) { $choices = array_map(function ($reference_name) use ($unresolved_path_parts, $remaining_parts) { $prior_parts = array_slice($unresolved_path_parts, 0, count($unresolved_path_parts) - count($remaining_parts)); return implode('.', array_merge($prior_parts, [$reference_name], $remaining_parts)); }, $unique_reference_names); // @todo Add test coverage for this in https://www.drupal.org/project/jsonapi/issues/2971281 $message = sprintf('Ambiguous path. Try one of the following: %s, in place of the given path: %s', implode(', ', $choices), implode('.', $unresolved_path_parts)); $cacheability = (new CacheableMetadata())->addCacheContexts(['url.query_args:filter', 'url.query_args:sort']); throw new CacheableBadRequestHttpException($cacheability, $message); } return $unique_reference_names[0]; } /** * Determines if a path part targets a specific field delta. * * @param string $part * The path part. * * @return bool * TRUE if the part is an integer, FALSE otherwise. */ protected static function isDelta($part) { return (bool) preg_match('/^[0-9]+$/', $part); } /** * Determines if a path part targets a field property, not a subsequent field. * * @param string $part * The path part. * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions * A list of targeted field item definitions which are specified by the * path. * * @return bool * TRUE if the part is a property of one of the candidate definitions, FALSE * otherwise. */ protected static function isCandidateDefinitionProperty($part, array $candidate_definitions) { $part = static::getPathPartPropertyName($part); foreach ($candidate_definitions as $definition) { if ($definition->getPropertyDefinition($part)) { return TRUE; } } return FALSE; } /** * Determines if a path part targets a reference property. * * @param string $part * The path part. * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface[] $candidate_definitions * A list of targeted field item definitions which are specified by the * path. * * @return bool * TRUE if the part is a property of one of the candidate definitions, FALSE * otherwise. */ protected static function isCandidateDefinitionReferenceProperty($part, array $candidate_definitions) { $part = static::getPathPartPropertyName($part); foreach ($candidate_definitions as $definition) { $property = $definition->getPropertyDefinition($part); if ($property && $property instanceof DataReferenceDefinitionInterface) { return TRUE; } } return FALSE; } /** * Gets the property name from an entity typed or untyped path part. * * A path part may contain an entity type specifier like `entity:node`. This * extracts the actual property name. If an entity type is not specified, then * the path part is simply returned. For example, both `foo` and `foo:bar` * will return `foo`. * * @param string $part * A path part. * * @return string * The property name from a path part. */ protected static function getPathPartPropertyName($part) { return strpos($part, ':') !== FALSE ? explode(':', $part)[0] : $part; } /** * Gets the field access result for the 'view' operation. * * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type * The JSON:API resource type on which the field exists. * @param string $internal_field_name * The field name for which access should be checked. * * @return \Drupal\Core\Access\AccessResultInterface * The 'view' access result. */ protected function getFieldAccess(ResourceType $resource_type, $internal_field_name) { $definitions = $this->fieldManager->getFieldDefinitions($resource_type->getEntityTypeId(), $resource_type->getBundle()); assert(isset($definitions[$internal_field_name]), 'The field name should have already been validated.'); $field_definition = $definitions[$internal_field_name]; $filter_access_results = $this->moduleHandler->invokeAll('jsonapi_entity_field_filter_access', [$field_definition, \Drupal::currentUser()]); $filter_access_result = array_reduce($filter_access_results, function (AccessResultInterface $combined_result, AccessResultInterface $result) { return $combined_result->orIf($result); }, AccessResult::neutral()); if (!$filter_access_result->isNeutral()) { return $filter_access_result; } $entity_access_control_handler = $this->entityTypeManager->getAccessControlHandler($resource_type->getEntityTypeId()); $field_access = $entity_access_control_handler->fieldAccess('view', $field_definition, NULL, NULL, TRUE); return $filter_access_result->orIf($field_access); } }