From 10846fed96678d09aa7d4ccf1749510193ca5494 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Thu, 23 Dec 2021 16:43:44 +0100 Subject: [PATCH 01/12] [Security] Add getting started example to README --- src/Symfony/Component/Security/Core/README.md | 36 +++++++++++++++++-- src/Symfony/Component/Security/Http/README.md | 14 +++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Security/Core/README.md b/src/Symfony/Component/Security/Core/README.md index 6b3e5c9901..cf9812d313 100644 --- a/src/Symfony/Component/Security/Core/README.md +++ b/src/Symfony/Component/Security/Core/README.md @@ -3,8 +3,40 @@ Security Component - Core Security provides an infrastructure for sophisticated authorization systems, which makes it possible to easily separate the actual authorization logic from -so called user providers that hold the users credentials. It is inspired by -the Java Spring framework. +so called user providers that hold the users credentials. + +Getting Started +--------------- + +``` +$ composer require symfony/security-core +``` + +```php +use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; +use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; +use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter; +use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Role\RoleHierarchy; + +$accessDecisionManager = new AccessDecisionManager([ + new AuthenticatedVoter(new AuthenticationTrustResolver()), + new RoleVoter(), + new RoleHierarchyVoter(new RoleHierarchy([ + 'ROLE_ADMIN' => ['ROLE_USER'], + ])) +]); + +$user = new \App\Entity\User(...); +$token = new UsernamePasswordToken($user, 'main', $user->getRoles()); + +if (!$accessDecisionManager->decide($token, ['ROLE_ADMIN'])) { + throw new AccessDeniedException(); +} +``` Resources --------- diff --git a/src/Symfony/Component/Security/Http/README.md b/src/Symfony/Component/Security/Http/README.md index e12d19fbeb..89e28ceb43 100644 --- a/src/Symfony/Component/Security/Http/README.md +++ b/src/Symfony/Component/Security/Http/README.md @@ -1,10 +1,16 @@ Security Component - HTTP Integration ===================================== -Security provides an infrastructure for sophisticated authorization systems, -which makes it possible to easily separate the actual authorization logic from -so called user providers that hold the users credentials. It is inspired by -the Java Spring framework. +The Security HTTP component provides an HTTP integration of the Security Core +component. It allows securing (parts of) your application using firewalls and +provides authenticators to authenticate visitors. + +Getting Started +--------------- + +``` +$ composer require symfony/security-http +``` Resources --------- From a9c9912e9de22d325eb13e1d2262802df7b66ea4 Mon Sep 17 00:00:00 2001 From: Vitali Tsyrkin Date: Mon, 27 Dec 2021 21:37:25 +0300 Subject: [PATCH 02/12] [HttpFoundation] Fix notice when HTTP_PHP_AUTH_USER passed without pass --- src/Symfony/Component/HttpFoundation/ServerBag.php | 2 +- .../Component/HttpFoundation/Tests/ServerBagTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpFoundation/ServerBag.php b/src/Symfony/Component/HttpFoundation/ServerBag.php index 7af111c865..25688d5230 100644 --- a/src/Symfony/Component/HttpFoundation/ServerBag.php +++ b/src/Symfony/Component/HttpFoundation/ServerBag.php @@ -89,7 +89,7 @@ class ServerBag extends ParameterBag // PHP_AUTH_USER/PHP_AUTH_PW if (isset($headers['PHP_AUTH_USER'])) { - $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.$headers['PHP_AUTH_PW']); + $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.($headers['PHP_AUTH_PW'] ?? '')); } elseif (isset($headers['PHP_AUTH_DIGEST'])) { $headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST']; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/ServerBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/ServerBagTest.php index 0663b118e6..e26714bc46 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ServerBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ServerBagTest.php @@ -57,6 +57,16 @@ class ServerBagTest extends TestCase ], $bag->getHeaders()); } + public function testHttpPasswordIsOptionalWhenPassedWithHttpPrefix() + { + $bag = new ServerBag(['HTTP_PHP_AUTH_USER' => 'foo']); + + $this->assertEquals([ + 'AUTHORIZATION' => 'Basic '.base64_encode('foo:'), + 'PHP_AUTH_USER' => 'foo', + ], $bag->getHeaders()); + } + public function testHttpBasicAuthWithPhpCgi() { $bag = new ServerBag(['HTTP_AUTHORIZATION' => 'Basic '.base64_encode('foo:bar')]); From 742279caa93c52ab69cac79d4387c413385c6a9d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 27 Dec 2021 14:24:52 +0100 Subject: [PATCH 03/12] [Messenger] fix Redis support on 32b arch --- .appveyor.yml | 4 + .github/workflows/integration-tests.yml | 4 - phpunit.xml.dist | 1 + .../Transport/RedisExt/ConnectionTest.php | 23 ++++- .../Transport/RedisExt/Connection.php | 86 ++++++++++++------- 5 files changed, 80 insertions(+), 38 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index cbb0098bcf..889aafe269 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,6 +21,8 @@ install: - cd ext - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_apcu-5.1.18-7.1-ts-vc14-x86.zip - 7z x php_apcu-5.1.18-7.1-ts-vc14-x86.zip -y >nul + - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_redis-5.1.1-7.1-ts-vc14-x86.zip + - 7z x php_redis-5.1.1-7.1-ts-vc14-x86.zip -y >nul - cd .. - copy /Y php.ini-development php.ini-min - echo memory_limit=-1 >> php.ini-min @@ -36,6 +38,7 @@ install: - echo opcache.enable_cli=1 >> php.ini-max - echo extension=php_openssl.dll >> php.ini-max - echo extension=php_apcu.dll >> php.ini-max + - echo extension=php_redis.dll >> php.ini-max - echo apc.enable_cli=1 >> php.ini-max - echo extension=php_intl.dll >> php.ini-max - echo extension=php_mbstring.dll >> php.ini-max @@ -54,6 +57,7 @@ install: - SET COMPOSER_ROOT_VERSION=%SYMFONY_VERSION%.x-dev - php composer.phar update --no-progress --ansi - php phpunit install + - choco install memurai-developer test_script: - SET X=0 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 9832c8a9d0..72002fa899 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -99,15 +99,11 @@ jobs: - name: Run tests run: ./phpunit --group integration -v env: - REDIS_HOST: localhost REDIS_CLUSTER_HOSTS: 'localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' REDIS_SENTINEL_HOSTS: 'localhost:26379' REDIS_SENTINEL_SERVICE: redis_sentinel MESSENGER_REDIS_DSN: redis://127.0.0.1:7006/messages MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages - MEMCACHED_HOST: localhost - LDAP_HOST: localhost - LDAP_PORT: 3389 #- name: Run HTTP push tests # if: matrix.php == '8.0' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c4e152a059..0c4dd3ee87 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,6 +18,7 @@ + diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php b/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php index 5484664e2c..70b260ad63 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php @@ -220,6 +220,7 @@ class ConnectionTest extends TestCase { $redis = new \Redis(); $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', [], $redis); + $connection->cleanup(); $connection->add('1', []); $connection->add('2', []); @@ -230,7 +231,7 @@ class ConnectionTest extends TestCase $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget'); $this->assertNotNull($connection->get()); - $redis->del('messenger-rejectthenget'); + $connection->cleanup(); } public function testGetNonBlocking() @@ -238,12 +239,30 @@ class ConnectionTest extends TestCase $redis = new \Redis(); $connection = Connection::fromDsn('redis://localhost/messenger-getnonblocking', [], $redis); + $connection->cleanup(); $this->assertNull($connection->get()); // no message, should return null immediately $connection->add('1', []); $this->assertNotEmpty($message = $connection->get()); $connection->reject($message['id']); - $redis->del('messenger-getnonblocking'); + + $connection->cleanup(); + } + + public function testGetDelayed() + { + $redis = new \Redis(); + + $connection = Connection::fromDsn('redis://localhost/messenger-delayed', [], $redis); + $connection->cleanup(); + + $connection->add('1', [], 100); + $this->assertNull($connection->get()); + usleep(300000); + $this->assertNotEmpty($message = $connection->get()); + $connection->reject($message['id']); + + $connection->cleanup(); } public function testJsonError() diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php b/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php index 4e372eecd7..29eb6cb12e 100644 --- a/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php @@ -141,33 +141,29 @@ class Connection if ($this->autoSetup) { $this->setup(); } + $now = microtime(); + $now = substr($now, 11).substr($now, 2, 3); - try { - $queuedMessageCount = $this->connection->zcount($this->queue, 0, $this->getCurrentTimeInMilliseconds()); - } catch (\RedisException $e) { - throw new TransportException($e->getMessage(), 0, $e); - } + $queuedMessageCount = $this->rawCommand('ZCOUNT', 0, $now); - if ($queuedMessageCount) { - for ($i = 0; $i < $queuedMessageCount; ++$i) { - try { - $queuedMessages = $this->connection->zpopmin($this->queue, 1); - } catch (\RedisException $e) { - throw new TransportException($e->getMessage(), 0, $e); - } - - foreach ($queuedMessages as $queuedMessage => $time) { - $queuedMessage = json_decode($queuedMessage, true); - // if a futured placed message is actually popped because of a race condition with - // another running message consumer, the message is readded to the queue by add function - // else its just added stream and will be available for all stream consumers - $this->add( - $queuedMessage['body'], - $queuedMessage['headers'], - $time - $this->getCurrentTimeInMilliseconds() - ); - } + while ($queuedMessageCount--) { + if (![$queuedMessage, $expiry] = $this->rawCommand('ZPOPMIN', 1)) { + break; } + + if (\strlen($expiry) === \strlen($now) ? $expiry > $now : \strlen($expiry) < \strlen($now)) { + // if a future-placed message is popped because of a race condition with + // another running consumer, the message is readded to the queue + + if (!$this->rawCommand('ZADD', 'NX', $expiry, $queuedMessage)) { + throw new TransportException('Could not add a message to the redis stream.'); + } + + break; + } + + $queuedMessage = json_decode($queuedMessage, true); + $this->add($queuedMessage['body'], $queuedMessage['headers'], 0); } $messageId = '>'; // will receive new messages @@ -255,7 +251,7 @@ class Connection } try { - if ($delayInMs > 0) { // the delay could be smaller 0 in a queued message + if ($delayInMs > 0) { // the delay is <= 0 for queued messages $message = json_encode([ 'body' => $body, 'headers' => $headers, @@ -267,8 +263,18 @@ class Connection throw new TransportException(json_last_error_msg()); } - $score = $this->getCurrentTimeInMilliseconds() + $delayInMs; - $added = $this->connection->zadd($this->queue, ['NX'], $score, $message); + $now = explode(' ', microtime(), 2); + $now[0] = str_pad($delayInMs + substr($now[0], 2, 3), 3, '0', \STR_PAD_LEFT); + if (3 < \strlen($now[0])) { + $now[1] += substr($now[0], 0, -3); + $now[0] = substr($now[0], -3); + + if (\is_float($now[1])) { + throw new TransportException("Message delay is too big: {$delayInMs}ms."); + } + } + + $added = $this->rawCommand('ZADD', 'NX', $now[1].$now[0], $message); } else { $message = json_encode([ 'body' => $body, @@ -316,14 +322,30 @@ class Connection $this->autoSetup = false; } - private function getCurrentTimeInMilliseconds(): int - { - return (int) (microtime(true) * 1000); - } - public function cleanup(): void { $this->connection->del($this->stream); $this->connection->del($this->queue); } + + /** + * @return mixed + */ + private function rawCommand(string $command, ...$arguments) + { + try { + $result = $this->connection->rawCommand($command, $this->queue, ...$arguments); + } catch (\RedisException $e) { + throw new TransportException($e->getMessage(), 0, $e); + } + + if (false === $result) { + if ($error = $this->connection->getLastError() ?: null) { + $this->connection->clearLastError(); + } + throw new TransportException($error ?? sprintf('Could not run "%s" on Redis queue.', $command)); + } + + return $result; + } } From be8cbd2aec5c99d901e611945ecfb33e569cb1c9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 28 Dec 2021 11:59:47 +0100 Subject: [PATCH 04/12] [Cache] Don't lock when doing nested computations --- src/Symfony/Component/Cache/LockRegistry.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Cache/LockRegistry.php b/src/Symfony/Component/Cache/LockRegistry.php index 70fcac9cc1..20fba4d3d4 100644 --- a/src/Symfony/Component/Cache/LockRegistry.php +++ b/src/Symfony/Component/Cache/LockRegistry.php @@ -88,7 +88,7 @@ final class LockRegistry $key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1; - if ($key < 0 || (self::$lockedFiles[$key] ?? false) || !$lock = self::open($key)) { + if ($key < 0 || self::$lockedFiles || !$lock = self::open($key)) { return $callback($item, $save); } From d9e1e82e8831536b955ede9bf98cac3949db4dbd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 27 Dec 2021 11:29:45 +0100 Subject: [PATCH 05/12] [Security] fix unserializing session payloads from v4 --- .../Component/Security/Core/Role/Role.php | 31 +++++++++++++++++++ .../Security/Core/Role/SwitchUserRole.php | 23 ++++++++++++++ .../Core/Tests/Role/LegacyRoleTest.php | 28 +++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/Symfony/Component/Security/Core/Role/Role.php create mode 100644 src/Symfony/Component/Security/Core/Role/SwitchUserRole.php create mode 100644 src/Symfony/Component/Security/Core/Tests/Role/LegacyRoleTest.php diff --git a/src/Symfony/Component/Security/Core/Role/Role.php b/src/Symfony/Component/Security/Core/Role/Role.php new file mode 100644 index 0000000000..374eb59fe8 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Role/Role.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Role; + +/** + * Allows migrating session payloads from v4. + * + * @internal + */ +class Role +{ + private $role; + + private function __construct() + { + } + + public function __toString(): string + { + return $this->role; + } +} diff --git a/src/Symfony/Component/Security/Core/Role/SwitchUserRole.php b/src/Symfony/Component/Security/Core/Role/SwitchUserRole.php new file mode 100644 index 0000000000..6a29fb4daa --- /dev/null +++ b/src/Symfony/Component/Security/Core/Role/SwitchUserRole.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Role; + +/** + * Allows migrating session payloads from v4. + * + * @internal + */ +class SwitchUserRole extends Role +{ + private $deprecationTriggered; + private $source; +} diff --git a/src/Symfony/Component/Security/Core/Tests/Role/LegacyRoleTest.php b/src/Symfony/Component/Security/Core/Tests/Role/LegacyRoleTest.php new file mode 100644 index 0000000000..44c9566720 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Role/LegacyRoleTest.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Tests\Role; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; + +class LegacyRoleTest extends TestCase +{ + public function testPayloadFromV4CanBeUnserialized() + { + $serialized = 'C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":236:{a:3:{i:0;N;i:1;s:4:"main";i:2;a:5:{i:0;s:2:"sf";i:1;b:1;i:2;a:1:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"Symfony\Component\Security\Core\Role\Role'."\0".'role'."\0".'";s:9:"ROLE_USER";}}i:3;a:0:{}i:4;a:1:{i:0;s:9:"ROLE_USER";}}}}'; + + $token = unserialize($serialized); + + $this->assertInstanceOf(UsernamePasswordToken::class, $token); + $this->assertSame(['ROLE_USER'], $token->getRoleNames()); + } +} From b69485887ca4388e1f5c2fb08cb895909cb693bd Mon Sep 17 00:00:00 2001 From: Daniel Gorgan Date: Tue, 28 Dec 2021 11:26:55 +0200 Subject: [PATCH 06/12] [Translation] [LocoProvider] Use rawurlencode and separate tag setting --- .../Translation/Bridge/Loco/LocoProvider.php | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php index 3ef725be12..763cc68b74 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php @@ -140,7 +140,7 @@ final class LocoProvider implements ProviderInterface foreach (array_keys($catalogue->all()) as $domain) { foreach ($this->getAssetsIds($domain) as $id) { - $responses[$id] = $this->client->request('DELETE', sprintf('assets/%s.json', $id)); + $responses[$id] = $this->client->request('DELETE', sprintf('assets/%s.json', rawurlencode($id))); } } @@ -202,7 +202,7 @@ final class LocoProvider implements ProviderInterface $responses = []; foreach ($translations as $id => $message) { - $responses[$id] = $this->client->request('POST', sprintf('translations/%s/%s', $id, $locale), [ + $responses[$id] = $this->client->request('POST', sprintf('translations/%s/%s', rawurlencode($id), rawurlencode($locale)), [ 'body' => $message, ]); } @@ -220,13 +220,35 @@ final class LocoProvider implements ProviderInterface $this->createTag($tag); } - $response = $this->client->request('POST', sprintf('tags/%s.json', $tag), [ - 'body' => implode(',', $ids), + // Separate ids with and without comma. + $idsWithComma = $idsWithoutComma = []; + foreach ($ids as $id) { + if (false !== strpos($id, ',')) { + $idsWithComma[] = $id; + } else { + $idsWithoutComma[] = $id; + } + } + + // Set tags for all ids without comma. + $response = $this->client->request('POST', sprintf('tags/%s.json', rawurlencode($tag)), [ + 'body' => implode(',', $idsWithoutComma), ]); if (200 !== $response->getStatusCode()) { $this->logger->error(sprintf('Unable to tag assets with "%s" on Loco: "%s".', $tag, $response->getContent(false))); } + + // Set tags for each id with comma one by one. + foreach ($idsWithComma as $id) { + $response = $this->client->request('POST', sprintf('assets/%s/tags', rawurlencode($id)), [ + 'body' => ['name' => $tag], + ]); + + if (200 !== $response->getStatusCode()) { + $this->logger->error(sprintf('Unable to tag asset "%s" with "%s" on Loco: "%s".', $id, $tag, $response->getContent(false))); + } + } } private function createTag(string $tag): void From 52ceda78e48d4018a4a29099b0006f3584e2882c Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Tue, 28 Dec 2021 14:57:09 +0100 Subject: [PATCH 07/12] [Mime] Fix missing sprintf in DkimSigner --- src/Symfony/Component/Mime/Crypto/DkimSigner.php | 4 ++-- .../Mime/Tests/Crypto/DkimSignerTest.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Mime/Crypto/DkimSigner.php b/src/Symfony/Component/Mime/Crypto/DkimSigner.php index dfb6f226b5..f0f7091ed4 100644 --- a/src/Symfony/Component/Mime/Crypto/DkimSigner.php +++ b/src/Symfony/Component/Mime/Crypto/DkimSigner.php @@ -65,7 +65,7 @@ final class DkimSigner { $options += $this->defaultOptions; if (!\in_array($options['algorithm'], [self::ALGO_SHA256, self::ALGO_ED25519], true)) { - throw new InvalidArgumentException('Invalid DKIM signing algorithm "%s".', $options['algorithm']); + throw new InvalidArgumentException(sprintf('Invalid DKIM signing algorithm "%s".', $options['algorithm'])); } $headersToIgnore['return-path'] = true; $headersToIgnore['x-transport'] = true; @@ -205,7 +205,7 @@ final class DkimSigner } // Add trailing Line return if last line is non empty - if (\strlen($currentLine) > 0) { + if ('' !== $currentLine) { hash_update($hash, "\r\n"); $length += \strlen("\r\n"); } diff --git a/src/Symfony/Component/Mime/Tests/Crypto/DkimSignerTest.php b/src/Symfony/Component/Mime/Tests/Crypto/DkimSignerTest.php index e48b0c8e4e..e0eaa54f18 100644 --- a/src/Symfony/Component/Mime/Tests/Crypto/DkimSignerTest.php +++ b/src/Symfony/Component/Mime/Tests/Crypto/DkimSignerTest.php @@ -16,6 +16,7 @@ use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Crypto\DkimSigner; use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Message; /** * @group time-sensitive @@ -90,6 +91,21 @@ EOF; ]; } + public function testSignWithUnsupportedAlgorithm() + { + $message = $this->createMock(Message::class); + + $signer = new DkimSigner(self::$pk, 'testdkim.symfony.net', 'sf', [ + 'algorithm' => 'unsupported-value', + ]); + + $this->expectExceptionObject( + new \LogicException('Invalid DKIM signing algorithm "unsupported-value".') + ); + + $signer->sign($message, []); + } + /** * @dataProvider getCanonicalizeHeaderData */ From 89232ee5ea7bf86b97503d8ed58b83cd1612b67b Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Tue, 28 Dec 2021 14:37:08 +0100 Subject: [PATCH 08/12] [HttpKernel] Do not attempt to register enum arguments in controller service locator --- ...RegisterControllerArgumentLocatorsPass.php | 5 ++++ ...sterControllerArgumentLocatorsPassTest.php | 27 +++++++++++++++++++ .../HttpKernel/Tests/Fixtures/Suit.php | 20 ++++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 src/Symfony/Component/HttpKernel/Tests/Fixtures/Suit.php diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index cf4ab60284..eef09fa822 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -127,6 +127,11 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface $type = ltrim($target = (string) ProxyHelper::getTypeHint($r, $p), '\\'); $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; + if (is_subclass_of($type, \UnitEnum::class, true)) { + // do not attempt to register enum typed arguments + continue; + } + if (isset($arguments[$r->name][$p->name])) { $target = $arguments[$r->name][$p->name]; if ('?' !== $target[0]) { diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index 5a0964f6c2..0207703d94 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -23,6 +23,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass; +use Symfony\Component\HttpKernel\Tests\Fixtures\Suit; class RegisterControllerArgumentLocatorsPassTest extends TestCase { @@ -369,6 +370,25 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase $this->assertInstanceOf(Reference::class, $locatorArgument); } + + /** + * @requires PHP 8.1 + */ + public function testEnumArgumentIsIgnored() + { + $container = new ContainerBuilder(); + $resolver = $container->register('argument_resolver.service')->addArgument([]); + + $container->register('foo', NonNullableEnumArgumentWithDefaultController::class) + ->addTag('controller.service_arguments') + ; + + $pass = new RegisterControllerArgumentLocatorsPass(); + $pass->process($container); + + $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); + $this->assertEmpty(array_keys($locator), 'enum typed argument is ignored'); + } } class RegisterTestController @@ -430,3 +450,10 @@ class ArgumentWithoutTypeController { } } + +class NonNullableEnumArgumentWithDefaultController +{ + public function fooAction(Suit $suit = Suit::Spades) + { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Suit.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Suit.php new file mode 100644 index 0000000000..5d9623b225 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Suit.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures; + +enum Suit: string +{ + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} From a1d33da66df573ac87d836b76a8e256a777992c3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 28 Dec 2021 15:43:46 +0100 Subject: [PATCH 09/12] cs fix --- .../RegisterControllerArgumentLocatorsPass.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index eef09fa822..daba473151 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -127,7 +127,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface $type = ltrim($target = (string) ProxyHelper::getTypeHint($r, $p), '\\'); $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; - if (is_subclass_of($type, \UnitEnum::class, true)) { + if (is_subclass_of($type, \UnitEnum::class)) { // do not attempt to register enum typed arguments continue; } From 53b3e40ab51d946c93b390a0b1ddc940dd8900d8 Mon Sep 17 00:00:00 2001 From: Titouan Galopin Date: Sun, 21 Nov 2021 14:26:08 +0100 Subject: [PATCH 10/12] [DomCrawler] Fix HTML5 parser charset option --- src/Symfony/Component/DomCrawler/Crawler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 70a4b607dc..d1c7a9690a 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -1107,7 +1107,7 @@ class Crawler implements \Countable, \IteratorAggregate private function parseHtml5(string $htmlContent, string $charset = 'UTF-8'): \DOMDocument { - return $this->html5Parser->parse($this->convertToHtmlEntities($htmlContent, $charset), [], $charset); + return $this->html5Parser->parse($this->convertToHtmlEntities($htmlContent, $charset)); } private function parseXhtml(string $htmlContent, string $charset = 'UTF-8'): \DOMDocument From e5b2f9efba595b07fcc12e71995ddf38539a9e4a Mon Sep 17 00:00:00 2001 From: Simon Watiau Date: Mon, 27 Dec 2021 10:23:03 +0100 Subject: [PATCH 11/12] [Lock] Release PostgreSqlStore connection lock on failure --- .../Component/Lock/Store/PostgreSqlStore.php | 60 ++++++++++++------- .../Lock/Tests/Store/PostgreSqlStoreTest.php | 28 +++++++++ 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/PostgreSqlStore.php b/src/Symfony/Component/Lock/Store/PostgreSqlStore.php index 0e472d2c82..67ed4ca05a 100644 --- a/src/Symfony/Component/Lock/Store/PostgreSqlStore.php +++ b/src/Symfony/Component/Lock/Store/PostgreSqlStore.php @@ -80,18 +80,28 @@ class PostgreSqlStore implements BlockingSharedLockStoreInterface, BlockingStore // prevent concurrency within the same connection $this->getInternalStore()->save($key); - $sql = 'SELECT pg_try_advisory_lock(:key)'; - $stmt = $this->getConnection()->prepare($sql); - $stmt->bindValue(':key', $this->getHashedKey($key)); - $result = $stmt->execute(); + $lockAcquired = false; - // Check if lock is acquired - if (true === (\is_object($result) ? $result->fetchOne() : $stmt->fetchColumn())) { - $key->markUnserializable(); - // release sharedLock in case of promotion - $this->unlockShared($key); + try { + $sql = 'SELECT pg_try_advisory_lock(:key)'; + $stmt = $this->getConnection()->prepare($sql); + $stmt->bindValue(':key', $this->getHashedKey($key)); + $result = $stmt->execute(); - return; + // Check if lock is acquired + if (true === (\is_object($result) ? $result->fetchOne() : $stmt->fetchColumn())) { + $key->markUnserializable(); + // release sharedLock in case of promotion + $this->unlockShared($key); + + $lockAcquired = true; + + return; + } + } finally { + if (!$lockAcquired) { + $this->getInternalStore()->delete($key); + } } throw new LockConflictedException(); @@ -102,19 +112,29 @@ class PostgreSqlStore implements BlockingSharedLockStoreInterface, BlockingStore // prevent concurrency within the same connection $this->getInternalStore()->saveRead($key); - $sql = 'SELECT pg_try_advisory_lock_shared(:key)'; - $stmt = $this->getConnection()->prepare($sql); + $lockAcquired = false; - $stmt->bindValue(':key', $this->getHashedKey($key)); - $result = $stmt->execute(); + try { + $sql = 'SELECT pg_try_advisory_lock_shared(:key)'; + $stmt = $this->getConnection()->prepare($sql); - // Check if lock is acquired - if (true === (\is_object($result) ? $result->fetchOne() : $stmt->fetchColumn())) { - $key->markUnserializable(); - // release lock in case of demotion - $this->unlock($key); + $stmt->bindValue(':key', $this->getHashedKey($key)); + $result = $stmt->execute(); - return; + // Check if lock is acquired + if (true === (\is_object($result) ? $result->fetchOne() : $stmt->fetchColumn())) { + $key->markUnserializable(); + // release lock in case of demotion + $this->unlock($key); + + $lockAcquired = true; + + return; + } + } finally { + if (!$lockAcquired) { + $this->getInternalStore()->delete($key); + } } throw new LockConflictedException(); diff --git a/src/Symfony/Component/Lock/Tests/Store/PostgreSqlStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/PostgreSqlStoreTest.php index d0358a8ef0..aef6ee7b86 100644 --- a/src/Symfony/Component/Lock/Tests/Store/PostgreSqlStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/PostgreSqlStoreTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Lock\Tests\Store; use Symfony\Component\Lock\Exception\InvalidArgumentException; +use Symfony\Component\Lock\Exception\LockConflictedException; use Symfony\Component\Lock\Key; use Symfony\Component\Lock\PersistingStoreInterface; use Symfony\Component\Lock\Store\PostgreSqlStore; @@ -50,4 +51,31 @@ class PostgreSqlStoreTest extends AbstractStoreTest $this->expectExceptionMessage('The adapter "Symfony\Component\Lock\Store\PostgreSqlStore" does not support'); $store->exists(new Key('foo')); } + + public function testSaveAfterConflict() + { + $store1 = $this->getStore(); + $store2 = $this->getStore(); + + $key = new Key(uniqid(__METHOD__, true)); + + $store1->save($key); + $this->assertTrue($store1->exists($key)); + + $lockConflicted = false; + + try { + $store2->save($key); + } catch (LockConflictedException $lockConflictedException) { + $lockConflicted = true; + } + + $this->assertTrue($lockConflicted); + $this->assertFalse($store2->exists($key)); + + $store1->delete($key); + + $store2->save($key); + $this->assertTrue($store2->exists($key)); + } } From 3d2fd7073eb33c25e6ee29bbe8447fd6f3915e97 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 28 Dec 2021 16:14:13 +0100 Subject: [PATCH 12/12] expand uninitialized session tests --- .../EventListener/SessionListenerTest.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php index 348f643c1f..c6513214dd 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php @@ -121,7 +121,7 @@ class SessionListenerTest extends TestCase $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } - public function testUninitializedSession() + public function testUninitializedSessionUsingInitializedSessionService() { $kernel = $this->createMock(HttpKernelInterface::class); $response = new Response(); @@ -142,6 +142,26 @@ class SessionListenerTest extends TestCase $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); } + public function testUninitializedSessionUsingSessionFromRequest() + { + $kernel = $this->createMock(HttpKernelInterface::class); + $response = new Response(); + $response->setSharedMaxAge(60); + $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); + + $request = new Request(); + $request->setSession(new Session()); + + $listener = new SessionListener(new Container()); + $listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response)); + $this->assertFalse($response->headers->has('Expires')); + $this->assertTrue($response->headers->hasCacheControlDirective('public')); + $this->assertFalse($response->headers->hasCacheControlDirective('private')); + $this->assertFalse($response->headers->hasCacheControlDirective('must-revalidate')); + $this->assertSame('60', $response->headers->getCacheControlDirective('s-maxage')); + $this->assertFalse($response->headers->has(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER)); + } + public function testUninitializedSessionWithoutInitializedSession() { $kernel = $this->createMock(HttpKernelInterface::class);