10000 Merge branch '6.1' into 6.2 · symfony/symfony@e1581a0 · GitHub
[go: up one dir, main page]

Skip to content

Commit e1581a0

Browse files
committed
Merge branch '6.1' into 6.2
* 6.1: [Security][AbstractToken] getUserIdentifier() must return a string [HttpFoundation] Prevent accepted rate limits with no remaining token to be preferred over denied ones Email image parts: regex for single closing quote [Serializer] Throw InvalidArgumentException if the data needed in the constructor doesn't belong to a backedEnum
2 parents 5b9c1c3 + 7d81056 commit e1581a0

File tree

10 files changed

+201
-10
lines changed

10 files changed

+201
-10
lines changed

src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ private function doConsume(Request $request, int $tokens): RateLimit
4545
foreach ($limiters as $limiter) {
4646
$rateLimit = $limiter->consume($tokens);
4747

48-
if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) {
49-
$minimalRateLimit = $rateLimit;
50-
}
48+
$minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit;
5149
}
5250

5351
return $minimalRateLimit;
@@ -64,4 +62,20 @@ public function reset(Request $request): void
6462
* @return LimiterInterface[] a set of limiters using keys extracted from the request
6563
*/
6664
abstract protected function getLimiters(Request $request): array;
65+
66+
private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit
67+
{
68+
if ($first->isAccepted() !== $second->isAccepted()) {
69+
return $first->isAccepted() ? $second : $first;
70+
}
71+
72+
$firstRemainingTokens = $first->getRemainingTokens();
73+
$secondRemainingTokens = $second->getRemainingTokens();
74+
75+
if ($firstRemainingTokens === $secondRemainingTokens) {
76+
return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first;
77+
}
78+
79+
return $firstRemainingTokens > $secondRemainingTokens ? $second : $first;
80+
}
6781
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
namespace Symfony\Component\HttpFoundation\Tests\RateLimiter;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\H B41A ttpFoundation\Request;
16+
use Symfony\Component\RateLimiter\LimiterInterface;
17+
use Symfony\Component\RateLimiter\RateLimit;
18+
19+
class AbstractRequestRateLimiterTest extends TestCase
20+
{
21+
/**
22+
* @dataProvider provideRateLimits
23+
*/
24+
public function testConsume(array $rateLimits, ?RateLimit $expected)
25+
{
26+
$rateLimiter = new MockAbstractRequestRateLimiter(array_map(function (RateLimit $rateLimit) {
27+
$limiter = $this->createStub(LimiterInterface::class);
28+
$limiter->method('consume')->willReturn($rateLimit);
29+
30+
return $limiter;
31+
}, $rateLimits));
32+
33+
$this->assertSame($expected, $rateLimiter->consume(new Request()));
34+
}
35+
36+
public function provideRateLimits()
37+
{
38+
$now = new \DateTimeImmutable();
39+
40+
yield 'Both accepted with different count of remaining tokens' => [
41+
[
42+
$expected = new RateLimit(0, $now, true, 1), // less remaining tokens
43+
new RateLimit(1, $now, true, 1),
44+
],
45+
$expected,
46+
];
47+
48+
yield 'Both accepted with same count of remaining tokens' => [
49+
[
50+
$expected = new RateLimit(0, $now->add(new \DateInterval('P1D')), true, 1), // longest wait time
51+
new RateLimit(0, $now, true, 1),
52+
],
53+
$expected,
54+
];
55+
56+
yield 'Accepted and denied' => [
57+
[
58+
new RateLimit(0, $now, true, 1),
59+
$expected = new RateLimit(0, $now, false, 1), // denied
60+
],
61+
$expected,
62+
];
63+
}
64+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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+
namespace Symfony\Component\HttpFoundation\Tests\RateLimiter;
13+
14+
use Symfony\Component\HttpFoundation\RateLimiter\AbstractRequestRateLimiter;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\RateLimiter\LimiterInterface;
17+
18+
class MockAbstractRequestRateLimiter extends AbstractRequestRateLimiter
19+
{
20+
/**
21+
* @var LimiterInterface[]
22+
*/
23+
private $limiters;
24+
25+
public function __construct(array $limiters)
26+
{
27+
$this->limiters = $limiters;
28+
}
29+
30+
protected function getLimiters(Request $request): array
31+
{
32+
return $this->limiters;
33+
}
34+
}

src/Symfony/Component/HttpFoundation/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"symfony/dependency-injection": "^5.4|^6.0",
2727
"symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4",
2828
"symfony/mime": "^5.4|^6.0",
29-
"symfony/expression-language": "^5.4|^6.0"
29+
"symfony/expression-language": "^5.4|^6.0",
30+
"symfony/rate-limiter": "^5.2|^6.0"
3031
},
3132
"suggest" : {
3233
"symfony/mime": "To use the file extension guesser"

src/Symfony/Component/Mime/Email.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -490,8 +490,8 @@ private function prepareParts(): ?array
490490
$html = $htmlPart->getBody();
491491

492492
$regexes = [
493-
'<img\s+[^>]*src\s*=\s*(?:([\'"])cid:([^"]+)\\1|cid:([^>\s]+))',
494-
'<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:([^"]+)\\1|cid:([^>\s]+))',
493+
'<img\s+[^>]*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))',
494+
'<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))',
495495
];
496496
$tmpMatches = [];
497497
foreach ($regexes as $regex) {

src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function getRoleNames(): array
4848

4949
public function getUserIdentifier(): string
5050
{
51-
return $this->user->getUserIdentifier();
51+
return $this->user ? $this->user->getUserIdentifier() : '';
5252
}
5353

5454
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function denormalize(mixed $data, string $type, string $format = null, ar
6060
try {
6161
return $type::from($data);
6262
} catch (\ValueError $e) {
63-
throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true, $e->getCode(), $e);
63+
throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type);
6464
}
6565
}
6666

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symfony\Component\Serializer\Tests\Fixtures;
4+
5+
use Symfony\Component\Serializer\Tests\Fixtures\StringBackedEnumDummy;
6+
7+
class DummyObjectWithEnumConstructor
8+
{
9+
public function __construct(public StringBackedEnumDummy $get)
10+
{
11+
}
12+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ public function testDenormalizeObjectThrowsException()
8888

8989
public function testDenormalizeBadBackingValueThrowsException()
9090
{
91-
$this->expectException(NotNormalizableValueException::class);
92-
$this->expectExceptionMessage('"POST" is not a valid backing value for enum "'.StringBackedEnumDummy::class.'"');
91+
$this->expectException(InvalidArgumentException::class);
92+
$this->expectExceptionMessage('The data must belong to a backed enumeration of type '.StringBackedEnumDummy::class);
93+
9394
$this->normalizer->denormalize('POST', StringBackedEnumDummy::class);
9495
}
9596

src/Symfony/Component/Serializer/Tests/SerializerTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
3737
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
3838
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
39+
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
3940
use Symfony\Component\Serializer\Normalizer\CustomNormalizer;
4041
use Symfony\Component\Serializer\Normalizer\DataUriNormalizer;
4142
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
@@ -58,6 +59,7 @@
5859
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface;
5960
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne;
6061
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo;
62+
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
6163
use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy;
6264
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
6365
use Symfony\Component\Serializer\Tests\Fixtures\Php74Full;
@@ -1181,6 +1183,69 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa
11811183
$this->assertSame($expected, $exceptionsAsArray);
11821184
}
11831185

1186+
/**
1187+
* @requires PHP 8.1
1188+
*/
1189+
public function testCollectDenormalizationErrorsWithEnumConstructor()
1190+
{
1191+
$serializer = new Serializer(
1192+
[
1193+
new BackedEnumNormalizer(),
1194+
new ObjectNormalizer(),
1195+
],
1196+
['json' => new JsonEncoder()]
1197+
);
1198+
1199+
try {
1200+
$serializer->deserialize('{"invalid": "GET"}', DummyObjectWithEnumConstructor::class, 'json', [
1201+
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
1202+
]);
1203+
} catch (\Throwable $th) {
1204+
$this->assertInstanceOf(PartialDenormalizationException::class, $th);
1205+
}
1206+
1207+
$exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array {
1208+
return [
1209+
'currentType' => $e->getCurrentType(),
1210+
'useMessageForUser' => $e->canUseMessageForUser(),
1211+
'message' => $e->getMessage(),
1212+
];
1213+
}, $th->getErrors());
1214+
1215+
$expected = [
1216+
[
1217+
'currentType' => 'array',
1218+
'useMessageForUser' => true,
1219+
'message' => 'Failed to create object because the class misses the "get" property.',
1220+
],
1221+
];
1222+
1223+
$this->assertSame($expected, $exceptionsAsArray);
1224+
}
1225+
1226+
/**
1227+
* @requires PHP 8.1
1228+
*/
1229+
public function testNoCollectDenormalizationErrorsWithWrongEnum()
1230+
{
1231+
$serializer = new Serializer(
1232+
[
1233+
new BackedEnumNormalizer(),
1234+
new ObjectNormalizer(),
1235+
],
1236+
['json' => new JsonEncoder()]
1237+
);
1238+
1239+
try {
1240+
$serializer->deserialize('{"get": "invalid"}', DummyObjectWithEnumConstructor::class, 'json', [
1241+
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
1242+
]);
1243+
} catch (\Throwable $th) {
1244+
$this->assertNotInstanceOf(PartialDenormalizationException::class, $th);
1245+
$this->assertInstanceOf(InvalidArgumentException::class, $th);
1246+
}
1247+
}
1248+
11841249
public function provideCollectDenormalizationErrors()
11851250
{
11861251
return [

0 commit comments

Comments
 (0)
0