From 97f889ee8e0462071cea6644e436484172bcd23d Mon Sep 17 00:00:00 2001 From: ili101 Date: Sun, 30 Jun 2024 18:45:35 +0000 Subject: [PATCH 1/4] fix(jsonld): allow @id, @context and @type when denormalizing --- features/main/standard_put.feature | 26 ++++++++++++++++++++++++ src/JsonLd/Serializer/ItemNormalizer.php | 21 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/features/main/standard_put.feature b/features/main/standard_put.feature index 8a95f9f5930..ee5a99e862e 100644 --- a/features/main/standard_put.feature +++ b/features/main/standard_put.feature @@ -26,6 +26,32 @@ Feature: Spec-compliant PUT support } """ + Scenario: Create a new resource with json-ld attributes + When I add "Content-Type" header equal to "application/ld+json" + And I send a "PUT" request to "/standard_puts/6" with body: + """ + { + "@id": "/standard_puts/6", + "@context": "/contexts/StandardPut", + "@type": "StandardPut", + "foo": "a", + "bar": "b" + } + """ + Then the response status code should be 201 + And the response should be in JSON + And the JSON should be equal to: + """ + { + "@context": "/contexts/StandardPut", + "@id": "/standard_puts/6", + "@type": "StandardPut", + "id": 6, + "foo": "a", + "bar": "b" + } + """ + Scenario: Replace an existing resource When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/standard_puts/5" with body: diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 25c6d90c028..c09394337b1 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -17,10 +17,12 @@ use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; use ApiPlatform\JsonLd\ContextBuilderInterface; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Put; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceAccessCheckerInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; @@ -148,9 +150,26 @@ public function denormalize(mixed $data, string $class, ?string $format = null, throw new NotNormalizableValueException('Update is not allowed for this operation.'); } - $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri($data['@id'], $context + ['fetch_data' => true]); + try { + $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri($data['@id'], $context + ['fetch_data' => true]); + } catch (ItemNotFoundException $e) { + $operation = $context['operation'] ?? null; + if (!($operation instanceof Put && ($operation->getExtraProperties()['standard_put'] ?? false))) { + throw $e; + } + } } return parent::denormalize($data, $class, $format, $context); } + + protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool + { + $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString); + if (\is_array($allowedAttributes) && ($context['api_denormalize'] ?? false)) { + $allowedAttributes = array_merge($allowedAttributes, ['@id', '@type', '@context']); + } + + return $allowedAttributes; + } } From 42a866be83aa5fbfa21be22d9e0c8d14b90b19d1 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Mon, 8 Jul 2024 15:23:22 +0200 Subject: [PATCH 2/4] Update features/main/standard_put.feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- features/main/standard_put.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/main/standard_put.feature b/features/main/standard_put.feature index ee5a99e862e..985a6dead97 100644 --- a/features/main/standard_put.feature +++ b/features/main/standard_put.feature @@ -26,7 +26,7 @@ Feature: Spec-compliant PUT support } """ - Scenario: Create a new resource with json-ld attributes + Scenario: Create a new resource with JSON-LD attributes When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/standard_puts/6" with body: """ From 66a49fd949d77d455d92a737e24303e28989627a Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 9 Jul 2024 07:47:20 +0200 Subject: [PATCH 3/4] fix: wrong resource iri --- features/main/standard_put.feature | 14 ++++++++++++ src/JsonLd/Serializer/ItemNormalizer.php | 27 ++++++++++++++++++++++-- src/Symfony/Routing/IriConverter.php | 4 ++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/features/main/standard_put.feature b/features/main/standard_put.feature index 985a6dead97..cffbce59f5a 100644 --- a/features/main/standard_put.feature +++ b/features/main/standard_put.feature @@ -52,6 +52,20 @@ Feature: Spec-compliant PUT support } """ + Scenario: Fails to create a new resource with the wrong JSON-LD @id + When I add "Content-Type" header equal to "application/ld+json" + And I send a "PUT" request to "/standard_puts/7" with body: + """ + { + "@id": "/dummies/6", + "@context": "/contexts/StandardPut", + "@type": "StandardPut", + "foo": "a", + "bar": "b" + } + """ + Then the response status code should be 400 + Scenario: Replace an existing resource When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/standard_puts/5" with body: diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index c09394337b1..6be01306ebb 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -49,6 +49,29 @@ final class ItemNormalizer extends AbstractItemNormalizer use JsonLdContextTrait; public const FORMAT = 'jsonld'; + public const JSONLD_KEYWORDS = [ + '@context', + '@direction', + '@graph', + '@id', + '@import', + '@included', + '@index', + '@json', + '@language', + '@list', + '@nest', + '@none', + '@prefix', + '@propagate', + '@protected', + '@reverse', + '@set', + '@type', + '@value', + '@version', + '@vocab', + ]; public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface|LegacyIriConverterInterface $iriConverter, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) { @@ -151,7 +174,7 @@ public function denormalize(mixed $data, string $class, ?string $format = null, } try { - $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri($data['@id'], $context + ['fetch_data' => true]); + $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri($data['@id'], $context + ['fetch_data' => true], $context['operation'] ?? null); } catch (ItemNotFoundException $e) { $operation = $context['operation'] ?? null; if (!($operation instanceof Put && ($operation->getExtraProperties()['standard_put'] ?? false))) { @@ -167,7 +190,7 @@ protected function getAllowedAttributes(string|object $classOrObject, array $con { $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString); if (\is_array($allowedAttributes) && ($context['api_denormalize'] ?? false)) { - $allowedAttributes = array_merge($allowedAttributes, ['@id', '@type', '@context']); + $allowedAttributes = array_merge($allowedAttributes, self::JSONLD_KEYWORDS); } return $allowedAttributes; diff --git a/src/Symfony/Routing/IriConverter.php b/src/Symfony/Routing/IriConverter.php index 9de95f35f87..a1f4b603558 100644 --- a/src/Symfony/Routing/IriConverter.php +++ b/src/Symfony/Routing/IriConverter.php @@ -78,6 +78,10 @@ public function getResourceFromIri(string $iri, array $context = [], ?Operation throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri)); } + if ($operation && $operation->getClass() !== $parameters['_api_resource_class']) { + throw new InvalidArgumentException(sprintf('The iri "%s" does not reference the correct resource.', $iri)); + } + $operation = $parameters['_api_operation'] = $this->resourceMetadataCollectionFactory->create($parameters['_api_resource_class'])->getOperation($parameters['_api_operation_name']); if ($operation instanceof CollectionOperationInterface) { From 14cc145ce646fe4a799652f5b451d0140076cf18 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 9 Jul 2024 08:07:36 +0200 Subject: [PATCH 4/4] fix: wrong resource iri --- features/main/standard_put.feature | 14 ++++++++++++++ src/JsonLd/Serializer/ItemNormalizer.php | 2 +- src/Symfony/Routing/IriConverter.php | 8 +++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/features/main/standard_put.feature b/features/main/standard_put.feature index cffbce59f5a..670ab6d0d0b 100644 --- a/features/main/standard_put.feature +++ b/features/main/standard_put.feature @@ -66,6 +66,20 @@ Feature: Spec-compliant PUT support """ Then the response status code should be 400 + Scenario: Fails to create a new resource when the JSON-LD @id doesn't match the URI + When I add "Content-Type" header equal to "application/ld+json" + And I send a "PUT" request to "/standard_puts/7" with body: + """ + { + "@id": "/standard_puts/6", + "@context": "/contexts/StandardPut", + "@type": "StandardPut", + "foo": "a", + "bar": "b" + } + """ + Then the response status code should be 400 + Scenario: Replace an existing resource When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/standard_puts/5" with body: diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 6be01306ebb..3c2ed5696ef 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -49,7 +49,7 @@ final class ItemNormalizer extends AbstractItemNormalizer use JsonLdContextTrait; public const FORMAT = 'jsonld'; - public const JSONLD_KEYWORDS = [ + private const JSONLD_KEYWORDS = [ '@context', '@direction', '@graph', diff --git a/src/Symfony/Routing/IriConverter.php b/src/Symfony/Routing/IriConverter.php index a1f4b603558..2f727e508e6 100644 --- a/src/Symfony/Routing/IriConverter.php +++ b/src/Symfony/Routing/IriConverter.php @@ -78,7 +78,13 @@ public function getResourceFromIri(string $iri, array $context = [], ?Operation throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri)); } - if ($operation && $operation->getClass() !== $parameters['_api_resource_class']) { + foreach ($context['uri_variables'] ?? [] as $key => $value) { + if (!isset($parameters[$key]) || $parameters[$key] !== (string) $value) { + throw new InvalidArgumentException(sprintf('The iri "%s" does not reference the correct resource.', $iri)); + } + } + + if ($operation && !is_a($parameters['_api_resource_class'], $operation->getClass(), true)) { throw new InvalidArgumentException(sprintf('The iri "%s" does not reference the correct resource.', $iri)); }