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