PageRenderTime 34ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/web/core/modules/jsonapi/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php

https://gitlab.com/mohamed_hussein/prodt
PHP | 335 lines | 215 code | 23 blank | 97 comment | 22 complexity | 0d1a4f6f7bfe961fe06108eb10ae22e8 MD5 | raw file
  1. <?php
  2. namespace Drupal\jsonapi\Normalizer;
  3. use Drupal\Component\Plugin\Exception\PluginNotFoundException;
  4. use Drupal\Component\Utility\Crypt;
  5. use Drupal\Component\Uuid\Uuid;
  6. use Drupal\Core\Cache\CacheableMetadata;
  7. use Drupal\Core\Entity\EntityTypeManagerInterface;
  8. use Drupal\jsonapi\JsonApiResource\ErrorCollection;
  9. use Drupal\jsonapi\JsonApiResource\OmittedData;
  10. use Drupal\jsonapi\JsonApiSpec;
  11. use Drupal\jsonapi\Normalizer\Value\CacheableOmission;
  12. use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
  13. use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
  14. use Drupal\jsonapi\ResourceType\ResourceType;
  15. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  16. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  17. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  18. use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
  19. use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
  20. use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
  21. use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
  22. /**
  23. * Normalizes the top-level document according to the JSON:API specification.
  24. *
  25. * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
  26. * class may change at any time and this will break any dependencies on it.
  27. *
  28. * @see https://www.drupal.org/project/drupal/issues/3032787
  29. * @see jsonapi.api.php
  30. *
  31. * @see \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel
  32. */
  33. class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements DenormalizerInterface, NormalizerInterface {
  34. /**
  35. * {@inheritdoc}
  36. */
  37. protected $supportedInterfaceOrClass = JsonApiDocumentTopLevel::class;
  38. /**
  39. * The entity type manager.
  40. *
  41. * @var \Drupal\Core\Entity\EntityTypeManagerInterface
  42. */
  43. protected $entityTypeManager;
  44. /**
  45. * The JSON:API resource type repository.
  46. *
  47. * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
  48. */
  49. protected $resourceTypeRepository;
  50. /**
  51. * Constructs a JsonApiDocumentTopLevelNormalizer object.
  52. *
  53. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
  54. * The entity type manager.
  55. * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
  56. * The JSON:API resource type repository.
  57. */
  58. public function __construct(EntityTypeManagerInterface $entity_type_manager, ResourceTypeRepositoryInterface $resource_type_repository) {
  59. $this->entityTypeManager = $entity_type_manager;
  60. $this->resourceTypeRepository = $resource_type_repository;
  61. }
  62. /**
  63. * {@inheritdoc}
  64. */
  65. public function denormalize($data, $class, $format = NULL, array $context = []) {
  66. $resource_type = $context['resource_type'];
  67. // Validate a few common errors in document formatting.
  68. static::validateRequestBody($data, $resource_type);
  69. $normalized = [];
  70. if (!empty($data['data']['attributes'])) {
  71. $normalized = $data['data']['attributes'];
  72. }
  73. if (!empty($data['data']['id'])) {
  74. $uuid_key = $this->entityTypeManager->getDefinition($resource_type->getEntityTypeId())->getKey('uuid');
  75. $normalized[$uuid_key] = $data['data']['id'];
  76. }
  77. if (!empty($data['data']['relationships'])) {
  78. // Turn all single object relationship data fields into an array of
  79. // objects.
  80. $relationships = array_map(function ($relationship) {
  81. if (isset($relationship['data']['type']) && isset($relationship['data']['id'])) {
  82. return ['data' => [$relationship['data']]];
  83. }
  84. else {
  85. return $relationship;
  86. }
  87. }, $data['data']['relationships']);
  88. // Get an array of ids for every relationship.
  89. $relationships = array_map(function ($relationship) {
  90. if (empty($relationship['data'])) {
  91. return [];
  92. }
  93. if (empty($relationship['data'][0]['id'])) {
  94. throw new BadRequestHttpException("No ID specified for related resource");
  95. }
  96. $id_list = array_column($relationship['data'], 'id');
  97. if (empty($relationship['data'][0]['type'])) {
  98. throw new BadRequestHttpException("No type specified for related resource");
  99. }
  100. if (!$resource_type = $this->resourceTypeRepository->getByTypeName($relationship['data'][0]['type'])) {
  101. throw new BadRequestHttpException("Invalid type specified for related resource: '" . $relationship['data'][0]['type'] . "'");
  102. }
  103. $entity_type_id = $resource_type->getEntityTypeId();
  104. try {
  105. $entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
  106. }
  107. catch (PluginNotFoundException $e) {
  108. throw new BadRequestHttpException("Invalid type specified for related resource: '" . $relationship['data'][0]['type'] . "'");
  109. }
  110. // In order to maintain the order ($delta) of the relationships, we need
  111. // to load the entities and create a mapping between id and uuid.
  112. $uuid_key = $this->entityTypeManager
  113. ->getDefinition($entity_type_id)->getKey('uuid');
  114. $related_entities = array_values($entity_storage->loadByProperties([$uuid_key => $id_list]));
  115. $map = [];
  116. foreach ($related_entities as $related_entity) {
  117. $map[$related_entity->uuid()] = $related_entity->id();
  118. }
  119. // $id_list has the correct order of uuids. We stitch this together with
  120. // $map which contains loaded entities, and then bring in the correct
  121. // meta values from the relationship, whose deltas match with $id_list.
  122. $canonical_ids = [];
  123. foreach ($id_list as $delta => $uuid) {
  124. if (!isset($map[$uuid])) {
  125. // @see \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer::normalize()
  126. if ($uuid === 'virtual') {
  127. continue;
  128. }
  129. throw new NotFoundHttpException(sprintf('The resource identified by `%s:%s` (given as a relationship item) could not be found.', $relationship['data'][$delta]['type'], $uuid));
  130. }
  131. $reference_item = [
  132. 'target_id' => $map[$uuid],
  133. ];
  134. if (isset($relationship['data'][$delta]['meta'])) {
  135. $reference_item += $relationship['data'][$delta]['meta'];
  136. }
  137. $canonical_ids[] = array_filter($reference_item, function ($key) {
  138. return substr($key, 0, strlen('drupal_internal__')) !== 'drupal_internal__';
  139. }, ARRAY_FILTER_USE_KEY);
  140. }
  141. return array_filter($canonical_ids);
  142. }, $relationships);
  143. // Add the relationship ids.
  144. $normalized = array_merge($normalized, $relationships);
  145. }
  146. // Override deserialization target class with the one in the ResourceType.
  147. $class = $context['resource_type']->getDeserializationTargetClass();
  148. return $this
  149. ->serializer
  150. ->denormalize($normalized, $class, $format, $context);
  151. }
  152. /**
  153. * {@inheritdoc}
  154. */
  155. public function normalize($object, $format = NULL, array $context = []) {
  156. assert($object instanceof JsonApiDocumentTopLevel);
  157. $data = $object->getData();
  158. $document['jsonapi'] = CacheableNormalization::permanent([
  159. 'version' => JsonApiSpec::SUPPORTED_SPECIFICATION_VERSION,
  160. 'meta' => [
  161. 'links' => [
  162. 'self' => [
  163. 'href' => JsonApiSpec::SUPPORTED_SPECIFICATION_PERMALINK,
  164. ],
  165. ],
  166. ],
  167. ]);
  168. if ($data instanceof ErrorCollection) {
  169. $document['errors'] = $this->normalizeErrorDocument($object, $format, $context);
  170. }
  171. else {
  172. // Add data.
  173. $document['data'] = $this->serializer->normalize($data, $format, $context);
  174. // Add includes.
  175. $document['included'] = $this->serializer->normalize($object->getIncludes(), $format, $context)->omitIfEmpty();
  176. // Add omissions and metadata.
  177. $normalized_omissions = $this->normalizeOmissionsLinks($object->getOmissions(), $format, $context);
  178. $meta = !$normalized_omissions instanceof CacheableOmission
  179. ? array_merge($object->getMeta(), ['omitted' => $normalized_omissions->getNormalization()])
  180. : $object->getMeta();
  181. $document['meta'] = (new CacheableNormalization($normalized_omissions, $meta))->omitIfEmpty();
  182. }
  183. // Add document links.
  184. $document['links'] = $this->serializer->normalize($object->getLinks(), $format, $context)->omitIfEmpty();
  185. // Every JSON:API document contains absolute URLs.
  186. return CacheableNormalization::aggregate($document)->withCacheableDependency((new CacheableMetadata())->addCacheContexts(['url.site']));
  187. }
  188. /**
  189. * Normalizes an error collection.
  190. *
  191. * @param \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel $document
  192. * The document to normalize.
  193. * @param string $format
  194. * The normalization format.
  195. * @param array $context
  196. * The normalization context.
  197. *
  198. * @return \Drupal\jsonapi\Normalizer\Value\CacheableNormalization
  199. * The normalized document.
  200. *
  201. * @todo: refactor this to use CacheableNormalization::aggregate in https://www.drupal.org/project/drupal/issues/3036284.
  202. */
  203. protected function normalizeErrorDocument(JsonApiDocumentTopLevel $document, $format, array $context = []) {
  204. $normalized_values = array_map(function (HttpExceptionInterface $exception) use ($format, $context) {
  205. return $this->serializer->normalize($exception, $format, $context);
  206. }, (array) $document->getData()->getIterator());
  207. $cacheability = new CacheableMetadata();
  208. $errors = [];
  209. foreach ($normalized_values as $normalized_error) {
  210. $cacheability->addCacheableDependency($normalized_error);
  211. $errors = array_merge($errors, $normalized_error->getNormalization());
  212. }
  213. return new CacheableNormalization($cacheability, $errors);
  214. }
  215. /**
  216. * Normalizes omitted data into a set of omission links.
  217. *
  218. * @param \Drupal\jsonapi\JsonApiResource\OmittedData $omissions
  219. * The omitted response data.
  220. * @param string $format
  221. * The normalization format.
  222. * @param array $context
  223. * The normalization context.
  224. *
  225. * @return \Drupal\jsonapi\Normalizer\Value\CacheableNormalization|\Drupal\jsonapi\Normalizer\Value\CacheableOmission
  226. * The normalized omissions.
  227. *
  228. * @todo: refactor this to use link collections in https://www.drupal.org/project/drupal/issues/3036279.
  229. */
  230. protected function normalizeOmissionsLinks(OmittedData $omissions, $format, array $context = []) {
  231. $normalized_omissions = array_map(function (HttpExceptionInterface $exception) use ($format, $context) {
  232. return $this->serializer->normalize($exception, $format, $context);
  233. }, $omissions->toArray());
  234. $cacheability = CacheableMetadata::createFromObject(CacheableNormalization::aggregate($normalized_omissions));
  235. if (empty($normalized_omissions)) {
  236. return new CacheableOmission($cacheability);
  237. }
  238. $omission_links = [
  239. 'detail' => 'Some resources have been omitted because of insufficient authorization.',
  240. 'links' => [
  241. 'help' => [
  242. 'href' => 'https://www.drupal.org/docs/8/modules/json-api/filtering#filters-access-control',
  243. ],
  244. ],
  245. ];
  246. $link_hash_salt = Crypt::randomBytesBase64();
  247. foreach ($normalized_omissions as $omission) {
  248. $cacheability->addCacheableDependency($omission);
  249. // Add the errors to the pre-existing errors.
  250. foreach ($omission->getNormalization() as $error) {
  251. // JSON:API links cannot be arrays and the spec generally favors link
  252. // relation types as keys. 'item' is the right link relation type, but
  253. // we need multiple values. To do that, we generate a meaningless,
  254. // random value to use as a unique key. That value is a hash of a
  255. // random salt and the link href. This ensures that the key is non-
  256. // deterministic while letting use deduplicate the links by their
  257. // href. The salt is *not* used for any cryptographic reason.
  258. $link_key = 'item--' . static::getLinkHash($link_hash_salt, $error['links']['via']['href']);
  259. $omission_links['links'][$link_key] = [
  260. 'href' => $error['links']['via']['href'],
  261. 'meta' => [
  262. 'rel' => 'item',
  263. 'detail' => $error['detail'],
  264. ],
  265. ];
  266. }
  267. }
  268. return new CacheableNormalization($cacheability, $omission_links);
  269. }
  270. /**
  271. * Performs minimal validation of the document.
  272. */
  273. protected static function validateRequestBody(array $document, ResourceType $resource_type) {
  274. // Ensure that the relationships key was not placed in the top level.
  275. if (isset($document['relationships']) && !empty($document['relationships'])) {
  276. throw new BadRequestHttpException("Found \"relationships\" within the document's top level. The \"relationships\" key must be within resource object.");
  277. }
  278. // Ensure that the resource object contains the "type" key.
  279. if (!isset($document['data']['type'])) {
  280. throw new BadRequestHttpException("Resource object must include a \"type\".");
  281. }
  282. // Ensure that the client provided ID is a valid UUID.
  283. if (isset($document['data']['id']) && !Uuid::isValid($document['data']['id'])) {
  284. throw new UnprocessableEntityHttpException('IDs should be properly generated and formatted UUIDs as described in RFC 4122.');
  285. }
  286. // Ensure that no relationship fields are being set via the attributes
  287. // resource object member.
  288. if (isset($document['data']['attributes'])) {
  289. $received_attribute_field_names = array_keys($document['data']['attributes']);
  290. $relationship_field_names = array_keys($resource_type->getRelatableResourceTypes());
  291. if ($relationship_fields_sent_as_attributes = array_intersect($received_attribute_field_names, $relationship_field_names)) {
  292. throw new UnprocessableEntityHttpException(sprintf("The following relationship fields were provided as attributes: [ %s ]", implode(', ', $relationship_fields_sent_as_attributes)));
  293. }
  294. }
  295. }
  296. /**
  297. * Hashes an omitted link.
  298. *
  299. * @param string $salt
  300. * A hash salt.
  301. * @param string $link_href
  302. * The omitted link.
  303. *
  304. * @return string
  305. * A 7 character hash.
  306. */
  307. protected static function getLinkHash($salt, $link_href) {
  308. return substr(str_replace(['-', '_'], '', Crypt::hashBase64($salt . $link_href)), 0, 7);
  309. }
  310. }