8000 Merge branch '6.0' into 6.1 · symfony/symfony@b0daf10 · GitHub
[go: up one dir, main page]

Skip to content

Commit b0daf10

Browse files
Merge branch '6.0' into 6.1
* 6.0: [HttpClient] Fix computing retry delay when using RetryableHttpClient [Uid] Fix validating UUID variant bits [Validator][UID] Stop to first ULID format violation [Bridge] Fix mkdir() race condition in ProxyCacheWarmer [Cache] update readme Bug #42343 [Security] Fix valid remember-me token exposure to the second consequent request Prevent exception if request stack is empty Psr18Client ignore invalid HTTP headers skip a transient test on AppVeyor
2 parents 23e7c4a + 563ef98 commit b0daf10

File tree

22 files changed

+145
-33
lines changed

22 files changed

+145
-33
lines changed

src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function warmUp(string $cacheDir): array
5050
foreach ($this->registry->getManagers() as $em) {
5151
// we need the directory no matter the proxy cache generation strategy
5252
if (!is_dir($proxyCacheDir = $em->getConfiguration()->getProxyDir())) {
53-
if (false === @mkdir($proxyCacheDir, 0777, true)) {
53+
if (false === @mkdir($proxyCacheDir, 0777, true) && !is_dir($proxyCacheDir)) {
5454
throw new \RuntimeException(sprintf('Unable to create the Doctrine Proxy directory "%s".', $proxyCacheDir));
5555
}
5656
} elseif (!is_writable($proxyCacheDir)) {

src/Symfony/Component/Cache/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
Symfony PSR-6 implementation for caching
22
========================================
33

4-
The Cache component provides an extended
5-
[PSR-6](http://www.php-fig.org/psr/psr-6/) implementation for adding cache to
4+
The Cache component provides extended
5+
[PSR-6](https://www.php-fig.org/psr/psr-6/) implementations for adding cache to
66
your applications. It is designed to have a low overhead so that caching is
7-
fastest. It ships with a few caching adapters for the most widespread and
8-
suited to caching backends. It also provides a `doctrine/cache` proxy adapter
9-
to cover more advanced caching needs and a proxy adapter for greater
10-
interoperability between PSR-6 implementations.
7+
fastest. It ships with adapters for the most widespread caching backends.
8+
It also provides a [PSR-16](https://www.php-fig.org/psr/psr-16/) adapter,
9+
and implementations for [symfony/cache-contracts](https://github.com/symfony/cache-contracts)'
10+
`CacheInterface` and `TagAwareCacheInterface`.
1111

1212
Resources
1313
---------

src/Symfony/Component/Cache/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "symfony/cache",
33
"type": "library",
4-
"description": "Provides an extended PSR-6, PSR-16 (and tags) implementation",
4+
"description": "Provides extended PSR-6, PSR-16 (and tags) implementations",
55
"keywords": ["caching", "psr6"],
66
"homepage": "https://symfony.com",
77
"license": "MIT",

src/Symfony/Component/HttpClient/Psr18Client.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ public function sendRequest(RequestInterface $request): ResponseInterface
101101

102102
foreach ($response->getHeaders(false) as $name => $values) {
103103
foreach ($values as $value) {
104-
$psrResponse = $psrResponse->withAddedHeader($name, $value);
104+
try {
105+
$psrResponse = $psrResponse->withAddedHeader($name, $value);
106+
} catch (\InvalidArgumentException $e) {
107+
// ignore invalid header
108+
}
105109
}
106110
}
107111

src/Symfony/Component/HttpClient/RetryableHttpClient.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ private function getDelayFromHeader(array $headers): ?int
138138
{
139139
if (null !== $after = $headers['retry-after'][0] ?? null) {
140140
if (is_numeric($after)) {
141-
return (int) $after * 1000;
141+
return (int) ($after * 1000);
142142
}
143143

144144
if (false !== $time = strtotime($after)) {

src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
use Nyholm\Psr7\Factory\Psr17Factory;
1515
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\HttpClient\MockHttpClient;
1617
use Symfony\Component\HttpClient\NativeHttpClient;
1718
use Symfony\Component\HttpClient\Psr18Client;
1819
use Symfony\Component\HttpClient\Psr18NetworkException;
1920
use Symfony\Component\HttpClient\Psr18RequestException;
21+
use Symfony\Component\HttpClient\Response\MockResponse;
2022
use Symfony\Contracts\ 6D HttpClient\Test\TestHttpServer;
2123

2224
class Psr18ClientTest extends TestCase
@@ -81,4 +83,22 @@ public function test404()
8183
$response = $client->sendRequest($factory->createRequest('GET', 'http://localhost:8057/404'));
8284
$this->assertSame(404, $response->getStatusCode());
8385
}
86+
87+
public function testInvalidHeaderResponse()
88+
{
89+
$responseHeaders = [
90+
// space in header name not allowed in RFC 7230
91+
' X-XSS-Protection' => '0',
92+
'Cache-Control' => 'no-cache',
93+
];
94+
$response = new MockResponse('body', ['response_headers' => $responseHeaders]);
95+
$this->assertArrayHasKey(' x-xss-protection', $response->getHeaders());
96+
97+
$client = new Psr18Client(new MockHttpClient($response));
98+
$request = $client->createRequest('POST', 'http://localhost:8057/post')
99+
->withBody($client->createStream('foo=0123456789'));
100+
101+
$resultResponse = $client->sendRequest($request);
102+
$this->assertCount(1, $resultResponse->getHeaders());
103+
}
84104
}

src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,42 @@ public function testCancelOnTimeout()
187187
$response->cancel();
188188
}
189189
}
190+
191+
public function testRetryWithDelay()
192+
{
193+
$retryAfter = '0.46';
194+
195+
$client = new RetryableHttpClient(
196+
new MockHttpClient([
197+
new MockResponse('', [
198+
'http_code' => 503,
199+
'response_headers' => [
200+
'retry-after' => $retryAfter,
201+
],
202+
]),
203+
new MockResponse('', [
204+
'http_code' => 200,
205+
]),
206+
]),
207+
new GenericRetryStrategy(),
208+
1,
209+
$logger = new class() extends TestLogger {
210+
public array $context = [];
211+
212+
public function log($level, $message, array $context = []): void
213+
{
214+
$this->context = $context;
215+
parent::log($level, $message, $context);
216+
}
217+
},
218+
);
219+
220+
$client->request('GET', 'http://example.com/foo-bar')->getContent();
221+
222+
$delay = $logger->context['delay'] ?? null;
223+
224+
$this->assertArrayHasKey('delay', $logger->context);
225+
$this->assertNotNull($delay);
226+
$this->assertSame((int) ($retryAfter * 1000), $delay);
227+
}
190228
}

src/Symfony/Component/HttpKernel/DataCollector/RequestDataCollector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public function collect(Request $request, Response $response, \Throwable $except
110110
'session_metadata' => $sessionMetadata,
111111
'session_attributes' => $sessionAttributes,
112112
'session_usages' => array_values($this->sessionUsages),
113-
'stateless_check' => $this->requestStack?->getMainRequest()->attributes->get('_stateless') ?? false,
113+
'stateless_check' => $this->requestStack?->getMainRequest()?->attributes->get('_stateless') ?? false,
114114
'flashes' => $flashes,
115115
'path_info' => $request->getPathInfo(),
116116
'controller' => 'n/a',

src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,15 @@ public function testStatelessCheck()
312312
$collector->lateCollect();
313313

314314
$this->assertTrue($collector->getStatelessCheck());
315+
316+
$requestStack = new RequestStack();
317+
$request = $this->createRequest();
318+
319+
$collector = new RequestDataCollector($requestStack);
320+
$collector->collect($request, $response = $this->createResponse());
321+
$collector->lateCollect();
322+
323+
$this->assertFalse($collector->getStatelessCheck());
315324
}
316325

317326
public function testItHidesPassword()

src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInte
7575

7676
if ($this->tokenVerifier) {
7777
$isTokenValid = $this->tokenVerifier->verifyToken($persistentToken, $tokenValue);
78-
$tokenValue = $persistentToken->getTokenValue();
7978
} else {
8079
$isTokenValid = hash_equals($persistentToken->getTokenValue(), $tokenValue);
8180
}
@@ -94,9 +93,9 @@ public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInte
9493
$tokenLastUsed = new \DateTime();
9594
$this->tokenVerifier?->updateExistingToken($persistentToken, $tokenValue, $tokenLastUsed);
9695
$this->tokenProvider->updateToken($series, $tokenValue, $tokenLastUsed);
97-
}
9896

99-
$this->createCookie($rememberMeDetails->withValue($series.':'.$tokenValue));
97+
$this->createCookie($rememberMeDetails->withValue($series.':'.$tokenValue));
98+
}
10099
}
101100

102101
/**

src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,7 @@ public function testConsumeRememberMeCookieValidByValidatorWithoutUpdate()
125125
$rememberMeDetails = new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:oldTokenValue');
126126
$handler->consumeRememberMeCookie($rememberMeDetails);
127127

128-
// assert that the cookie has been updated with a new base64 encoded token value
129-
$this->assertTrue($this->request->attributes->has(ResponseListener::COOKIE_ATTR_NAME));
130-
131-
/** @var Cookie $cookie */
132-
$cookie = $this->request->attributes->get(ResponseListener::COOKIE_ATTR_NAME);
133-
134-
$cookieParts = explode(':', base64_decode($cookie->getValue()), 4);
135-
136-
$this->assertSame(InMemoryUser::class, $cookieParts[0]); // class
137-
$this->assertSame(base64_encode('wouter'), $cookieParts[1]); // identifier
138-
$this->assertSame('360', $cookieParts[2]); // expire
139-
$this->assertSame('series1:tokenvalue', $cookieParts[3]); // value
128+
$this->assertFalse($this->request->attributes->has(ResponseListener::COOKIE_ATTR_NAME));
140129
}
141130

142131
public function testConsumeRememberMeCookieInvalidToken()

src/Symfony/Component/Uid/Tests/UuidTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ public function provideInvalidUuids(): iterable
4444
yield ['these are just thirty-six characters'];
4545
}
4646

47+
/**
48+
* @dataProvider provideInvalidVariant
49+
*/
50+
public function testInvalidVariant(string $uuid)
51+
{
52+
$uuid = new Uuid($uuid);
53+
$this->assertFalse(Uuid::isValid($uuid));
54+
55+
$uuid = (string) $uuid;
56+
$class = Uuid::class.'V'.$uuid[14];
57+
58+
$this->expectException(\InvalidArgumentException::class);
59+
$this->expectExceptionMessage('Invalid UUIDv'.$uuid[14].': "'.$uuid.'".');
60+
61+
new $class($uuid);
62+
}
63+
64+
public function provideInvalidVariant(): iterable
65+
{
66+
yield ['8dac64d3-937a-1e7c-fa1d-d5d6c06a61f5'];
67+
yield ['8dac64d3-937a-3e7c-fa1d-d5d6c06a61f5'];
68+
yield ['8dac64d3-937a-4e7c-fa1d-d5d6c06a61f5'];
69+
yield ['8dac64d3-937a-5e7c-fa1d-d5d6c06a61f5'];
70+
yield ['8dac64d3-937a-6e7c-fa1d-d5d6c06a61f5'];
71+
}
72+
4773
public function testConstructorWithValidUuid()
4874
{
4975
$uuid = new UuidV4(self::A_UUID_V4);

src/Symfony/Component/Uid/Ulid.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ public static function isValid(string $ulid): bool
6464
*/
6565
public static function fromString(string $ulid): static
6666
{
67-
if (36 === \strlen($ulid) && Uuid::isValid($ulid)) {
68-
$ulid = (new Uuid($ulid))->toBinary();
67+
if (36 === \strlen($ulid) && preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $ulid)) {
68+
$ulid = uuid_parse($ulid);
6969
} elseif (22 === \strlen($ulid) && 22 === strspn($ulid, BinaryUtil::BASE58[''])) {
7070
$ulid = str_pad(BinaryUtil::fromBase($ulid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT);
7171
}

src/Symfony/Component/Uid/Uuid.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Uuid extends AbstractUid
2626
protected const TYPE = 0;
2727
protected const NIL = '00000000-0000-0000-0000-000000000000';
2828

29-
public function __construct(string $uuid)
29+
public function __construct(string $uuid, bool $checkVariant = false)
3030
{
3131
$type = preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $uuid) ? (int) $uuid[14] : false;
3232

@@ -35,6 +35,10 @@ public function __construct(string $uuid)
3535
}
3636

3737
$this->uid = strtolower($uuid);
38+
39+
if ($checkVariant && !\in_array($this->uid[19], ['8', '9', 'a', 'b'], true)) {
40+
throw new \InvalidArgumentException(sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid));
41+
}
3842
}
3943

4044
public static function fromString(string $uuid): static
@@ -64,6 +68,10 @@ public static function fromString(string $uuid): static
6468
return new NilUuid();
6569
}
6670

71+
if (!\in_array($uuid[19], ['8', '9', 'a', 'b', 'A', 'B'], true)) {
72+
return new self($uuid);
73+
}
74+
6775
return match ((int) $uuid[14]) {
6876
UuidV1::TYPE => new UuidV1($uuid),
6977
UuidV3::TYPE => new UuidV3($uuid),
@@ -107,7 +115,7 @@ final public static function v6(): UuidV6
107115

108116
public static function isValid(string $uuid): bool
109117
{
110-
if (!preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $uuid)) {
118+
if (!preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){2}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}Di', $uuid)) {
111119
return false;
112120
}
113121

src/Symfony/Component/Uid/UuidV1.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function __construct(string $uuid = null)
2727
if (null === $uuid) {
2828
$this->uid = uuid_create(static::TYPE);
2929
} else {
30-
parent::__construct($uuid);
30+
parent::__construct($uuid, true);
3131
}
3232
}
3333

src/Symfony/Component/Uid/UuidV3.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@
2121
class UuidV3 extends Uuid
2222
{
2323
protected const TYPE = 3;
24+
25+
public function __construct(string $uuid)
26+
{
27+
parent::__construct($uuid, true);
28+
}
2429
}

src/Symfony/Component/Uid/UuidV4.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function __construct(string $uuid = null)
3030

3131
$this->uid = substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12);
3232
} else {
33-
parent::__construct($uuid);
33+
parent::__construct($uuid, true);
3434
}
3535
}
3636
}

src/Symfony/Component/Uid/UuidV5.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@
2121
class UuidV5 extends Uuid
2222
{
2323
protected const TYPE = 5;
24+
25+
public function __construct(string $uuid)
26+
{
27+
parent::__construct($uuid, true);
28+
}
2429
}

src/Symfony/Component/Uid/UuidV6.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function __construct(string $uuid = null)
2929
if (null === $uuid) {
3030
$this->uid = static::generate();
3131
} else {
32-
parent::__construct($uuid);
32+
parent::__construct($uuid, true);
3333
}
3434
}
3535

src/Symfony/Component/Validator/Constraints/UlidValidator.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,17 @@ public function validate(mixed $value, Constraint $constraint)
4848
->setParameter('{{ value }}', $this->formatValue($value))
4949
->setCode(26 > \strlen($value) ? Ulid::TOO_SHORT_ERROR : Ulid::TOO_LONG_ERROR)
5050
->addViolation();
51+
52+
return;
5153
}
5254

5355
if (\strlen($value) !== strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) {
5456
$this->context->buildViolation($constraint->message)
5557
->setParameter('{{ value }}', $this->formatValue($value))
5658
->setCode(Ulid::INVALID_CHARACTERS_ERROR)
5759
->addViolation();
60+
61+
return;
5862
}
5963

6064
// Largest valid ULID is '7ZZZZZZZZZZZZZZZZZZZZZZZZZ'

src/Symfony/Component/Validator/Tests/Constraints/UlidValidatorTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public function getInvalidUlids()
7878
['01ARZ3NDEKTSV4RRFFQ69G5FAVA', Ulid::TOO_LONG_ERROR],
7979
['01ARZ3NDEKTSV4RRFFQ69G5FAO', Ulid::INVALID_CHARACTERS_ERROR],
8080
['Z1ARZ3NDEKTSV4RRFFQ69G5FAV', Ulid::TOO_LARGE_ERROR],
81+
['not-even-ulid-like', Ulid::TOO_SHORT_ERROR],
8182
];
8283
}
8384

src/Symfony/Component/VarDumper/Tests/Dumper/ServerDumperTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public function testDumpForwardsToWrappedDumperWhenServerIsUnavailable()
3939

4040
public function testDump()
4141
{
42+
if ('True' === getenv('APPVEYOR')) {
43+
$this->markTestSkipped('Skip transient test on AppVeyor');
44+
}
45+
4246
$wrappedDumper = $this->createMock(DataDumperInterface::class);
4347
$wrappedDumper->expects($this->never())->method('dump'); // test wrapped dumper is not used
4448

0 commit comments

Comments
 (0)
0