Mercurial > hg > cmmr2012-drupal-site
comparison core/modules/jsonapi/tests/src/Functional/ResourceResponseTestTrait.php @ 5:12f9dff5fda9 tip
Update to Drupal core 8.7.1
author | Chris Cannam |
---|---|
date | Thu, 09 May 2019 15:34:47 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
4:a9cd425dd02b | 5:12f9dff5fda9 |
---|---|
1 <?php | |
2 | |
3 namespace Drupal\Tests\jsonapi\Functional; | |
4 | |
5 use Drupal\Component\Serialization\Json; | |
6 use Drupal\Component\Utility\Crypt; | |
7 use Drupal\Core\Access\AccessResultInterface; | |
8 use Drupal\Core\Access\AccessResultReasonInterface; | |
9 use Drupal\Core\Cache\Cache; | |
10 use Drupal\Core\Cache\CacheableMetadata; | |
11 use Drupal\Core\Entity\EntityInterface; | |
12 use Drupal\Core\Entity\RevisionableInterface; | |
13 use Drupal\Core\Url; | |
14 use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer; | |
15 use Drupal\jsonapi\ResourceResponse; | |
16 use Psr\Http\Message\ResponseInterface; | |
17 | |
18 /** | |
19 * Utility methods for handling resource responses. | |
20 * | |
21 * @internal | |
22 */ | |
23 trait ResourceResponseTestTrait { | |
24 | |
25 /** | |
26 * Merges individual responses into a collection response. | |
27 * | |
28 * Here, a collection response refers to a response with multiple resource | |
29 * objects. Not necessarily to a response to a collection route. In both | |
30 * cases, the document should indistinguishable. | |
31 * | |
32 * @param \Drupal\jsonapi\ResourceResponse[] $responses | |
33 * An array or ResourceResponses to be merged. | |
34 * @param string|null $self_link | |
35 * The self link for the merged document if one should be set. | |
36 * @param bool $is_multiple | |
37 * Whether the responses are for a multiple cardinality field. This cannot | |
38 * be deduced from the number of responses, because a multiple cardinality | |
39 * field may have only one value. | |
40 * | |
41 * @return \Drupal\jsonapi\ResourceResponse | |
42 * The merged ResourceResponse. | |
43 */ | |
44 protected static function toCollectionResourceResponse(array $responses, $self_link, $is_multiple) { | |
45 assert(count($responses) > 0); | |
46 $merged_document = []; | |
47 $merged_cacheability = new CacheableMetadata(); | |
48 foreach ($responses as $response) { | |
49 $response_document = $response->getResponseData(); | |
50 // If any of the response documents had top-level errors, we should later | |
51 // expect the merged document to have all errors as omitted links under | |
52 // the 'meta.omitted' member. | |
53 if (!empty($response_document['errors'])) { | |
54 static::addOmittedObject($merged_document, static::errorsToOmittedObject($response_document['errors'])); | |
55 } | |
56 if (!empty($response_document['meta']['omitted'])) { | |
57 static::addOmittedObject($merged_document, $response_document['meta']['omitted']); | |
58 } | |
59 elseif (isset($response_document['data'])) { | |
60 $response_data = $response_document['data']; | |
61 if (!isset($merged_document['data'])) { | |
62 $merged_document['data'] = static::isResourceIdentifier($response_data) && $is_multiple | |
63 ? [$response_data] | |
64 : $response_data; | |
65 } | |
66 else { | |
67 $response_resources = static::isResourceIdentifier($response_data) | |
68 ? [$response_data] | |
69 : $response_data; | |
70 foreach ($response_resources as $response_resource) { | |
71 $merged_document['data'][] = $response_resource; | |
72 } | |
73 } | |
74 } | |
75 $merged_cacheability->addCacheableDependency($response->getCacheableMetadata()); | |
76 } | |
77 $merged_document['jsonapi'] = [ | |
78 'meta' => [ | |
79 'links' => [ | |
80 'self' => ['href' => 'http://jsonapi.org/format/1.0/'], | |
81 ], | |
82 ], | |
83 'version' => '1.0', | |
84 ]; | |
85 // Until we can reasonably know what caused an error, we shouldn't include | |
86 // 'self' links in error documents. For example, a 404 shouldn't have a | |
87 // 'self' link because HATEOAS links shouldn't point to resources which do | |
88 // not exist. | |
89 if (isset($merged_document['errors'])) { | |
90 unset($merged_document['links']); | |
91 } | |
92 else { | |
93 if (!isset($merged_document['data'])) { | |
94 $merged_document['data'] = $is_multiple ? [] : NULL; | |
95 } | |
96 $merged_document['links'] = [ | |
97 'self' => [ | |
98 'href' => $self_link, | |
99 ], | |
100 ]; | |
101 } | |
102 // All collections should be 200, without regard for the status of the | |
103 // individual resources in those collections, which means any '4xx-response' | |
104 // cache tags on the individual responses should also be omitted. | |
105 $merged_cacheability->setCacheTags(array_diff($merged_cacheability->getCacheTags(), ['4xx-response'])); | |
106 return (new ResourceResponse($merged_document, 200))->addCacheableDependency($merged_cacheability); | |
107 } | |
108 | |
109 /** | |
110 * Gets an array of expected ResourceResponses for the given include paths. | |
111 * | |
112 * @param array $include_paths | |
113 * The list of relationship include paths for which to get expected data. | |
114 * @param array $request_options | |
115 * Request options to apply. | |
116 * | |
117 * @return \Drupal\jsonapi\ResourceResponse | |
118 * The expected ResourceResponse. | |
119 * | |
120 * @see \GuzzleHttp\ClientInterface::request() | |
121 */ | |
122 protected function getExpectedIncludedResourceResponse(array $include_paths, array $request_options) { | |
123 $resource_type = $this->resourceType; | |
124 $resource_data = array_reduce($include_paths, function ($data, $path) use ($request_options, $resource_type) { | |
125 $field_names = explode('.', $path); | |
126 /* @var \Drupal\Core\Entity\EntityInterface $entity */ | |
127 $entity = $this->entity; | |
128 $collected_responses = []; | |
129 foreach ($field_names as $public_field_name) { | |
130 $resource_type = $this->container->get('jsonapi.resource_type.repository')->get($entity->getEntityTypeId(), $entity->bundle()); | |
131 $field_name = $resource_type->getInternalName($public_field_name); | |
132 $field_access = static::entityFieldAccess($entity, $field_name, 'view', $this->account); | |
133 if (!$field_access->isAllowed()) { | |
134 if (!$entity->access('view') && $entity->access('view label') && $field_access instanceof AccessResultReasonInterface && empty($field_access->getReason())) { | |
135 $field_access->setReason("The user only has authorization for the 'view label' operation."); | |
136 } | |
137 $via_link = Url::fromRoute( | |
138 sprintf('jsonapi.%s.%s.related', $entity->getEntityTypeId() . '--' . $entity->bundle(), $public_field_name), | |
139 ['entity' => $entity->uuid()] | |
140 ); | |
141 $collected_responses[] = static::getAccessDeniedResponse($entity, $field_access, $via_link, $field_name, 'The current user is not allowed to view this relationship.', $field_name); | |
142 break; | |
143 } | |
144 if ($target_entity = $entity->{$field_name}->entity) { | |
145 $target_access = static::entityAccess($target_entity, 'view', $this->account); | |
146 if (!$target_access->isAllowed()) { | |
147 $target_access = static::entityAccess($target_entity, 'view label', $this->account)->addCacheableDependency($target_access); | |
148 } | |
149 if (!$target_access->isAllowed()) { | |
150 $resource_identifier = static::toResourceIdentifier($target_entity); | |
151 if (!static::collectionHasResourceIdentifier($resource_identifier, $data['already_checked'])) { | |
152 $data['already_checked'][] = $resource_identifier; | |
153 $via_link = Url::fromRoute( | |
154 sprintf('jsonapi.%s.individual', $resource_identifier['type']), | |
155 ['entity' => $resource_identifier['id']] | |
156 ); | |
157 $collected_responses[] = static::getAccessDeniedResponse($entity, $target_access, $via_link, NULL, NULL, '/data'); | |
158 } | |
159 break; | |
160 } | |
161 } | |
162 $psr_responses = $this->getResponses([static::getRelatedLink(static::toResourceIdentifier($entity), $public_field_name)], $request_options); | |
163 $collected_responses[] = static::toCollectionResourceResponse(static::toResourceResponses($psr_responses), NULL, TRUE); | |
164 $entity = $entity->{$field_name}->entity; | |
165 } | |
166 if (!empty($collected_responses)) { | |
167 $data['responses'][$path] = static::toCollectionResourceResponse($collected_responses, NULL, TRUE); | |
168 } | |
169 return $data; | |
170 }, ['responses' => [], 'already_checked' => []]); | |
171 | |
172 $individual_document = $this->getExpectedDocument(); | |
173 | |
174 $expected_base_url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), ['entity' => $this->entity->uuid()])->setAbsolute(); | |
175 $include_url = clone $expected_base_url; | |
176 $query = ['include' => implode(',', $include_paths)]; | |
177 $include_url->setOption('query', $query); | |
178 $individual_document['links']['self']['href'] = $include_url->toString(); | |
179 | |
180 // The test entity reference field should always be present. | |
181 if (!isset($individual_document['data']['relationships']['field_jsonapi_test_entity_ref'])) { | |
182 if (static::$resourceTypeIsVersionable) { | |
183 assert($this->entity instanceof RevisionableInterface); | |
184 $version_identifier = 'id:' . $this->entity->getRevisionId(); | |
185 $version_query_string = '?resourceVersion=' . urlencode($version_identifier); | |
186 } | |
187 else { | |
188 $version_query_string = ''; | |
189 } | |
190 $individual_document['data']['relationships']['field_jsonapi_test_entity_ref'] = [ | |
191 'data' => [], | |
192 'links' => [ | |
193 'related' => [ | |
194 'href' => $expected_base_url->toString() . '/field_jsonapi_test_entity_ref' . $version_query_string, | |
195 ], | |
196 'self' => [ | |
197 'href' => $expected_base_url->toString() . '/relationships/field_jsonapi_test_entity_ref' . $version_query_string, | |
198 ], | |
199 ], | |
200 ]; | |
201 } | |
202 | |
203 $basic_cacheability = (new CacheableMetadata()) | |
204 ->addCacheTags($this->getExpectedCacheTags()) | |
205 ->addCacheContexts($this->getExpectedCacheContexts()); | |
206 return static::decorateExpectedResponseForIncludedFields(ResourceResponse::create($individual_document), $resource_data['responses']) | |
207 ->addCacheableDependency($basic_cacheability); | |
208 } | |
209 | |
210 /** | |
211 * Maps an array of PSR responses to JSON:API ResourceResponses. | |
212 * | |
213 * @param \Psr\Http\Message\ResponseInterface[] $responses | |
214 * The PSR responses to be mapped. | |
215 * | |
216 * @return \Drupal\jsonapi\ResourceResponse[] | |
217 * The ResourceResponses. | |
218 */ | |
219 protected static function toResourceResponses(array $responses) { | |
220 return array_map([self::class, 'toResourceResponse'], $responses); | |
221 } | |
222 | |
223 /** | |
224 * Maps a response object to a JSON:API ResourceResponse. | |
225 * | |
226 * This helper can be used to ease comparing, recording and merging | |
227 * cacheable responses and to have easier access to the JSON:API document as | |
228 * an array instead of a string. | |
229 * | |
230 * @param \Psr\Http\Message\ResponseInterface $response | |
231 * A PSR response to be mapped. | |
232 * | |
233 * @return \Drupal\jsonapi\ResourceResponse | |
234 * The ResourceResponse. | |
235 */ | |
236 protected static function toResourceResponse(ResponseInterface $response) { | |
237 $cacheability = new CacheableMetadata(); | |
238 if ($cache_tags = $response->getHeader('X-Drupal-Cache-Tags')) { | |
239 $cacheability->addCacheTags(explode(' ', $cache_tags[0])); | |
240 } | |
241 if (!empty($response->getHeaderLine('X-Drupal-Cache-Contexts'))) { | |
242 $cacheability->addCacheContexts(explode(' ', $response->getHeader('X-Drupal-Cache-Contexts')[0])); | |
243 } | |
244 if ($dynamic_cache = $response->getHeader('X-Drupal-Dynamic-Cache')) { | |
245 $cacheability->setCacheMaxAge(($dynamic_cache[0] === 'UNCACHEABLE' && $response->getStatusCode() < 400) ? 0 : Cache::PERMANENT); | |
246 } | |
247 $related_document = Json::decode($response->getBody()); | |
248 $resource_response = new ResourceResponse($related_document, $response->getStatusCode()); | |
249 return $resource_response->addCacheableDependency($cacheability); | |
250 } | |
251 | |
252 /** | |
253 * Maps an entity to a resource identifier. | |
254 * | |
255 * @param \Drupal\Core\Entity\EntityInterface $entity | |
256 * The entity to map to a resource identifier. | |
257 * | |
258 * @return array | |
259 * A resource identifier for the given entity. | |
260 */ | |
261 protected static function toResourceIdentifier(EntityInterface $entity) { | |
262 return [ | |
263 'type' => $entity->getEntityTypeId() . '--' . $entity->bundle(), | |
264 'id' => $entity->uuid(), | |
265 ]; | |
266 } | |
267 | |
268 /** | |
269 * Checks if a given array is a resource identifier. | |
270 * | |
271 * @param array $data | |
272 * An array to check. | |
273 * | |
274 * @return bool | |
275 * TRUE if the array has a type and ID, FALSE otherwise. | |
276 */ | |
277 protected static function isResourceIdentifier(array $data) { | |
278 return array_key_exists('type', $data) && array_key_exists('id', $data); | |
279 } | |
280 | |
281 /** | |
282 * Sorts a collection of resources or resource identifiers. | |
283 * | |
284 * This is useful for asserting collections or resources where order cannot | |
285 * be known in advance. | |
286 * | |
287 * @param array $resources | |
288 * The resource or resource identifier. | |
289 */ | |
290 protected static function sortResourceCollection(array &$resources) { | |
291 usort($resources, function ($a, $b) { | |
292 return strcmp("{$a['type']}:{$a['id']}", "{$b['type']}:{$b['id']}"); | |
293 }); | |
294 } | |
295 | |
296 /** | |
297 * Determines if a given resource exists in a list of resources. | |
298 * | |
299 * @param array $needle | |
300 * The resource or resource identifier. | |
301 * @param array $haystack | |
302 * The list of resources or resource identifiers to search. | |
303 * | |
304 * @return bool | |
305 * TRUE if the needle exists is present in the haystack, FALSE otherwise. | |
306 */ | |
307 protected static function collectionHasResourceIdentifier(array $needle, array $haystack) { | |
308 foreach ($haystack as $resource) { | |
309 if ($resource['type'] == $needle['type'] && $resource['id'] == $needle['id']) { | |
310 return TRUE; | |
311 } | |
312 } | |
313 return FALSE; | |
314 } | |
315 | |
316 /** | |
317 * Turns a list of relationship field names into an array of link paths. | |
318 * | |
319 * @param array $relationship_field_names | |
320 * The relationships field names for which to build link paths. | |
321 * @param string $type | |
322 * The type of link to get. Either 'relationship' or 'related'. | |
323 * | |
324 * @return array | |
325 * An array of link paths, keyed by relationship field name. | |
326 */ | |
327 protected static function getLinkPaths(array $relationship_field_names, $type) { | |
328 assert($type === 'relationship' || $type === 'related'); | |
329 return array_reduce($relationship_field_names, function ($link_paths, $relationship_field_name) use ($type) { | |
330 $tail = $type === 'relationship' ? 'self' : $type; | |
331 $link_paths[$relationship_field_name] = "data.relationships.$relationship_field_name.links.$tail.href"; | |
332 return $link_paths; | |
333 }, []); | |
334 } | |
335 | |
336 /** | |
337 * Extracts links from a document using a list of relationship field names. | |
338 * | |
339 * @param array $link_paths | |
340 * A list of paths to link values keyed by a name. | |
341 * @param array $document | |
342 * A JSON:API document. | |
343 * | |
344 * @return array | |
345 * The extracted links, keyed by the original associated key name. | |
346 */ | |
347 protected static function extractLinks(array $link_paths, array $document) { | |
348 return array_map(function ($link_path) use ($document) { | |
349 $link = array_reduce( | |
350 explode('.', $link_path), | |
351 'array_column', | |
352 [$document] | |
353 ); | |
354 return ($link) ? reset($link) : NULL; | |
355 }, $link_paths); | |
356 } | |
357 | |
358 /** | |
359 * Creates individual resource links for a list of resource identifiers. | |
360 * | |
361 * @param array $resource_identifiers | |
362 * A list of resource identifiers for which to create links. | |
363 * | |
364 * @return string[] | |
365 * The resource links. | |
366 */ | |
367 protected static function getResourceLinks(array $resource_identifiers) { | |
368 return array_map([static::class, 'getResourceLink'], $resource_identifiers); | |
369 } | |
370 | |
371 /** | |
372 * Creates an individual resource link for a given resource identifier. | |
373 * | |
374 * @param array $resource_identifier | |
375 * A resource identifier for which to create a link. | |
376 * | |
377 * @return string | |
378 * The resource link. | |
379 */ | |
380 protected static function getResourceLink(array $resource_identifier) { | |
381 assert(static::isResourceIdentifier($resource_identifier)); | |
382 $resource_type = $resource_identifier['type']; | |
383 $resource_id = $resource_identifier['id']; | |
384 $url = Url::fromRoute(sprintf('jsonapi.%s.individual', $resource_type), ['entity' => $resource_id]); | |
385 return $url->setAbsolute()->toString(); | |
386 } | |
387 | |
388 /** | |
389 * Creates a relationship link for a given resource identifier and field. | |
390 * | |
391 * @param array $resource_identifier | |
392 * A resource identifier for which to create a link. | |
393 * @param string $relationship_field_name | |
394 * The relationship field for which to create a link. | |
395 * | |
396 * @return string | |
397 * The relationship link. | |
398 */ | |
399 protected static function getRelationshipLink(array $resource_identifier, $relationship_field_name) { | |
400 return static::getResourceLink($resource_identifier) . "/relationships/$relationship_field_name"; | |
401 } | |
402 | |
403 /** | |
404 * Creates a related resource link for a given resource identifier and field. | |
405 * | |
406 * @param array $resource_identifier | |
407 * A resource identifier for which to create a link. | |
408 * @param string $relationship_field_name | |
409 * The relationship field for which to create a link. | |
410 * | |
411 * @return string | |
412 * The related resource link. | |
413 */ | |
414 protected static function getRelatedLink(array $resource_identifier, $relationship_field_name) { | |
415 return static::getResourceLink($resource_identifier) . "/$relationship_field_name"; | |
416 } | |
417 | |
418 /** | |
419 * Gets an array of related responses for the given field names. | |
420 * | |
421 * @param array $relationship_field_names | |
422 * The list of relationship field names for which to get responses. | |
423 * @param array $request_options | |
424 * Request options to apply. | |
425 * @param \Drupal\Core\Entity\EntityInterface|null $entity | |
426 * (optional) The entity for which to get expected related responses. | |
427 * | |
428 * @return array | |
429 * The related responses, keyed by relationship field names. | |
430 * | |
431 * @see \GuzzleHttp\ClientInterface::request() | |
432 */ | |
433 protected function getRelatedResponses(array $relationship_field_names, array $request_options, EntityInterface $entity = NULL) { | |
434 $entity = $entity ?: $this->entity; | |
435 $links = array_map(function ($relationship_field_name) use ($entity) { | |
436 return static::getRelatedLink(static::toResourceIdentifier($entity), $relationship_field_name); | |
437 }, array_combine($relationship_field_names, $relationship_field_names)); | |
438 return $this->getResponses($links, $request_options); | |
439 } | |
440 | |
441 /** | |
442 * Gets an array of relationship responses for the given field names. | |
443 * | |
444 * @param array $relationship_field_names | |
445 * The list of relationship field names for which to get responses. | |
446 * @param array $request_options | |
447 * Request options to apply. | |
448 * | |
449 * @return array | |
450 * The relationship responses, keyed by relationship field names. | |
451 * | |
452 * @see \GuzzleHttp\ClientInterface::request() | |
453 */ | |
454 protected function getRelationshipResponses(array $relationship_field_names, array $request_options) { | |
455 $links = array_map(function ($relationship_field_name) { | |
456 return static::getRelationshipLink(static::toResourceIdentifier($this->entity), $relationship_field_name); | |
457 }, array_combine($relationship_field_names, $relationship_field_names)); | |
458 return $this->getResponses($links, $request_options); | |
459 } | |
460 | |
461 /** | |
462 * Gets responses from an array of links. | |
463 * | |
464 * @param array $links | |
465 * A keyed array of links. | |
466 * @param array $request_options | |
467 * Request options to apply. | |
468 * | |
469 * @return array | |
470 * The fetched array of responses, keys are preserved. | |
471 * | |
472 * @see \GuzzleHttp\ClientInterface::request() | |
473 */ | |
474 protected function getResponses(array $links, array $request_options) { | |
475 return array_reduce(array_keys($links), function ($related_responses, $key) use ($links, $request_options) { | |
476 $related_responses[$key] = $this->request('GET', Url::fromUri($links[$key]), $request_options); | |
477 return $related_responses; | |
478 }, []); | |
479 } | |
480 | |
481 /** | |
482 * Gets a generic forbidden response. | |
483 * | |
484 * @param \Drupal\Core\Entity\EntityInterface $entity | |
485 * The entity for which to generate the forbidden response. | |
486 * @param \Drupal\Core\Access\AccessResultInterface $access | |
487 * The denied AccessResult. This can carry a reason and cacheability data. | |
488 * @param \Drupal\Core\Url $via_link | |
489 * The source URL for the errors of the response. | |
490 * @param string|null $relationship_field_name | |
491 * (optional) The field name to which the forbidden result applies. Useful | |
492 * for testing related/relationship routes and includes. | |
493 * @param string|null $detail | |
494 * (optional) Details for the JSON:API error object. | |
495 * @param string|bool|null $pointer | |
496 * (optional) Document pointer for the JSON:API error object. FALSE to omit | |
497 * the pointer. | |
498 * | |
499 * @return \Drupal\jsonapi\ResourceResponse | |
500 * The forbidden ResourceResponse. | |
501 */ | |
502 protected static function getAccessDeniedResponse(EntityInterface $entity, AccessResultInterface $access, Url $via_link, $relationship_field_name = NULL, $detail = NULL, $pointer = NULL) { | |
503 $detail = ($detail) ? $detail : 'The current user is not allowed to GET the selected resource.'; | |
504 if ($access instanceof AccessResultReasonInterface && ($reason = $access->getReason())) { | |
505 $detail .= ' ' . $reason; | |
506 } | |
507 $error = [ | |
508 'status' => '403', | |
509 'title' => 'Forbidden', | |
510 'detail' => $detail, | |
511 'links' => [ | |
512 'info' => ['href' => HttpExceptionNormalizer::getInfoUrl(403)], | |
513 ], | |
514 ]; | |
515 if ($pointer || $pointer !== FALSE && $relationship_field_name) { | |
516 $error['source']['pointer'] = ($pointer) ? $pointer : $relationship_field_name; | |
517 } | |
518 if ($via_link) { | |
519 $error['links']['via']['href'] = $via_link->setAbsolute()->toString(); | |
520 } | |
521 | |
522 return (new ResourceResponse([ | |
523 'jsonapi' => static::$jsonApiMember, | |
524 'errors' => [$error], | |
525 ], 403)) | |
526 ->addCacheableDependency((new CacheableMetadata())->addCacheTags(['4xx-response', 'http_response'])->addCacheContexts(['url.site'])) | |
527 ->addCacheableDependency($access); | |
528 } | |
529 | |
530 /** | |
531 * Gets a generic empty collection response. | |
532 * | |
533 * @param int $cardinality | |
534 * The cardinality of the resource collection. 1 for a to-one related | |
535 * resource collection; -1 for an unlimited cardinality. | |
536 * @param string $self_link | |
537 * The self link for collection ResourceResponse. | |
538 * | |
539 * @return \Drupal\jsonapi\ResourceResponse | |
540 * The empty collection ResourceResponse. | |
541 */ | |
542 protected function getEmptyCollectionResponse($cardinality, $self_link) { | |
543 // If the entity type is revisionable, add a resource version cache context. | |
544 $cache_contexts = Cache::mergeContexts([ | |
545 // Cache contexts for JSON:API URL query parameters. | |
546 'url.query_args:fields', | |
547 'url.query_args:include', | |
548 // Drupal defaults. | |
549 'url.site', | |
550 ], $this->entity->getEntityType()->isRevisionable() ? ['url.query_args:resourceVersion'] : []); | |
551 $cacheability = (new CacheableMetadata())->addCacheContexts($cache_contexts)->addCacheTags(['http_response']); | |
552 return (new ResourceResponse([ | |
553 // Empty to-one relationships should be NULL and empty to-many | |
554 // relationships should be an empty array. | |
555 'data' => $cardinality === 1 ? NULL : [], | |
556 'jsonapi' => static::$jsonApiMember, | |
557 'links' => ['self' => ['href' => $self_link]], | |
558 ]))->addCacheableDependency($cacheability); | |
559 } | |
560 | |
561 /** | |
562 * Add the omitted object to the document or merges it if one already exists. | |
563 * | |
564 * @param array $document | |
565 * The JSON:API response document. | |
566 * @param array $omitted | |
567 * The omitted object. | |
568 */ | |
569 protected static function addOmittedObject(array &$document, array $omitted) { | |
570 if (isset($document['meta']['omitted'])) { | |
571 $document['meta']['omitted'] = static::mergeOmittedObjects($document['meta']['omitted'], $omitted); | |
572 } | |
573 else { | |
574 $document['meta']['omitted'] = $omitted; | |
575 } | |
576 } | |
577 | |
578 /** | |
579 * Maps error objects into an omitted object. | |
580 * | |
581 * @param array $errors | |
582 * An array of error objects. | |
583 * | |
584 * @return array | |
585 * A new omitted object. | |
586 */ | |
587 protected static function errorsToOmittedObject(array $errors) { | |
588 $omitted = [ | |
589 'detail' => 'Some resources have been omitted because of insufficient authorization.', | |
590 'links' => [ | |
591 'help' => [ | |
592 'href' => 'https://www.drupal.org/docs/8/modules/json-api/filtering#filters-access-control', | |
593 ], | |
594 ], | |
595 ]; | |
596 foreach ($errors as $error) { | |
597 $omitted['links']['item:' . substr(Crypt::hashBase64($error['links']['via']['href']), 0, 7)] = [ | |
598 'href' => $error['links']['via']['href'], | |
599 'meta' => [ | |
600 'detail' => $error['detail'], | |
601 'rel' => 'item', | |
602 ], | |
603 ]; | |
604 } | |
605 return $omitted; | |
606 } | |
607 | |
608 /** | |
609 * Merges the links of two omitted objects and returns a new omitted object. | |
610 * | |
611 * @param array $a | |
612 * The first omitted object. | |
613 * @param array $b | |
614 * The second omitted object. | |
615 * | |
616 * @return mixed | |
617 * A new, merged omitted object. | |
618 */ | |
619 protected static function mergeOmittedObjects(array $a, array $b) { | |
620 $merged['detail'] = 'Some resources have been omitted because of insufficient authorization.'; | |
621 $merged['links']['help']['href'] = 'https://www.drupal.org/docs/8/modules/json-api/filtering#filters-access-control'; | |
622 $a_links = array_diff_key($a['links'], array_flip(['help'])); | |
623 $b_links = array_diff_key($b['links'], array_flip(['help'])); | |
624 foreach (array_merge(array_values($a_links), array_values($b_links)) as $link) { | |
625 $merged['links'][$link['href'] . $link['meta']['detail']] = $link; | |
626 } | |
627 static::resetOmittedLinkKeys($merged); | |
628 return $merged; | |
629 } | |
630 | |
631 /** | |
632 * Sorts an omitted link object array by href. | |
633 * | |
634 * @param array $omitted | |
635 * An array of JSON:API omitted link objects. | |
636 */ | |
637 protected static function sortOmittedLinks(array &$omitted) { | |
638 $help = $omitted['links']['help']; | |
639 $links = array_diff_key($omitted['links'], array_flip(['help'])); | |
640 uasort($links, function ($a, $b) { | |
641 return strcmp($a['href'], $b['href']); | |
642 }); | |
643 $omitted['links'] = ['help' => $help] + $links; | |
644 } | |
645 | |
646 /** | |
647 * Resets omitted link keys. | |
648 * | |
649 * Omitted link keys are a link relation type + a random string. This string | |
650 * is meaningless and only serves to differentiate link objects. Given that | |
651 * these are random, we can't assert their value. | |
652 * | |
653 * @param array $omitted | |
654 * An array of JSON:API omitted link objects. | |
655 */ | |
656 protected static function resetOmittedLinkKeys(array &$omitted) { | |
657 $help = $omitted['links']['help']; | |
658 $reindexed = []; | |
659 $links = array_diff_key($omitted['links'], array_flip(['help'])); | |
660 foreach (array_values($links) as $index => $link) { | |
661 $reindexed['item:' . $index] = $link; | |
662 } | |
663 $omitted['links'] = ['help' => $help] + $reindexed; | |
664 } | |
665 | |
666 } |