diff --git a/.appveyor.yml b/.appveyor.yml
index 5474f89111..8a880a9b3c 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -22,6 +22,8 @@ install:
- cd ext
- appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_apcu-5.1.21-8.0-ts-vs16-x86.zip
- 7z x php_apcu-5.1.21-8.0-ts-vs16-x86.zip -y >nul
+ - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_redis-5.3.5-8.0-ts-vs16-x86.zip
+ - 7z x php_redis-5.3.5-8.0-ts-vs16-x86.zip -y >nul
- cd ..
- copy /Y php.ini-development php.ini-min
- echo memory_limit=-1 >> php.ini-min
@@ -37,6 +39,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
@@ -55,6 +58,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 91fcd5ea0f..89f7bf8929 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -157,7 +157,6 @@ 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
@@ -165,10 +164,6 @@ jobs:
MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages
MESSENGER_SQS_DSN: "sqs://localhost:9494/messages?sslmode=disable&poll_timeout=0.01"
MESSENGER_SQS_FIFO_QUEUE_DSN: "sqs://localhost:9494/messages.fifo?sslmode=disable&poll_timeout=0.01"
- MEMCACHED_HOST: localhost
- LDAP_HOST: localhost
- LDAP_PORT: 3389
- MONGODB_HOST: localhost
KAFKA_BROKER: 127.0.0.1:9092
POSTGRES_HOST: localhost
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 0577683469..d0f1bf87a1 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -18,6 +18,7 @@
+
diff --git a/src/Symfony/Component/Cache/LockRegistry.php b/src/Symfony/Component/Cache/LockRegistry.php
index bbed905b9f..69838c417a 100644
--- a/src/Symfony/Component/Cache/LockRegistry.php
+++ b/src/Symfony/Component/Cache/LockRegistry.php
@@ -90,7 +90,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);
}
diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php
index f72078c3ac..fce98cff91 100644
--- a/src/Symfony/Component/DomCrawler/Crawler.php
+++ b/src/Symfony/Component/DomCrawler/Crawler.php
@@ -1053,7 +1053,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
diff --git a/src/Symfony/Component/HttpFoundation/ServerBag.php b/src/Symfony/Component/HttpFoundation/ServerBag.php
index 6f40bbc7cf..5b0fc8ac46 100644
--- a/src/Symfony/Component/HttpFoundation/ServerBag.php
+++ b/src/Symfony/Component/HttpFoundation/ServerBag.php
@@ -87,7 +87,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')]);
diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php
index 39fa8fc939..4de55d4838 100644
--- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php
+++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php
@@ -123,6 +123,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)) {
+ // 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 003c754568..5694f4f0f4 100644
--- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php
@@ -24,6 +24,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
{
@@ -400,6 +401,25 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
$this->assertEqualsCanonicalizing([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator));
}
+ /**
+ * @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');
+ }
+
public function testBindWithTarget()
{
$container = new ContainerBuilder();
@@ -479,6 +499,13 @@ class ArgumentWithoutTypeController
}
}
+class NonNullableEnumArgumentWithDefaultController
+{
+ public function fooAction(Suit $suit = Suit::Spades)
+ {
+ }
+}
+
class WithTarget
{
public function fooAction(
diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php
index 5a0a4938c3..2da7169002 100644
--- a/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/SessionListenerTest.php
@@ -308,17 +308,18 @@ class SessionListenerTest extends TestCase
$this->assertSame('123456', $cookies[0]->getValue());
}
- public function testUninitializedSession()
+ public function testUninitializedSessionUsingSessionFromRequest()
{
$kernel = $this->createMock(HttpKernelInterface::class);
$response = new Response();
$response->setSharedMaxAge(60);
$response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true');
- $container = new Container();
+ $request = new Request();
+ $request->setSession(new Session());
- $listener = new SessionListener($container);
- $listener->onKernelResponse(new ResponseEvent($kernel, new Request(), HttpKernelInterface::MAIN_REQUEST, $response));
+ $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'));
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';
+}
diff --git a/src/Symfony/Component/Lock/Store/PostgreSqlStore.php b/src/Symfony/Component/Lock/Store/PostgreSqlStore.php
index 660df95744..9fcce03375 100644
--- a/src/Symfony/Component/Lock/Store/PostgreSqlStore.php
+++ b/src/Symfony/Component/Lock/Store/PostgreSqlStore.php
@@ -72,18 +72,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 === $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 === $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();
@@ -94,19 +104,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 === $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 === $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));
+ }
}
diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php
index 387695c520..87223d61bf 100644
--- a/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php
+++ b/src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php
@@ -51,10 +51,10 @@ class Connection
private bool $autoSetup;
private int $maxEntries;
private int $redeliverTimeout;
- private int $nextClaim = 0;
- private mixed $claimInterval;
- private mixed $deleteAfterAck;
- private mixed $deleteAfterReject;
+ private float $nextClaim = 0.0;
+ private float $claimInterval;
+ private bool $deleteAfterAck;
+ private bool $deleteAfterReject;
private bool $couldHavePendingMessages = true;
public function __construct(array $configuration, array $connectionCredentials = [], array $redisOptions = [], \Redis|\RedisCluster $redis = null)
@@ -104,7 +104,7 @@ class Connection
$this->deleteAfterAck = $configuration['delete_after_ack'] ?? self::DEFAULT_OPTIONS['delete_after_ack'];
$this->deleteAfterReject = $configuration['delete_after_reject'] ?? self::DEFAULT_OPTIONS['delete_after_reject'];
$this->redeliverTimeout = ($configuration['redeliver_timeout'] ?? self::DEFAULT_OPTIONS['redeliver_timeout']) * 1000;
- $this->claimInterval = $configuration['claim_interval'] ?? self::DEFAULT_OPTIONS['claim_interval'];
+ $this->claimInterval = ($configuration['claim_interval'] ?? self::DEFAULT_OPTIONS['claim_interval']) / 1000;
}
/**
@@ -320,7 +320,7 @@ class Connection
}
}
- $this->nextClaim = $this->getCurrentTimeInMilliseconds() + $this->claimInterval;
+ $this->nextClaim = microtime(true) + $this->claimInterval;
}
public function get(): ?array
@@ -328,36 +328,32 @@ 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) {
- $decodedQueuedMessage = 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(
- \array_key_exists('body', $decodedQueuedMessage) ? $decodedQueuedMessage['body'] : $queuedMessage,
- $decodedQueuedMessage['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;
+ }
+
+ $decodedQueuedMessage = json_decode($queuedMessage, true);
+ $this->add(\array_key_exists('body', $decodedQueuedMessage) ? $decodedQueuedMessage['body'] : $queuedMessage, $decodedQueuedMessage['headers'] ?? [], 0);
}
- if (!$this->couldHavePendingMessages && $this->nextClaim <= $this->getCurrentTimeInMilliseconds()) {
+ if (!$this->couldHavePendingMessages && $this->nextClaim <= microtime(true)) {
$this->claimOldPendingMessages();
}
@@ -448,7 +444,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,
@@ -460,8 +456,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,
@@ -542,6 +548,28 @@ class Connection
$this->connection->del($this->stream, $this->queue);
}
}
+
+ private function rawCommand(string $command, ...$arguments): mixed
+ {
+ try {
+ if ($this->connection instanceof \RedisCluster || $this->connection instanceof RedisClusterProxy) {
+ $result = $this->connection->rawCommand($this->queue, $command, $this->queue, ...$arguments);
+ } else {
+ $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;
+ }
}
if (!class_exists(\Symfony\Component\Messenger\Transport\RedisExt\Connection::class, false)) {
diff --git a/src/Symfony/Component/Mime/Crypto/DkimSigner.php b/src/Symfony/Component/Mime/Crypto/DkimSigner.php
index 4bda946c54..ce07747dfd 100644
--- a/src/Symfony/Component/Mime/Crypto/DkimSigner.php
+++ b/src/Symfony/Component/Mime/Crypto/DkimSigner.php
@@ -62,7 +62,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;
@@ -202,7 +202,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
*/
diff --git a/src/Symfony/Component/Security/Core/README.md b/src/Symfony/Component/Security/Core/README.md
index b47ab331c8..6e31770c49 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();
+}
+```
Sponsor
-------
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());
+ }
+}
diff --git a/src/Symfony/Component/Security/Http/README.md b/src/Symfony/Component/Security/Http/README.md
index 594f5adb3a..91a7583373 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
+```
Sponsor
-------
diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php
index 54ee2a2174..ec88efe8fe 100644
--- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php
+++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php
@@ -138,7 +138,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)));
}
}
@@ -200,7 +200,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,
]);
}
@@ -218,13 +218,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