View source
<?php
namespace Drupal\Tests\jsonapi\Functional;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Url;
use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
use Drupal\jsonapi\ResourceResponse;
use Psr\Http\Message\ResponseInterface;
trait ResourceResponseTestTrait {
protected static function toCollectionResourceResponse(array $responses, $self_link, $is_multiple) {
assert(count($responses) > 0);
$merged_document = [];
$merged_cacheability = new CacheableMetadata();
foreach ($responses as $response) {
$response_document = $response
->getResponseData();
if (!empty($response_document['errors'])) {
static::addOmittedObject($merged_document, static::errorsToOmittedObject($response_document['errors']));
}
if (!empty($response_document['meta']['omitted'])) {
static::addOmittedObject($merged_document, $response_document['meta']['omitted']);
}
elseif (isset($response_document['data'])) {
$response_data = $response_document['data'];
if (!isset($merged_document['data'])) {
$merged_document['data'] = static::isResourceIdentifier($response_data) && $is_multiple ? [
$response_data,
] : $response_data;
}
else {
$response_resources = static::isResourceIdentifier($response_data) ? [
$response_data,
] : $response_data;
foreach ($response_resources as $response_resource) {
$merged_document['data'][] = $response_resource;
}
}
}
$merged_cacheability
->addCacheableDependency($response
->getCacheableMetadata());
}
$merged_document['jsonapi'] = [
'meta' => [
'links' => [
'self' => [
'href' => 'http://jsonapi.org/format/1.0/',
],
],
],
'version' => '1.0',
];
if (isset($merged_document['errors'])) {
unset($merged_document['links']);
}
else {
if (!isset($merged_document['data'])) {
$merged_document['data'] = $is_multiple ? [] : NULL;
}
$merged_document['links'] = [
'self' => [
'href' => $self_link,
],
];
}
$merged_cacheability
->setCacheTags(array_diff($merged_cacheability
->getCacheTags(), [
'4xx-response',
]));
return (new ResourceResponse($merged_document, 200))
->addCacheableDependency($merged_cacheability);
}
protected function getExpectedIncludedResourceResponse(array $include_paths, array $request_options) {
$resource_type = $this->resourceType;
$resource_data = array_reduce($include_paths, function ($data, $path) use ($request_options, $resource_type) {
$field_names = explode('.', $path);
$entity = $this->entity;
$collected_responses = [];
foreach ($field_names as $public_field_name) {
$resource_type = $this->container
->get('jsonapi.resource_type.repository')
->get($entity
->getEntityTypeId(), $entity
->bundle());
$field_name = $resource_type
->getInternalName($public_field_name);
$field_access = static::entityFieldAccess($entity, $field_name, 'view', $this->account);
if (!$field_access
->isAllowed()) {
if (!$entity
->access('view') && $entity
->access('view label') && $field_access instanceof AccessResultReasonInterface && empty($field_access
->getReason())) {
$field_access
->setReason("The user only has authorization for the 'view label' operation.");
}
$via_link = Url::fromRoute(sprintf('jsonapi.%s.%s.related', $entity
->getEntityTypeId() . '--' . $entity
->bundle(), $public_field_name), [
'entity' => $entity
->uuid(),
]);
$collected_responses[] = static::getAccessDeniedResponse($entity, $field_access, $via_link, $field_name, 'The current user is not allowed to view this relationship.', $field_name);
break;
}
if ($target_entity = $entity->{$field_name}->entity) {
$target_access = static::entityAccess($target_entity, 'view', $this->account);
if (!$target_access
->isAllowed()) {
$target_access = static::entityAccess($target_entity, 'view label', $this->account)
->addCacheableDependency($target_access);
}
if (!$target_access
->isAllowed()) {
$resource_identifier = static::toResourceIdentifier($target_entity);
if (!static::collectionHasResourceIdentifier($resource_identifier, $data['already_checked'])) {
$data['already_checked'][] = $resource_identifier;
$via_link = Url::fromRoute(sprintf('jsonapi.%s.individual', $resource_identifier['type']), [
'entity' => $resource_identifier['id'],
]);
$collected_responses[] = static::getAccessDeniedResponse($entity, $target_access, $via_link, NULL, NULL, '/data');
}
break;
}
}
$psr_responses = $this
->getResponses([
static::getRelatedLink(static::toResourceIdentifier($entity), $public_field_name),
], $request_options);
$collected_responses[] = static::toCollectionResourceResponse(static::toResourceResponses($psr_responses), NULL, TRUE);
$entity = $entity->{$field_name}->entity;
}
if (!empty($collected_responses)) {
$data['responses'][$path] = static::toCollectionResourceResponse($collected_responses, NULL, TRUE);
}
return $data;
}, [
'responses' => [],
'already_checked' => [],
]);
$individual_document = $this
->getExpectedDocument();
$expected_base_url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
'entity' => $this->entity
->uuid(),
])
->setAbsolute();
$include_url = clone $expected_base_url;
$query = [
'include' => implode(',', $include_paths),
];
$include_url
->setOption('query', $query);
$individual_document['links']['self']['href'] = $include_url
->toString();
if (!isset($individual_document['data']['relationships']['field_jsonapi_test_entity_ref'])) {
if (static::$resourceTypeIsVersionable) {
assert($this->entity instanceof RevisionableInterface);
$version_identifier = 'id:' . $this->entity
->getRevisionId();
$version_query_string = '?resourceVersion=' . urlencode($version_identifier);
}
else {
$version_query_string = '';
}
$individual_document['data']['relationships']['field_jsonapi_test_entity_ref'] = [
'data' => [],
'links' => [
'related' => [
'href' => $expected_base_url
->toString() . '/field_jsonapi_test_entity_ref' . $version_query_string,
],
'self' => [
'href' => $expected_base_url
->toString() . '/relationships/field_jsonapi_test_entity_ref' . $version_query_string,
],
],
];
}
$basic_cacheability = (new CacheableMetadata())
->addCacheTags($this
->getExpectedCacheTags())
->addCacheContexts($this
->getExpectedCacheContexts());
return static::decorateExpectedResponseForIncludedFields(ResourceResponse::create($individual_document), $resource_data['responses'])
->addCacheableDependency($basic_cacheability);
}
protected static function toResourceResponses(array $responses) {
return array_map([
self::class,
'toResourceResponse',
], $responses);
}
protected static function toResourceResponse(ResponseInterface $response) {
$cacheability = new CacheableMetadata();
if ($cache_tags = $response
->getHeader('X-Drupal-Cache-Tags')) {
$cacheability
->addCacheTags(explode(' ', $cache_tags[0]));
}
if (!empty($response
->getHeaderLine('X-Drupal-Cache-Contexts'))) {
$cacheability
->addCacheContexts(explode(' ', $response
->getHeader('X-Drupal-Cache-Contexts')[0]));
}
if ($dynamic_cache = $response
->getHeader('X-Drupal-Dynamic-Cache')) {
$cacheability
->setCacheMaxAge($dynamic_cache[0] === 'UNCACHEABLE' && $response
->getStatusCode() < 400 ? 0 : Cache::PERMANENT);
}
$related_document = Json::decode($response
->getBody());
$resource_response = new ResourceResponse($related_document, $response
->getStatusCode());
return $resource_response
->addCacheableDependency($cacheability);
}
protected static function toResourceIdentifier(EntityInterface $entity) {
return [
'type' => $entity
->getEntityTypeId() . '--' . $entity
->bundle(),
'id' => $entity
->uuid(),
];
}
protected static function isResourceIdentifier(array $data) {
return array_key_exists('type', $data) && array_key_exists('id', $data);
}
protected static function sortResourceCollection(array &$resources) {
usort($resources, function ($a, $b) {
return strcmp("{$a['type']}:{$a['id']}", "{$b['type']}:{$b['id']}");
});
}
protected static function collectionHasResourceIdentifier(array $needle, array $haystack) {
foreach ($haystack as $resource) {
if ($resource['type'] == $needle['type'] && $resource['id'] == $needle['id']) {
return TRUE;
}
}
return FALSE;
}
protected static function getLinkPaths(array $relationship_field_names, $type) {
assert($type === 'relationship' || $type === 'related');
return array_reduce($relationship_field_names, function ($link_paths, $relationship_field_name) use ($type) {
$tail = $type === 'relationship' ? 'self' : $type;
$link_paths[$relationship_field_name] = "data.relationships.{$relationship_field_name}.links.{$tail}.href";
return $link_paths;
}, []);
}
protected static function extractLinks(array $link_paths, array $document) {
return array_map(function ($link_path) use ($document) {
$link = array_reduce(explode('.', $link_path), 'array_column', [
$document,
]);
return $link ? reset($link) : NULL;
}, $link_paths);
}
protected static function getResourceLinks(array $resource_identifiers) {
return array_map([
static::class,
'getResourceLink',
], $resource_identifiers);
}
protected static function getResourceLink(array $resource_identifier) {
assert(static::isResourceIdentifier($resource_identifier));
$resource_type = $resource_identifier['type'];
$resource_id = $resource_identifier['id'];
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', $resource_type), [
'entity' => $resource_id,
]);
return $url
->setAbsolute()
->toString();
}
protected static function getRelationshipLink(array $resource_identifier, $relationship_field_name) {
return static::getResourceLink($resource_identifier) . "/relationships/{$relationship_field_name}";
}
protected static function getRelatedLink(array $resource_identifier, $relationship_field_name) {
return static::getResourceLink($resource_identifier) . "/{$relationship_field_name}";
}
protected function getRelatedResponses(array $relationship_field_names, array $request_options, EntityInterface $entity = NULL) {
$entity = $entity ?: $this->entity;
$links = array_map(function ($relationship_field_name) use ($entity) {
return static::getRelatedLink(static::toResourceIdentifier($entity), $relationship_field_name);
}, array_combine($relationship_field_names, $relationship_field_names));
return $this
->getResponses($links, $request_options);
}
protected function getRelationshipResponses(array $relationship_field_names, array $request_options) {
$links = array_map(function ($relationship_field_name) {
return static::getRelationshipLink(static::toResourceIdentifier($this->entity), $relationship_field_name);
}, array_combine($relationship_field_names, $relationship_field_names));
return $this
->getResponses($links, $request_options);
}
protected function getResponses(array $links, array $request_options) {
return array_reduce(array_keys($links), function ($related_responses, $key) use ($links, $request_options) {
$related_responses[$key] = $this
->request('GET', Url::fromUri($links[$key]), $request_options);
return $related_responses;
}, []);
}
protected static function getAccessDeniedResponse(EntityInterface $entity, AccessResultInterface $access, Url $via_link, $relationship_field_name = NULL, $detail = NULL, $pointer = NULL) {
$detail = $detail ? $detail : 'The current user is not allowed to GET the selected resource.';
if ($access instanceof AccessResultReasonInterface && ($reason = $access
->getReason())) {
$detail .= ' ' . $reason;
}
$error = [
'status' => '403',
'title' => 'Forbidden',
'detail' => $detail,
'links' => [
'info' => [
'href' => HttpExceptionNormalizer::getInfoUrl(403),
],
],
];
if ($pointer || $pointer !== FALSE && $relationship_field_name) {
$error['source']['pointer'] = $pointer ? $pointer : $relationship_field_name;
}
if ($via_link) {
$error['links']['via']['href'] = $via_link
->setAbsolute()
->toString();
}
return (new ResourceResponse([
'jsonapi' => static::$jsonApiMember,
'errors' => [
$error,
],
], 403))
->addCacheableDependency((new CacheableMetadata())
->addCacheTags([
'4xx-response',
'http_response',
])
->addCacheContexts([
'url.site',
]))
->addCacheableDependency($access);
}
protected function getEmptyCollectionResponse($cardinality, $self_link) {
$cache_contexts = Cache::mergeContexts([
'url.query_args:fields',
'url.query_args:include',
'url.site',
], $this->entity
->getEntityType()
->isRevisionable() ? [
'url.query_args:resourceVersion',
] : []);
$cacheability = (new CacheableMetadata())
->addCacheContexts($cache_contexts)
->addCacheTags([
'http_response',
]);
return (new ResourceResponse([
'data' => $cardinality === 1 ? NULL : [],
'jsonapi' => static::$jsonApiMember,
'links' => [
'self' => [
'href' => $self_link,
],
],
]))
->addCacheableDependency($cacheability);
}
protected static function addOmittedObject(array &$document, array $omitted) {
if (isset($document['meta']['omitted'])) {
$document['meta']['omitted'] = static::mergeOmittedObjects($document['meta']['omitted'], $omitted);
}
else {
$document['meta']['omitted'] = $omitted;
}
}
protected static function errorsToOmittedObject(array $errors) {
$omitted = [
'detail' => 'Some resources have been omitted because of insufficient authorization.',
'links' => [
'help' => [
'href' => 'https://www.drupal.org/docs/8/modules/json-api/filtering#filters-access-control',
],
],
];
foreach ($errors as $error) {
$omitted['links']['item:' . substr(Crypt::hashBase64($error['links']['via']['href']), 0, 7)] = [
'href' => $error['links']['via']['href'],
'meta' => [
'detail' => $error['detail'],
'rel' => 'item',
],
];
}
return $omitted;
}
protected static function mergeOmittedObjects(array $a, array $b) {
$merged['detail'] = 'Some resources have been omitted because of insufficient authorization.';
$merged['links']['help']['href'] = 'https://www.drupal.org/docs/8/modules/json-api/filtering#filters-access-control';
$a_links = array_diff_key($a['links'], array_flip([
'help',
]));
$b_links = array_diff_key($b['links'], array_flip([
'help',
]));
foreach (array_merge(array_values($a_links), array_values($b_links)) as $link) {
$merged['links'][$link['href'] . $link['meta']['detail']] = $link;
}
static::resetOmittedLinkKeys($merged);
return $merged;
}
protected static function sortOmittedLinks(array &$omitted) {
$help = $omitted['links']['help'];
$links = array_diff_key($omitted['links'], array_flip([
'help',
]));
uasort($links, function ($a, $b) {
return strcmp($a['href'], $b['href']);
});
$omitted['links'] = [
'help' => $help,
] + $links;
}
protected static function resetOmittedLinkKeys(array &$omitted) {
$help = $omitted['links']['help'];
$reindexed = [];
$links = array_diff_key($omitted['links'], array_flip([
'help',
]));
foreach (array_values($links) as $index => $link) {
$reindexed['item:' . $index] = $link;
}
$omitted['links'] = [
'help' => $help,
] + $reindexed;
}
}