Skip to content

Commit 32ef3d4

Browse files
ili101soyukadunglas
authored
fix(jsonld): allow @id, @context and @type on denormalization 2 (#6451)
* fix(jsonld): allow @id, @context and @type when denormalizing * Update features/main/standard_put.feature Co-authored-by: Kévin Dunglas <[email protected]> * fix: wrong resource iri * fix: wrong resource iri --------- Co-authored-by: Antoine Bluchet <[email protected]> Co-authored-by: Kévin Dunglas <[email protected]>
1 parent 4de43ba commit 32ef3d4

File tree

3 files changed

+107
-1
lines changed

3 files changed

+107
-1
lines changed

features/main/standard_put.feature

+54
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,60 @@ Feature: Spec-compliant PUT support
2626
}
2727
"""
2828

29+
Scenario: Create a new resource with JSON-LD attributes
30+
When I add "Content-Type" header equal to "application/ld+json"
31+
And I send a "PUT" request to "/standard_puts/6" with body:
32+
"""
33+
{
34+
"@id": "/standard_puts/6",
35+
"@context": "/contexts/StandardPut",
36+
"@type": "StandardPut",
37+
"foo": "a",
38+
"bar": "b"
39+
}
40+
"""
41+
Then the response status code should be 201
42+
And the response should be in JSON
43+
And the JSON should be equal to:
44+
"""
45+
{
46+
"@context": "/contexts/StandardPut",
47+
"@id": "/standard_puts/6",
48+
"@type": "StandardPut",
49+
"id": 6,
50+
"foo": "a",
51+
"bar": "b"
52+
}
53+
"""
54+
55+
Scenario: Fails to create a new resource with the wrong JSON-LD @id
56+
When I add "Content-Type" header equal to "application/ld+json"
57+
And I send a "PUT" request to "/standard_puts/7" with body:
58+
"""
59+
{
60+
"@id": "/dummies/6",
61+
"@context": "/contexts/StandardPut",
62+
"@type": "StandardPut",
63+
"foo": "a",
64+
"bar": "b"
65+
}
66+
"""
67+
Then the response status code should be 400
68+
69+
Scenario: Fails to create a new resource when the JSON-LD @id doesn't match the URI
70+
When I add "Content-Type" header equal to "application/ld+json"
71+
And I send a "PUT" request to "/standard_puts/7" with body:
72+
"""
73+
{
74+
"@id": "/standard_puts/6",
75+
"@context": "/contexts/StandardPut",
76+
"@type": "StandardPut",
77+
"foo": "a",
78+
"bar": "b"
79+
}
80+
"""
81+
Then the response status code should be 400
82+
2983
Scenario: Replace an existing resource
3084
When I add "Content-Type" header equal to "application/ld+json"
3185
And I send a "PUT" request to "/standard_puts/5" with body:

src/JsonLd/Serializer/ItemNormalizer.php

+43-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface;
1818
use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
1919
use ApiPlatform\JsonLd\ContextBuilderInterface;
20+
use ApiPlatform\Metadata\Exception\ItemNotFoundException;
2021
use ApiPlatform\Metadata\HttpOperation;
2122
use ApiPlatform\Metadata\IriConverterInterface;
2223
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2324
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
25+
use ApiPlatform\Metadata\Put;
2426
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2527
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
2628
use ApiPlatform\Metadata\ResourceClassResolverInterface;
@@ -47,6 +49,29 @@ final class ItemNormalizer extends AbstractItemNormalizer
4749
use JsonLdContextTrait;
4850

4951
public const FORMAT = 'jsonld';
52+
private const JSONLD_KEYWORDS = [
53+
'@context',
54+
'@direction',
55+
'@graph',
56+
'@id',
57+
'@import',
58+
'@included',
59+
'@index',
60+
'@json',
61+
'@language',
62+
'@list',
63+
'@nest',
64+
'@none',
65+
'@prefix',
66+
'@propagate',
67+
'@protected',
68+
'@reverse',
69+
'@set',
70+
'@type',
71+
'@value',
72+
'@version',
73+
'@vocab',
74+
];
5075

5176
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)
5277
{
@@ -148,9 +173,26 @@ public function denormalize(mixed $data, string $class, ?string $format = null,
148173
throw new NotNormalizableValueException('Update is not allowed for this operation.');
149174
}
150175

151-
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri($data['@id'], $context + ['fetch_data' => true]);
176+
try {
177+
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri($data['@id'], $context + ['fetch_data' => true], $context['operation'] ?? null);
178+
} catch (ItemNotFoundException $e) {
179+
$operation = $context['operation'] ?? null;
180+
if (!($operation instanceof Put && ($operation->getExtraProperties()['standard_put'] ?? false))) {
181+
throw $e;
182+
}
183+
}
152184
}
153185

154186
return parent::denormalize($data, $class, $format, $context);
155187
}
188+
189+
protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
190+
{
191+
$allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
192+
if (\is_array($allowedAttributes) && ($context['api_denormalize'] ?? false)) {
193+
$allowedAttributes = array_merge($allowedAttributes, self::JSONLD_KEYWORDS);
194+
}
195+
196+
return $allowedAttributes;
197+
}
156198
}

src/Symfony/Routing/IriConverter.php

+10
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ public function getResourceFromIri(string $iri, array $context = [], ?Operation
7878
throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri));
7979
}
8080

81+
foreach ($context['uri_variables'] ?? [] as $key => $value) {
82+
if (!isset($parameters[$key]) || $parameters[$key] !== (string) $value) {
83+
throw new InvalidArgumentException(sprintf('The iri "%s" does not reference the correct resource.', $iri));
84+
}
85+
}
86+
87+
if ($operation && !is_a($parameters['_api_resource_class'], $operation->getClass(), true)) {
88+
throw new InvalidArgumentException(sprintf('The iri "%s" does not reference the correct resource.', $iri));
89+
}
90+
8191
$operation = $parameters['_api_operation'] = $this->resourceMetadataCollectionFactory->create($parameters['_api_resource_class'])->getOperation($parameters['_api_operation_name']);
8292

8393
if ($operation instanceof CollectionOperationInterface) {

0 commit comments

Comments
 (0)