Skip to content

Commit fabee6f

Browse files
committed
fix(doctrine): handle invalid uuid in ORM SearchFilter
1 parent 985a9a0 commit fabee6f

10 files changed

+295
-22
lines changed

src/Doctrine/Common/Filter/SearchFilterTrait.php

+35-2
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ abstract protected function normalizePropertyName(string $property): string;
122122
*/
123123
protected function getIdFromValue(string $value): mixed
124124
{
125+
if (is_numeric($value)) {
126+
return $value;
127+
}
128+
129+
if ($this->isValidUuid($value)) {
130+
return $value;
131+
}
132+
125133
try {
126134
$iriConverter = $this->getIriConverter();
127135
$item = $iriConverter->getResourceFromIri($value, ['fetch_data' => false]);
@@ -163,16 +171,41 @@ protected function normalizeValues(array $values, string $property): ?array
163171
}
164172

165173
/**
166-
* When the field should be an integer, check that the given value is a valid one.
174+
* Check if the values are valid for the given Doctrine type.
167175
*/
168176
protected function hasValidValues(array $values, ?string $type = null): bool
169177
{
170178
foreach ($values as $value) {
171-
if (null !== $value && \in_array($type, (array) self::DOCTRINE_INTEGER_TYPE, true) && false === filter_var($value, \FILTER_VALIDATE_INT)) {
179+
if (null === $value) {
180+
continue;
181+
}
182+
183+
if (\in_array($type, (array) self::DOCTRINE_INTEGER_TYPE, true) && false === filter_var($value, \FILTER_VALIDATE_INT)) {
184+
return false;
185+
}
186+
187+
if (\in_array($type, (array) self::DOCTRINE_UUID_TYPE, true) && false === $this->isValidUuid($value)) {
172188
return false;
173189
}
174190
}
175191

176192
return true;
177193
}
194+
195+
protected function isValidUuid(mixed $value): bool
196+
{
197+
if (!\is_string($value)) {
198+
return false;
199+
}
200+
201+
if (class_exists('\Symfony\Component\Uid\Uuid')) {
202+
return \Symfony\Component\Uid\Uuid::isValid($value);
203+
}
204+
205+
if (class_exists('\Ramsey\Uuid\Uuid')) {
206+
return \Ramsey\Uuid\Uuid::isValid($value);
207+
}
208+
209+
return 1 === preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value);
210+
}
178211
}

src/Doctrine/Odm/Filter/SearchFilter.php

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface
141141
use SearchFilterTrait;
142142

143143
public const DOCTRINE_INTEGER_TYPE = [MongoDbType::INTEGER, MongoDbType::INT];
144+
public const DOCTRINE_UUID_TYPE = [];
144145

145146
public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface|LegacyIriConverterInterface $iriConverter, IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null)
146147
{

src/Doctrine/Orm/Filter/SearchFilter.php

+6
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface
140140
use SearchFilterTrait;
141141

142142
public const DOCTRINE_INTEGER_TYPE = Types::INTEGER;
143+
public const DOCTRINE_UUID_TYPE = 'uuid';
143144

144145
public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface|LegacyIriConverterInterface $iriConverter, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor = null, ?NameConverterInterface $nameConverter = null)
145146
{
@@ -231,6 +232,11 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB
231232
if (is_numeric($value)) {
232233
return $value;
233234
}
235+
236+
if ($this->isValidUuid($value)) {
237+
return $value;
238+
}
239+
234240
try {
235241
$item = $this->getIriConverter()->getResourceFromIri($value, ['fetch_data' => false]);
236242

src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ protected function setUp(): void
4242
self::bootKernel();
4343

4444
$this->managerRegistry = self::$kernel->getContainer()->get('doctrine');
45-
$this->repository = $this->managerRegistry->getManagerForClass(Dummy::class)->getRepository(Dummy::class);
45+
$this->repository = $this->managerRegistry->getManagerForClass($this->resourceClass)->getRepository($this->resourceClass);
4646
}
4747

4848
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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\Doctrine\Orm\Tests\Filter;
15+
16+
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
17+
use ApiPlatform\Doctrine\Orm\Tests\DoctrineOrmFilterTestCase;
18+
use ApiPlatform\Doctrine\Orm\Tests\Fixtures\CustomConverter;
19+
use ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\UuidIdentifierDummy;
20+
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
21+
use ApiPlatform\Metadata\IriConverterInterface;
22+
use Doctrine\Persistence\ManagerRegistry;
23+
use Prophecy\Argument;
24+
use Prophecy\PhpUnit\ProphecyTrait;
25+
26+
/**
27+
* @author Oleksii Polyvanyi <[email protected]>
28+
*/
29+
class SearchFilterWithUuidTest extends DoctrineOrmFilterTestCase
30+
{
31+
use ProphecyTrait;
32+
33+
protected string $resourceClass = UuidIdentifierDummy::class;
34+
protected string $filterClass = SearchFilter::class;
35+
36+
public static function provideApplyTestData(): array
37+
{
38+
$filterFactory = self::buildSearchFilter(...);
39+
$validUuid = '9584fbef-e849-41e3-912b-f2c509874a70';
40+
41+
return [
42+
'invalid uuid for id' => [
43+
[
44+
'id' => 'exact',
45+
],
46+
[
47+
'id' => 'some-invalid-uuid',
48+
],
49+
'SELECT o FROM ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\UuidIdentifierDummy o',
50+
[],
51+
$filterFactory,
52+
],
53+
54+
'valid uuid for id' => [
55+
[
56+
'id' => 'exact',
57+
],
58+
[
59+
'id' => $validUuid,
60+
],
61+
'SELECT o FROM ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\UuidIdentifierDummy o WHERE o.id = :id_p1',
62+
['id_p1' => $validUuid],
63+
$filterFactory,
64+
],
65+
66+
'invalid uuid for uuidField' => [
67+
[
68+
'uuidField' => 'exact',
69+
],
70+
[
71+
'uuidField' => 'some-invalid-uuid',
72+
],
73+
'SELECT o FROM ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\UuidIdentifierDummy o',
74+
[],
75+
$filterFactory,
76+
],
77+
78+
'valid uuid for uuidField' => [
79+
[
80+
'uuidField' => 'exact',
81+
],
82+
[
83+
'uuidField' => $validUuid,
84+
],
85+
'SELECT o FROM ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\UuidIdentifierDummy o WHERE o.uuidField = :uuidField_p1',
86+
['uuidField_p1' => $validUuid],
87+
$filterFactory,
88+
],
89+
90+
'invalid uuid for relatedUuidIdentifierDummy' => [
91+
[
92+
'relatedUuidIdentifierDummy' => 'exact',
93+
],
94+
[
95+
'relatedUuidIdentifierDummy' => 'some-invalid-uuid',
96+
],
97+
'SELECT o FROM ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\UuidIdentifierDummy o',
98+
[],
99+
$filterFactory,
100+
],
101+
102+
'valid uuid for relatedUuidIdentifierDummy' => [
103+
[
104+
'relatedUuidIdentifierDummy' => 'exact',
105+
],
106+
[
107+
'relatedUuidIdentifierDummy' => $validUuid,
108+
],
109+
'SELECT o FROM ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\UuidIdentifierDummy o WHERE o.relatedUuidIdentifierDummy = :relatedUuidIdentifierDummy_p1',
110+
['relatedUuidIdentifierDummy_p1' => $validUuid],
111+
$filterFactory,
112+
],
113+
];
114+
}
115+
116+
protected static function buildSearchFilter(self $that, ManagerRegistry $managerRegistry, ?array $properties = null): SearchFilter
117+
{
118+
$iriConverterProphecy = $that->prophesize(IriConverterInterface::class);
119+
120+
$iriConverterProphecy->getResourceFromIri(Argument::type('string'), ['fetch_data' => false])->will(function (): void {
121+
throw new InvalidArgumentException();
122+
});
123+
124+
$iriConverter = $iriConverterProphecy->reveal();
125+
$propertyAccessor = static::$kernel->getContainer()->get('test.property_accessor');
126+
127+
return new SearchFilter($managerRegistry, $iriConverter, $propertyAccessor, null, $properties, null, new CustomConverter());
128+
}
129+
}

src/Doctrine/Orm/Tests/Fixtures/Entity/DummyCar.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ class DummyCar
4949
#[ORM\OneToMany(targetEntity: DummyCarColor::class, mappedBy: 'car')]
5050
private Collection|iterable|null $thirdColors = null;
5151
#[ApiFilter(SearchFilter::class, strategy: 'exact')]
52-
#[ORM\ManyToMany(targetEntity: UuidIdentifierDummy::class, indexBy: 'uuid')]
52+
#[ORM\ManyToMany(targetEntity: GuidIdentifierDummy::class, indexBy: 'guid')]
5353
#[ORM\JoinColumn(name: 'car_id', referencedColumnName: 'id_id')]
54-
#[ORM\InverseJoinColumn(name: 'uuid_uuid', referencedColumnName: 'uuid')]
55-
#[ORM\JoinTable(name: 'uuid_cars')]
54+
#[ORM\InverseJoinColumn(name: 'guid_guid', referencedColumnName: 'guid')]
55+
#[ORM\JoinTable(name: 'guid_cars')]
5656
private Collection|iterable|null $uuid = null;
5757

5858
#[ApiFilter(SearchFilter::class, strategy: 'partial')]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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\Doctrine\Orm\Tests\Fixtures\Entity;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use Doctrine\ORM\Mapping as ORM;
18+
19+
/**
20+
* Custom identifier dummy.
21+
*/
22+
#[ApiResource]
23+
#[ORM\Entity]
24+
class GuidIdentifierDummy
25+
{
26+
#[ORM\Column(type: 'guid')]
27+
#[ORM\Id]
28+
private ?string $guid = null;
29+
#[ORM\Column(length: 30)]
30+
private ?string $name = null;
31+
32+
public function getGuid(): ?string
33+
{
34+
return $this->guid;
35+
}
36+
37+
public function setGuid(string $guid): void
38+
{
39+
$this->guid = $guid;
40+
}
41+
42+
public function getName(): ?string
43+
{
44+
return $this->name;
45+
}
46+
47+
public function setName(string $name): void
48+
{
49+
$this->name = $name;
50+
}
51+
}
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\Doctrine\Orm\Tests\Fixtures\Entity;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use Doctrine\ORM\Mapping as ORM;
18+
use Symfony\Component\Uid\Uuid;
19+
20+
#[ApiResource]
21+
#[ORM\Entity]
22+
class RelatedUuidIdentifierDummy
23+
{
24+
#[ORM\Column(type: 'uuid')]
25+
#[ORM\Id]
26+
private Uuid $id;
27+
28+
public function getId(): Uuid
29+
{
30+
return $this->id;
31+
}
32+
33+
public function setId(Uuid $id): void
34+
{
35+
$this->id = $id;
36+
}
37+
}

0 commit comments

Comments
 (0)