Skip to content

Commit 4e3170d

Browse files
meyerbaptistedunglas
authored andcommitted
Fix api-platform/api-platform#132: disallow update for non-PUT methods (#776)
1 parent 4d3a2c5 commit 4e3170d

File tree

7 files changed

+97
-38
lines changed

7 files changed

+97
-38
lines changed

features/hydra/error.feature

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,42 @@ Feature: Error handling
8484
And the JSON node "hydra:title" should be equal to "An error occurred"
8585
And the JSON node "hydra:description" should exist
8686
And the JSON node "trace" should exist
87+
88+
Scenario: Get an error during update of an existing resource with a non-allowed update operation
89+
When I add "Content-Type" header equal to "application/ld+json"
90+
And I send a "POST" request to "/dummies" with body:
91+
"""
92+
{
93+
"@id": "/dummies/1",
94+
"name": "Foo"
95+
}
96+
"""
97+
Then the response status code should be 400
98+
And the response should be in JSON
99+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
100+
And the JSON node "@context" should be equal to "/contexts/Error"
101+
And the JSON node "@type" should be equal to "Error"
102+
And the JSON node "hydra:title" should be equal to "An error occurred"
103+
And the JSON node "hydra:description" should be equal to "Update is not allowed for this operation."
104+
And the JSON node "trace" should exist
105+
106+
Scenario: Get an error during update of an existing relation with a non-allowed update operation
107+
When I add "Content-Type" header equal to "application/ld+json"
108+
And I send a "POST" request to "/relation_embedders" with body:
109+
"""
110+
{
111+
"anotherRelated": {
112+
"@id": "/related_dummies/2",
113+
"@type": "https://schema.org/Product",
114+
"symfony": "phalcon"
115+
}
116+
}
117+
"""
118+
Then the response status code should be 400
119+
And the response should be in JSON
120+
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
121+
And the JSON node "@context" should be equal to "/contexts/Error"
122+
And the JSON node "@type" should be equal to "Error"
123+
And the JSON node "hydra:title" should be equal to "An error occurred"
124+
And the JSON node "hydra:description" should be equal to "Update is not allowed for this operation."
125+
And the JSON node "trace" should exist

features/relation.feature

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ Feature: Relations support
303303
And the response should be in JSON
304304
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
305305

306+
@dropSchema
306307
Scenario: Update an embedded relation
307308
When I add "Content-Type" header equal to "application/ld+json"
308309
And I send a "PUT" request to "/relation_embedders/2" with body:
@@ -333,36 +334,3 @@ Feature: Relations support
333334
"related": null
334335
}
335336
"""
336-
337-
@dropSchema
338-
Scenario: Update an existing relation
339-
When I add "Content-Type" header equal to "application/ld+json"
340-
And I send a "POST" request to "/relation_embedders" with body:
341-
"""
342-
{
343-
"anotherRelated": {
344-
"@id": "/related_dummies/2",
345-
"@type": "https://schema.org/Product",
346-
"symfony": "phalcon"
347-
}
348-
}
349-
"""
350-
Then the response status code should be 201
351-
And the response should be in JSON
352-
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
353-
And the JSON should be equal to:
354-
"""
355-
{
356-
"@context": "/contexts/RelationEmbedder",
357-
"@id": "/relation_embedders/3",
358-
"@type": "RelationEmbedder",
359-
"krondstadt": "Krondstadt",
360-
"anotherRelated": {
361-
"@id": "/related_dummies/2",
362-
"@type": "https://schema.org/Product",
363-
"symfony": "phalcon",
364-
"thirdLevel": null
365-
},
366-
"related": null
367-
}
368-
"""

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use ApiPlatform\Core\Api\IriConverterInterface;
1515
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
16+
use ApiPlatform\Core\Exception\InvalidArgumentException;
1617
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
1718
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1819
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
@@ -83,11 +84,17 @@ public function supportsDenormalization($data, $type, $format = null)
8384

8485
/**
8586
* {@inheritdoc}
87+
*
88+
* @throws InvalidArgumentException
8689
*/
8790
public function denormalize($data, $class, $format = null, array $context = [])
8891
{
8992
// Avoid issues with proxies if we populated the object
9093
if (isset($data['@id']) && !isset($context['object_to_populate'])) {
94+
if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
95+
throw new InvalidArgumentException('Update is not allowed for this operation.');
96+
}
97+
9198
$context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['@id'], true);
9299
}
93100

src/Serializer/ItemNormalizer.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace ApiPlatform\Core\Serializer;
1313

14+
use ApiPlatform\Core\Exception\InvalidArgumentException;
15+
1416
/**
1517
* Generic item normalizer.
1618
*
@@ -20,11 +22,17 @@ final class ItemNormalizer extends AbstractItemNormalizer
2022
{
2123
/**
2224
* {@inheritdoc}
25+
*
26+
* @throws InvalidArgumentException
2327
*/
2428
public function denormalize($data, $class, $format = null, array $context = [])
2529
{
2630
// Avoid issues with proxies if we populated the object
2731
if (isset($data['id']) && !isset($context['object_to_populate'])) {
32+
if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
33+
throw new InvalidArgumentException('Update is not allowed for this operation.');
34+
}
35+
2836
$context['object_to_populate'] = $this->iriConverter->getItemFromIri($data['id'], true);
2937
}
3038

src/Serializer/SerializerContextBuilder.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ public function createFromRequest(Request $request, bool $normalization, array $
4949
$context['item_operation_name'] = $attributes['item_operation_name'];
5050
}
5151

52+
if (!$normalization && !isset($context['api_allow_update'])) {
53+
$context['api_allow_update'] = Request::METHOD_PUT === $request->getMethod();
54+
}
55+
5256
$context['resource_class'] = $attributes['resource_class'];
5357
$context['request_uri'] = $request->getRequestUri();
5458

tests/Serializer/ItemNormalizerTest.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public function testNormalize()
9797

9898
public function testDenormalize()
9999
{
100-
$context = ['resource_class' => Dummy::class];
100+
$context = ['resource_class' => Dummy::class, 'api_allow_update' => true];
101101

102102
$propertyNameCollection = new PropertyNameCollection(['name']);
103103
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
@@ -124,4 +124,33 @@ public function testDenormalize()
124124

125125
$this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['name' => 'hello'], Dummy::class, null, $context));
126126
}
127+
128+
/**
129+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
130+
* @expectedExceptionMessage Update is not allowed for this operation.
131+
*/
132+
public function testDenormalizeWithIdAndUpdateNotAllowed()
133+
{
134+
$context = ['resource_class' => Dummy::class, 'api_allow_update' => false];
135+
136+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
137+
138+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
139+
140+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
141+
142+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
143+
144+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
145+
$serializerProphecy->willImplement(DenormalizerInterface::class);
146+
147+
$normalizer = new ItemNormalizer(
148+
$propertyNameCollectionFactoryProphecy->reveal(),
149+
$propertyMetadataFactoryProphecy->reveal(),
150+
$iriConverterProphecy->reveal(),
151+
$resourceClassResolverProphecy->reveal()
152+
);
153+
$normalizer->setSerializer($serializerProphecy->reveal());
154+
$normalizer->denormalize(['id' => '/dummies/12', 'name' => 'hello'], Dummy::class, null, $context);
155+
}
127156
}

tests/Serializer/SerializerContextBuilderTest.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,15 @@ public function testCreateFromRequest()
5454
$this->assertEquals($expected, $this->builder->createFromRequest($request, true));
5555

5656
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_item_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
57-
$expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => ''];
57+
$expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '', 'api_allow_update' => false];
5858
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
5959

60-
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']);
61-
$expected = ['bar' => 'baz', 'collection_operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => ''];
60+
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml'], [], [], ['REQUEST_METHOD' => 'POST']);
61+
$expected = ['bar' => 'baz', 'collection_operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '', 'api_allow_update' => false];
62+
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
63+
64+
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'put', '_api_format' => 'xml', '_api_mime_type' => 'text/xml'], [], [], ['REQUEST_METHOD' => 'PUT']);
65+
$expected = ['bar' => 'baz', 'collection_operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '', 'api_allow_update' => true];
6266
$this->assertEquals($expected, $this->builder->createFromRequest($request, false));
6367
}
6468

@@ -72,7 +76,7 @@ public function testThrowExceptionOnInvalidRequest()
7276

7377
public function testReuseExistingAttributes()
7478
{
75-
$expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => ''];
79+
$expected = ['bar' => 'baz', 'item_operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '', 'api_allow_update' => false];
7680
$this->assertEquals($expected, $this->builder->createFromRequest(new Request(), false, ['resource_class' => 'Foo', 'item_operation_name' => 'get']));
7781
}
7882
}

0 commit comments

Comments
 (0)