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