Skip to content

Commit 51997f6

Browse files
committed
Prevent cloning generator when instanciating previous_data
1 parent f1802ea commit 51997f6

File tree

10 files changed

+149
-3
lines changed

10 files changed

+149
-3
lines changed

features/authorization/deny.feature

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ Feature: Authorization checking
1616
Then the response status code should be 200
1717
And the response should be in JSON
1818

19+
Scenario: Data provider that's return generator has null previous object
20+
When I add "Accept" header equal to "application/ld+json"
21+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
22+
And I send a "GET" request to "/custom_data_provider_generator"
23+
Then the response status code should be 200
24+
1925
Scenario: A standard user cannot create a secured resource
2026
When I add "Accept" header equal to "application/ld+json"
2127
And I add "Content-Type" header equal to "application/ld+json"

src/EventListener/ReadListener.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2424
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
2525
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
26+
use ApiPlatform\Core\Util\CloneTrait;
2627
use ApiPlatform\Core\Util\RequestAttributesExtractor;
2728
use ApiPlatform\Core\Util\RequestParser;
2829
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
@@ -35,6 +36,7 @@
3536
*/
3637
final class ReadListener
3738
{
39+
use CloneTrait;
3840
use OperationDataProviderTrait;
3941
use ToggleableOperationAttributeTrait;
4042

@@ -115,6 +117,6 @@ public function onKernelRequest(GetResponseEvent $event): void
115117
}
116118

117119
$request->attributes->set('data', $data);
118-
$request->attributes->set('previous_data', \is_object($data) ? clone $data : $data);
120+
$request->attributes->set('previous_data', $this->clone($data));
119121
}
120122
}

src/GraphQl/Resolver/Factory/CollectionResolverFactory.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
2424
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2525
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
26+
use ApiPlatform\Core\Util\CloneTrait;
2627
use GraphQL\Error\Error;
2728
use GraphQL\Type\Definition\ResolveInfo;
2829
use Symfony\Component\HttpFoundation\RequestStack;
@@ -38,6 +39,7 @@
3839
*/
3940
final class CollectionResolverFactory implements ResolverFactoryInterface
4041
{
42+
use CloneTrait;
4143
use FieldsToAttributesTrait;
4244
use ResourceAccessCheckerTrait;
4345

@@ -97,7 +99,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
9799

98100
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, [
99101
'object' => $collection,
100-
'previous_object' => \is_object($collection) ? clone $collection : $collection,
102+
'previous_object' => $this->clone($collection),
101103
], $operationName ?? 'query');
102104

103105
if (!$this->paginationEnabled) {

src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
2525
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
2626
use ApiPlatform\Core\Util\ClassInfoTrait;
27+
use ApiPlatform\Core\Util\CloneTrait;
2728
use ApiPlatform\Core\Validator\Exception\ValidationException;
2829
use ApiPlatform\Core\Validator\ValidatorInterface;
2930
use GraphQL\Error\Error;
@@ -41,6 +42,7 @@
4142
final class ItemMutationResolverFactory implements ResolverFactoryInterface
4243
{
4344
use ClassInfoTrait;
45+
use CloneTrait;
4446
use FieldsToAttributesTrait;
4547
use ResourceAccessCheckerTrait;
4648

@@ -96,7 +98,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
9698
throw Error::createLocatedError(sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $resourceMetadata->getShortName()), $info->fieldNodes, $info->path);
9799
}
98100
}
99-
$previousItem = \is_object($item) ? clone $item : $item;
101+
$previousItem = $this->clone($item);
100102

101103
$inputMetadata = $resourceMetadata->getGraphqlAttribute($operationName, 'input', null, true);
102104
$inputClass = null;

src/Util/CloneTrait.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Util;
15+
16+
/**
17+
* Clones given data if cloneable.
18+
*
19+
* @internal
20+
*
21+
* @author Quentin Barloy <[email protected]>
22+
*/
23+
trait CloneTrait
24+
{
25+
public function clone($data)
26+
{
27+
if (!\is_object($data)) {
28+
return $data;
29+
}
30+
31+
try {
32+
return (new \ReflectionClass($data))->isCloneable() ? clone $data : null;
33+
} catch (\ReflectionException $reflectionException) {
34+
return null;
35+
}
36+
}
37+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\DataProvider;
15+
16+
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
17+
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface;
18+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\SecuredDummy;
19+
20+
class GeneratorDataProvider implements CollectionDataProviderInterface, RestrictedDataProviderInterface
21+
{
22+
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
23+
{
24+
return SecuredDummy::class === $resourceClass && 'get_from_data_provider_generator' === $operationName;
25+
}
26+
27+
public function getCollection(string $resourceClass, string $operationName = null)
28+
{
29+
yield from [new class() {
30+
}, new class() {
31+
}];
32+
}
33+
}

tests/Fixtures/TestBundle/Document/SecuredDummy.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
* attributes={"access_control"="is_granted('ROLE_USER')"},
2828
* collectionOperations={
2929
* "get",
30+
* "get_from_data_provider_generator"={
31+
* "method"="GET",
32+
* "path"="custom_data_provider_generator",
33+
* "access_control"="is_granted('ROLE_USER')"
34+
* },
3035
* "post"={"access_control"="is_granted('ROLE_ADMIN')"}
3136
* },
3237
* itemOperations={

tests/Fixtures/TestBundle/Entity/SecuredDummy.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
* attributes={"access_control"="is_granted('ROLE_USER')"},
2727
* collectionOperations={
2828
* "get",
29+
* "get_from_data_provider_generator"={
30+
* "method"="GET",
31+
* "path"="custom_data_provider_generator",
32+
* "access_control"="is_granted('ROLE_USER')"
33+
* },
2934
* "post"={"access_control"="is_granted('ROLE_ADMIN')"}
3035
* },
3136
* itemOperations={

tests/Fixtures/app/config/config_test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ services:
7373
arguments: [ { 'name': 'ipartial', 'description': 'ipartial' } ]
7474
tags: [ { name: 'api_platform.filter', id: 'related_to_dummy_friend.name' } ]
7575

76+
ApiPlatform\Core\Tests\Fixtures\TestBundle\DataProvider\GeneratorDataProvider:
77+
public: false
78+
tags:
79+
- name: 'api_platform.item_data_provider'
80+
7681
ApiPlatform\Core\Tests\Fixtures\TestBundle\DataProvider\ProductItemDataProvider:
7782
public: false
7883
arguments:

tests/Util/CloneTraitTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Util;
15+
16+
use ApiPlatform\Core\Util\CloneTrait;
17+
use PHPUnit\Framework\TestCase;
18+
19+
/**
20+
* @author Quentin Barloy <[email protected]>
21+
*/
22+
class CloneTraitTest extends TestCase
23+
{
24+
use CloneTrait;
25+
26+
public function testScalarClone(): void
27+
{
28+
$this->assertSame(5, $this->clone(5));
29+
}
30+
31+
public function testObjectClone(): void
32+
{
33+
$data = new \stdClass();
34+
$result = $this->clone($data);
35+
36+
$this->assertNotSame($data, $result);
37+
$this->assertEquals($data, $result);
38+
}
39+
40+
public function testGeneratorClone(): void
41+
{
42+
$this->assertNull($this->clone($this->generator()));
43+
}
44+
45+
private function generator(): \Generator
46+
{
47+
yield 1;
48+
}
49+
}

0 commit comments

Comments
 (0)