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