PageRenderTime 40ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/web/core/modules/jsonapi/src/IncludeResolver.php

https://gitlab.com/mohamed_hussein/prodt
PHP | 258 lines | 120 code | 14 blank | 124 comment | 9 complexity | 8e9508740fe19ce378a428f29ba6a866 MD5 | raw file
  1. <?php
  2. namespace Drupal\jsonapi;
  3. use Drupal\Core\Access\AccessResult;
  4. use Drupal\Core\Entity\EntityInterface;
  5. use Drupal\Core\Entity\EntityTypeManagerInterface;
  6. use Drupal\Core\Field\FieldItemListInterface;
  7. use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
  8. use Drupal\jsonapi\Access\EntityAccessChecker;
  9. use Drupal\jsonapi\Context\FieldResolver;
  10. use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
  11. use Drupal\jsonapi\JsonApiResource\Data;
  12. use Drupal\jsonapi\JsonApiResource\IncludedData;
  13. use Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject;
  14. use Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface;
  15. use Drupal\jsonapi\JsonApiResource\ResourceObject;
  16. use Drupal\jsonapi\JsonApiResource\ResourceObjectData;
  17. use Drupal\jsonapi\ResourceType\ResourceType;
  18. /**
  19. * Resolves included resources for an entity or collection of entities.
  20. *
  21. * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
  22. * class may change at any time and this will break any dependencies on it.
  23. *
  24. * @see https://www.drupal.org/project/drupal/issues/3032787
  25. * @see jsonapi.api.php
  26. */
  27. class IncludeResolver {
  28. /**
  29. * The entity type manager.
  30. *
  31. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  32. */
  33. protected $entityTypeManager;
  34. /**
  35. * The JSON:API entity access checker.
  36. *
  37. * @var \Drupal\jsonapi\Access\EntityAccessChecker
  38. */
  39. protected $entityAccessChecker;
  40. /**
  41. * IncludeResolver constructor.
  42. */
  43. public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityAccessChecker $entity_access_checker) {
  44. $this->entityTypeManager = $entity_type_manager;
  45. $this->entityAccessChecker = $entity_access_checker;
  46. }
  47. /**
  48. * Resolves included resources.
  49. *
  50. * @param \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface|\Drupal\jsonapi\JsonApiResource\ResourceObjectData $data
  51. * The resource(s) for which to resolve includes.
  52. * @param string $include_parameter
  53. * The include query parameter to resolve.
  54. *
  55. * @return \Drupal\jsonapi\JsonApiResource\IncludedData
  56. * An IncludedData object of resolved resources to be included.
  57. *
  58. * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
  59. * Thrown if an included entity type doesn't exist.
  60. * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
  61. * Thrown if a storage handler couldn't be loaded.
  62. */
  63. public function resolve($data, $include_parameter) {
  64. assert($data instanceof ResourceObject || $data instanceof ResourceObjectData);
  65. $data = $data instanceof ResourceObjectData ? $data : new ResourceObjectData([$data], 1);
  66. $include_tree = static::toIncludeTree($data, $include_parameter);
  67. return IncludedData::deduplicate($this->resolveIncludeTree($include_tree, $data));
  68. }
  69. /**
  70. * Receives a tree of include field names and resolves resources for it.
  71. *
  72. * This method takes a tree of relationship field names and JSON:API Data
  73. * object. For the top-level of the tree and for each entity in the
  74. * collection, it gets the target entity type and IDs for each relationship
  75. * field. The method then loads all of those targets and calls itself
  76. * recursively with the next level of the tree and those loaded resources.
  77. *
  78. * @param array $include_tree
  79. * The include paths, represented as a tree.
  80. * @param \Drupal\jsonapi\JsonApiResource\Data $data
  81. * The entity collection from which includes should be resolved.
  82. * @param \Drupal\jsonapi\JsonApiResource\Data|null $includes
  83. * (Internal use only) Any prior resolved includes.
  84. *
  85. * @return \Drupal\jsonapi\JsonApiResource\Data
  86. * A JSON:API Data of included items.
  87. *
  88. * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
  89. * Thrown if an included entity type doesn't exist.
  90. * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
  91. * Thrown if a storage handler couldn't be loaded.
  92. */
  93. protected function resolveIncludeTree(array $include_tree, Data $data, Data $includes = NULL) {
  94. $includes = is_null($includes) ? new IncludedData([]) : $includes;
  95. foreach ($include_tree as $field_name => $children) {
  96. $references = [];
  97. foreach ($data as $resource_object) {
  98. // Some objects in the collection may be LabelOnlyResourceObjects or
  99. // EntityAccessDeniedHttpException objects.
  100. assert($resource_object instanceof ResourceIdentifierInterface);
  101. $public_field_name = $resource_object->getResourceType()->getPublicName($field_name);
  102. if ($resource_object instanceof LabelOnlyResourceObject) {
  103. $message = "The current user is not allowed to view this relationship.";
  104. $exception = new EntityAccessDeniedHttpException($resource_object->getEntity(), AccessResult::forbidden("The user only has authorization for the 'view label' operation."), '', $message, $public_field_name);
  105. $includes = IncludedData::merge($includes, new IncludedData([$exception]));
  106. continue;
  107. }
  108. elseif (!$resource_object instanceof ResourceObject) {
  109. continue;
  110. }
  111. // Not all entities in $entity_collection will be of the same bundle and
  112. // may not have all of the same fields. Therefore, calling
  113. // $resource_object->get($a_missing_field_name) will result in an
  114. // exception.
  115. if (!$resource_object->hasField($public_field_name)) {
  116. continue;
  117. }
  118. $field_list = $resource_object->getField($public_field_name);
  119. // Config entities don't have real fields and can't have relationships.
  120. if (!$field_list instanceof FieldItemListInterface) {
  121. continue;
  122. }
  123. $field_access = $field_list->access('view', NULL, TRUE);
  124. if (!$field_access->isAllowed()) {
  125. $message = 'The current user is not allowed to view this relationship.';
  126. $exception = new EntityAccessDeniedHttpException($field_list->getEntity(), $field_access, '', $message, $public_field_name);
  127. $includes = IncludedData::merge($includes, new IncludedData([$exception]));
  128. continue;
  129. }
  130. $target_type = $field_list->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
  131. assert(!empty($target_type));
  132. foreach ($field_list as $field_item) {
  133. assert($field_item instanceof EntityReferenceItem);
  134. $references[$target_type][] = $field_item->get($field_item::mainPropertyName())->getValue();
  135. }
  136. }
  137. foreach ($references as $target_type => $ids) {
  138. $entity_storage = $this->entityTypeManager->getStorage($target_type);
  139. $targeted_entities = $entity_storage->loadMultiple(array_unique($ids));
  140. $access_checked_entities = array_map(function (EntityInterface $entity) {
  141. return $this->entityAccessChecker->getAccessCheckedResourceObject($entity);
  142. }, $targeted_entities);
  143. $targeted_collection = new IncludedData(array_filter($access_checked_entities, function (ResourceIdentifierInterface $resource_object) {
  144. return !$resource_object->getResourceType()->isInternal();
  145. }));
  146. $includes = static::resolveIncludeTree($children, $targeted_collection, IncludedData::merge($includes, $targeted_collection));
  147. }
  148. }
  149. return $includes;
  150. }
  151. /**
  152. * Returns a tree of field names to include from an include parameter.
  153. *
  154. * @param \Drupal\jsonapi\JsonApiResource\ResourceObjectData $data
  155. * The base resources for which includes should be resolved.
  156. * @param string $include_parameter
  157. * The raw include parameter value.
  158. *
  159. * @return array
  160. * A multi-dimensional array representing a tree of field names to be
  161. * included. Array keys are the field names. Leaves are empty arrays.
  162. */
  163. protected static function toIncludeTree(ResourceObjectData $data, $include_parameter) {
  164. // $include_parameter: 'one.two.three, one.two.four'.
  165. $include_paths = array_map('trim', explode(',', $include_parameter));
  166. // $exploded_paths: [['one', 'two', 'three'], ['one', 'two', 'four']].
  167. $exploded_paths = array_map(function ($include_path) {
  168. return array_map('trim', explode('.', $include_path));
  169. }, $include_paths);
  170. $resolved_paths_per_resource_type = [];
  171. /** @var \Drupal\jsonapi\JsonApiResource\ResourceIdentifierInterface $resource_object */
  172. foreach ($data as $resource_object) {
  173. $resource_type = $resource_object->getResourceType();
  174. $resource_type_name = $resource_type->getTypeName();
  175. if (isset($resolved_paths_per_resource_type[$resource_type_name])) {
  176. continue;
  177. }
  178. $resolved_paths_per_resource_type[$resource_type_name] = static::resolveInternalIncludePaths($resource_type, $exploded_paths);
  179. }
  180. $resolved_paths = array_reduce($resolved_paths_per_resource_type, 'array_merge', []);
  181. return static::buildTree($resolved_paths);
  182. }
  183. /**
  184. * Resolves an array of public field paths.
  185. *
  186. * @param \Drupal\jsonapi\ResourceType\ResourceType $base_resource_type
  187. * The base resource type from which to resolve an internal include path.
  188. * @param array $paths
  189. * An array of exploded include paths.
  190. *
  191. * @return array
  192. * An array of all possible internal include paths derived from the given
  193. * public include paths.
  194. *
  195. * @see self::buildTree
  196. */
  197. protected static function resolveInternalIncludePaths(ResourceType $base_resource_type, array $paths) {
  198. $internal_paths = array_map(function ($exploded_path) use ($base_resource_type) {
  199. if (empty($exploded_path)) {
  200. return [];
  201. }
  202. return FieldResolver::resolveInternalIncludePath($base_resource_type, $exploded_path);
  203. }, $paths);
  204. $flattened_paths = array_reduce($internal_paths, 'array_merge', []);
  205. return $flattened_paths;
  206. }
  207. /**
  208. * Takes an array of exploded paths and builds a tree of field names.
  209. *
  210. * Input example: [
  211. * ['one', 'two', 'three'],
  212. * ['one', 'two', 'four'],
  213. * ['one', 'two', 'internal'],
  214. * ]
  215. *
  216. * Output example: [
  217. * 'one' => [
  218. * 'two' [
  219. * 'three' => [],
  220. * 'four' => [],
  221. * 'internal' => [],
  222. * ],
  223. * ],
  224. * ]
  225. *
  226. * @param array $paths
  227. * An array of exploded include paths.
  228. *
  229. * @return array
  230. * A multi-dimensional array representing a tree of field names to be
  231. * included. Array keys are the field names. Leaves are empty arrays.
  232. */
  233. protected static function buildTree(array $paths) {
  234. $merged = [];
  235. foreach ($paths as $parts) {
  236. if (!$field_name = array_shift($parts)) {
  237. continue;
  238. }
  239. $previous = $merged[$field_name] ?? [];
  240. $merged[$field_name] = array_merge($previous, [$parts]);
  241. }
  242. return !empty($merged) ? array_map([static::class, __FUNCTION__], $merged) : $merged;
  243. }
  244. }