Skip to content

Commit 2efdc7d

Browse files
authored
Merge pull request #5792 from soyuka/merge
Merge 3.1
2 parents 16b8111 + fdd1024 commit 2efdc7d

31 files changed

+245
-148
lines changed

src/Doctrine/Common/State/PersistProcessor.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,6 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
109109

110110
/**
111111
* Checks if doctrine does not manage data automatically.
112-
*
113-
* @param mixed $data
114112
*/
115113
private function isDeferredExplicit(DoctrineObjectManager $manager, $data): bool
116114
{

src/Doctrine/Common/State/RemoveProcessor.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
3939

4040
/**
4141
* Gets the Doctrine object manager associated with given data.
42-
*
43-
* @param mixed $data
4442
*/
4543
private function getManager($data): ?DoctrineObjectManager
4644
{

src/Doctrine/Odm/Filter/AbstractFilter.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, Operat
5252

5353
/**
5454
* Passes a property through the filter.
55-
*
56-
* @param mixed $value
5755
*/
5856
abstract protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, Operation $operation = null, array &$context = []): void;
5957

src/Doctrine/Odm/Filter/DateFilter.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,6 @@ protected function filterProperty(string $property, $values, Builder $aggregatio
194194

195195
/**
196196
* Adds the match stage according to the chosen null management.
197-
*
198-
* @param mixed $value
199197
*/
200198
private function addMatch(Builder $aggregationBuilder, string $field, string $operator, $value, string $nullManagement = null): void
201199
{

src/Doctrine/Orm/Filter/AbstractFilter.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q
4646

4747
/**
4848
* Passes a property through the filter.
49-
*
50-
* @param mixed $value
5149
*/
5250
abstract protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void;
5351

src/Elasticsearch/Serializer/ItemNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public function getSupportedTypes($format): array
111111
];
112112
}
113113

114-
return DocumentNormalizer::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : [];
114+
return DocumentNormalizer::FORMAT !== $format ? $this->decorated->getSupportedTypes($format) : [];
115115
}
116116

117117
/**

src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ public function getSupportedTypes(?string $format): array
166166
}
167167
});
168168

169-
$this->assertEmpty($this->itemNormalizer->getSupportedTypes('json'));
170-
$this->assertSame(['*' => true], $this->itemNormalizer->getSupportedTypes($this->itemNormalizer::FORMAT));
169+
$this->assertEmpty($this->itemNormalizer->getSupportedTypes($this->itemNormalizer::FORMAT));
170+
$this->assertSame(['*' => true], $this->itemNormalizer->getSupportedTypes('json'));
171171
}
172172
}

src/GraphQl/ExecutorInterface.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ interface ExecutorInterface
2525
{
2626
/**
2727
* @see http://webonyx.github.io/graphql-php/executing-queries/#using-facade-method
28-
*
29-
* @param mixed $source
3028
*/
3129
public function executeQuery(Schema $schema, $source, mixed $rootValue = null, mixed $context = null, array $variableValues = null, string $operationName = null, callable $fieldResolver = null, array $validationRules = null): ExecutionResult;
3230
}

src/GraphQl/Serializer/ItemNormalizer.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2222
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2323
use ApiPlatform\Metadata\Util\ClassInfoTrait;
24+
use ApiPlatform\Serializer\CacheKeyTrait;
2425
use ApiPlatform\Serializer\ItemNormalizer as BaseItemNormalizer;
2526
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
2627
use Psr\Log\LoggerInterface;
@@ -37,12 +38,15 @@
3738
*/
3839
final class ItemNormalizer extends BaseItemNormalizer
3940
{
41+
use CacheKeyTrait;
4042
use ClassInfoTrait;
4143

4244
public const FORMAT = 'graphql';
4345
public const ITEM_RESOURCE_CLASS_KEY = '#itemResourceClass';
4446
public const ITEM_IDENTIFIERS_KEY = '#itemIdentifiers';
4547

48+
private array $safeCacheKeysCache = [];
49+
4650
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, private readonly IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, LoggerInterface $logger = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null)
4751
{
4852
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $logger ?: new NullLogger(), $resourceMetadataCollectionFactory, $resourceAccessChecker);
@@ -81,7 +85,11 @@ public function normalize(mixed $object, string $format = null, array $context =
8185
return parent::normalize($object, $format, $context);
8286
}
8387

84-
unset($context['operation_name'], $context['operation']);
88+
if ($this->isCacheKeySafe($context)) {
89+
$context['cache_key'] = $this->getCacheKey($format, $context);
90+
}
91+
92+
unset($context['operation_name'], $context['operation']); // Remove operation and operation_name only when cache key has been created
8593
$data = parent::normalize($object, $format, $context);
8694
if (!\is_array($data)) {
8795
throw new UnexpectedValueException('Expected data to be an array.');
@@ -140,4 +148,32 @@ protected function setAttributeValue($object, $attribute, $value, $format = null
140148

141149
parent::setAttributeValue($object, $attribute, $value, $format, $context);
142150
}
151+
152+
/**
153+
* Check if any property contains a security grants, which makes the cache key not safe,
154+
* as allowed_properties can differ for 2 instances of the same object.
155+
*/
156+
private function isCacheKeySafe(array $context): bool
157+
{
158+
if (!isset($context['resource_class']) || !$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
159+
return false;
160+
}
161+
$resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']);
162+
if (isset($this->safeCacheKeysCache[$resourceClass])) {
163+
return $this->safeCacheKeysCache[$resourceClass];
164+
}
165+
$options = $this->getFactoryOptions($context);
166+
$propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
167+
168+
$this->safeCacheKeysCache[$resourceClass] = true;
169+
foreach ($propertyNames as $propertyName) {
170+
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
171+
if (null !== $propertyMetadata->getSecurity()) {
172+
$this->safeCacheKeysCache[$resourceClass] = false;
173+
break;
174+
}
175+
}
176+
177+
return $this->safeCacheKeysCache[$resourceClass];
178+
}
143179
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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\GraphQl\Tests\Fixtures\ApiResource;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\ApiResource;
18+
use Symfony\Component\Validator\Constraints as Assert;
19+
20+
/**
21+
* Secured resource.
22+
*
23+
* @author Kévin Dunglas <[email protected]>
24+
*/
25+
#[ApiResource]
26+
class SecuredDummy
27+
{
28+
private ?int $id = null;
29+
30+
/**
31+
* @var string The title
32+
*/
33+
#[Assert\NotBlank]
34+
private string $title;
35+
36+
/**
37+
* @var string The description
38+
*/
39+
private string $description = '';
40+
41+
/**
42+
* @var string Secret property, only readable/writable by owners
43+
*/
44+
#[ApiProperty(security: 'object == null or object.getOwner() == user', securityPostDenormalize: 'object.getOwner() == user')]
45+
private string $ownerOnlyProperty = '';
46+
47+
public function __construct()
48+
{
49+
}
50+
51+
public function getId(): ?int
52+
{
53+
return $this->id;
54+
}
55+
56+
public function getTitle(): ?string
57+
{
58+
return $this->title;
59+
}
60+
61+
public function setTitle(string $title): void
62+
{
63+
$this->title = $title;
64+
}
65+
66+
public function getDescription(): string
67+
{
68+
return $this->description;
69+
}
70+
71+
public function setDescription(string $description): void
72+
{
73+
$this->description = $description;
74+
}
75+
76+
public function getOwnerOnlyProperty(): ?string
77+
{
78+
return $this->ownerOnlyProperty;
79+
}
80+
81+
public function setOwnerOnlyProperty(?string $ownerOnlyProperty): void
82+
{
83+
$this->ownerOnlyProperty = $ownerOnlyProperty;
84+
}
85+
}

src/GraphQl/Tests/Serializer/ItemNormalizerTest.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\GraphQl\Serializer\ItemNormalizer;
1717
use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy;
18+
use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\SecuredDummy;
1819
use ApiPlatform\Metadata\ApiProperty;
1920
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
2021
use ApiPlatform\Metadata\IriConverterInterface;
@@ -23,6 +24,7 @@
2324
use ApiPlatform\Metadata\Property\PropertyNameCollection;
2425
use ApiPlatform\Metadata\ResourceClassResolverInterface;
2526
use ApiPlatform\Metadata\UrlGeneratorInterface;
27+
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
2628
use PHPUnit\Framework\TestCase;
2729
use Prophecy\Argument;
2830
use Prophecy\PhpUnit\ProphecyTrait;
@@ -115,6 +117,95 @@ public function testNormalize(): void
115117
];
116118
$this->assertEquals($expected, $normalizer->normalize($dummy, ItemNormalizer::FORMAT, [
117119
'resources' => [],
120+
'resource_class' => Dummy::class,
121+
]));
122+
}
123+
124+
public function testNormalizeWithUnsafeCacheProperty(): void
125+
{
126+
$securedDummyWithOwnerOnlyPropertyAllowed = new SecuredDummy();
127+
$securedDummyWithOwnerOnlyPropertyAllowed->setTitle('hello');
128+
$securedDummyWithOwnerOnlyPropertyAllowed->setOwnerOnlyProperty('ownerOnly');
129+
$securedDummyWithoutOwnerOnlyPropertyAllowed = clone $securedDummyWithOwnerOnlyPropertyAllowed;
130+
$securedDummyWithoutOwnerOnlyPropertyAllowed->setTitle('hello from secured dummy');
131+
132+
$propertyNameCollection = new PropertyNameCollection(['title', 'ownerOnlyProperty']);
133+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
134+
$propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn($propertyNameCollection);
135+
136+
$unsecuredPropertyMetadata = (new ApiProperty())->withReadable(true);
137+
$securedPropertyMetadata = (new ApiProperty())->withReadable(true)->withSecurity('object == null or object.getOwner() == user');
138+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
139+
$propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn($unsecuredPropertyMetadata);
140+
$propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn($securedPropertyMetadata);
141+
142+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
143+
$iriConverterProphecy->getIriFromResource($securedDummyWithOwnerOnlyPropertyAllowed, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/1');
144+
$iriConverterProphecy->getIriFromResource($securedDummyWithoutOwnerOnlyPropertyAllowed, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/2');
145+
146+
$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
147+
$identifiersExtractorProphecy->getIdentifiersFromItem($securedDummyWithOwnerOnlyPropertyAllowed, Argument::any())->willReturn(['id' => 1])->shouldBeCalled();
148+
$identifiersExtractorProphecy->getIdentifiersFromItem($securedDummyWithoutOwnerOnlyPropertyAllowed, Argument::any())->willReturn(['id' => 2])->shouldBeCalled();
149+
150+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
151+
$resourceClassResolverProphecy->getResourceClass($securedDummyWithOwnerOnlyPropertyAllowed, null)->willReturn(SecuredDummy::class);
152+
$resourceClassResolverProphecy->getResourceClass($securedDummyWithoutOwnerOnlyPropertyAllowed, null)->willReturn(SecuredDummy::class);
153+
$resourceClassResolverProphecy->getResourceClass(null, SecuredDummy::class)->willReturn(SecuredDummy::class);
154+
$resourceClassResolverProphecy->isResourceClass(SecuredDummy::class)->willReturn(true);
155+
156+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
157+
$serializerProphecy->willImplement(NormalizerInterface::class);
158+
$serializerProphecy->normalize('hello', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('hello');
159+
$serializerProphecy->normalize('hello from secured dummy', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('hello from secured dummy');
160+
$serializerProphecy->normalize('ownerOnly', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('ownerOnly');
161+
162+
$resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class);
163+
$resourceAccessCheckerProphecy->isGranted(
164+
SecuredDummy::class,
165+
'object == null or object.getOwner() == user',
166+
Argument::type('array')
167+
)->will(function (array $args) {
168+
return 'hello' === $args[2]['object']->getTitle(); // Allow access only for securedDummyWithOwnerOnlyPropertyAllowed
169+
});
170+
171+
$normalizer = new ItemNormalizer(
172+
$propertyNameCollectionFactoryProphecy->reveal(),
173+
$propertyMetadataFactoryProphecy->reveal(),
174+
$iriConverterProphecy->reveal(),
175+
$identifiersExtractorProphecy->reveal(),
176+
$resourceClassResolverProphecy->reveal(),
177+
null,
178+
null,
179+
null,
180+
null,
181+
null,
182+
$resourceAccessCheckerProphecy->reveal()
183+
);
184+
$normalizer->setSerializer($serializerProphecy->reveal());
185+
186+
$expected = [
187+
'title' => 'hello',
188+
'ownerOnlyProperty' => 'ownerOnly',
189+
ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => SecuredDummy::class,
190+
ItemNormalizer::ITEM_IDENTIFIERS_KEY => [
191+
'id' => 1,
192+
],
193+
];
194+
$this->assertEquals($expected, $normalizer->normalize($securedDummyWithOwnerOnlyPropertyAllowed, ItemNormalizer::FORMAT, [
195+
'resources' => [],
196+
'resource_class' => SecuredDummy::class,
197+
]));
198+
199+
$expected = [
200+
'title' => 'hello from secured dummy',
201+
ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => SecuredDummy::class,
202+
ItemNormalizer::ITEM_IDENTIFIERS_KEY => [
203+
'id' => 2,
204+
],
205+
];
206+
$this->assertEquals($expected, $normalizer->normalize($securedDummyWithoutOwnerOnlyPropertyAllowed, ItemNormalizer::FORMAT, [
207+
'resources' => [],
208+
'resource_class' => SecuredDummy::class,
118209
]));
119210
}
120211

src/Metadata/Tests/Property/Factory/SerializerPropertyMetadataFactoryTest.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@ public static function groupsProvider(): array
4242

4343
/**
4444
* @dataProvider groupsProvider
45-
*
46-
* @param mixed $readGroups
47-
* @param mixed $writeGroups
4845
*/
4946
public function testCreate($readGroups, $writeGroups): void
5047
{

src/Metadata/Tests/Property/PropertyInfoPropertyNameCollectionFactoryTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,11 @@ public function testCreateMethodReturnsProperPropertyNameCollectionForObjectWith
9393
$collection = $factory->create(DummyIgnoreProperty::class, ['serializer_groups' => ['dummy']]);
9494

9595
self::assertCount(1, $collection);
96-
self::assertNotContains('ignored', $collection);
96+
self::assertNotContains('ignored', (array) $collection);
9797

9898
$collection = $factory->create(DummyIgnoreProperty::class);
9999

100100
self::assertCount(2, $collection);
101-
self::assertNotContains('ignored', $collection);
101+
self::assertNotContains('ignored', (array) $collection);
102102
}
103103
}

0 commit comments

Comments
 (0)