Skip to content

Commit e21a01a

Browse files
fix(doctrine): iri converter injection in search filter (#4688)
* fix(doctrine): iri converter injection in search filter * fix: cass aliases for backward compatibility * fix: phpunit as dev dependency * revert: install phpunit * try not loading the class? * try not loading the class? * try not loading the class? * fix: typo in class name and xml identation * fix: rename misleading file * fix(metada): add aliases for backward compatibility * fix: load yaml bc metadata if installed * tes * move service declarations * fix: test case classes * deprec and alias * fix iri_converter * temp * fix searchfilter * fix tests * fix graphql not installed * fixes * fixes Co-authored-by: emmanuel <[email protected]>
1 parent 00d8ed0 commit e21a01a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1395
-532
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
"doctrine/mongodb-odm": "^2.2",
3939
"doctrine/mongodb-odm-bundle": "^4.0",
4040
"doctrine/orm": "^2.6.4",
41-
"friends-of-behat/mink-browserkit-driver": "^1.3.1",
4241
"elasticsearch/elasticsearch": "^7.11.0",
42+
"friends-of-behat/mink-browserkit-driver": "^1.3.1",
4343
"friends-of-behat/mink-extension": "^2.2",
4444
"friends-of-behat/symfony-extension": "^2.1",
4545
"guzzlehttp/guzzle": "^6.0 || ^7.0",

phpunit.xml.dist

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,12 @@
2929
<directory>features</directory>
3030
<directory>tests</directory>
3131
<directory>vendor</directory>
32-
<directory>src/Bridge/NelmioApiDoc</directory>
33-
<directory>src/Bridge/FosUser</directory>
34-
<directory>src/Bridge/Symfony/Maker/Resources/skeleton</directory>
35-
<directory>src/Core/Bridge/Rector</directory>
32+
<directory>src/Core/Bridge/NelmioApiDoc</directory>
33+
<directory>src/Core/Bridge/FosUser</directory>
34+
<directory>src/Core/Bridge/Symfony/Maker/Resources/skeleton</directory>
3635
<file>.php-cs-fixer.dist.php</file>
37-
<file>src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php</file>
3836
<file>src/Symfony/Bundle/Test/Constraint/ArraySubsetLegacy.php</file>
37+
<file>src/Core/Bridge/Symfony/Bundle/Test/Constraint/ArraySubsetLegacy.php</file>
3938
</exclude>
4039
</coverage>
4140

src/Core/Api/FilterInterface.php renamed to src/Api/FilterInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Core\Api;
14+
namespace ApiPlatform\Api;
1515

1616
/**
1717
* Filters applicable on a resource.

src/Api/FilterLocatorTrait.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace ApiPlatform\Api;
1515

1616
use ApiPlatform\Core\Api\FilterCollection;
17-
use ApiPlatform\Core\Api\FilterInterface;
1817
use ApiPlatform\Exception\InvalidArgumentException;
1918
use Psr\Container\ContainerInterface;
2019

src/Core/Api/ResourceClassResolverInterface.php renamed to src/Api/ResourceClassResolverInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Core\Api;
14+
namespace ApiPlatform\Api;
1515

1616
use ApiPlatform\Exception\InvalidArgumentException;
1717

src/Core/Annotation/ApiFilter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Annotation;
1515

16-
use ApiPlatform\Core\Api\FilterInterface;
16+
use ApiPlatform\Api\FilterInterface;
1717
use ApiPlatform\Exception\InvalidArgumentException;
1818

1919
/**
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
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\Bridge\Doctrine\MongoDbOdm\Filter;
15+
16+
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
17+
use ApiPlatform\Core\Api\IriConverterInterface;
18+
use ApiPlatform\Core\Exception\InvalidArgumentException;
19+
use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface;
20+
use ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait;
21+
use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter;
22+
use Doctrine\ODM\MongoDB\Aggregation\Builder;
23+
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as MongoDBClassMetadata;
24+
use Doctrine\ODM\MongoDB\Types\Type as MongoDbType;
25+
use Doctrine\Persistence\ManagerRegistry;
26+
use Doctrine\Persistence\Mapping\ClassMetadata;
27+
use MongoDB\BSON\Regex;
28+
use Psr\Log\LoggerInterface;
29+
use Symfony\Component\PropertyAccess\PropertyAccess;
30+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
31+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
32+
33+
/**
34+
* Filter the collection by given properties.
35+
*
36+
* @experimental
37+
*
38+
* @author Kévin Dunglas <[email protected]>
39+
* @author Alan Poulain <[email protected]>
40+
*/
41+
final class SearchFilter extends AbstractFilter implements SearchFilterInterface
42+
{
43+
use SearchFilterTrait;
44+
45+
public const DOCTRINE_INTEGER_TYPE = [MongoDbType::INTEGER, MongoDbType::INT];
46+
47+
public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, IdentifiersExtractorInterface $identifiersExtractor, PropertyAccessorInterface $propertyAccessor = null, LoggerInterface $logger = null, array $properties = null, NameConverterInterface $nameConverter = null)
48+
{
49+
parent::__construct($managerRegistry, $logger, $properties, $nameConverter);
50+
51+
$this->iriConverter = $iriConverter;
52+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
53+
$this->identifiersExtractor = $identifiersExtractor;
54+
}
55+
56+
protected function getIriConverter(): IriConverterInterface
57+
{
58+
return $this->iriConverter;
59+
}
60+
61+
protected function getPropertyAccessor(): PropertyAccessorInterface
62+
{
63+
return $this->propertyAccessor;
64+
}
65+
66+
/**
67+
* {@inheritdoc}
68+
*/
69+
protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, string $operationName = null, array &$context = [])
70+
{
71+
if (
72+
null === $value ||
73+
!$this->isPropertyEnabled($property, $resourceClass) ||
74+
!$this->isPropertyMapped($property, $resourceClass, true)
75+
) {
76+
return;
77+
}
78+
79+
$matchField = $field = $property;
80+
81+
$values = $this->normalizeValues((array) $value, $property);
82+
if (null === $values) {
83+
return;
84+
}
85+
86+
$associations = [];
87+
if ($this->isPropertyNested($property, $resourceClass)) {
88+
[$matchField, $field, $associations] = $this->addLookupsForNestedProperty($property, $aggregationBuilder, $resourceClass);
89+
}
90+
91+
$caseSensitive = true;
92+
$strategy = $this->properties[$property] ?? self::STRATEGY_EXACT;
93+
94+
// prefixing the strategy with i makes it case insensitive
95+
if (0 === strpos($strategy, 'i')) {
96+
$strategy = substr($strategy, 1);
97+
$caseSensitive = false;
98+
}
99+
100+
/** @var MongoDBClassMetadata */
101+
$metadata = $this->getNestedMetadata($resourceClass, $associations);
102+
103+
if ($metadata->hasField($field) && !$metadata->hasAssociation($field)) {
104+
if ('id' === $field) {
105+
$values = array_map([$this, 'getIdFromValue'], $values);
106+
}
107+
108+
if (!$this->hasValidValues($values, $this->getDoctrineFieldType($property, $resourceClass))) {
109+
$this->logger->notice('Invalid filter ignored', [
110+
'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $field)),
111+
]);
112+
113+
return;
114+
}
115+
116+
$this->addEqualityMatchStrategy($strategy, $aggregationBuilder, $field, $matchField, $values, $caseSensitive, $metadata);
117+
118+
return;
119+
}
120+
121+
// metadata doesn't have the field, nor an association on the field
122+
if (!$metadata->hasAssociation($field)) {
123+
return;
124+
}
125+
126+
$values = array_map([$this, 'getIdFromValue'], $values);
127+
$doctrineTypeField = $this->getDoctrineFieldType($property, $resourceClass);
128+
129+
if (null !== $this->identifiersExtractor) {
130+
$associationResourceClass = $metadata->getAssociationTargetClass($field);
131+
$associationFieldIdentifier = $this->identifiersExtractor->getIdentifiersFromResourceClass($associationResourceClass)[0];
132+
$doctrineTypeField = $this->getDoctrineFieldType($associationFieldIdentifier, $associationResourceClass);
133+
}
134+
135+
if (!$this->hasValidValues($values, $doctrineTypeField)) {
136+
$this->logger->notice('Invalid filter ignored', [
137+
'exception' => new InvalidArgumentException(sprintf('Values for field "%s" are not valid according to the doctrine type.', $property)),
138+
]);
139+
140+
return;
141+
}
142+
143+
$this->addEqualityMatchStrategy($strategy, $aggregationBuilder, $field, $matchField, $values, $caseSensitive, $metadata);
144+
}
145+
146+
/**
147+
* Add equality match stage according to the strategy.
148+
*
149+
* @param mixed $values
150+
*/
151+
private function addEqualityMatchStrategy(string $strategy, Builder $aggregationBuilder, string $field, string $matchField, $values, bool $caseSensitive, ClassMetadata $metadata): void
152+
{
153+
$inValues = [];
154+
foreach ($values as $inValue) {
155+
$inValues[] = $this->getEqualityMatchStrategyValue($strategy, $field, $inValue, $caseSensitive, $metadata);
156+
}
157+
158+
$aggregationBuilder
159+
->match()
160+
->field($matchField)
161+
->in($inValues);
162+
}
163+
164+
/**
165+
* Get equality match value according to the strategy.
166+
*
167+
* @param mixed $value
168+
*
169+
* @throws InvalidArgumentException If strategy does not exist
170+
*
171+
* @return Regex|string
172+
*/
173+
private function getEqualityMatchStrategyValue(string $strategy, string $field, $value, bool $caseSensitive, ClassMetadata $metadata)
174+
{
175+
$type = $metadata->getTypeOfField($field);
176+
177+
if (!MongoDbType::hasType($type)) {
178+
return $value;
179+
}
180+
if (MongoDbType::STRING !== $type) {
181+
return MongoDbType::getType($type)->convertToDatabaseValue($value);
182+
}
183+
184+
$quotedValue = preg_quote($value);
185+
186+
switch ($strategy) {
187+
case null:
188+
case self::STRATEGY_EXACT:
189+
return $caseSensitive ? $value : new Regex("^$quotedValue$", 'i');
190+
case self::STRATEGY_PARTIAL:
191+
return new Regex($quotedValue, $caseSensitive ? '' : 'i');
192+
case self::STRATEGY_START:
193+
return new Regex("^$quotedValue", $caseSensitive ? '' : 'i');
194+
case self::STRATEGY_END:
195+
return new Regex("$quotedValue$", $caseSensitive ? '' : 'i');
196+
case self::STRATEGY_WORD_START:
197+
return new Regex("(^$quotedValue.*|.*\s$quotedValue.*)", $caseSensitive ? '' : 'i');
198+
default:
199+
throw new InvalidArgumentException(sprintf('strategy %s does not exist.', $strategy));
200+
}
201+
}
202+
203+
/**
204+
* {@inheritdoc}
205+
*/
206+
protected function getType(string $doctrineType): string
207+
{
208+
switch ($doctrineType) {
209+
case MongoDbType::INT:
210+
case MongoDbType::INTEGER:
211+
return 'int';
212+
case MongoDbType::BOOL:
213+
case MongoDbType::BOOLEAN:
214+
return 'bool';
215+
case MongoDbType::DATE:
216+
return \DateTimeInterface::class;
217+
case MongoDbType::FLOAT:
218+
return 'float';
219+
}
220+
221+
return 'string';
222+
}
223+
}

0 commit comments

Comments
 (0)