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