Skip to content

Commit 900d034

Browse files
mtarldmathias-drdata
authored andcommitted
[Serializer] Fix unexpected allowed attributes
1 parent 998fa9d commit 900d034

File tree

6 files changed

+194
-15
lines changed

6 files changed

+194
-15
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@
137137
service('property_info')->ignoreOnInvalid(),
138138
service('serializer.mapping.class_discriminator_resolver')->ignoreOnInvalid(),
139139
null,
140+
[],
141+
service('property_info')->ignoreOnInvalid(),
140142
])
141143

142144
->alias(PropertyNormalizer::class, 'serializer.normalizer.property')

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ public function supportsNormalization($data, ?string $format = null)
147147
*/
148148
public function normalize($object, ?string $format = null, array $context = [])
149149
{
150+
$context['_read_attributes'] = true;
151+
150152
if (!isset($context['cache_key'])) {
151153
$context['cache_key'] = $this->getCacheKey($format, $context);
152154
}
@@ -359,6 +361,8 @@ public function supportsDenormalization($data, string $type, ?string $format = n
359361
*/
360362
public function denormalize($data, string $type, ?string $format = null, array $context = [])
361363
{
364+
$context['_read_attributes'] = false;
365+
362366
if (!isset($context['cache_key'])) {
363367
$context['cache_key'] = $this->getCacheKey($format, $context);
364368
}

src/Symfony/Component/Serializer/Normalizer/GetSetMethodNormalizer.php

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,23 @@
3636
*/
3737
class GetSetMethodNormalizer extends AbstractObjectNormalizer
3838
{
39+
private static $reflectionCache = [];
3940
private static $setterAccessibleCache = [];
4041

4142
/**
4243
* {@inheritdoc}
4344
*/
4445
public function supportsNormalization($data, ?string $format = null)
4546
{
46-
return parent::supportsNormalization($data, $format) && $this->supports(\get_class($data));
47+
return parent::supportsNormalization($data, $format) && $this->supports(\get_class($data), true);
4748
}
4849

4950
/**
5051
* {@inheritdoc}
5152
*/
5253
public function supportsDenormalization($data, string $type, ?string $format = null)
5354
{
54-
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type);
55+
return parent::supportsDenormalization($data, $type, $format) && $this->supports($type, false);
5556
}
5657

5758
/**
@@ -63,18 +64,22 @@ public function hasCacheableSupportsMethod(): bool
6364
}
6465

6566
/**
66-
* Checks if the given class has any getter method.
67+
* Checks if the given class has any getter or setter method.
6768
*/
68-
private function supports(string $class): bool
69+
private function supports(string $class, bool $readAttributes): bool
6970
{
7071
if (null !== $this->classDiscriminatorResolver && $this->classDiscriminatorResolver->getMappingForClass($class)) {
7172
return true;
7273
}
7374

74-
$class = new \ReflectionClass($class);
75-
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
76-
foreach ($methods as $method) {
77-
if ($this->isGetMethod($method)) {
75+
if (!isset(self::$reflectionCache[$class])) {
76+
self::$reflectionCache[$class] = new \ReflectionClass($class);
77+
}
78+
79+
$reflection = self::$reflectionCache[$class];
80+
81+
foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
82+
if ($readAttributes ? $this->isGetMethod($reflectionMethod) : $this->isSetMethod($reflectionMethod)) {
7883
return true;
7984
}
8085
}
@@ -95,6 +100,17 @@ private function isGetMethod(\ReflectionMethod $method): bool
95100
);
96101
}
97102

103+
/**
104+
* Checks if a method's name matches /^set.+$/ and can be called non-statically with one parameter.
105+
*/
106+
private function isSetMethod(\ReflectionMethod $method): bool
107+
{
108+
return !$method->isStatic()
109+
&& (\PHP_VERSION_ID < 80000 || !$method->getAttributes(Ignore::class))
110+
&& 1 === $method->getNumberOfRequiredParameters()
111+
&& str_starts_with($method->name, 'set');
112+
}
113+
98114
/**
99115
* {@inheritdoc}
100116
*/
@@ -124,19 +140,17 @@ protected function extractAttributes(object $object, ?string $format = null, arr
124140
*/
125141
protected function getAttributeValue(object $object, string $attribute, ?string $format = null, array $context = [])
126142
{
127-
$ucfirsted = ucfirst($attribute);
128-
129-
$getter = 'get'.$ucfirsted;
143+
$getter = 'get'.$attribute;
130144
if (method_exists($object, $getter) && \is_callable([$object, $getter])) {
131145
return $object->$getter();
132146
}
133147

134-
$isser = 'is'.$ucfirsted;
148+
$isser = 'is'.$attribute;
135149
if (method_exists($object, $isser) && \is_callable([$object, $isser])) {
136150
return $object->$isser();
137151
}
138152

139-
$haser = 'has'.$ucfirsted;
153+
$haser = 'has'.$attribute;
140154
if (method_exists($object, $haser) && \is_callable([$object, $haser])) {
141155
return $object->$haser();
142156
}
@@ -149,7 +163,7 @@ protected function getAttributeValue(object $object, string $attribute, ?string
149163
*/
150164
protected function setAttributeValue(object $object, string $attribute, $value, ?string $format = null, array $context = [])
151165
{
152-
$setter = 'set'.ucfirst($attribute);
166+
$setter = 'set'.$attribute;
153167
$key = \get_class($object).':'.$setter;
154168

155169
if (!isset(self::$setterAccessibleCache[$key])) {
@@ -160,4 +174,48 @@ protected function setAttributeValue(object $object, string $attribute, $value,
160174
$object->$setter($value);
161175
}
162176
}
177+
178+
protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = [])
179+
{
180+
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
181+
return false;
182+
}
183+
184+
$class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
185+
186+
if (!isset(self::$reflectionCache[$class])) {
187+
self::$reflectionCache[$class] = new \ReflectionClass($class);
188+
}
189+
190+
$reflection = self::$reflectionCache[$class];
191+
192+
if ($context['_read_attributes'] ?? true) {
193+
foreach (['get', 'is', 'has'] as $getterPrefix) {
194+
$getter = $getterPrefix.$attribute;
195+
$reflectionMethod = $reflection->hasMethod($getter) ? $reflection->getMethod($getter) : null;
196+
if ($reflectionMethod && $this->isGetMethod($reflectionMethod)) {
197+
return true;
198+
}
199+
}
200+
201+
return false;
202+
}
203+
204+
$setter = 'set'.$attribute;
205+
if ($reflection->hasMethod($setter) && $this->isSetMethod($reflection->getMethod($setter))) {
206+
return true;
207+
}
208+
209+
$constructor = $reflection->getConstructor();
210+
211+
if ($constructor && $constructor->isPublic()) {
212+
foreach ($constructor->getParameters() as $parameter) {
213+
if ($parameter->getName() === $attribute) {
214+
return true;
215+
}
216+
}
217+
}
218+
219+
return false;
220+
}
163221
}

src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1515
use Symfony\Component\PropertyAccess\PropertyAccess;
1616
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
17+
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
18+
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
1719
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
20+
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
21+
use Symfony\Component\Serializer\Annotation\Ignore;
1822
use Symfony\Component\Serializer\Exception\LogicException;
1923
use Symfony\Component\Serializer\Mapping\AttributeMetadata;
2024
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
@@ -28,11 +32,14 @@
2832
*/
2933
class ObjectNormalizer extends AbstractObjectNormalizer
3034
{
35+
private static $reflectionCache = [];
36+
3137
protected $propertyAccessor;
38+
protected $propertyInfoExtractor;
3239

3340
private $objectClassResolver;
3441

35-
public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, ?PropertyAccessorInterface $propertyAccessor = null, ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, array $defaultContext = [])
42+
public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory = null, ?NameConverterInterface $nameConverter = null, ?PropertyAccessorInterface $propertyAccessor = null, ?PropertyTypeExtractorInterface $propertyTypeExtractor = null, ?ClassDiscriminatorResolverInterface $classDiscriminatorResolver = null, ?callable $objectClassResolver = null, array $defaultContext = [], ?PropertyInfoExtractorInterface $propertyInfoExtractor = null)
3643
{
3744
if (!class_exists(PropertyAccess::class)) {
3845
throw new LogicException('The ObjectNormalizer class requires the "PropertyAccess" component. Install "symfony/property-access" to use it.');
@@ -45,6 +52,8 @@ public function __construct(?ClassMetadataFactoryInterface $classMetadataFactory
4552
$this->objectClassResolver = $objectClassResolver ?? function ($class) {
4653
return \is_object($class) ? \get_class($class) : $class;
4754
};
55+
56+
$this->propertyInfoExtractor = $propertyInfoExtractor ?: new ReflectionExtractor();
4857
}
4958

5059
/**
@@ -182,4 +191,38 @@ protected function getAllowedAttributes($classOrObject, array $context, bool $at
182191

183192
return $allowedAttributes;
184193
}
194+
195+
protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = [])
196+
{
197+
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
198+
return false;
199+
}
200+
$class = \is_object($classOrObject) ? \get_class($classOrObject) : $classOrObject;
201+
202+
if ($context['_read_attributes'] ?? true) {
203+
return $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute);
204+
}
205+
206+
return $this->propertyInfoExtractor->isWritable($class, $attribute)
207+
|| ($writeInfo = $this->propertyInfoExtractor->getWriteInfo($class, $attribute)) && PropertyWriteInfo::TYPE_NONE !== $writeInfo->getType();
208+
}
209+
210+
private function hasAttributeAccessorMethod(string $class, string $attribute): bool
211+
{
212+
if (!isset(self::$reflectionCache[$class])) {
213+
self::$reflectionCache[$class] = new \ReflectionClass($class);
214+
}
215+
216+
$reflection = self::$reflectionCache[$class];
217+
218+
if (!$reflection->hasMethod($attribute)) {
219+
return false;
220+
}
221+
222+
$method = $reflection->getMethod($attribute);
223+
224+
return !$method->isStatic()
225+
&& (\PHP_VERSION_ID < 80000 || !$method->getAttributes(Ignore::class))
226+
&& !$method->getNumberOfRequiredParameters();
227+
}
185228
}

src/Symfony/Component/Serializer/Tests/Normalizer/GetSetMethodNormalizerTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,23 @@ public function testDenormalizeWithDiscriminator()
521521

522522
$this->assertEquals($denormalized, $normalizer->denormalize(['type' => 'two', 'url' => 'url'], GetSetMethodDummyInterface::class));
523523
}
524+
525+
public function testSupportsAndNormalizeWithOnlyParentGetter()
526+
{
527+
$obj = new GetSetDummyChild();
528+
$obj->setFoo('foo');
529+
530+
$this->assertTrue($this->normalizer->supportsNormalization($obj));
531+
$this->assertSame(['foo' => 'foo'], $this->normalizer->normalize($obj));
532+
}
533+
534+
public function testSupportsAndDenormalizeWithOnlyParentSetter()
535+
{
536+
$this->assertTrue($this->normalizer->supportsDenormalization(['foo' => 'foo'], GetSetDummyChild::class));
537+
538+
$obj = $this->normalizer->denormalize(['foo' => 'foo'], GetSetDummyChild::class);
539+
$this->assertSame('foo', $obj->getFoo());
540+
}
524541
}
525542

526543
class GetSetDummy
@@ -825,3 +842,22 @@ public function setUrl(string $url): void
825842
$this->url = $url;
826843
}
827844
}
845+
846+
class GetSetDummyChild extends GetSetDummyParent
847+
{
848+
}
849+
850+
class GetSetDummyParent
851+
{
852+
private $foo;
853+
854+
public function getFoo()
855+
{
856+
return $this->foo;
857+
}
858+
859+
public function setFoo($foo)
860+
{
861+
$this->foo = $foo;
862+
}
863+
}

src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
1919
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
2020
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
21+
use Symfony\Component\Serializer\Annotation\Ignore;
2122
use Symfony\Component\Serializer\Exception\LogicException;
2223
use Symfony\Component\Serializer\Exception\RuntimeException;
2324
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
@@ -919,6 +920,31 @@ public function testSamePropertyAsMethodWithMethodSerializedName()
919920

920921
$this->assertSame($expected, $this->normalizer->normalize($object));
921922
}
923+
924+
public function testNormalizeWithIgnoreAnnotationAndPrivateProperties()
925+
{
926+
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
927+
$normalizer = new ObjectNormalizer($classMetadataFactory);
928+
929+
$this->assertSame(['foo' => 'foo'], $normalizer->normalize(new ObjectDummyWithIgnoreAnnotationAndPrivateProperty()));
930+
}
931+
932+
public function testDenormalizeWithIgnoreAnnotationAndPrivateProperties()
933+
{
934+
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
935+
$normalizer = new ObjectNormalizer($classMetadataFactory);
936+
937+
$obj = $normalizer->denormalize([
938+
'foo' => 'set',
939+
'ignore' => 'set',
940+
'private' => 'set',
941+
], ObjectDummyWithIgnoreAnnotationAndPrivateProperty::class);
942+
943+
$expected = new ObjectDummyWithIgnoreAnnotationAndPrivateProperty();
944+
$expected->foo = 'set';
945+
946+
$this->assertEquals($expected, $obj);
947+
}
922948
}
923949

924950
class ProxyObjectDummy extends ObjectDummy
@@ -1207,3 +1233,13 @@ public function getInner()
12071233
return $this->inner;
12081234
}
12091235
}
1236+
1237+
class ObjectDummyWithIgnoreAnnotationAndPrivateProperty
1238+
{
1239+
public $foo = 'foo';
1240+
1241+
/** @Ignore */
1242+
public $ignored = 'ignored';
1243+
1244+
private $private = 'private';
1245+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy