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