diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 84dd98f43ad2b..62662f876fd3a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 5.4 for features / 4.4, 5.2 or 5.3 for bug fixes +| Branch? | 5.4 for features / 4.4 or 5.3 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no diff --git a/.github/patch-types.php b/.github/patch-types.php index 33ba6347a3ef0..2b633c9d99557 100644 --- a/.github/patch-types.php +++ b/.github/patch-types.php @@ -32,6 +32,7 @@ case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'): + case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php'): case false !== strpos($file, '/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php'): case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'): case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'): diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index acdb9ff52b6f4..19e3da080c93b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -180,3 +180,13 @@ jobs: # docker run --rm -e COMPOSER_ROOT_VERSION -v $(pwd):/app -v $(which composer):/usr/local/bin/composer -v $(which vulcain):/usr/local/bin/vulcain -w /app php:8.0-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push # sudo rm -rf .phpunit # [ -d .phpunit.bak ] && mv .phpunit.bak .phpunit + + - uses: marceloprado/has-changed-path@v1 + id: changed-translation-files + with: + paths: src/**/Resources/translations/*.xlf + + - name: Check Translation Status + if: steps.changed-translation-files.outputs.changed == 'true' + run: | + php src/Symfony/Component/Translation/Resources/bin/translation-status.php -v diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index a9adb8e7cf532..9c126e2ef2422 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -52,4 +52,5 @@ jobs: - name: Psalm run: | + ./vendor/bin/psalm.phar --no-progress ./vendor/bin/psalm.phar --output-format=github --no-progress diff --git a/CHANGELOG-5.2.md b/CHANGELOG-5.2.md index b3ee27e04730b..d5799da0cdfae 100644 --- a/CHANGELOG-5.2.md +++ b/CHANGELOG-5.2.md @@ -7,6 +7,12 @@ in 5.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v5.2.0...v5.2.1 +* 5.2.14 (2021-07-29) + + * bug #42307 [Mailer] Fixed decode exception when sendgrid response is 202 (rubanooo) + * bug #42296 [Dotenv][Yaml] Remove PHP 8.0 polyfill (derrabus) + * bug #42289 [HttpFoundation] Fixed type mismatch (Toflar) + * 5.2.13 (2021-07-27) * bug #42270 [WebProfilerBundle] [WebProfiler] "empty" filter bugfix. Filter with name "empty" is not … (luzrain) diff --git a/CHANGELOG-5.3.md b/CHANGELOG-5.3.md index 76537968a1c3c..c949725e03b9b 100644 --- a/CHANGELOG-5.3.md +++ b/CHANGELOG-5.3.md @@ -7,6 +7,42 @@ in 5.3 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v5.3.0...v5.3.1 +* 5.3.7 (2021-08-30) + + * bug #42769 [HttpClient] Don't pass float to `usleep()` (derrabus) + * bug #42753 Cast ini_get to an integer to match expected type (natewiebe13) + * bug #42345 [Messenger] Remove indices in messenger table on MySQL to prevent deadlocks while removing messages when running multiple consumers (jeroennoten) + * bug #41378 [Messenger] Fix `ErrorDetailsStamp` denormalization (wucdbm) + * bug #42160 [Translation] Extract translatable content on twig set (natewiebe13) + * bug #42053 [Notifier] [Smsapi] fixed checking whether message is sent (damlox) + * bug #40744 allow null for framework.translator.default_path (SimonHeimberg) + * bug #39856 [DomCrawler] improve failure messages of the CrawlerSelectorTextContains constraint (xabbuh) + * bug #40545 [HttpFoundation] Fix isNotModified determination logic (ol0lll) + * bug #42368 [FrameworkBundle] Fall back to default configuration in debug:config and consistently resolve parameter values (herndlm) + * bug #41684 Fix Url Validator false positives (sidz) + * bug #42576 [Translation] Reverse fallback locales (ro0NL) + * bug #42721 Escape all special characters for parse_mode MARKDOWN_V2 (thomas2411) + * bug #42707 [Messenger] [AMQP] Do not leak any credentials when connection fails (ruudk) + * bug #42690 [Notifier] Firebase error handling (mishaklomp) + * bug #42628 [PropertyInfo] Support for the `never` return type (derrabus) + * bug #42685 Fix ProgressBar to correctly clear multi-line formats (rtek) + * bug #42649 [Translation] Fix message key handling for the Localise provider (xepozz) + * bug #42659 Ignoring X-Transport header while signing email with DKIM (metaer) + * bug #42585 [ExpressionLanguage] [Lexer] Remove PHP 8.0 polyfill (nigelmann) + * bug #42621 [Security] Don't produce TypeErrors for non-string CSRF tokens (derrabus) + * bug #42596 [Security] Fix wrong cache directive when using the new PUBLIC_ACCESS attribute (wouterj) + * bug #42445 [Cache] fix wiring async cache recomputing in debug mode (nicolas-grekas) + * bug #42365 [Cache] Do not add namespace argument to `NullAdapter` in `CachePoolPass` (olsavmic) + * bug #42331 [HttpKernel] always close open stopwatch section after handling `kernel.request` events (xabbuh) + * bug #42381 [Console] Don't pass null to preg_replace() (derrabus) + * bug #42347 Fix ServiceLocator indexing when service is decorated (shyim) + * bug #42380 [HttpFoundation] Don't pass null to strpos() (derrabus) + * bug #42377 [HttpKernel] Remove preloading legacy event dispatcher (OskarStark) + * bug #42260 Fix return types for PHP 8.1 (derrabus) + * bug #42360 [DoctrineBridge] Typehint against doctrine/persistence interfaces (malarzm) + * bug #42341 [Validator] Update MIR card scheme (ossinkine) + * bug #42321 [PasswordHasher] Fix usage of PasswordHasherAdapter in PasswordHasherFactory (peter17) + * 5.3.6 (2021-07-29) * bug #42307 [Mailer] Fixed decode exception when sendgrid response is 202 (rubanooo) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8e58e8b338eb2..5c9b27862671f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -139,6 +139,7 @@ The Symfony Connect username in parenthesis allows to get more information - Włodzimierz Gajda (gajdaw) - Christian Scheb - Adrien Brault (adrienbrault) + - Yanick Witschi (toflar) - Julien Falque (julienfalque) - Jacob Dreesen (jdreesen) - Malte Schlüter (maltemaltesich) @@ -148,13 +149,12 @@ The Symfony Connect username in parenthesis allows to get more information - Colin Frei - Javier Spagnoletti (phansys) - Joshua Thijssen - - Yanick Witschi (toflar) + - HypeMC (hypemc) - Daniel Wehner (dawehner) - Tugdual Saunier (tucksaun) - excelwebzone - Gordon Franke (gimler) - Saif Eddin Gmati (azjezz) - - HypeMC (hypemc) - Jesse Rushlow (geeshoe) - Fabien Pennequin (fabienpennequin) - Théo FIDRY (theofidry) @@ -163,10 +163,12 @@ The Symfony Connect username in parenthesis allows to get more information - Eric GELOEN (gelo) - Matthieu Napoli (mnapoli) - Jannik Zschiesche (apfelbox) + - Mathieu Santostefano (welcomattic) - Robert Schönthal (digitalkaoz) - Florian Lonqueu-Brochard (florianlb) - Richard van Laak (rvanlaak) - Tigran Azatyan (tigranazatyan) + - YaFou - Gary PEGEOT (gary-p) - Gabriel Caruso (carusogabriel) - Stefano Sala (stefano.sala) @@ -184,12 +186,10 @@ The Symfony Connect username in parenthesis allows to get more information - Albert Casademont (acasademont) - Arnaud Kleinpeter (nanocom) - Guilherme Blanco (guilhermeblanco) - - Mathieu Santostefano (welcomattic) - Smaine Milianni (ismail1432) - SpacePossum - Pablo Godel (pgodel) - Andreas Braun - - YaFou - Jérémie Augustin (jaugustin) - François-Xavier de Guillebon (de-gui_f) - Oleg Voronkovich @@ -291,6 +291,7 @@ The Symfony Connect username in parenthesis allows to get more information - sun (sun) - Larry Garfield (crell) - Edi Modrić (emodric) + - Roman Martinuk (a2a4) - Leo Feyer (leofeyer) - Nikolay Labinskiy (e-moe) - Martin Schuhfuß (usefulthink) @@ -330,7 +331,6 @@ The Symfony Connect username in parenthesis allows to get more information - Pascal Montoya - Julien Brochet (mewt) - Gocha Ossinkine (ossinkine) - - Roman Martinuk (a2a4) - Tristan Darricau (nicofuma) - Victor Bocharsky (bocharsky_bw) - Bozhidar Hristov (warxcell) @@ -453,6 +453,7 @@ The Symfony Connect username in parenthesis allows to get more information - Serkan Yildiz (srknyldz) - Andrew Moore (finewolf) - Bertrand Zuchuat (garfield-fr) + - Sébastien Alfaiate (seb33300) - Gabor Toth (tgabi333) - realmfoo - Thomas Tourlourat (armetiz) @@ -556,7 +557,6 @@ The Symfony Connect username in parenthesis allows to get more information - Loïc Chardonnet (gnusat) - Marek Kalnik (marekkalnik) - Vyacheslav Salakhutdinov (megazoll) - - Sébastien Alfaiate (seb33300) - Phil Taylor (prazgod) - Hassan Amouhzi - Tamas Szijarto @@ -632,6 +632,7 @@ The Symfony Connect username in parenthesis allows to get more information - Gunnstein Lye (glye) - Matthias Krauser (mkrauser) - Erkhembayar Gantulga (erheme318) + - Lorenzo Millucci (lmillucci) - Jérôme Tamarelle (jtamarelle-prismamedia) - Andrii Popov (andrii-popov) - Islam93 @@ -887,7 +888,6 @@ The Symfony Connect username in parenthesis allows to get more information - Julien Turby - Marvin Butkereit - Eduard Bulava (nonanerz) - - Lorenzo Millucci (lmillucci) - Renan - Ricky Su (ricky) - Igor Timoshenko (igor.timoshenko) @@ -1418,6 +1418,7 @@ The Symfony Connect username in parenthesis allows to get more information - Valérian Galliat - d-ph - Renan Taranto (renan-taranto) + - Adrien Chinour - Rikijs Murgs - Uladzimir Tsykun - Amaury Leroux de Lens (amo__) @@ -1710,6 +1711,7 @@ The Symfony Connect username in parenthesis allows to get more information - Nicolas Fabre (nfabre) - Raul Rodriguez (raul782) - Piet Steinhart + - mousezheng - mshavliuk - Rémy LESCALLIER - WybrenKoelmans @@ -2099,6 +2101,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andrei C. (moldman) - Povilas S. (povilas) - Laurent Negre (raulnet) + - Sergey Fokin (tyraelqp) - Evrard Boulou - pborreli - Boris Betzholz @@ -2796,6 +2799,7 @@ The Symfony Connect username in parenthesis allows to get more information - Benedict Massolle (bemas) - Gerard Berengue Llobera (bere) - Bernd Matzner (bmatzner) + - Anton (bonio) - Bram Tweedegolf (bram_tweedegolf) - Brandon Kelly (brandonkelly) - Choong Wei Tjeng (choonge) diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index a8825e75e915f..01dce37978c94 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -117,7 +117,7 @@ public function createNewToken(PersistentTokenInterface $token) $sql = 'INSERT INTO rememberme_token (class, username, series, value, lastUsed) VALUES (:class, :username, :series, :value, :lastUsed)'; $paramValues = [ 'class' => $token->getClass(), - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 'username' => method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(), 'series' => $token->getSeries(), 'value' => $token->getTokenValue(), diff --git a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php index 20264f4d954dd..b9bdf26e0d1ca 100644 --- a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php @@ -66,7 +66,7 @@ public function loadUserByIdentifier(string $identifier): UserInterface throw new \InvalidArgumentException(sprintf('You must either make the "%s" entity Doctrine Repository ("%s") implement "Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.', $this->classOrAlias, get_debug_type($repository))); } - // @deprecated since 5.3, change to $repository->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $repository->loadUserByIdentifier() in 6.0 if (method_exists($repository, 'loadUserByIdentifier')) { $user = $repository->loadUserByIdentifier($identifier); } else { diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index 9d794b430cdbb..ab35078f85fca 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -865,6 +865,10 @@ public function resultWithEmptyIterator(): array return [ [$entity, new class() implements \Iterator { + /** + * @return mixed + */ + #[\ReturnTypeWillChange] public function current() { return null; @@ -875,19 +879,28 @@ public function valid(): bool return false; } - public function next() + public function next(): void { } + /** + * @return mixed + */ + #[\ReturnTypeWillChange] public function key() { + return false; } - public function rewind() + public function rewind(): void { } }], [$entity, new class() implements \Iterator { + /** + * @return mixed + */ + #[\ReturnTypeWillChange] public function current() { return false; @@ -898,15 +911,20 @@ public function valid(): bool return false; } - public function next() + public function next(): void { } + /** + * @return mixed + */ + #[\ReturnTypeWillChange] public function key() { + return false; } - public function rewind() + public function rewind(): void { } }], diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index 2cf9ca8c0d074..4996848143b73 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -11,9 +11,9 @@ namespace Symfony\Bridge\Doctrine\Validator\Constraints; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -177,7 +177,7 @@ public function validate($entity, Constraint $constraint) ->addViolation(); } - private function formatWithIdentifiers(EntityManagerInterface $em, ClassMetadata $class, $value) + private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, $value) { if (!\is_object($value) || $value instanceof \DateTimeInterface) { return $this->formatValue($value, self::PRETTY_DATE); diff --git a/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php b/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php index 15919978857c3..1f37d88aea4e2 100644 --- a/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/AbstractTokenProcessor.php @@ -46,7 +46,7 @@ public function __invoke(array $record): array 'roles' => $token->getRoleNames(), ]; - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 if (method_exists($token, 'getUserIdentifier')) { $record['extra'][$this->getKey()]['username'] = $record['extra'][$this->getKey()]['user_identifier'] = $token->getUserIdentifier(); } else { diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index 63d7562a1d5ad..c92b47f3c0236 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -224,10 +224,10 @@ @copy("$PHPUNIT_VERSION_DIR/phpunit.xsd", 'phpunit.xsd'); chdir("$PHPUNIT_VERSION_DIR"); if ($SYMFONY_PHPUNIT_REMOVE) { - $passthruOrFail("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE); + $passthruOrFail("$COMPOSER remove --no-update --no-interaction ".$SYMFONY_PHPUNIT_REMOVE); } if ($SYMFONY_PHPUNIT_REQUIRE) { - $passthruOrFail("$COMPOSER require --no-update ".$SYMFONY_PHPUNIT_REQUIRE); + $passthruOrFail("$COMPOSER require --no-update --no-interaction ".$SYMFONY_PHPUNIT_REQUIRE); } if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) { $passthruOrFail("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php index e5b0fc4ea1b73..d42245e2b89a4 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php @@ -69,12 +69,10 @@ protected function doEnterNode(Node $node, Environment $env): Node $this->getReadDomainFromArguments($node->getNode('arguments'), 1), ]; } elseif ( - $node instanceof FilterExpression && - 'trans' === $node->getNode('filter')->getAttribute('value') && - $node->getNode('node') instanceof FunctionExpression && - 't' === $node->getNode('node')->getAttribute('name') + $node instanceof FunctionExpression && + 't' === $node->getAttribute('name') ) { - $nodeArguments = $node->getNode('node')->getNode('arguments'); + $nodeArguments = $node->getNode('arguments'); if ($nodeArguments->getIterator()->current() instanceof ConstantExpression) { $this->messages[] = [ diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index 6a7336d7b1995..8013714d7b40c 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -66,6 +66,7 @@ public function getExtractData() ['{% set foo = "new key" | trans %}', ['new key' => 'messages']], ['{{ 1 ? "new key" | trans : "another key" | trans }}', ['new key' => 'messages', 'another key' => 'messages']], ['{{ t("new key") | trans() }}', ['new key' => 'messages']], + ['{% set foo = t("new key") %}', ['new key' => 'messages']], ['{{ t("new key", {}, "domain") | trans() }}', ['new key' => 'domain']], ['{{ 1 ? t("new key") | trans : t("another key") | trans }}', ['new key' => 'messages', 'another key' => 'messages']], diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index a44777adbdf87..b0d5565398d2d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -61,7 +61,7 @@ protected function configure() ]) ->setDescription(self::$defaultDescription) ->setHelp(<<<'EOF' -The %command.name% command clears the application cache for a given environment +The %command.name% command clears and warms up the application cache for a given environment and debug mode: php %command.full_name% --env=dev diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php index 8aad13098e1bd..e88de6e31f257 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -90,22 +91,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $extension = $this->findExtension($name); - $container = $this->compileContainer(); - $extensionAlias = $extension->getAlias(); - $extensionConfig = []; - foreach ($container->getCompilerPassConfig()->getPasses() as $pass) { - if ($pass instanceof ValidateEnvPlaceholdersPass) { - $extensionConfig = $pass->getExtensionConfig(); - break; - } - } - - if (!isset($extensionConfig[$extensionAlias])) { - throw new \LogicException(sprintf('The extension with alias "%s" does not have configuration.', $extensionAlias)); - } + $container = $this->compileContainer(); - $config = $container->resolveEnvPlaceholders($extensionConfig[$extensionAlias]); + $config = $container->resolveEnvPlaceholders( + $container->getParameterBag()->resolveValue( + $this->getConfigForExtension($extension, $container) + ) + ); if (null === $path = $input->getArgument('path')) { $io->title( @@ -166,4 +159,33 @@ private function getConfigForPath(array $config, string $path, string $alias) return $config; } + + private function getConfigForExtension(ExtensionInterface $extension, ContainerBuilder $container): array + { + $extensionAlias = $extension->getAlias(); + + $extensionConfig = []; + foreach ($container->getCompilerPassConfig()->getPasses() as $pass) { + if ($pass instanceof ValidateEnvPlaceholdersPass) { + $extensionConfig = $pass->getExtensionConfig(); + break; + } + } + + if (isset($extensionConfig[$extensionAlias])) { + return $extensionConfig[$extensionAlias]; + } + + // Fall back to default config if the extension has one + + if (!$extension instanceof ConfigurationExtensionInterface) { + throw new \LogicException(sprintf('The extension with alias "%s" does not have configuration.', $extensionAlias)); + } + + $configs = $container->getExtensionConfig($extensionAlias); + $configuration = $extension->getConfiguration($configs, $container); + $this->validateConfiguration($extension, $configuration); + + return (new Processor())->processConfiguration($configuration, $configs); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index e16295293fcd5..669a5a9ff5dcb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1278,7 +1278,9 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $container->getDefinition('console.command.translation_update')->replaceArgument(6, $transPaths); } - if ($container->fileExists($defaultDir)) { + if (null === $defaultDir) { + // allow null + } elseif ($container->fileExists($defaultDir)) { $dirs[] = $defaultDir; } else { $nonExistingDirs[] = $defaultDir; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DefaultConfigTestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DefaultConfigTestBundle.php new file mode 100644 index 0000000000000..8c7a89574729f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DefaultConfigTestBundle.php @@ -0,0 +1,9 @@ +getRootNode() + ->children() + ->scalarNode('foo')->defaultValue('%default_config_test_foo%')->end() + ->scalarNode('baz')->defaultValue('%env(BAZ)%')->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/DefaultConfigTestExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/DefaultConfigTestExtension.php new file mode 100644 index 0000000000000..d380bcaad17fa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/DefaultConfigTestExtension.php @@ -0,0 +1,18 @@ +processConfiguration($configuration, $configs); + + $container->setParameter('default_config_test', $config['foo']); + $container->setParameter('default_config_test', $config['baz']); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php new file mode 100644 index 0000000000000..79f0a7006c89b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php @@ -0,0 +1,28 @@ +assertStringContainsString('secret: test', $tester->getDisplay()); } + public function testDefaultParameterValueIsResolvedIfConfigIsExisting() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'framework']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $kernelCacheDir = $this->application->getKernel()->getContainer()->getParameter('kernel.cache_dir'); + $this->assertStringContainsString(sprintf("dsn: 'file:%s/profiler'", $kernelCacheDir), $tester->getDisplay()); + } + public function testDumpUndefinedBundleOption() { $tester = $this->createCommandTester(); @@ -74,6 +84,33 @@ public function testDumpWithPrefixedEnv() $this->assertStringContainsString("cookie_httponly: '%env(bool:COOKIE_HTTPONLY)%'", $tester->getDisplay()); } + public function testDumpFallsBackToDefaultConfigAndResolvesParameterValue() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'DefaultConfigTestBundle']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString('foo: bar', $tester->getDisplay()); + } + + public function testDumpFallsBackToDefaultConfigAndResolvesEnvPlaceholder() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'DefaultConfigTestBundle']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString("baz: '%env(BAZ)%'", $tester->getDisplay()); + } + + public function testDumpThrowsExceptionWhenDefaultConfigFallbackIsImpossible() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The extension with alias "extension_without_config_test" does not have configuration.'); + + $tester = $this->createCommandTester(); + $tester->execute(['name' => 'ExtensionWithoutConfigTestBundle']); + } + private function createCommandTester(): CommandTester { $command = $this->application->find('debug:config'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php index 15ff182c6fed5..c4fb0bbe8ce48 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php @@ -10,9 +10,13 @@ */ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DefaultConfigTestBundle\DefaultConfigTestBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ExtensionWithoutConfigTestBundle\ExtensionWithoutConfigTestBundle; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; return [ + new DefaultConfigTestBundle(), + new ExtensionWithoutConfigTestBundle(), new FrameworkBundle(), new TestBundle(), ]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml index 1ec484a7f5208..9d7765d5e583e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml @@ -12,3 +12,4 @@ parameters: env(LOCALE): en env(COOKIE_HTTPONLY): '1' secret: test + default_config_test_foo: bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php index 0e1ed19e414df..f6098ea6e8eca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php @@ -199,7 +199,7 @@ public function testAssertSelectorTextNotContains() { $this->getCrawlerTester(new Crawler('

Foo'))->assertSelectorTextNotContains('body > h1', 'Bar'); $this->expectException(AssertionFailedError::class); - $this->expectExceptionMessage('matches selector "body > h1" and does not have a node matching selector "body > h1" with content containing "Foo".'); + $this->expectExceptionMessage('matches selector "body > h1" and the text "Foo" of the node matching selector "body > h1" does not contain "Foo".'); $this->getCrawlerTester(new Crawler('

Foo'))->assertSelectorTextNotContains('body > h1', 'Foo'); } @@ -215,7 +215,7 @@ public function testAssertPageTitleContains() { $this->getCrawlerTester(new Crawler('Foobar'))->assertPageTitleContains('Foo'); $this->expectException(AssertionFailedError::class); - $this->expectExceptionMessage('matches selector "title" and has a node matching selector "title" with content containing "Bar".'); + $this->expectExceptionMessage('matches selector "title" and the text "Foo" of the node matching selector "title" contains "Bar".'); $this->getCrawlerTester(new Crawler('<html><head><title>Foo'))->assertPageTitleContains('Bar'); } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index f27bfd8afcc72..460249f72a2c3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -40,7 +40,7 @@ "symfony/browser-kit": "^4.4|^5.0", "symfony/console": "^5.2", "symfony/css-selector": "^4.4|^5.0", - "symfony/dom-crawler": "^4.4|^5.0", + "symfony/dom-crawler": "^4.4.30|^5.3.7", "symfony/dotenv": "^5.1", "symfony/polyfill-intl-icu": "~1.0", "symfony/form": "^5.2", diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 45447f50e82b5..e6456dd05b4da 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -100,7 +100,7 @@ public function collect(Request $request, Response $response, \Throwable $except $impersonatorUser = null; if ($token instanceof SwitchUserToken) { $originalToken = $token->getOriginalToken(); - // @deprecated since 5.3, change to $originalToken->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $originalToken->getUserIdentifier() in 6.0 $impersonatorUser = method_exists($originalToken, 'getUserIdentifier') ? $originalToken->getUserIdentifier() : $originalToken->getUsername(); } @@ -130,7 +130,7 @@ public function collect(Request $request, Response $response, \Throwable $except 'token' => $token, 'token_class' => $this->hasVarDumper ? new ClassStub(\get_class($token)) : \get_class($token), 'logout_url' => $logoutUrl, - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 'user' => method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(), 'roles' => $assignedRoles, 'inherited_roles' => array_unique($inheritedRoles), diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php index e7206f4020726..a5306b6bf1607 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/RememberMeBundle/Security/UserChangingUserProvider.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security; +use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; @@ -39,7 +40,7 @@ public function refreshUser(UserInterface $user) { $user = $this->inner->refreshUser($user); - $alterUser = \Closure::bind(function (User $user) { $user->password = 'foo'; }, null, User::class); + $alterUser = \Closure::bind(function (InMemoryUser $user) { $user->password = 'foo'; }, null, class_exists(User::class) ? User::class : InMemoryUser::class); $alterUser($user); return $user; diff --git a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php index 1146d1b7e98e2..0cfd10ae86be8 100644 --- a/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php @@ -52,7 +52,7 @@ static function ($key, $value, $isHit) { // Detect wrapped values that encode for their expiry and creation duration // For compactness, these values are packed in the key of an array using // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F - if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { + if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { $item->value = $v[$k]; $v = unpack('Ve/Nc', substr($k, 1, -1)); $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; diff --git a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php index 90266c78f7251..6fda0ef37f262 100644 --- a/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ApcuAdapter.php @@ -122,7 +122,7 @@ protected function doSave(array $values, int $lifetime) } catch (\Throwable $e) { if (1 === \count($values)) { // Workaround https://github.com/krakjoe/apcu/issues/170 - apcu_delete(key($values)); + apcu_delete(array_key_first($values)); } throw $e; diff --git a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php index 436880ff3785a..80211a84236bc 100644 --- a/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php @@ -25,7 +25,6 @@ class MemcachedAdapter extends AbstractAdapter /** * We are replacing characters that are illegal in Memcached keys with reserved characters from * {@see \Symfony\Contracts\Cache\ItemInterface::RESERVED_CHARACTERS} that are legal in Memcached. -This conversation was marked as resolved by lstrojny * Note: don’t use {@see \Symfony\Component\Cache\Adapter\AbstractAdapter::NS_SEPARATOR}. */ private const RESERVED_MEMCACHED = " \n\r\t\v\f\0"; diff --git a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php index fdf48a7442b99..7291a7e48f6e0 100644 --- a/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/ProxyAdapter.php @@ -63,7 +63,7 @@ static function ($key, $innerItem, $poolHash) { // Detect wrapped values that encode for their expiry and creation duration // For compactness, these values are packed in the key of an array using // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F - if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { + if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = (string) array_key_first($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) { $item->value = $v[$k]; $v = unpack('Ve/Nc', substr($k, 1, -1)); $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET; diff --git a/src/Symfony/Component/Cache/DependencyInjection/CacheCollectorPass.php b/src/Symfony/Component/Cache/DependencyInjection/CacheCollectorPass.php index 0bc7bc78e0ca7..843232e441530 100644 --- a/src/Symfony/Component/Cache/DependencyInjection/CacheCollectorPass.php +++ b/src/Symfony/Component/Cache/DependencyInjection/CacheCollectorPass.php @@ -72,6 +72,15 @@ private function addToCollector(string $id, string $name, ContainerBuilder $cont } $recorder->setArguments([new Reference($innerId = $id.$this->cachePoolRecorderInnerSuffix)]); + foreach ($definition->getMethodCalls() as [$method, $args]) { + if ('setCallbackWrapper' !== $method || !$args[0] instanceof Definition || !($args[0]->getArguments()[2] ?? null) instanceof Definition) { + continue; + } + if ([new Reference($id), 'setCallbackWrapper'] == $args[0]->getArguments()[2]->getFactory()) { + $args[0]->getArguments()[2]->setFactory([new Reference($innerId), 'setCallbackWrapper']); + } + } + $definition->setTags([]); $definition->setPublic(false); diff --git a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php index 107734c3ecd17..c5f64745ac62e 100644 --- a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php +++ b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php @@ -14,6 +14,7 @@ use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\ParameterNormalizer; use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -144,7 +145,7 @@ public function process(ContainerBuilder $container) $chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider']))); } - if (isset($tags[0]['namespace']) && ArrayAdapter::class !== $adapter->getClass()) { + if (isset($tags[0]['namespace']) && !\in_array($adapter->getClass(), [ArrayAdapter::class, NullAdapter::class], true)) { $chainedPool->replaceArgument($i++, $tags[0]['namespace']); } @@ -180,7 +181,7 @@ public function process(ContainerBuilder $container) ), ]); $pool->addTag($this->reversibleTag); - } elseif ('namespace' !== $attr || ArrayAdapter::class !== $class) { + } elseif ('namespace' !== $attr || !\in_array($class, [ArrayAdapter::class, NullAdapter::class], true)) { $argument = $tags[0][$attr]; if ('default_lifetime' === $attr && !is_numeric($argument)) { diff --git a/src/Symfony/Component/Cache/Tests/DependencyInjection/CacheCollectorPassTest.php b/src/Symfony/Component/Cache/Tests/DependencyInjection/CacheCollectorPassTest.php index d0ea55313b885..8ea6950429faa 100644 --- a/src/Symfony/Component/Cache/Tests/DependencyInjection/CacheCollectorPassTest.php +++ b/src/Symfony/Component/Cache/Tests/DependencyInjection/CacheCollectorPassTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Cache\Tests\Fixtures\ArrayCache; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; class CacheCollectorPassTest extends TestCase @@ -31,6 +32,13 @@ public function testProcess() $container = new ContainerBuilder(); $container ->register('fs', FilesystemAdapter::class) + ->addMethodCall('setCallbackWrapper', [(new Definition()) + ->addArgument(null) + ->addArgument(null) + ->addArgument((new Definition('callable')) + ->setFactory([new Reference('fs'), 'setCallbackWrapper']) + ), + ]) ->addTag('cache.pool'); $container ->register('tagged_fs', TagAwareAdapter::class) @@ -60,6 +68,9 @@ public function testProcess() $this->assertSame(TagAwareAdapter::class, $container->getDefinition('php')->getClass()); $this->assertFalse($collector->isPublic(), 'The "data_collector.cache" should be private after processing'); + + $innerFs = $container->getDefinition('fs.recorder_inner'); + $this->assertEquals([new Reference('fs.recorder_inner'), 'setCallbackWrapper'], $innerFs->getMethodCalls()[0][1][0]->getArgument(2)->getFactory()); } public function testProcessCacheObjectsAreDecorated() diff --git a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php index 5ce26b9d7f660..39350274aea33 100644 --- a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php +++ b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -116,6 +117,23 @@ public function testNamespaceArgumentIsNotReplacedIfArrayAdapterIsUsed() $this->assertCount(0, $container->getDefinition('app.cache_pool')->getArguments()); } + public function testNamespaceArgumentIsNotReplacedIfNullAdapterIsUsed() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.container_class', 'app'); + $container->setParameter('kernel.project_dir', 'foo'); + + $container->register('cache.adapter.null', NullAdapter::class); + + $cachePool = new ChildDefinition('cache.adapter.null'); + $cachePool->addTag('cache.pool'); + $container->setDefinition('app.cache_pool', $cachePool); + + $this->cachePoolPass->process($container); + + $this->assertCount(0, $container->getDefinition('app.cache_pool')->getArguments()); + } + public function testArgsAreReplaced() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/Cache/composer.json b/src/Symfony/Component/Cache/composer.json index d680ef51bcaaf..207f311595570 100644 --- a/src/Symfony/Component/Cache/composer.json +++ b/src/Symfony/Component/Cache/composer.json @@ -26,6 +26,7 @@ "psr/log": "^1.1|^2|^3", "symfony/cache-contracts": "^1.1.7|^2", "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php73": "^1.9", "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.1|^2", "symfony/var-exporter": "^4.4|^5.0" diff --git a/src/Symfony/Component/Console/Event/ConsoleEvent.php b/src/Symfony/Component/Console/Event/ConsoleEvent.php index 89ab645594ce1..4e00f7c73d6d4 100644 --- a/src/Symfony/Component/Console/Event/ConsoleEvent.php +++ b/src/Symfony/Component/Console/Event/ConsoleEvent.php @@ -28,7 +28,7 @@ class ConsoleEvent extends Event private $input; private $output; - public function __construct(Command $command = null, InputInterface $input, OutputInterface $output) + public function __construct(?Command $command, InputInterface $input, OutputInterface $output) { $this->command = $command; $this->input = $input; diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 881b4dc4fb4a1..cfcbbd9a125f5 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -42,7 +42,7 @@ public function getHelperSet() /** * Returns the length of a string, using mb_strwidth if it is available. * - * @deprecated since 5.3 + * @deprecated since Symfony 5.3 * * @return int The length of the string */ @@ -154,7 +154,7 @@ public static function formatMemory(int $memory) } /** - * @deprecated since 5.3 + * @deprecated since Symfony 5.3 */ public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string) { @@ -170,7 +170,7 @@ public static function removeDecoration(OutputFormatterInterface $formatter, ?st // remove <...> formatting $string = $formatter->format($string ?? ''); // remove already formatted characters - $string = preg_replace("/\033\[[^m]*m/", '', $string); + $string = preg_replace("/\033\[[^m]*m/", '', $string ?? ''); $formatter->setDecorated($isDecorated); return $string; diff --git a/src/Symfony/Component/Console/Helper/HelperSet.php b/src/Symfony/Component/Console/Helper/HelperSet.php index 5c08a7606d289..679dceab16921 100644 --- a/src/Symfony/Component/Console/Helper/HelperSet.php +++ b/src/Symfony/Component/Console/Helper/HelperSet.php @@ -89,8 +89,9 @@ public function getCommand() } /** - * @return Helper[] + * @return \Traversable<Helper> */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->helpers); diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index 91fba2b587364..1c03a1d968afc 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -482,8 +482,10 @@ private function overwrite(string $message): void } $this->output->clear($lineCount); } else { - if ($this->formatLineCount > 0) { - $this->cursor->moveUp($this->formatLineCount); + for ($i = 0; $i < $this->formatLineCount; ++$i) { + $this->cursor->moveToColumn(1); + $this->cursor->clearLine(); + $this->cursor->moveUp(); } $this->cursor->moveToColumn(1); diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index e424a29dfa4a0..ef5f0622269db 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -812,7 +812,7 @@ public function testMultilineFormat() $this->assertEquals( ">---------------------------\nfoobar". $this->generateOutput("=========>------------------\nfoobar"). - "\x1B[1A\x1B[1G\x1B[2K". + "\x1B[1G\x1B[2K\x1B[1A\x1B[1G\x1B[2K". $this->generateOutput("============================\nfoobar"), stream_get_contents($output->getStream()) ); @@ -983,7 +983,7 @@ protected function generateOutput($expected) { $count = substr_count($expected, "\n"); - return ($count ? sprintf("\x1B[%dA\x1B[1G\x1b[2K", $count) : "\x1B[1G\x1B[2K").$expected; + return ($count ? str_repeat("\x1B[1G\x1b[2K\x1B[1A", $count) : '')."\x1B[1G\x1B[2K".$expected; } public function testBarWidthWithMultilineFormat() @@ -1095,4 +1095,33 @@ public function testNoWriteWhenMessageIsSame() stream_get_contents($output->getStream()) ); } + + public function testMultiLineFormatIsFullyCleared() + { + $bar = new ProgressBar($output = $this->getOutputStream(), 3); + $bar->setFormat("%current%/%max%\n%message%\nFoo"); + + $bar->setMessage('1234567890'); + $bar->start(); + $bar->display(); + + $bar->setMessage('ABC'); + $bar->advance(); + $bar->display(); + + $bar->setMessage('A'); + $bar->advance(); + $bar->display(); + + $bar->finish(); + + rewind($output->getStream()); + $this->assertEquals( + "0/3\n1234567890\nFoo". + $this->generateOutput("1/3\nABC\nFoo"). + $this->generateOutput("2/3\nA\nFoo"). + $this->generateOutput("3/3\nA\nFoo"), + stream_get_contents($output->getStream()) + ); + } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 12d3b26f740df..47f51c2478940 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -62,10 +62,10 @@ public function __construct() new AutowireRequiredMethodsPass(), new AutowireRequiredPropertiesPass(), new ResolveBindingsPass(), - new DecoratorServicePass(), new CheckDefinitionValidityPass(), new AutowirePass(false), new ServiceLocatorTagPass(), + new DecoratorServicePass(), new ResolveTaggedIteratorArgumentPass(), new ResolveServiceSubscribersPass(), new ResolveReferencesToAliasesPass(), diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php index 25063d35ff3b5..24088fe0f504a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ServiceLocatorTagPassTest.php @@ -13,8 +13,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -143,4 +146,56 @@ public function testBindingsAreCopied() $this->assertSame(['foo'], array_keys($locator->getBindings())); $this->assertInstanceOf(BoundArgument::class, $locator->getBindings()['foo']); } + + public function testIndexedByServiceIdWithDecoration() + { + $container = new ContainerBuilder(); + + $locator = new Definition(Locator::class); + $locator->setPublic(true); + $locator->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('test_tag', null, null, true))); + + $container->setDefinition(Locator::class, $locator); + + $service = new Definition(Service::class); + $service->setPublic(true); + $service->addTag('test_tag'); + + $container->setDefinition(Service::class, $service); + + $decorated = new Definition(Decorated::class); + $decorated->setPublic(true); + $decorated->setDecoratedService(Service::class); + + $container->setDefinition(Decorated::class, $decorated); + + $container->compile(); + + /** @var ServiceLocator $locator */ + $locator = $container->get(Locator::class)->locator; + static::assertTrue($locator->has(Service::class)); + static::assertFalse($locator->has(Decorated::class)); + static::assertInstanceOf(Decorated::class, $locator->get(Service::class)); + } +} + +class Locator +{ + /** + * @var ServiceLocator + */ + public $locator; + + public function __construct(ServiceLocator $locator) + { + $this->locator = $locator; + } +} + +class Service +{ +} + +class DecoratedService +{ } diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 6c61f7e677b29..19ead5213a37a 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -1112,6 +1112,7 @@ public function getNode(int $position) /** * @return int */ + #[\ReturnTypeWillChange] public function count() { return \count($this->nodes); @@ -1120,6 +1121,7 @@ public function count() /** * @return \ArrayIterator|\DOMNode[] */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->nodes); diff --git a/src/Symfony/Component/DomCrawler/Form.php b/src/Symfony/Component/DomCrawler/Form.php index 2e9e01dc65dfe..baa81209902d4 100644 --- a/src/Symfony/Component/DomCrawler/Form.php +++ b/src/Symfony/Component/DomCrawler/Form.php @@ -315,6 +315,7 @@ public function all() * * @return bool true if the field exists, false otherwise */ + #[\ReturnTypeWillChange] public function offsetExists($name) { return $this->has($name); @@ -329,6 +330,7 @@ public function offsetExists($name) * * @throws \InvalidArgumentException if the field does not exist */ + #[\ReturnTypeWillChange] public function offsetGet($name) { return $this->fields->get($name); @@ -344,6 +346,7 @@ public function offsetGet($name) * * @throws \InvalidArgumentException if the field does not exist */ + #[\ReturnTypeWillChange] public function offsetSet($name, $value) { $this->fields->set($name, $value); @@ -356,6 +359,7 @@ public function offsetSet($name, $value) * * @return void */ + #[\ReturnTypeWillChange] public function offsetUnset($name) { $this->fields->remove($name); diff --git a/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php index 7008779e14203..a198784d448df 100644 --- a/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php +++ b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorAttributeValueSame.php @@ -47,7 +47,7 @@ protected function matches($crawler): bool return false; } - return $this->expectedText === trim($crawler->attr($this->attribute)); + return $this->expectedText === trim($crawler->attr($this->attribute) ?? ''); } /** diff --git a/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorTextContains.php b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorTextContains.php index ffc6f5677cc5d..c4697ecf75376 100644 --- a/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorTextContains.php +++ b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerSelectorTextContains.php @@ -18,6 +18,8 @@ final class CrawlerSelectorTextContains extends Constraint { private $selector; private $expectedText; + private $hasNode = false; + private $nodeText; public function __construct(string $selector, string $expectedText) { @@ -30,7 +32,11 @@ public function __construct(string $selector, string $expectedText) */ public function toString(): string { - return sprintf('has a node matching selector "%s" with content containing "%s"', $this->selector, $this->expectedText); + if ($this->hasNode) { + return sprintf('the text "%s" of the node matching selector "%s" contains "%s"', $this->nodeText, $this->selector, $this->expectedText); + } + + return sprintf('the Crawler has a node matching selector "%s"', $this->selector); } /** @@ -42,10 +48,15 @@ protected function matches($crawler): bool { $crawler = $crawler->filter($this->selector); if (!\count($crawler)) { + $this->hasNode = false; + return false; } - return false !== mb_strpos($crawler->text(null, true), $this->expectedText); + $this->hasNode = true; + $this->nodeText = $crawler->text(null, true); + + return false !== mb_strpos($this->nodeText, $this->expectedText); } /** @@ -55,6 +66,6 @@ protected function matches($crawler): bool */ protected function failureDescription($crawler): string { - return 'the Crawler '.$this->toString(); + return $this->toString(); } } diff --git a/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerSelectorAttributeValueSameTest.php b/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerSelectorAttributeValueSameTest.php index ab528ab356443..47ecdc8a04438 100644 --- a/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerSelectorAttributeValueSameTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerSelectorAttributeValueSameTest.php @@ -23,6 +23,7 @@ public function testConstraint() { $constraint = new CrawlerSelectorAttributeValueSame('input[name="username"]', 'value', 'Fabien'); $this->assertTrue($constraint->evaluate(new Crawler('<html><body><form><input type="text" name="username" value="Fabien">'), '', true)); + $this->assertFalse($constraint->evaluate(new Crawler('<html><body><form><input type="text" name="username">'), '', true)); $this->assertFalse($constraint->evaluate(new Crawler('<html><head><title>Bar'), '', true)); try { diff --git a/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerSelectorTextContainsTest.php b/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerSelectorTextContainsTest.php index b92a13187bd5f..0d7656e589b49 100644 --- a/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerSelectorTextContainsTest.php +++ b/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerSelectorTextContainsTest.php @@ -24,15 +24,22 @@ public function testConstraint() $constraint = new CrawlerSelectorTextContains('title', 'Foo'); $this->assertTrue($constraint->evaluate(new Crawler('<html><head><title>Foobar'), '', true)); $this->assertFalse($constraint->evaluate(new Crawler('<html><head><title>Bar'), '', true)); + $this->assertFalse($constraint->evaluate(new Crawler('<html><head></head><body>Bar'), '', true)); try { $constraint->evaluate(new Crawler('<html><head><title>Bar')); - } catch (ExpectationFailedException $e) { - $this->assertEquals("Failed asserting that the Crawler has a node matching selector \"title\" with content containing \"Foo\".\n", TestFailure::exceptionToString($e)); - return; + $this->fail(); + } catch (ExpectationFailedException $e) { + $this->assertEquals("Failed asserting that the text \"Bar\" of the node matching selector \"title\" contains \"Foo\".\n", TestFailure::exceptionToString($e)); } - $this->fail(); + try { + $constraint->evaluate(new Crawler('<html><head></head><body>Bar')); + + $this->fail(); + } catch (ExpectationFailedException $e) { + $this->assertEquals("Failed asserting that the Crawler has a node matching selector \"title\".\n", TestFailure::exceptionToString($e)); + } } } diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt index 15933828bd426..92f7e12fde227 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt @@ -1,5 +1,7 @@ --TEST-- Test catching fatal errors when handlers are nested +--SKIPIF-- +<?php if (\PHP_VERSION_ID < 80100) echo 'skip' ?> --FILE-- <?php @@ -36,6 +38,9 @@ array(1) { string(37) "Error and exception handlers do match" } object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) { + ["message":protected]=> + string(186) "Error: Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" +%a ["error":"Symfony\Component\ErrorHandler\Error\FatalError":private]=> array(4) { ["type"]=> @@ -47,7 +52,4 @@ object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) { ["line"]=> int(%d) } - ["message":protected]=> - string(186) "Error: Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" -%a } diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers_pre81.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers_pre81.phpt new file mode 100644 index 0000000000000..d6b2baa6c904d --- /dev/null +++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers_pre81.phpt @@ -0,0 +1,55 @@ +--TEST-- +Test catching fatal errors when handlers are nested +--SKIPIF-- +<?php if (\PHP_VERSION_ID >= 80100) echo 'skip' ?> +--FILE-- +<?php + +namespace Symfony\Component\ErrorHandler; + +$vendor = __DIR__; +while (!file_exists($vendor.'/vendor')) { + $vendor = \dirname($vendor); +} +require $vendor.'/vendor/autoload.php'; + +Debug::enable(); +ini_set('display_errors', 0); + +$eHandler = set_error_handler('var_dump'); +$xHandler = set_exception_handler('var_dump'); + +var_dump([ + $eHandler[0] === $xHandler[0] ? 'Error and exception handlers do match' : 'Error and exception handlers are different', +]); + +$eHandler[0]->setExceptionHandler('print_r'); + +if (true) { + class Broken implements \JsonSerializable + { + } +} + +?> +--EXPECTF-- +array(1) { + [0]=> + string(37) "Error and exception handlers do match" +} +object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) { + ["error":"Symfony\Component\ErrorHandler\Error\FatalError":private]=> + array(4) { + ["type"]=> + int(1) + ["message"]=> + string(179) "Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" + ["file"]=> + string(%d) "%s" + ["line"]=> + int(%d) + } + ["message":protected]=> + string(186) "Error: Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)" +%a +} diff --git a/src/Symfony/Component/EventDispatcher/GenericEvent.php b/src/Symfony/Component/EventDispatcher/GenericEvent.php index e903cafea8890..691ab3620f4d8 100644 --- a/src/Symfony/Component/EventDispatcher/GenericEvent.php +++ b/src/Symfony/Component/EventDispatcher/GenericEvent.php @@ -118,6 +118,7 @@ public function hasArgument(string $key) * * @throws \InvalidArgumentException if key does not exist in $this->args */ + #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->getArgument($key); @@ -131,6 +132,7 @@ public function offsetGet($key) * * @return void */ + #[\ReturnTypeWillChange] public function offsetSet($key, $value) { $this->setArgument($key, $value); @@ -143,6 +145,7 @@ public function offsetSet($key, $value) * * @return void */ + #[\ReturnTypeWillChange] public function offsetUnset($key) { if ($this->hasArgument($key)) { @@ -157,6 +160,7 @@ public function offsetUnset($key) * * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($key) { return $this->hasArgument($key); @@ -167,6 +171,7 @@ public function offsetExists($key) * * @return \ArrayIterator */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->arguments); diff --git a/src/Symfony/Component/ExpressionLanguage/Lexer.php b/src/Symfony/Component/ExpressionLanguage/Lexer.php index a8966f6fa52fb..6e3cc488b1ee3 100644 --- a/src/Symfony/Component/ExpressionLanguage/Lexer.php +++ b/src/Symfony/Component/ExpressionLanguage/Lexer.php @@ -48,13 +48,13 @@ public function tokenize(string $expression) } $tokens[] = new Token(Token::NUMBER_TYPE, $number, $cursor + 1); $cursor += \strlen($match[0]); - } elseif (str_contains('([{', $expression[$cursor])) { + } elseif (false !== strpos('([{', $expression[$cursor])) { // opening bracket $brackets[] = [$expression[$cursor], $cursor]; $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); ++$cursor; - } elseif (str_contains(')]}', $expression[$cursor])) { + } elseif (false !== strpos(')]}', $expression[$cursor])) { // closing bracket if (empty($brackets)) { throw new SyntaxError(sprintf('Unexpected "%s".', $expression[$cursor]), $cursor, $expression); @@ -75,7 +75,7 @@ public function tokenize(string $expression) // operators $tokens[] = new Token(Token::OPERATOR_TYPE, $match[0], $cursor + 1); $cursor += \strlen($match[0]); - } elseif (str_contains('.,?:', $expression[$cursor])) { + } elseif (false !== strpos('.,?:', $expression[$cursor])) { // punctuation $tokens[] = new Token(Token::PUNCTUATION_TYPE, $expression[$cursor], $cursor + 1); ++$cursor; diff --git a/src/Symfony/Component/ExpressionLanguage/composer.json b/src/Symfony/Component/ExpressionLanguage/composer.json index 6bc260e4b9bae..a026b7956d7bb 100644 --- a/src/Symfony/Component/ExpressionLanguage/composer.json +++ b/src/Symfony/Component/ExpressionLanguage/composer.json @@ -18,7 +18,6 @@ "require": { "php": ">=7.2.5", "symfony/cache": "^4.4|^5.0", - "symfony/polyfill-php80": "^1.16", "symfony/service-contracts": "^1.1|^2" }, "autoload": { diff --git a/src/Symfony/Component/Finder/Finder.php b/src/Symfony/Component/Finder/Finder.php index f04ce81360a95..c9bd21fe444f8 100644 --- a/src/Symfony/Component/Finder/Finder.php +++ b/src/Symfony/Component/Finder/Finder.php @@ -605,6 +605,7 @@ public function in($dirs) * * @throws \LogicException if the in() method has not been called */ + #[\ReturnTypeWillChange] public function getIterator() { if (0 === \count($this->dirs) && 0 === \count($this->iterators)) { @@ -687,6 +688,7 @@ public function hasResults() * * @return int */ + #[\ReturnTypeWillChange] public function count() { return iterator_count($this->getIterator()); diff --git a/src/Symfony/Component/Finder/Iterator/CustomFilterIterator.php b/src/Symfony/Component/Finder/Iterator/CustomFilterIterator.php index a30bbd0b9d265..f85cb7bffb772 100644 --- a/src/Symfony/Component/Finder/Iterator/CustomFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/CustomFilterIterator.php @@ -46,6 +46,7 @@ public function __construct(\Iterator $iterator, array $filters) * * @return bool true if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { $fileinfo = $this->current(); diff --git a/src/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php b/src/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php index 2e97e00d37456..90616f471b1f7 100644 --- a/src/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php @@ -38,6 +38,7 @@ public function __construct(\Iterator $iterator, array $comparators) * * @return bool true if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { $fileinfo = $this->current(); diff --git a/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php b/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php index 18e751d77b928..e96fefd961b16 100644 --- a/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php @@ -38,6 +38,7 @@ public function __construct(\RecursiveIteratorIterator $iterator, int $minDepth * * @return bool true if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { return $this->getInnerIterator()->getDepth() >= $this->minDepth; diff --git a/src/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php b/src/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php index 366ad70cdb9bc..cf9e678771da9 100644 --- a/src/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php @@ -52,6 +52,7 @@ public function __construct(\Iterator $iterator, array $directories) * * @return bool True if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { if ($this->isRecursive && isset($this->excludedDirs[$this->getFilename()]) && $this->isDir()) { @@ -71,6 +72,7 @@ public function accept() /** * @return bool */ + #[\ReturnTypeWillChange] public function hasChildren() { return $this->isRecursive && $this->iterator->hasChildren(); @@ -79,6 +81,7 @@ public function hasChildren() /** * @return self */ + #[\ReturnTypeWillChange] public function getChildren() { $children = new self($this->iterator->getChildren(), []); diff --git a/src/Symfony/Component/Finder/Iterator/FileTypeFilterIterator.php b/src/Symfony/Component/Finder/Iterator/FileTypeFilterIterator.php index 0ea2c508802de..d054cefb9fff9 100644 --- a/src/Symfony/Component/Finder/Iterator/FileTypeFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/FileTypeFilterIterator.php @@ -39,6 +39,7 @@ public function __construct(\Iterator $iterator, int $mode) * * @return bool true if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { $fileinfo = $this->current(); diff --git a/src/Symfony/Component/Finder/Iterator/FilecontentFilterIterator.php b/src/Symfony/Component/Finder/Iterator/FilecontentFilterIterator.php index b26a368483261..9493b5e87313e 100644 --- a/src/Symfony/Component/Finder/Iterator/FilecontentFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/FilecontentFilterIterator.php @@ -24,6 +24,7 @@ class FilecontentFilterIterator extends MultiplePcreFilterIterator * * @return bool true if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { if (!$this->matchRegexps && !$this->noMatchRegexps) { diff --git a/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php b/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php index dedd1ca55e878..137c2c443d087 100644 --- a/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php @@ -25,6 +25,7 @@ class FilenameFilterIterator extends MultiplePcreFilterIterator * * @return bool true if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { return $this->isAccepted($this->current()->getFilename()); diff --git a/src/Symfony/Component/Finder/Iterator/PathFilterIterator.php b/src/Symfony/Component/Finder/Iterator/PathFilterIterator.php index 67b71f46d7724..1d09c2674875a 100644 --- a/src/Symfony/Component/Finder/Iterator/PathFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/PathFilterIterator.php @@ -24,6 +24,7 @@ class PathFilterIterator extends MultiplePcreFilterIterator * * @return bool true if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { $filename = $this->current()->getRelativePathname(); diff --git a/src/Symfony/Component/Finder/Iterator/SizeRangeFilterIterator.php b/src/Symfony/Component/Finder/Iterator/SizeRangeFilterIterator.php index 2aeef67b87f73..4078f3692e052 100644 --- a/src/Symfony/Component/Finder/Iterator/SizeRangeFilterIterator.php +++ b/src/Symfony/Component/Finder/Iterator/SizeRangeFilterIterator.php @@ -38,6 +38,7 @@ public function __construct(\Iterator $iterator, array $comparators) * * @return bool true if the value should be kept, false otherwise */ + #[\ReturnTypeWillChange] public function accept() { $fileinfo = $this->current(); diff --git a/src/Symfony/Component/Finder/Iterator/SortableIterator.php b/src/Symfony/Component/Finder/Iterator/SortableIterator.php index 2e51f36ad39f3..04ca8ebf28d1e 100644 --- a/src/Symfony/Component/Finder/Iterator/SortableIterator.php +++ b/src/Symfony/Component/Finder/Iterator/SortableIterator.php @@ -81,6 +81,7 @@ public function __construct(\Traversable $iterator, $sort, bool $reverseOrder = /** * @return \Traversable */ + #[\ReturnTypeWillChange] public function getIterator() { if (1 === $this->sort) { diff --git a/src/Symfony/Component/Finder/Tests/Iterator/Iterator.php b/src/Symfony/Component/Finder/Tests/Iterator/Iterator.php index bc2eb53b393e0..108c66a273c99 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/Iterator.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/Iterator.php @@ -23,12 +23,12 @@ public function __construct(array $values = []) $this->rewind(); } - public function attach(\SplFileInfo $fileinfo) + public function attach(\SplFileInfo $fileinfo): void { $this->values[] = $fileinfo; } - public function rewind() + public function rewind(): void { reset($this->values); } @@ -38,16 +38,24 @@ public function valid(): bool return false !== $this->current(); } - public function next() + public function next(): void { next($this->values); } + /** + * @return mixed + */ + #[\ReturnTypeWillChange] public function current() { return current($this->values); } + /** + * @return mixed + */ + #[\ReturnTypeWillChange] public function key() { return key($this->values); diff --git a/src/Symfony/Component/Finder/Tests/Iterator/MultiplePcreFilterIteratorTest.php b/src/Symfony/Component/Finder/Tests/Iterator/MultiplePcreFilterIteratorTest.php index 69f020e9ec0e5..d7059b5ec3861 100644 --- a/src/Symfony/Component/Finder/Tests/Iterator/MultiplePcreFilterIteratorTest.php +++ b/src/Symfony/Component/Finder/Tests/Iterator/MultiplePcreFilterIteratorTest.php @@ -54,7 +54,7 @@ public function __construct() { } - public function accept() + public function accept(): bool { throw new \BadFunctionCallException('Not implemented'); } diff --git a/src/Symfony/Component/Form/Button.php b/src/Symfony/Component/Form/Button.php index 7b2744e16147e..017ff8b9758ed 100644 --- a/src/Symfony/Component/Form/Button.php +++ b/src/Symfony/Component/Form/Button.php @@ -51,6 +51,7 @@ public function __construct(FormConfigInterface $config) * * @return bool Always returns false */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { return false; @@ -67,6 +68,7 @@ public function offsetExists($offset) * * @throws BadMethodCallException */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { throw new BadMethodCallException('Buttons cannot have children.'); @@ -84,6 +86,7 @@ public function offsetGet($offset) * * @throws BadMethodCallException */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { throw new BadMethodCallException('Buttons cannot have children.'); @@ -100,6 +103,7 @@ public function offsetSet($offset, $value) * * @throws BadMethodCallException */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { throw new BadMethodCallException('Buttons cannot have children.'); @@ -430,6 +434,7 @@ public function createView(FormView $parent = null) * * @return int Always returns 0 */ + #[\ReturnTypeWillChange] public function count() { return 0; @@ -440,6 +445,7 @@ public function count() * * @return \EmptyIterator Always returns an empty iterator */ + #[\ReturnTypeWillChange] public function getIterator() { return new \EmptyIterator(); diff --git a/src/Symfony/Component/Form/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php index 7c1213b879955..82ce9af05d0d9 100644 --- a/src/Symfony/Component/Form/ButtonBuilder.php +++ b/src/Symfony/Component/Form/ButtonBuilder.php @@ -715,6 +715,7 @@ public function getIsEmptyCallback(): ?callable * * @return int Always returns 0 */ + #[\ReturnTypeWillChange] public function count() { return 0; @@ -725,6 +726,7 @@ public function count() * * @return \EmptyIterator Always returns an empty iterator */ + #[\ReturnTypeWillChange] public function getIterator() { return new \EmptyIterator(); diff --git a/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php b/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php index 0c0654ed9f4e2..5ad89c6b1f82e 100644 --- a/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php +++ b/src/Symfony/Component/Form/ChoiceList/View/ChoiceGroupView.php @@ -35,8 +35,9 @@ public function __construct(string $label, array $choices = []) /** * {@inheritdoc} * - * @return self[]|ChoiceView[] + * @return \Traversable<ChoiceGroupView|ChoiceView> */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->choices); diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php index 6e318a62da770..b8fab2b9605df 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationPath.php @@ -221,6 +221,7 @@ public function mapsForm(int $index) * * @return ViolationPathIterator */ + #[\ReturnTypeWillChange] public function getIterator() { return new ViolationPathIterator($this); diff --git a/src/Symfony/Component/Form/Form.php b/src/Symfony/Component/Form/Form.php index 4bc42c08416b1..55a3d500cbbb2 100644 --- a/src/Symfony/Component/Form/Form.php +++ b/src/Symfony/Component/Form/Form.php @@ -965,6 +965,7 @@ public function get(string $name) * * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($name) { return $this->has($name); @@ -979,6 +980,7 @@ public function offsetExists($name) * * @throws OutOfBoundsException if the named child does not exist */ + #[\ReturnTypeWillChange] public function offsetGet($name) { return $this->get($name); @@ -997,6 +999,7 @@ public function offsetGet($name) * * @see self::add() */ + #[\ReturnTypeWillChange] public function offsetSet($name, $child) { $this->add($child); @@ -1011,6 +1014,7 @@ public function offsetSet($name, $child) * * @throws AlreadySubmittedException if the form has already been submitted */ + #[\ReturnTypeWillChange] public function offsetUnset($name) { $this->remove($name); @@ -1021,6 +1025,7 @@ public function offsetUnset($name) * * @return \Traversable<FormInterface> */ + #[\ReturnTypeWillChange] public function getIterator() { return $this->children; @@ -1031,6 +1036,7 @@ public function getIterator() * * @return int The number of embedded form children */ + #[\ReturnTypeWillChange] public function count() { return \count($this->children); diff --git a/src/Symfony/Component/Form/FormBuilder.php b/src/Symfony/Component/Form/FormBuilder.php index 79ed1747b313a..f15a1506599e8 100644 --- a/src/Symfony/Component/Form/FormBuilder.php +++ b/src/Symfony/Component/Form/FormBuilder.php @@ -161,6 +161,7 @@ public function all() /** * @return int */ + #[\ReturnTypeWillChange] public function count() { if ($this->locked) { @@ -215,6 +216,7 @@ public function getForm() * * @return FormBuilderInterface[]|\Traversable */ + #[\ReturnTypeWillChange] public function getIterator() { if ($this->locked) { diff --git a/src/Symfony/Component/Form/FormErrorIterator.php b/src/Symfony/Component/Form/FormErrorIterator.php index 7a62ba7fa8d24..f339e8621b7bd 100644 --- a/src/Symfony/Component/Form/FormErrorIterator.php +++ b/src/Symfony/Component/Form/FormErrorIterator.php @@ -93,6 +93,7 @@ public function getForm() * * @return FormError|self An error or an iterator containing nested errors */ + #[\ReturnTypeWillChange] public function current() { return current($this->errors); @@ -101,6 +102,7 @@ public function current() /** * Advances the iterator to the next position. */ + #[\ReturnTypeWillChange] public function next() { next($this->errors); @@ -111,6 +113,7 @@ public function next() * * @return int The 0-indexed position */ + #[\ReturnTypeWillChange] public function key() { return key($this->errors); @@ -121,6 +124,7 @@ public function key() * * @return bool Whether the iterator is valid */ + #[\ReturnTypeWillChange] public function valid() { return null !== key($this->errors); @@ -132,6 +136,7 @@ public function valid() * This method detects if errors have been added to the form since the * construction of the iterator. */ + #[\ReturnTypeWillChange] public function rewind() { reset($this->errors); @@ -144,6 +149,7 @@ public function rewind() * * @return bool Whether that position exists */ + #[\ReturnTypeWillChange] public function offsetExists($position) { return isset($this->errors[$position]); @@ -158,6 +164,7 @@ public function offsetExists($position) * * @throws OutOfBoundsException If the given position does not exist */ + #[\ReturnTypeWillChange] public function offsetGet($position) { if (!isset($this->errors[$position])) { @@ -174,6 +181,7 @@ public function offsetGet($position) * * @throws BadMethodCallException */ + #[\ReturnTypeWillChange] public function offsetSet($position, $value) { throw new BadMethodCallException('The iterator doesn\'t support modification of elements.'); @@ -186,6 +194,7 @@ public function offsetSet($position, $value) * * @throws BadMethodCallException */ + #[\ReturnTypeWillChange] public function offsetUnset($position) { throw new BadMethodCallException('The iterator doesn\'t support modification of elements.'); @@ -197,6 +206,7 @@ public function offsetUnset($position) * * @return bool Whether the current element is an instance of this class */ + #[\ReturnTypeWillChange] public function hasChildren() { return current($this->errors) instanceof self; @@ -207,6 +217,7 @@ public function hasChildren() * * @return self */ + #[\ReturnTypeWillChange] public function getChildren() { return current($this->errors); @@ -229,6 +240,7 @@ public function getChildren() * * @return int The number of iterated elements */ + #[\ReturnTypeWillChange] public function count() { return \count($this->errors); @@ -243,6 +255,7 @@ public function count() * * @throws OutOfBoundsException If the position is invalid */ + #[\ReturnTypeWillChange] public function seek($position) { if (!isset($this->errors[$position])) { diff --git a/src/Symfony/Component/Form/FormView.php b/src/Symfony/Component/Form/FormView.php index b5f9757c67322..76d7b73f0c445 100644 --- a/src/Symfony/Component/Form/FormView.php +++ b/src/Symfony/Component/Form/FormView.php @@ -108,6 +108,7 @@ public function setMethodRendered() * * @return self The child view */ + #[\ReturnTypeWillChange] public function offsetGet($name) { return $this->children[$name]; @@ -120,6 +121,7 @@ public function offsetGet($name) * * @return bool Whether the child view exists */ + #[\ReturnTypeWillChange] public function offsetExists($name) { return isset($this->children[$name]); @@ -132,6 +134,7 @@ public function offsetExists($name) * * @throws BadMethodCallException always as setting a child by name is not allowed */ + #[\ReturnTypeWillChange] public function offsetSet($name, $value) { throw new BadMethodCallException('Not supported.'); @@ -144,6 +147,7 @@ public function offsetSet($name, $value) * * @return void */ + #[\ReturnTypeWillChange] public function offsetUnset($name) { unset($this->children[$name]); @@ -154,6 +158,7 @@ public function offsetUnset($name) * * @return \ArrayIterator<string, FormView> The iterator */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->children); @@ -164,6 +169,7 @@ public function getIterator() * * @return int The number of children views */ + #[\ReturnTypeWillChange] public function count() { return \count($this->children); diff --git a/src/Symfony/Component/Form/Resources/translations/validators.da.xlf b/src/Symfony/Component/Form/Resources/translations/validators.da.xlf index dafe20fa02c32..b4f078ff35f40 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.da.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.da.xlf @@ -120,7 +120,7 @@ </trans-unit> <trans-unit id="125"> <source>Please enter a valid email address.</source> - <target>Indtast venligst en gyldig emailaddresse.</target> + <target>Indtast venligst en gyldig e-mailadresse.</target> </trans-unit> <trans-unit id="126"> <source>Please select a valid option.</source> diff --git a/src/Symfony/Component/Form/Resources/translations/validators.et.xlf b/src/Symfony/Component/Form/Resources/translations/validators.et.xlf index 1a9867fa20953..6524c86b144ee 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.et.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.et.xlf @@ -14,6 +14,126 @@ <source>The CSRF token is invalid. Please try to resubmit the form.</source> <target>CSRF-märgis on vigane. Palun proovi vormi uuesti esitada.</target> </trans-unit> + <trans-unit id="99"> + <source>This value is not a valid HTML5 color.</source> + <target>See väärtus ei ole korrektne HTML5 värv.</target> + </trans-unit> + <trans-unit id="100"> + <source>Please enter a valid birthdate.</source> + <target>Palun sisesta korrektne sünnikuupäev.</target> + </trans-unit> + <trans-unit id="101"> + <source>The selected choice is invalid.</source> + <target>Tehtud valik on vigane.</target> + </trans-unit> + <trans-unit id="102"> + <source>The collection is invalid.</source> + <target>Kogum on vigane.</target> + </trans-unit> + <trans-unit id="103"> + <source>Please select a valid color.</source> + <target>Palun vali korrektne värv.</target> + </trans-unit> + <trans-unit id="104"> + <source>Please select a valid country.</source> + <target>Palun vali korrektne riik.</target> + </trans-unit> + <trans-unit id="105"> + <source>Please select a valid currency.</source> + <target>Palun vali korrektne valuuta.</target> + </trans-unit> + <trans-unit id="106"> + <source>Please choose a valid date interval.</source> + <target>Palun vali korrektne kuupäevade vahemik.</target> + </trans-unit> + <trans-unit id="107"> + <source>Please enter a valid date and time.</source> + <target>Palun sisesta korrektne kuupäev ja kellaaeg.</target> + </trans-unit> + <trans-unit id="108"> + <source>Please enter a valid date.</source> + <target>Palun sisesta korrektne kuupäev.</target> + </trans-unit> + <trans-unit id="109"> + <source>Please select a valid file.</source> + <target>Palun vali korrektne fail.</target> + </trans-unit> + <trans-unit id="110"> + <source>The hidden field is invalid.</source> + <target>Peidetud väli on vigane.</target> + </trans-unit> + <trans-unit id="111"> + <source>Please enter an integer.</source> + <target>Palun sisesta täisarv.</target> + </trans-unit> + <trans-unit id="112"> + <source>Please select a valid language.</source> + <target>Palun vali korrektne keel.</target> + </trans-unit> + <trans-unit id="113"> + <source>Please select a valid locale.</source> + <target>Palun vali korrektne keelekood.</target> + </trans-unit> + <trans-unit id="114"> + <source>Please enter a valid money amount.</source> + <target>Palun sisesta korrektne rahaline väärtus.</target> + </trans-unit> + <trans-unit id="115"> + <source>Please enter a number.</source> + <target>Palun sisesta number.</target> + </trans-unit> + <trans-unit id="116"> + <source>The password is invalid.</source> + <target>Vigane parool.</target> + </trans-unit> + <trans-unit id="117"> + <source>Please enter a percentage value.</source> + <target>Palun sisesta protsendiline väärtus.</target> + </trans-unit> + <trans-unit id="118"> + <source>The values do not match.</source> + <target>Väärtused ei klapi.</target> + </trans-unit> + <trans-unit id="119"> + <source>Please enter a valid time.</source> + <target>Palun sisesta korrektne aeg.</target> + </trans-unit> + <trans-unit id="120"> + <source>Please select a valid timezone.</source> + <target>Palun vali korrektne ajavöönd.</target> + </trans-unit> + <trans-unit id="121"> + <source>Please enter a valid URL.</source> + <target>Palun sisesta korrektne URL.</target> + </trans-unit> + <trans-unit id="122"> + <source>Please enter a valid search term.</source> + <target>Palun sisesta korrektne otsingutermin.</target> + </trans-unit> + <trans-unit id="123"> + <source>Please provide a valid phone number.</source> + <target>Palun sisesta korrektne telefoninumber.</target> + </trans-unit> + <trans-unit id="124"> + <source>The checkbox has an invalid value.</source> + <target>Märkeruudu väärtus on vigane.</target> + </trans-unit> + <trans-unit id="125"> + <source>Please enter a valid email address.</source> + <target>Palun sisesta korrektne e-posti aadress.</target> + </trans-unit> + <trans-unit id="126"> + <source>Please select a valid option.</source> + <target>Palun tee korrektne valik.</target> + </trans-unit> + <trans-unit id="127"> + <source>Please select a valid range.</source> + <target>Palun vali korrektne vahemik.</target> + </trans-unit> + <trans-unit id="128"> + <source>Please enter a valid week.</source> + <target>Palun sisesta korrektne nädal.</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php index 60d9d5a84e428..dc01ba15503d9 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php @@ -17,9 +17,9 @@ class DateTimeToStringTransformerTest extends TestCase { - public function dataProvider() + public function dataProvider(): array { - $data = [ + return [ ['Y-m-d H:i:s', '2010-02-03 16:05:06', '2010-02-03 16:05:06 UTC'], ['Y-m-d H:i:00', '2010-02-03 16:05:00', '2010-02-03 16:05:00 UTC'], ['Y-m-d H:i', '2010-02-03 16:05', '2010-02-03 16:05:00 UTC'], @@ -36,7 +36,6 @@ public function dataProvider() // different day representations ['Y-m-j', '2010-02-3', '2010-02-03 00:00:00 UTC'], - ['z', '33', '1970-02-03 00:00:00 UTC'], // not bijective // this will not work as PHP will use actual date to replace missing info @@ -63,8 +62,6 @@ public function dataProvider() ['Y-z', '2010-33', '2010-02-03 00:00:00 UTC'], ]; - - return $data; } /** diff --git a/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php b/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php index 5c12b6b400bb8..942add40e3736 100644 --- a/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php +++ b/src/Symfony/Component/Form/Tests/Fixtures/CustomArrayObject.php @@ -29,12 +29,18 @@ public function offsetExists($offset): bool return \array_key_exists($offset, $this->array); } + /** + * @param mixed $offset + * + * @return mixed + */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->array[$offset]; } - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { if (null === $offset) { $this->array[] = $value; @@ -43,7 +49,7 @@ public function offsetSet($offset, $value) } } - public function offsetUnset($offset) + public function offsetUnset($offset): void { unset($this->array[$offset]); } @@ -73,7 +79,7 @@ public function __unserialize(array $data): void $this->array = $data; } - public function unserialize($serialized) + public function unserialize($serialized): void { $this->__unserialize((array) unserialize((string) $serialized)); } diff --git a/src/Symfony/Component/Form/Util/InheritDataAwareIterator.php b/src/Symfony/Component/Form/Util/InheritDataAwareIterator.php index 1bb324423623b..7cba17dbebe1c 100644 --- a/src/Symfony/Component/Form/Util/InheritDataAwareIterator.php +++ b/src/Symfony/Component/Form/Util/InheritDataAwareIterator.php @@ -30,6 +30,7 @@ class InheritDataAwareIterator extends \IteratorIterator implements \RecursiveIt * * @return static */ + #[\ReturnTypeWillChange] public function getChildren() { return new static($this->current()); @@ -38,6 +39,7 @@ public function getChildren() /** * @return bool */ + #[\ReturnTypeWillChange] public function hasChildren() { return (bool) $this->current()->getConfig()->getInheritData(); diff --git a/src/Symfony/Component/Form/Util/OrderedHashMap.php b/src/Symfony/Component/Form/Util/OrderedHashMap.php index 7b1ca264d2bbb..b60a7ce4b0959 100644 --- a/src/Symfony/Component/Form/Util/OrderedHashMap.php +++ b/src/Symfony/Component/Form/Util/OrderedHashMap.php @@ -101,6 +101,7 @@ public function __construct(array $elements = []) /** * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($key) { return isset($this->elements[$key]); @@ -111,6 +112,7 @@ public function offsetExists($key) * * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($key) { if (!isset($this->elements[$key])) { @@ -125,6 +127,7 @@ public function offsetGet($key) * * @return void */ + #[\ReturnTypeWillChange] public function offsetSet($key, $value) { if (null === $key || !isset($this->elements[$key])) { @@ -148,6 +151,7 @@ public function offsetSet($key, $value) * * @return void */ + #[\ReturnTypeWillChange] public function offsetUnset($key) { if (false !== ($position = array_search((string) $key, $this->orderedKeys))) { @@ -165,6 +169,7 @@ public function offsetUnset($key) /** * @return \Traversable */ + #[\ReturnTypeWillChange] public function getIterator() { return new OrderedHashMapIterator($this->elements, $this->orderedKeys, $this->managedCursors); @@ -173,6 +178,7 @@ public function getIterator() /** * @return int */ + #[\ReturnTypeWillChange] public function count() { return \count($this->elements); diff --git a/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php b/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php index 6e295e1871451..87cab46052b40 100644 --- a/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php +++ b/src/Symfony/Component/Form/Util/OrderedHashMapIterator.php @@ -105,6 +105,7 @@ public function __destruct() * * @return mixed */ + #[\ReturnTypeWillChange] public function current() { return $this->current; @@ -112,10 +113,8 @@ public function current() /** * {@inheritdoc} - * - * @return void */ - public function next() + public function next(): void { ++$this->cursor; @@ -133,6 +132,7 @@ public function next() * * @return mixed */ + #[\ReturnTypeWillChange] public function key() { if (null === $this->key) { @@ -154,10 +154,8 @@ public function valid(): bool /** * {@inheritdoc} - * - * @return void */ - public function rewind() + public function rewind(): void { $this->cursor = 0; diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index a645a5ff3e03d..9d289d84770fa 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -353,7 +353,7 @@ private static function select(ClientState $multi, float $timeout): int } if ($multi->pauseExpiries && 0 < $timeout -= microtime(true) - $now) { - usleep(1E6 * $timeout); + usleep((int) (1E6 * $timeout)); } return 0; diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index 1c17d8274ab08..55ba6410f77c3 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -366,7 +366,7 @@ private static function select(ClientState $multi, float $timeout): int } if (!$handles) { - usleep(1E6 * $timeout); + usleep((int) (1E6 * $timeout)); return 0; } diff --git a/src/Symfony/Component/HttpFoundation/File/Stream.php b/src/Symfony/Component/HttpFoundation/File/Stream.php index 4a08e7f2dd355..cef3e03977cfe 100644 --- a/src/Symfony/Component/HttpFoundation/File/Stream.php +++ b/src/Symfony/Component/HttpFoundation/File/Stream.php @@ -23,6 +23,7 @@ class Stream extends File * * @return int|false */ + #[\ReturnTypeWillChange] public function getSize() { return false; diff --git a/src/Symfony/Component/HttpFoundation/HeaderBag.php b/src/Symfony/Component/HttpFoundation/HeaderBag.php index cd74adcc5c3ef..47152ec5d16dd 100644 --- a/src/Symfony/Component/HttpFoundation/HeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/HeaderBag.php @@ -252,6 +252,7 @@ public function removeCacheControlDirective(string $key) * * @return \ArrayIterator An \ArrayIterator instance */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->headers); @@ -262,6 +263,7 @@ public function getIterator() * * @return int The number of headers */ + #[\ReturnTypeWillChange] public function count() { return \count($this->headers); diff --git a/src/Symfony/Component/HttpFoundation/ParameterBag.php b/src/Symfony/Component/HttpFoundation/ParameterBag.php index c6e2dff27e397..cd916c25f49e3 100644 --- a/src/Symfony/Component/HttpFoundation/ParameterBag.php +++ b/src/Symfony/Component/HttpFoundation/ParameterBag.php @@ -207,6 +207,7 @@ public function filter(string $key, $default = null, int $filter = \FILTER_DEFAU * * @return \ArrayIterator An \ArrayIterator instance */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->parameters); @@ -217,6 +218,7 @@ public function getIterator() * * @return int The number of parameters */ + #[\ReturnTypeWillChange] public function count() { return \count($this->parameters); diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index ab22200fc941b..f0da852fd969b 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -1333,7 +1333,7 @@ public static function getMimeTypes(string $format) public function getFormat(?string $mimeType) { $canonicalMimeType = null; - if (false !== $pos = strpos($mimeType, ';')) { + if ($mimeType && false !== $pos = strpos($mimeType, ';')) { $canonicalMimeType = trim(substr($mimeType, 0, $pos)); } @@ -1599,7 +1599,7 @@ public function toArray() */ public function getETags() { - return preg_split('/\s*,\s*/', $this->headers->get('if_none_match', ''), -1, \PREG_SPLIT_NO_EMPTY); + return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY); } /** diff --git a/src/Symfony/Component/HttpFoundation/Response.php b/src/Symfony/Component/HttpFoundation/Response.php index da2e89766a35f..6393cc2790d62 100644 --- a/src/Symfony/Component/HttpFoundation/Response.php +++ b/src/Symfony/Component/HttpFoundation/Response.php @@ -1090,12 +1090,27 @@ public function isNotModified(Request $request): bool $lastModified = $this->headers->get('Last-Modified'); $modifiedSince = $request->headers->get('If-Modified-Since'); - if ($etags = $request->getETags()) { - $notModified = \in_array($this->getEtag(), $etags) || \in_array('*', $etags); - } + if ($ifNoneMatchEtags = $request->getETags()) { + $etag = $this->getEtag(); + if (0 == strncmp($etag, 'W/', 2)) { + $etag = substr($etag, 2); + } + + // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2. + foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) { + if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) { + $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2); + } - if ($modifiedSince && $lastModified) { - $notModified = strtotime($modifiedSince) >= strtotime($lastModified) && (!$etags || $notModified); + if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) { + $notModified = true; + break; + } + } + } + // Only do If-Modified-Since date comparison when If-None-Match is not present as per https://tools.ietf.org/html/rfc7232#section-3.3. + elseif ($modifiedSince && $lastModified) { + $notModified = strtotime($modifiedSince) >= strtotime($lastModified); } if ($notModified) { diff --git a/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBag.php b/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBag.php index aad6b610e7bb3..e9b4d8d772601 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBag.php +++ b/src/Symfony/Component/HttpFoundation/Session/Attribute/AttributeBag.php @@ -131,6 +131,7 @@ public function clear() * * @return \ArrayIterator An \ArrayIterator instance */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->attributes); @@ -141,6 +142,7 @@ public function getIterator() * * @return int The number of attributes */ + #[\ReturnTypeWillChange] public function count() { return \count($this->attributes); diff --git a/src/Symfony/Component/HttpFoundation/Session/Session.php b/src/Symfony/Component/HttpFoundation/Session/Session.php index e4e25356376ed..5c6c25d7fd73e 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Session.php +++ b/src/Symfony/Component/HttpFoundation/Session/Session.php @@ -128,6 +128,7 @@ public function isStarted() * * @return \ArrayIterator An \ArrayIterator instance */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->getAttributeBag()->all()); @@ -138,6 +139,7 @@ public function getIterator() * * @return int */ + #[\ReturnTypeWillChange] public function count() { return \count($this->getAttributeBag()->all()); diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MarshallingSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MarshallingSessionHandler.php index e1cad6a9454e5..c321c8c932377 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MarshallingSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MarshallingSessionHandler.php @@ -30,6 +30,7 @@ public function __construct(AbstractSessionHandler $handler, MarshallerInterface /** * @return bool */ + #[\ReturnTypeWillChange] public function open($savePath, $name) { return $this->handler->open($savePath, $name); @@ -38,6 +39,7 @@ public function open($savePath, $name) /** * @return bool */ + #[\ReturnTypeWillChange] public function close() { return $this->handler->close(); @@ -46,14 +48,16 @@ public function close() /** * @return bool */ + #[\ReturnTypeWillChange] public function destroy($sessionId) { return $this->handler->destroy($sessionId); } /** - * @return bool + * @return int|false */ + #[\ReturnTypeWillChange] public function gc($maxlifetime) { return $this->handler->gc($maxlifetime); @@ -62,6 +66,7 @@ public function gc($maxlifetime) /** * @return string */ + #[\ReturnTypeWillChange] public function read($sessionId) { return $this->marshaller->unmarshall($this->handler->read($sessionId)); @@ -70,6 +75,7 @@ public function read($sessionId) /** * @return bool */ + #[\ReturnTypeWillChange] public function write($sessionId, $data) { $failed = []; @@ -85,6 +91,7 @@ public function write($sessionId, $data) /** * @return bool */ + #[\ReturnTypeWillChange] public function validateId($sessionId) { return $this->handler->validateId($sessionId); @@ -93,6 +100,7 @@ public function validateId($sessionId) /** * @return bool */ + #[\ReturnTypeWillChange] public function updateTimestamp($sessionId, $data) { return $this->handler->updateTimestamp($sessionId, $data); diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php index 4bd2bd523f736..75aee3c919c23 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -57,6 +57,7 @@ public function __construct(\Memcached $memcached, array $options = []) /** * @return bool */ + #[\ReturnTypeWillChange] public function close() { return $this->memcached->quit(); @@ -73,6 +74,7 @@ protected function doRead(string $sessionId) /** * @return bool */ + #[\ReturnTypeWillChange] public function updateTimestamp($sessionId, $data) { $this->memcached->touch($this->prefix.$sessionId, time() + $this->ttl); diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php b/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php index 5c42fcaa347eb..048bc276a33eb 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/MetadataBag.php @@ -161,6 +161,6 @@ private function stampCreated(int $lifetime = null): void { $timeStamp = time(); $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp; - $this->meta[self::LIFETIME] = $lifetime ?? ini_get('session.cookie_lifetime'); + $this->meta[self::LIFETIME] = $lifetime ?? (int) ini_get('session.cookie_lifetime'); } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php index 16a7a3d1671e1..6f56940683778 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php @@ -86,7 +86,7 @@ class NativeSessionStorage implements SessionStorageInterface * name, "PHPSESSID" * referer_check, "" * serialize_handler, "php" - * use_strict_mode, "0" + * use_strict_mode, "1" * use_cookies, "1" * use_only_cookies, "1" * use_trans_sid, "0" diff --git a/src/Symfony/Component/HttpFoundation/Tests/File/FakeFile.php b/src/Symfony/Component/HttpFoundation/Tests/File/FakeFile.php index c415989f2f7f8..8b2f12f4144cf 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/File/FakeFile.php +++ b/src/Symfony/Component/HttpFoundation/Tests/File/FakeFile.php @@ -17,28 +17,28 @@ class FakeFile extends OrigFile { private $realpath; - public function __construct($realpath, $path) + public function __construct(string $realpath, string $path) { $this->realpath = $realpath; parent::__construct($path, false); } - public function isReadable() + public function isReadable(): bool { return true; } - public function getRealpath() + public function getRealpath(): string { return $this->realpath; } - public function getSize() + public function getSize(): int { return 42; } - public function getMTime() + public function getMTime(): int { return time(); } diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php index 77a04fefdfc5d..c44ecc1f6099b 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php @@ -197,7 +197,7 @@ public function testIsNotModifiedEtag() $etagTwo = 'randomly_generated_etag_2'; $request = new Request(); - $request->headers->set('if_none_match', sprintf('%s, %s, %s', $etagOne, $etagTwo, 'etagThree')); + $request->headers->set('If-None-Match', sprintf('%s, %s, %s', $etagOne, $etagTwo, 'etagThree')); $response = new Response(); @@ -209,6 +209,38 @@ public function testIsNotModifiedEtag() $response->headers->set('ETag', ''); $this->assertFalse($response->isNotModified($request)); + + // Test wildcard + $request = new Request(); + $request->headers->set('If-None-Match', '*'); + + $response->headers->set('ETag', $etagOne); + $this->assertTrue($response->isNotModified($request)); + } + + public function testIsNotModifiedWeakEtag() + { + $etag = 'randomly_generated_etag'; + $weakEtag = 'W/randomly_generated_etag'; + + $request = new Request(); + $request->headers->set('If-None-Match', $etag); + $response = new Response(); + + $response->headers->set('ETag', $etag); + $this->assertTrue($response->isNotModified($request)); + + $response->headers->set('ETag', $weakEtag); + $this->assertTrue($response->isNotModified($request)); + + $request->headers->set('If-None-Match', $weakEtag); + $response = new Response(); + + $response->headers->set('ETag', $etag); + $this->assertTrue($response->isNotModified($request)); + + $response->headers->set('ETag', $weakEtag); + $this->assertTrue($response->isNotModified($request)); } public function testIsNotModifiedLastModifiedAndEtag() @@ -219,14 +251,14 @@ public function testIsNotModifiedLastModifiedAndEtag() $etag = 'randomly_generated_etag'; $request = new Request(); - $request->headers->set('if_none_match', sprintf('%s, %s', $etag, 'etagThree')); + $request->headers->set('If-None-Match', sprintf('%s, %s', $etag, 'etagThree')); $request->headers->set('If-Modified-Since', $modified); $response = new Response(); $response->headers->set('ETag', $etag); $response->headers->set('Last-Modified', $after); - $this->assertFalse($response->isNotModified($request)); + $this->assertTrue($response->isNotModified($request)); $response->headers->set('ETag', 'non-existent-etag'); $response->headers->set('Last-Modified', $before); @@ -243,7 +275,7 @@ public function testIsNotModifiedIfModifiedSinceAndEtagWithoutLastModified() $etag = 'randomly_generated_etag'; $request = new Request(); - $request->headers->set('if_none_match', sprintf('%s, %s', $etag, 'etagThree')); + $request->headers->set('If-None-Match', sprintf('%s, %s', $etag, 'etagThree')); $request->headers->set('If-Modified-Since', $modified); $response = new Response(); diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php index db8c7432707c0..7216cdd1ece74 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MarshallingSessionHandlerTest.php @@ -72,7 +72,7 @@ public function testGc() $marshallingSessionHandler = new MarshallingSessionHandler($this->handler, $this->marshaller); $this->handler->expects($this->once())->method('gc') - ->with(4711)->willReturn(true); + ->with(4711)->willReturn(1); $marshallingSessionHandler->gc(4711); } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php index e040f4862755b..51a1b6472f764 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/MetadataBagTest.php @@ -136,4 +136,14 @@ public function testDoesNotSkipLastUsedUpdate() $this->assertEquals($timeStamp, $sessionMetadata[MetadataBag::UPDATED]); } + + public function testLifetimeIsInt() + { + $sessionMetadata = []; + + $bag = new MetadataBag(); + $bag->initialize($sessionMetadata); + + $this->assertIsInt($bag->getLifetime()); + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php index 1422eccfbc049..972a2745132e1 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php @@ -103,17 +103,21 @@ public function testRead() public function testWrite() { $this->mock->expects($this->once()) - ->method('write'); + ->method('write') + ->willReturn(true) + ; - $this->proxy->write('id', 'data'); + $this->assertTrue($this->proxy->write('id', 'data')); } public function testDestroy() { $this->mock->expects($this->once()) - ->method('destroy'); + ->method('destroy') + ->willReturn(true) + ; - $this->proxy->destroy('id'); + $this->assertTrue($this->proxy->destroy('id')); } public function testGc() @@ -149,7 +153,9 @@ public function testUpdateTimestamp() $proxy->updateTimestamp('id', 'data'); $this->mock->expects($this->once()) - ->method('write'); + ->method('write') + ->willReturn(true) + ; $this->proxy->updateTimestamp('id', 'data'); } diff --git a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php index 1ece493f4e708..fd1cf9e5845d6 100644 --- a/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php @@ -30,6 +30,7 @@ protected function beforeDispatch(string $eventName, object $event) { switch ($eventName) { case KernelEvents::REQUEST: + $event->getRequest()->attributes->set('_stopwatch_token', substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6)); $this->stopwatch->openSection(); break; case KernelEvents::VIEW: @@ -40,8 +41,8 @@ protected function beforeDispatch(string $eventName, object $event) } break; case KernelEvents::TERMINATE: - $token = $event->getResponse()->headers->get('X-Debug-Token'); - if (null === $token) { + $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); + if (null === $sectionId) { break; } // There is a very special case when using built-in AppCache class as kernel wrapper, in the case @@ -50,7 +51,7 @@ protected function beforeDispatch(string $eventName, object $event) // is equal to the [A] debug token. Trying to reopen section with the [B] token throws an exception // which must be caught. try { - $this->stopwatch->openSection($token); + $this->stopwatch->openSection($sectionId); } catch (\LogicException $e) { } break; @@ -67,21 +68,21 @@ protected function afterDispatch(string $eventName, object $event) $this->stopwatch->start('controller', 'section'); break; case KernelEvents::RESPONSE: - $token = $event->getResponse()->headers->get('X-Debug-Token'); - if (null === $token) { + $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); + if (null === $sectionId) { break; } - $this->stopwatch->stopSection($token); + $this->stopwatch->stopSection($sectionId); break; case KernelEvents::TERMINATE: // In the special case described in the `preDispatch` method above, the `$token` section // does not exist, then closing it throws an exception which must be caught. - $token = $event->getResponse()->headers->get('X-Debug-Token'); - if (null === $token) { + $sectionId = $event->getRequest()->attributes->get('_stopwatch_token'); + if (null === $sectionId) { break; } try { - $this->stopwatch->stopSection($token); + $this->stopwatch->stopSection($sectionId); } catch (\LogicException $e) { } break; diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 18d9565ad40c3..15fb09d77c930 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -5,12 +5,14 @@ * * (c) Fabien Potencier <fabien@symfony.com> * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/* * This code is partially based on the Rack-Cache library by Ryan Tomayko, * which is released under the MIT license. * (based on commit 02d2b48d75bcb63cf1c0c7149c077ad256542801) - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\HttpCache; @@ -382,7 +384,7 @@ protected function validate(Request $request, Response $entry, bool $catch = fal // add our cached last-modified validator if ($entry->headers->has('Last-Modified')) { - $subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified')); + $subRequest->headers->set('If-Modified-Since', $entry->headers->get('Last-Modified')); } // Add our cached etag validator to the environment. @@ -391,7 +393,7 @@ protected function validate(Request $request, Response $entry, bool $catch = fal $cachedEtags = $entry->getEtag() ? [$entry->getEtag()] : []; $requestEtags = $request->getETags(); if ($etags = array_unique(array_merge($cachedEtags, $requestEtags))) { - $subRequest->headers->set('if_none_match', implode(', ', $etags)); + $subRequest->headers->set('If-None-Match', implode(', ', $etags)); } $response = $this->forward($subRequest, $catch, $entry); @@ -444,8 +446,8 @@ protected function fetch(Request $request, bool $catch = false) } // avoid that the backend sends no content - $subRequest->headers->remove('if_modified_since'); - $subRequest->headers->remove('if_none_match'); + $subRequest->headers->remove('If-Modified-Since'); + $subRequest->headers->remove('If-None-Match'); $response = $this->forward($subRequest, $catch); diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 6cfd5b8cf6f1c..6587f32bd7966 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -33,7 +33,6 @@ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; // Help opcache.preload discover always-needed symbols -class_exists(LegacyEventDispatcherProxy::class); class_exists(ControllerArgumentsEvent::class); class_exists(ControllerEvent::class); class_exists(ExceptionEvent::class); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 0d0332d21703f..21e7e8543254b 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -75,11 +75,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - public const VERSION = '5.3.6'; - public const VERSION_ID = 50306; + public const VERSION = '5.3.7'; + public const VERSION_ID = 50307; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 3; - public const RELEASE_VERSION = 6; + public const RELEASE_VERSION = 7; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '01/2022'; diff --git a/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php b/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php index 53ea623ae7747..a307bbff55d6c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php @@ -28,12 +28,12 @@ class TraceableEventDispatcherTest extends TestCase public function testStopwatchSections() { $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch = new Stopwatch()); - $kernel = $this->getHttpKernel($dispatcher, function () { return new Response('', 200, ['X-Debug-Token' => '292e1e']); }); + $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $response = $kernel->handle($request); $kernel->terminate($request, $response); - $events = $stopwatch->getSectionEvents($response->headers->get('X-Debug-Token')); + $events = $stopwatch->getSectionEvents($request->attributes->get('_stopwatch_token')); $this->assertEquals([ '__section__', 'kernel.request', @@ -56,7 +56,7 @@ public function testStopwatchCheckControllerOnRequestEvent() $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch); - $kernel = $this->getHttpKernel($dispatcher, function () { return new Response(); }); + $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $kernel->handle($request); } @@ -69,12 +69,12 @@ public function testStopwatchStopControllerOnRequestEvent() $stopwatch->expects($this->once()) ->method('isStarted') ->willReturn(true); - $stopwatch->expects($this->once()) + $stopwatch->expects($this->exactly(3)) ->method('stop'); $dispatcher = new TraceableEventDispatcher(new EventDispatcher(), $stopwatch); - $kernel = $this->getHttpKernel($dispatcher, function () { return new Response(); }); + $kernel = $this->getHttpKernel($dispatcher); $request = Request::create('/'); $kernel->handle($request); } @@ -110,10 +110,12 @@ public function testListenerCanRemoveItselfWhenExecuted() $this->assertCount(1, $eventDispatcher->getListeners('foo'), 'expected listener1 to be removed'); } - protected function getHttpKernel($dispatcher, $controller) + protected function getHttpKernel($dispatcher) { $controllerResolver = $this->createMock(ControllerResolverInterface::class); - $controllerResolver->expects($this->once())->method('getController')->willReturn($controller); + $controllerResolver->expects($this->once())->method('getController')->willReturn(function () { + return new Response(); + }); $argumentResolver = $this->createMock(ArgumentResolverInterface::class); $argumentResolver->expects($this->once())->method('getArguments')->willReturn([]); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php index 7d820747e2e4c..cef30fad05ed6 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php @@ -156,7 +156,7 @@ public function testRespondsWith304WhenIfNoneMatchMatchesETag() $this->assertTraceContains('store'); } - public function testRespondsWith304OnlyIfIfNoneMatchAndIfModifiedSinceBothMatch() + public function testRespondsWith304WhenIfNoneMatchAndIfModifiedSinceBothMatch() { $time = \DateTime::createFromFormat('U', time()); @@ -172,7 +172,7 @@ public function testRespondsWith304OnlyIfIfNoneMatchAndIfModifiedSinceBothMatch( $t = \DateTime::createFromFormat('U', time() - 3600); $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '12345', 'HTTP_IF_MODIFIED_SINCE' => $t->format(\DATE_RFC2822)]); $this->assertHttpKernelIsCalled(); - $this->assertEquals(200, $this->response->getStatusCode()); + $this->assertEquals(304, $this->response->getStatusCode()); // only Last-Modified matches $this->request('GET', '/', ['HTTP_IF_NONE_MATCH' => '1234', 'HTTP_IF_MODIFIED_SINCE' => $time->format(\DATE_RFC2822)]); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelBrowserTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelBrowserTest.php index 2411dd41cb23e..b6e391625c038 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelBrowserTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelBrowserTest.php @@ -143,6 +143,10 @@ public function testUploadedFileWhenNoFileSelected() public function testUploadedFileWhenSizeExceedsUploadMaxFileSize() { + if (UploadedFile::getMaxFilesize() > \PHP_INT_MAX) { + $this->markTestSkipped('Requires PHP_INT_MAX to be greater than "upload_max_filesize" and "post_max_size" ini settings'); + } + $source = tempnam(sys_get_temp_dir(), 'source'); $kernel = new TestHttpKernel(); @@ -157,7 +161,7 @@ public function testUploadedFileWhenSizeExceedsUploadMaxFileSize() /* should be modified when the getClientSize will be removed */ $file->expects($this->any()) ->method('getSize') - ->willReturn(\INF) + ->willReturn(\PHP_INT_MAX) ; $file->expects($this->any()) ->method('getClientSize') diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index d8501ab870fe0..6c0df6090490a 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -21,7 +21,7 @@ "symfony/error-handler": "^4.4|^5.0", "symfony/event-dispatcher": "^5.0", "symfony/http-client-contracts": "^1.1|^2", - "symfony/http-foundation": "^5.3", + "symfony/http-foundation": "^5.3.7", "symfony/polyfill-ctype": "^1.8", "symfony/polyfill-php73": "^1.9", "symfony/polyfill-php80": "^1.16", diff --git a/src/Symfony/Component/Intl/Data/Util/ArrayAccessibleResourceBundle.php b/src/Symfony/Component/Intl/Data/Util/ArrayAccessibleResourceBundle.php index da6faf67810b6..803e5561f6573 100644 --- a/src/Symfony/Component/Intl/Data/Util/ArrayAccessibleResourceBundle.php +++ b/src/Symfony/Component/Intl/Data/Util/ArrayAccessibleResourceBundle.php @@ -45,25 +45,22 @@ public function offsetExists($offset): bool } /** + * @param mixed $offset + * * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); } - /** - * @return void - */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { throw new BadMethodCallException('Resource bundles cannot be modified.'); } - /** - * @return void - */ - public function offsetUnset($offset) + public function offsetUnset($offset): void { throw new BadMethodCallException('Resource bundles cannot be modified.'); } diff --git a/src/Symfony/Component/Intl/Data/Util/RingBuffer.php b/src/Symfony/Component/Intl/Data/Util/RingBuffer.php index 76bca285bbbc2..72d86025f718a 100644 --- a/src/Symfony/Component/Intl/Data/Util/RingBuffer.php +++ b/src/Symfony/Component/Intl/Data/Util/RingBuffer.php @@ -52,6 +52,7 @@ public function offsetExists($key): bool * * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($key) { if (!isset($this->indices[$key])) { @@ -66,7 +67,7 @@ public function offsetGet($key) * * @return void */ - public function offsetSet($key, $value) + public function offsetSet($key, $value): void { if (false !== ($keyToRemove = array_search($this->cursor, $this->indices))) { unset($this->indices[$keyToRemove]); @@ -83,7 +84,7 @@ public function offsetSet($key, $value) * * @return void */ - public function offsetUnset($key) + public function offsetUnset($key): void { if (isset($this->indices[$key])) { $this->values[$this->indices[$key]] = null; diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php index 05e81c8378e57..d8da8ddbf6586 100644 --- a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php @@ -45,6 +45,7 @@ public function toArray() /** * @return int */ + #[\ReturnTypeWillChange] public function count() { $con = $this->connection->getResource(); diff --git a/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php b/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php index e32793c3d3bf4..c8458909b7b60 100644 --- a/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php +++ b/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php @@ -83,7 +83,7 @@ public function onCheckPassport(CheckPassportEvent $event) } else { throw new LogicException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.'); } - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 $username = $ldap->escape(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), '', LdapInterface::ESCAPE_FILTER); $query = str_replace('{username}', $username, $ldapBadge->getQueryString()); $result = $ldap->query($ldapBadge->getDnString(), $query)->execute(); @@ -93,7 +93,7 @@ public function onCheckPassport(CheckPassportEvent $event) $dn = $result[0]->getDn(); } else { - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 $username = $ldap->escape(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), '', LdapInterface::ESCAPE_DN); $dn = str_replace('{username}', $username, $ldapBadge->getDnString()); } diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php index 0134a6ff5ef7f..07108f19aab3c 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php @@ -64,7 +64,7 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e throw new HttpTransportException('Unable to send an email: '.implode('; ', array_column($result['errors'], 'message')).sprintf(' (code %d).', $statusCode), $response); } catch (DecodingExceptionInterface $e) { - throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).sprintf(' (code %d).', $statusCode), $response); + throw new HttpTransportException('Unable to send an email: '.$response->getContent(false).sprintf(' (code %d).', $statusCode), $response, 0, $e); } } diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php index c37d89676b2c2..c2a70639e9526 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php @@ -559,10 +559,10 @@ public function testItDelaysTheMessageWithADifferentRoutingKeyAndTTLs() $connection->publish('{}', [], 120000); } - public function testObfuscatePasswordInDsn() + public function testNoCredentialLeakageWhenConnectionFails() { $this->expectException(\AMQPException::class); - $this->expectExceptionMessage('Could not connect to the AMQP server. Please verify the provided DSN. ({"host":"localhost","port":5672,"vhost":"/","login":"user","password":"********"})'); + $this->expectExceptionMessage('Could not connect to the AMQP server. Please verify the provided DSN.'); $factory = new TestAmqpFactory( $amqpConnection = $this->createMock(\AMQPConnection::class), $amqpChannel = $this->createMock(\AMQPChannel::class), diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php index e03dc72a472c6..1dead1503ade4 100644 --- a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php @@ -495,11 +495,7 @@ public function channel(): \AMQPChannel try { $connection->{$connectMethod}(); } catch (\AMQPConnectionException $e) { - $credentials = $this->connectionOptions; - $credentials['password'] = '********'; - unset($credentials['delay']); - - throw new \AMQPException(sprintf('Could not connect to the AMQP server. Please verify the provided DSN. (%s).', json_encode($credentials, \JSON_UNESCAPED_SLASHES)), 0, $e); + throw new \AMQPException('Could not connect to the AMQP server. Please verify the provided DSN.', 0, $e); } $this->amqpChannel = $this->amqpFactory->createChannel($connection); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php index cb0d3e79bd663..13be9a333289e 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport; use Doctrine\DBAL\Abstraction\Result as AbstractionResult; +use Doctrine\DBAL\Configuration; use Doctrine\DBAL\Connection as DBALConnection; use Doctrine\DBAL\DBALException; use Doctrine\DBAL\Driver\Result as DriverResult; @@ -25,7 +26,9 @@ use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\SchemaConfig; +use Doctrine\DBAL\Schema\TableDiff; use Doctrine\DBAL\Statement; +use Doctrine\DBAL\Types\Types; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Bridge\Doctrine\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Bridge\Doctrine\Transport\Connection; @@ -406,6 +409,56 @@ public function providePlatformSql(): iterable ]; } + /** + * @dataProvider setupIndicesProvider + */ + public function testSetupIndices(string $platformClass, array $expectedIndices) + { + $driverConnection = $this->createMock(DBALConnection::class); + $driverConnection->method('getConfiguration')->willReturn(new Configuration()); + + $schemaManager = $this->createMock(AbstractSchemaManager::class); + $schema = new Schema(); + $expectedTable = $schema->createTable('messenger_messages'); + $expectedTable->addColumn('id', Types::BIGINT); + $expectedTable->setPrimaryKey(['id']); + // Make sure columns for indices exists so addIndex() will not throw + foreach (array_unique(array_merge(...$expectedIndices)) as $columnName) { + $expectedTable->addColumn($columnName, Types::STRING); + } + foreach ($expectedIndices as $indexColumns) { + $expectedTable->addIndex($indexColumns); + } + $schemaManager->method('createSchema')->willReturn($schema); + $driverConnection->method('getSchemaManager')->willReturn($schemaManager); + + $platformMock = $this->createMock($platformClass); + $platformMock + ->expects(self::once()) + ->method('getAlterTableSQL') + ->with(self::callback(static function (TableDiff $tableDiff): bool { + return 0 === \count($tableDiff->addedIndexes) && 0 === \count($tableDiff->changedIndexes) && 0 === \count($tableDiff->removedIndexes); + })) + ->willReturn([]); + $driverConnection->method('getDatabasePlatform')->willReturn($platformMock); + + $connection = new Connection([], $driverConnection); + $connection->setup(); + } + + public function setupIndicesProvider(): iterable + { + yield 'MySQL' => [ + MySQL57Platform::class, + [['delivered_at']], + ]; + + yield 'Other platforms' => [ + AbstractPlatform::class, + [['queue_name'], ['available_at'], ['delivered_at']], + ]; + } + public function testConfigureSchema() { $driverConnection = $this->getDBALConnectionMock(); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php index c4b398aa43ae9..43974a53e1f06 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -17,6 +17,7 @@ use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\TableNotFoundException; use Doctrine\DBAL\LockMode; +use Doctrine\DBAL\Platforms\MySqlPlatform; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\Comparator; @@ -404,7 +405,6 @@ private function addTableToSchema(Schema $schema): void $table->addColumn('headers', Types::TEXT) ->setNotnull(true); $table->addColumn('queue_name', Types::STRING) - ->setLength(190) // MySQL 5.6 only supports 191 characters on an indexed column in utf8mb4 mode ->setNotnull(true); $table->addColumn('created_at', Types::DATETIME_MUTABLE) ->setNotnull(true); @@ -413,8 +413,11 @@ private function addTableToSchema(Schema $schema): void $table->addColumn('delivered_at', Types::DATETIME_MUTABLE) ->setNotnull(false); $table->setPrimaryKey(['id']); - $table->addIndex(['queue_name']); - $table->addIndex(['available_at']); + // No indices on queue_name and available_at on MySQL to prevent deadlock issues when running multiple consumers. + if (!$this->driverConnection->getDatabasePlatform() instanceof MySqlPlatform) { + $table->addIndex(['queue_name']); + $table->addIndex(['available_at']); + } $table->addIndex(['delivered_at']); } diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php index 333a32b90366c..9223d5a833135 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Messenger\Bridge\Redis\Tests\Transport; -use PHPUnit\Framework\SkippedTestSuiteError; use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection; @@ -19,37 +18,11 @@ /** * @requires extension redis >= 4.3.0 - * @group integration */ class ConnectionTest extends TestCase { use ExpectDeprecationTrait; - public static function setUpBeforeClass(): void - { - try { - $redis = Connection::fromDsn('redis://localhost/queue'); - $redis->get(); - } catch (TransportException $e) { - if (str_starts_with($e->getMessage(), 'ERR unknown command \'X')) { - throw new SkippedTestSuiteError('Redis server >= 5 is required'); - } - - throw $e; - } catch (\RedisException $e) { - throw new SkippedTestSuiteError($e->getMessage()); - } - } - - private function skipIfRedisClusterUnavailable() - { - try { - new \RedisCluster(null, explode(' ', getenv('REDIS_CLUSTER_HOSTS'))); - } catch (\Exception $e) { - self::markTestSkipped($e->getMessage()); - } - } - public function testFromInvalidDsn() { $this->expectException(\InvalidArgumentException::class); @@ -64,25 +37,11 @@ public function testFromDsn() new Connection(['stream' => 'queue'], [ 'host' => 'localhost', 'port' => 6379, - ]), - Connection::fromDsn('redis://localhost/queue') + ], [], $this->createMock(\Redis::class)), + Connection::fromDsn('redis://localhost/queue', [], $this->createMock(\Redis::class)) ); } - public function testFromDsnWithMultipleHosts() - { - $this->skipIfRedisClusterUnavailable(); - - $hosts = explode(' ', getenv('REDIS_CLUSTER_HOSTS')); - - $dsn = array_map(function ($host) { - return 'redis://'.$host; - }, $hosts); - $dsn = implode(',', $dsn); - - $this->assertInstanceOf(Connection::class, Connection::fromDsn($dsn)); - } - public function testFromDsnOnUnixSocket() { $this->assertEquals( @@ -97,16 +56,16 @@ public function testFromDsnOnUnixSocket() public function testFromDsnWithOptions() { $this->assertEquals( - Connection::fromDsn('redis://localhost', ['stream' => 'queue', 'group' => 'group1', 'consumer' => 'consumer1', 'auto_setup' => false, 'serializer' => 2]), - Connection::fromDsn('redis://localhost/queue/group1/consumer1?serializer=2&auto_setup=0') + Connection::fromDsn('redis://localhost', ['stream' => 'queue', 'group' => 'group1', 'consumer' => 'consumer1', 'auto_setup' => false, 'serializer' => 2], $this->createMock(\Redis::class)), + Connection::fromDsn('redis://localhost/queue/group1/consumer1?serializer=2&auto_setup=0', [], $this->createMock(\Redis::class)) ); } public function testFromDsnWithOptionsAndTrailingSlash() { $this->assertEquals( - Connection::fromDsn('redis://localhost/', ['stream' => 'queue', 'group' => 'group1', 'consumer' => 'consumer1', 'auto_setup' => false, 'serializer' => 2]), - Connection::fromDsn('redis://localhost/queue/group1/consumer1?serializer=2&auto_setup=0') + Connection::fromDsn('redis://localhost/', ['stream' => 'queue', 'group' => 'group1', 'consumer' => 'consumer1', 'auto_setup' => false, 'serializer' => 2], $this->createMock(\Redis::class)), + Connection::fromDsn('redis://localhost/queue/group1/consumer1?serializer=2&auto_setup=0', [], $this->createMock(\Redis::class)) ); } @@ -157,21 +116,21 @@ public function testFromDsnWithQueryOptions() 'port' => 6379, ], [ 'serializer' => 2, - ]), - Connection::fromDsn('redis://localhost/queue/group1/consumer1?serializer=2') + ], $this->createMock(\Redis::class)), + Connection::fromDsn('redis://localhost/queue/group1/consumer1?serializer=2', [], $this->createMock(\Redis::class)) ); } public function testFromDsnWithMixDsnQueryOptions() { $this->assertEquals( - Connection::fromDsn('redis://localhost/queue/group1?serializer=2', ['consumer' => 'specific-consumer']), - Connection::fromDsn('redis://localhost/queue/group1/specific-consumer?serializer=2') + Connection::fromDsn('redis://localhost/queue/group1?serializer=2', ['consumer' => 'specific-consumer'], $this->createMock(\Redis::class)), + Connection::fromDsn('redis://localhost/queue/group1/specific-consumer?serializer=2', [], $this->createMock(\Redis::class)) ); $this->assertEquals( - Connection::fromDsn('redis://localhost/queue/group1/consumer1', ['consumer' => 'specific-consumer']), - Connection::fromDsn('redis://localhost/queue/group1/consumer1') + Connection::fromDsn('redis://localhost/queue/group1/consumer1', ['consumer' => 'specific-consumer'], $this->createMock(\Redis::class)), + Connection::fromDsn('redis://localhost/queue/group1/consumer1', [], $this->createMock(\Redis::class)) ); } @@ -181,14 +140,12 @@ public function testFromDsnWithMixDsnQueryOptions() public function testDeprecationIfInvalidOptionIsPassedWithDsn() { $this->expectDeprecation('Since symfony/messenger 5.1: Invalid option(s) "foo" passed to the Redis Messenger transport. Passing invalid options is deprecated.'); - Connection::fromDsn('redis://localhost/queue?foo=bar'); + Connection::fromDsn('redis://localhost/queue?foo=bar', [], $this->createMock(\Redis::class)); } public function testRedisClusterInstanceIsSupported() { - $this->skipIfRedisClusterUnavailable(); - - $redis = new \RedisCluster(null, explode(' ', getenv('REDIS_CLUSTER_HOSTS'))); + $redis = $this->createMock(\RedisCluster::class); $this->assertInstanceOf(Connection::class, new Connection([], [], [], $redis)); } @@ -274,15 +231,6 @@ public function testFailedAuth() Connection::fromDsn('redis://password@localhost/queue', [], $redis); } - public function testDbIndex() - { - $redis = new \Redis(); - - Connection::fromDsn('redis://localhost/queue?dbindex=2', [], $redis); - - $this->assertSame(2, $redis->getDbNum()); - } - public function testGetPendingMessageFirst() { $redis = $this->createMock(\Redis::class); @@ -361,48 +309,6 @@ public function testUnexpectedRedisError() $connection->get(); } - public function testGetAfterReject() - { - $redis = new \Redis(); - $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', [], $redis); - - $connection->add('1', []); - $connection->add('2', []); - - $failing = $connection->get(); - $connection->reject($failing['id']); - - $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget'); - $this->assertNotNull($connection->get()); - - $redis->del('messenger-rejectthenget'); - } - - public function testGetNonBlocking() - { - $redis = new \Redis(); - - $connection = Connection::fromDsn('redis://localhost/messenger-getnonblocking', [], $redis); - - $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'); - } - - public function testJsonError() - { - $redis = new \Redis(); - $connection = Connection::fromDsn('redis://localhost/json-error', [], $redis); - try { - $connection->add("\xB1\x31", []); - } catch (TransportException $e) { - } - - $this->assertSame('Malformed UTF-8 characters, possibly incorrectly encoded', $e->getMessage()); - } - public function testMaxEntries() { $redis = $this->createMock(\Redis::class); @@ -471,32 +377,4 @@ public function testLastErrorGetsCleared() $this->assertSame('xack error', $e->getMessage()); } - - public function testLazy() - { - $redis = new \Redis(); - $connection = Connection::fromDsn('redis://localhost/messenger-lazy?lazy=1', [], $redis); - - $connection->add('1', []); - $this->assertNotEmpty($message = $connection->get()); - $this->assertSame('1', $message['body']); - $connection->reject($message['id']); - $redis->del('messenger-lazy'); - } - - public function testLazyCluster() - { - $this->skipIfRedisClusterUnavailable(); - - $connection = new Connection( - ['lazy' => true], - ['host' => explode(' ', getenv('REDIS_CLUSTER_HOSTS'))] - ); - - $connection->add('1', []); - $this->assertNotEmpty($message = $connection->get()); - $this->assertSame('1', $message['body']); - $connection->reject($message['id']); - $connection->cleanup(); - } } diff --git a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php index 0c92605d1120d..8ffc08c08f0bf 100644 --- a/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisExtIntegrationTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Bridge\Redis\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Bridge\Redis\Transport\Connection; +use Symfony\Component\Messenger\Exception\TransportException; /** * @requires extension redis @@ -173,6 +174,99 @@ public function testConnectionClaimAndRedeliver() $connection->ack($encoded['id']); } + public function testLazyCluster() + { + $this->skipIfRedisClusterUnavailable(); + + $connection = new Connection( + ['lazy' => true], + ['host' => explode(' ', getenv('REDIS_CLUSTER_HOSTS'))] + ); + + $connection->add('1', []); + $this->assertNotEmpty($message = $connection->get()); + $this->assertSame('1', $message['body']); + $connection->reject($message['id']); + $connection->cleanup(); + } + + public function testLazy() + { + $redis = new \Redis(); + $connection = Connection::fromDsn('redis://localhost/messenger-lazy?lazy=1', [], $redis); + + $connection->add('1', []); + $this->assertNotEmpty($message = $connection->get()); + $this->assertSame('1', $message['body']); + $connection->reject($message['id']); + $redis->del('messenger-lazy'); + } + + public function testDbIndex() + { + $redis = new \Redis(); + + Connection::fromDsn('redis://localhost/queue?dbindex=2', [], $redis); + + $this->assertSame(2, $redis->getDbNum()); + } + + public function testFromDsnWithMultipleHosts() + { + $this->skipIfRedisClusterUnavailable(); + + $hosts = explode(' ', getenv('REDIS_CLUSTER_HOSTS')); + + $dsn = array_map(function ($host) { + return 'redis://'.$host; + }, $hosts); + $dsn = implode(',', $dsn); + + $this->assertInstanceOf(Connection::class, Connection::fromDsn($dsn)); + } + + public function testJsonError() + { + $redis = new \Redis(); + $connection = Connection::fromDsn('redis://localhost/json-error', [], $redis); + try { + $connection->add("\xB1\x31", []); + } catch (TransportException $e) { + } + + $this->assertSame('Malformed UTF-8 characters, possibly incorrectly encoded', $e->getMessage()); + } + + public function testGetNonBlocking() + { + $redis = new \Redis(); + + $connection = Connection::fromDsn('redis://localhost/messenger-getnonblocking', [], $redis); + + $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'); + } + + public function testGetAfterReject() + { + $redis = new \Redis(); + $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget', [], $redis); + + $connection->add('1', []); + $connection->add('2', []); + + $failing = $connection->get(); + $connection->reject($failing['id']); + + $connection = Connection::fromDsn('redis://localhost/messenger-rejectthenget'); + $this->assertNotNull($connection->get()); + + $redis->del('messenger-rejectthenget'); + } + private function getConnectionGroup(Connection $connection): string { $property = (new \ReflectionClass(Connection::class))->getProperty('group'); @@ -188,4 +282,13 @@ private function getConnectionStream(Connection $connection): string return $property->getValue($connection); } + + private function skipIfRedisClusterUnavailable() + { + try { + new \RedisCluster(null, explode(' ', getenv('REDIS_CLUSTER_HOSTS'))); + } catch (\Exception $e) { + self::markTestSkipped($e->getMessage()); + } + } } diff --git a/src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php b/src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php index ae03de5a6c12f..62abf23423ddd 100644 --- a/src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php +++ b/src/Symfony/Component/Messenger/Stamp/ErrorDetailsStamp.php @@ -23,7 +23,7 @@ final class ErrorDetailsStamp implements StampInterface /** @var string */ private $exceptionClass; - /** @var int|mixed */ + /** @var int|string */ private $exceptionCode; /** @var string */ @@ -33,7 +33,7 @@ final class ErrorDetailsStamp implements StampInterface private $flattenException; /** - * @param int|mixed $exceptionCode + * @param int|string $exceptionCode */ public function __construct(string $exceptionClass, $exceptionCode, string $exceptionMessage, FlattenException $flattenException = null) { diff --git a/src/Symfony/Component/Messenger/Tests/Stamp/StringErrorCodeException.php b/src/Symfony/Component/Messenger/Tests/Stamp/StringErrorCodeException.php new file mode 100644 index 0000000000000..2c575d8f80046 --- /dev/null +++ b/src/Symfony/Component/Messenger/Tests/Stamp/StringErrorCodeException.php @@ -0,0 +1,12 @@ +<?php + +namespace Symfony\Component\Messenger\Tests\Stamp; + +class StringErrorCodeException extends \Exception +{ + public function __construct(string $message, string $code) + { + parent::__construct($message); + $this->code = $code; + } +} diff --git a/src/Symfony/Component/Mime/Crypto/DkimSigner.php b/src/Symfony/Component/Mime/Crypto/DkimSigner.php index 299c82a5ccb4b..dfb6f226b5e51 100644 --- a/src/Symfony/Component/Mime/Crypto/DkimSigner.php +++ b/src/Symfony/Component/Mime/Crypto/DkimSigner.php @@ -68,6 +68,7 @@ public function sign(Message $message, array $options = []): Message throw new InvalidArgumentException('Invalid DKIM signing algorithm "%s".', $options['algorithm']); } $headersToIgnore['return-path'] = true; + $headersToIgnore['x-transport'] = true; foreach ($options['headers_to_ignore'] as $name) { $headersToIgnore[strtolower($name)] = true; } @@ -203,7 +204,13 @@ private function hashBody(AbstractPart $body, string $bodyCanon, int $maxLength) hash_update($hash, $canon); } - if (0 === $length) { + // Add trailing Line return if last line is non empty + if (\strlen($currentLine) > 0) { + hash_update($hash, "\r\n"); + $length += \strlen("\r\n"); + } + + if (!$relaxed && 0 === $length) { hash_update($hash, "\r\n"); $length = 2; } diff --git a/src/Symfony/Component/Mime/Tests/Crypto/DkimSignerTest.php b/src/Symfony/Component/Mime/Tests/Crypto/DkimSignerTest.php index 94905dd7be9e3..e48b0c8e4e3c0 100644 --- a/src/Symfony/Component/Mime/Tests/Crypto/DkimSignerTest.php +++ b/src/Symfony/Component/Mime/Tests/Crypto/DkimSignerTest.php @@ -122,14 +122,14 @@ public function getCanonicalizeHeaderData() DkimSigner::CANON_SIMPLE, "\r\n", '', \PHP_INT_MAX, ]; yield 'relaxed_empty' => [ - DkimSigner::CANON_RELAXED, "\r\n", '', \PHP_INT_MAX, + DkimSigner::CANON_RELAXED, '', '', \PHP_INT_MAX, ]; yield 'simple_empty_single_ending_CLRF' => [ DkimSigner::CANON_SIMPLE, "\r\n", "\r\n", \PHP_INT_MAX, ]; yield 'relaxed_empty_single_ending_CLRF' => [ - DkimSigner::CANON_RELAXED, "\r\n", "\r\n", \PHP_INT_MAX, + DkimSigner::CANON_RELAXED, '', "\r\n", \PHP_INT_MAX, ]; yield 'simple_multiple_ending_CLRF' => [ diff --git a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php index bd1833947ee54..2ad0848bf2049 100644 --- a/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Firebase/FirebaseTransport.php @@ -78,7 +78,7 @@ protected function doSend(MessageInterface $message): SentMessage try { $statusCode = $response->getStatusCode(); } catch (TransportExceptionInterface $e) { - throw new TransportException('Could not reach the remote Forebase server.', $response, 0, $e); + throw new TransportException('Could not reach the remote Firebase server.', $response, 0, $e); } $contentType = $response->getHeaders(false)['content-type'][0] ?? ''; @@ -89,8 +89,8 @@ protected function doSend(MessageInterface $message): SentMessage throw new TransportException('Unable to post the Firebase message: '.$errorMessage, $response); } - if ($jsonContents && isset($jsonContents['results']['error'])) { - throw new TransportException('Unable to post the Firebase message: '.$jsonContents['error'], $response); + if ($jsonContents && isset($jsonContents['results'][0]['error'])) { + throw new TransportException('Unable to post the Firebase message: '.$jsonContents['results'][0]['error'], $response); } $success = $response->toArray(false); diff --git a/src/Symfony/Component/Notifier/Bridge/Smsapi/SmsapiTransport.php b/src/Symfony/Component/Notifier/Bridge/Smsapi/SmsapiTransport.php index dee0ce5d324e8..700447dfdf3a7 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsapi/SmsapiTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Smsapi/SmsapiTransport.php @@ -18,6 +18,7 @@ use Symfony\Component\Notifier\Message\SmsMessage; use Symfony\Component\Notifier\Transport\AbstractTransport; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -72,10 +73,14 @@ protected function doSend(MessageInterface $message): SentMessage throw new TransportException('Could not reach the remote Smsapi server.', $response, 0, $e); } - if (200 !== $statusCode) { - $error = $response->toArray(false); + try { + $content = $response->toArray(false); + } catch (DecodingExceptionInterface $e) { + throw new TransportException('Could not decode body to an array.', $response, 0, $e); + } - throw new TransportException(sprintf('Unable to send the SMS: "%s".', $error['message']), $response); + if (isset($content['error']) || 200 !== $statusCode) { + throw new TransportException(sprintf('Unable to send the SMS: "%s".', $content['message'] ?? 'unknown error'), $response); } return new SentMessage($message, (string) $this); diff --git a/src/Symfony/Component/Notifier/Bridge/Smsapi/Tests/SmsapiTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Smsapi/Tests/SmsapiTransportTest.php index 615d55de6b33b..3af790460bf28 100644 --- a/src/Symfony/Component/Notifier/Bridge/Smsapi/Tests/SmsapiTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Smsapi/Tests/SmsapiTransportTest.php @@ -11,7 +11,10 @@ namespace Symfony\Component\Notifier\Bridge\Smsapi\Tests; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Notifier\Bridge\Smsapi\SmsapiTransport; +use Symfony\Component\Notifier\Exception\TransportException; use Symfony\Component\Notifier\Message\ChatMessage; use Symfony\Component\Notifier\Message\MessageInterface; use Symfony\Component\Notifier\Message\SmsMessage; @@ -44,4 +47,40 @@ public function unsupportedMessagesProvider(): iterable yield [new ChatMessage('Hello!')]; yield [$this->createMock(MessageInterface::class)]; } + + public function createClient(int $statusCode, string $content): HttpClientInterface + { + return new MockHttpClient(new MockResponse($content, ['http_code' => $statusCode])); + } + + public function responseProvider(): iterable + { + $responses = [ + ['status' => 200, 'content' => '{"error":101,"message":"Authorization failed"}', 'errorMessage' => 'Unable to send the SMS: "Authorization failed".'], + ['status' => 500, 'content' => '{}', 'errorMessage' => 'Unable to send the SMS: "unknown error".'], + ['status' => 500, 'content' => '{"error":null,"message":"Unknown"}', 'errorMessage' => 'Unable to send the SMS: "Unknown".'], + ['status' => 500, 'content' => '{"error":null,"message":null}', 'errorMessage' => 'Unable to send the SMS: "unknown error".'], + ['status' => 500, 'content' => 'Internal error', 'errorMessage' => 'Could not decode body to an array.'], + ['status' => 200, 'content' => 'Internal error', 'errorMessage' => 'Could not decode body to an array.'], + ]; + + foreach ($responses as $response) { + yield [$response['status'], $response['content'], $response['errorMessage']]; + } + } + + /** + * @dataProvider responseProvider + */ + public function testThrowExceptionWhenMessageWasNotSent(int $statusCode, string $content, string $errorMessage) + { + $client = $this->createClient($statusCode, $content); + $transport = $this->createTransport($client); + $message = new SmsMessage('0611223344', 'Hello!'); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage($errorMessage); + + $transport->send($message); + } } diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/TelegramTransport.php b/src/Symfony/Component/Notifier/Bridge/Telegram/TelegramTransport.php index 60eee07d9c5d5..5b5cad8e0fb8c 100644 --- a/src/Symfony/Component/Notifier/Bridge/Telegram/TelegramTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Telegram/TelegramTransport.php @@ -82,7 +82,7 @@ protected function doSend(MessageInterface $message): SentMessage if (!isset($options['parse_mode']) || TelegramOptions::PARSE_MODE_MARKDOWN_V2 === $options['parse_mode']) { $options['parse_mode'] = TelegramOptions::PARSE_MODE_MARKDOWN_V2; - $options['text'] = str_replace('.', '\.', $message->getSubject()); + $options['text'] = preg_replace('/([_*\[\]()~`>#+\-=|{}.!])/', '\\\\$1', $message->getSubject()); } $response = $this->client->request('POST', $endpoint, [ diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php index 2b0cbbdf17c45..718f566b0c240 100644 --- a/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php @@ -186,7 +186,7 @@ public function testSendWithChannelOverride() $this->assertEquals('telegram://api.telegram.org?channel=defaultChannel', $sentMessage->getTransport()); } - public function testSendWithMarkdownShouldEscapeDots() + public function testSendWithMarkdownShouldEscapeSpecialCharacters() { $response = $this->createMock(ResponseInterface::class); $response->expects($this->exactly(2)) @@ -223,7 +223,7 @@ public function testSendWithMarkdownShouldEscapeDots() $expectedBody = [ 'chat_id' => 'testChannel', - 'text' => 'I contain a \.', + 'text' => 'I contain special characters \_ \* \[ \] \( \) \~ \` \> \# \+ \- \= \| \{ \} \. \! to send\.', 'parse_mode' => 'MarkdownV2', ]; @@ -235,6 +235,6 @@ public function testSendWithMarkdownShouldEscapeDots() $transport = $this->createTransport($client, 'testChannel'); - $transport->send(new ChatMessage('I contain a .')); + $transport->send(new ChatMessage('I contain special characters _ * [ ] ( ) ~ ` > # + - = | { } . ! to send.')); } } diff --git a/src/Symfony/Component/OptionsResolver/OptionsResolver.php b/src/Symfony/Component/OptionsResolver/OptionsResolver.php index 21442f6b72e27..bb95548260bc7 100644 --- a/src/Symfony/Component/OptionsResolver/OptionsResolver.php +++ b/src/Symfony/Component/OptionsResolver/OptionsResolver.php @@ -942,6 +942,7 @@ public function resolve(array $options = []) * @throws OptionDefinitionException If there is a cyclic dependency between * lazy options and/or normalizers */ + #[\ReturnTypeWillChange] public function offsetGet($option, bool $triggerDeprecation = true) { if (!$this->locked) { @@ -1199,6 +1200,7 @@ private function verifyTypes(string $type, $value, array &$invalidTypes, int $le * * @see \ArrayAccess::offsetExists() */ + #[\ReturnTypeWillChange] public function offsetExists($option) { if (!$this->locked) { @@ -1215,6 +1217,7 @@ public function offsetExists($option) * * @throws AccessException */ + #[\ReturnTypeWillChange] public function offsetSet($option, $value) { throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); @@ -1227,6 +1230,7 @@ public function offsetSet($option, $value) * * @throws AccessException */ + #[\ReturnTypeWillChange] public function offsetUnset($option) { throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); @@ -1243,6 +1247,7 @@ public function offsetUnset($option) * * @see \Countable::count() */ + #[\ReturnTypeWillChange] public function count() { if (!$this->locked) { diff --git a/src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php b/src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php index a2f24224744aa..dd7e015c1ecd2 100644 --- a/src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php +++ b/src/Symfony/Component/PasswordHasher/Hasher/PasswordHasherFactory.php @@ -61,14 +61,7 @@ public function getPasswordHasher($user): PasswordHasherInterface throw new \RuntimeException(sprintf('No password hasher has been configured for account "%s".', \is_object($user) ? get_debug_type($user) : $user)); } - if (!$this->passwordHashers[$hasherKey] instanceof PasswordHasherInterface) { - $this->passwordHashers[$hasherKey] = $this->passwordHashers[$hasherKey] instanceof PasswordEncoderInterface - ? new PasswordHasherAdapter($this->passwordHashers[$hasherKey]) - : $this->createHasher($this->passwordHashers[$hasherKey]) - ; - } - - return $this->passwordHashers[$hasherKey]; + return $this->createHasherUsingAdapter($hasherKey); } /** @@ -111,6 +104,18 @@ private function createHasher(array $config, bool $isExtra = false): PasswordHas return new MigratingPasswordHasher($hasher, ...$extrapasswordHashers); } + private function createHasherUsingAdapter(string $hasherKey): PasswordHasherInterface + { + if (!$this->passwordHashers[$hasherKey] instanceof PasswordHasherInterface) { + $this->passwordHashers[$hasherKey] = $this->passwordHashers[$hasherKey] instanceof PasswordEncoderInterface + ? new PasswordHasherAdapter($this->passwordHashers[$hasherKey]) + : $this->createHasher($this->passwordHashers[$hasherKey]) + ; + } + + return $this->passwordHashers[$hasherKey]; + } + private function getHasherConfigFromAlgorithm(array $config): array { if ('auto' === $config['algorithm']) { @@ -142,8 +147,8 @@ private function getHasherConfigFromAlgorithm(array $config): array $hasherChain = [$this->createHasher($config, true)]; foreach ($frompasswordHashers as $name) { - if ($hasher = $this->passwordHashers[$name] ?? false) { - $hasher = $hasher instanceof PasswordHasherInterface ? $hasher : $this->createHasher($hasher, true); + if (isset($this->passwordHashers[$name])) { + $hasher = $this->createHasherUsingAdapter($name); } else { $hasher = $this->createHasher(['algorithm' => $name], true); } diff --git a/src/Symfony/Component/PasswordHasher/Hasher/UserPasswordHasher.php b/src/Symfony/Component/PasswordHasher/Hasher/UserPasswordHasher.php index 4ec8a1b6ab248..dcc65e9dfc8a6 100644 --- a/src/Symfony/Component/PasswordHasher/Hasher/UserPasswordHasher.php +++ b/src/Symfony/Component/PasswordHasher/Hasher/UserPasswordHasher.php @@ -50,7 +50,7 @@ public function hashPassword($user, string $plainPassword): string } elseif ($user instanceof UserInterface) { $salt = $user->getSalt(); - if (null !== $salt) { + if ($salt) { trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user)); } } diff --git a/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php b/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php index 1f8fcb3e79531..1f24a0d3cace2 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Hasher/PasswordHasherFactoryTest.php @@ -163,6 +163,29 @@ public function testMigrateFrom() $this->assertStringStartsWith(\SODIUM_CRYPTO_PWHASH_STRPREFIX, $hasher->hash('foo', null)); } + /** + * @group legacy + */ + public function testMigrateFromLegacy() + { + if (!SodiumPasswordHasher::isSupported()) { + $this->markTestSkipped('Sodium is not available'); + } + + $factory = new PasswordHasherFactory([ + 'plaintext_encoder' => $plaintext = new PlaintextPasswordEncoder(), + SomeUser::class => ['algorithm' => 'sodium', 'migrate_from' => ['bcrypt', 'plaintext_encoder']], + ]); + + $hasher = $factory->getPasswordHasher(SomeUser::class); + $this->assertInstanceOf(MigratingPasswordHasher::class, $hasher); + + $this->assertTrue($hasher->verify((new SodiumPasswordHasher())->hash('foo', null), 'foo', null)); + $this->assertTrue($hasher->verify((new NativePasswordHasher(null, null, null, \PASSWORD_BCRYPT))->hash('foo', null), 'foo', null)); + $this->assertTrue($hasher->verify($plaintext->encodePassword('foo', null), 'foo', null)); + $this->assertStringStartsWith(\SODIUM_CRYPTO_PWHASH_STRPREFIX, $hasher->hash('foo', null)); + } + public function testDefaultMigratingHashers() { $this->assertInstanceOf( diff --git a/src/Symfony/Component/PasswordHasher/Tests/Hasher/UserPasswordHasherTest.php b/src/Symfony/Component/PasswordHasher/Tests/Hasher/UserPasswordHasherTest.php index b483864d22d53..32805b1917ec7 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Hasher/UserPasswordHasherTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Hasher/UserPasswordHasherTest.php @@ -158,7 +158,7 @@ public function testNeedsRehash() $passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory); - \Closure::bind(function () use ($passwordHasher) { $this->password = $passwordHasher->hashPassword($this, 'foo', 'salt'); }, $user, User::class)(); + \Closure::bind(function () use ($passwordHasher) { $this->password = $passwordHasher->hashPassword($this, 'foo', 'salt'); }, $user, class_exists(User::class) ? User::class : InMemoryUser::class)(); $this->assertFalse($passwordHasher->needsRehash($user)); $this->assertTrue($passwordHasher->needsRehash($user)); $this->assertFalse($passwordHasher->needsRehash($user)); diff --git a/src/Symfony/Component/Process/InputStream.php b/src/Symfony/Component/Process/InputStream.php index c86fca86878df..4f8f71331aafc 100644 --- a/src/Symfony/Component/Process/InputStream.php +++ b/src/Symfony/Component/Process/InputStream.php @@ -69,6 +69,7 @@ public function isClosed() /** * @return \Traversable */ + #[\ReturnTypeWillChange] public function getIterator() { $this->open = true; diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 02379b8927f43..a541cd66a40b5 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -620,6 +620,7 @@ public function getIncrementalOutput() * * @return \Generator */ + #[\ReturnTypeWillChange] public function getIterator(int $flags = 0) { $this->readPipesForOutput(__FUNCTION__, false); diff --git a/src/Symfony/Component/PropertyAccess/PropertyPath.php b/src/Symfony/Component/PropertyAccess/PropertyPath.php index 2c36b063a7ac1..0c1c4346a0f89 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyPath.php +++ b/src/Symfony/Component/PropertyAccess/PropertyPath.php @@ -154,6 +154,7 @@ public function getParent() * * @return PropertyPathIteratorInterface */ + #[\ReturnTypeWillChange] public function getIterator() { return new PropertyPathIterator($this); diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php index cf02ee69f2979..72e053171c841 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/NonTraversableArrayObject.php @@ -29,12 +29,18 @@ public function offsetExists($offset): bool return \array_key_exists($offset, $this->array); } + /** + * @param mixed $offset + * + * @return mixed + */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->array[$offset]; } - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { if (null === $offset) { $this->array[] = $value; @@ -43,7 +49,7 @@ public function offsetSet($offset, $value) } } - public function offsetUnset($offset) + public function offsetUnset($offset): void { unset($this->array[$offset]); } @@ -68,7 +74,7 @@ public function __unserialize(array $data): void $this->array = $data; } - public function unserialize($serialized) + public function unserialize($serialized): void { $this->__unserialize((array) unserialize((string) $serialized)); } diff --git a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php index 5693c6b73e685..eb4da3f201342 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php +++ b/src/Symfony/Component/PropertyAccess/Tests/Fixtures/TraversableArrayObject.php @@ -29,12 +29,18 @@ public function offsetExists($offset): bool return \array_key_exists($offset, $this->array); } + /** + * @param mixed $offset + * + * @return mixed + */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->array[$offset]; } - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { if (null === $offset) { $this->array[] = $value; @@ -43,7 +49,7 @@ public function offsetSet($offset, $value) } } - public function offsetUnset($offset) + public function offsetUnset($offset): void { unset($this->array[$offset]); } diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php index aaae8198c3544..041502d18fe34 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php @@ -554,7 +554,7 @@ private function extractFromReflectionType(\ReflectionType $reflectionType, \Ref foreach ($reflectionType instanceof \ReflectionUnionType ? $reflectionType->getTypes() : [$reflectionType] as $type) { $phpTypeOrClass = $reflectionType instanceof \ReflectionNamedType ? $reflectionType->getName() : (string) $type; - if ('null' === $phpTypeOrClass || 'mixed' === $phpTypeOrClass) { + if ('null' === $phpTypeOrClass || 'mixed' === $phpTypeOrClass || 'never' === $phpTypeOrClass) { continue; } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index cc8958d1241f5..253b9032ba2cb 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -257,7 +257,7 @@ public function php71TypesProvider() } /** - * * @dataProvider php80TypesProvider + * @dataProvider php80TypesProvider * @requires PHP 8 */ public function testExtractPhp80Type($property, array $type = null) @@ -279,6 +279,14 @@ public function php80TypesProvider() ]; } + /** + * @requires PHP 8.1 + */ + public function testExtractPhp81Type() + { + $this->assertNull($this->extractor->getTypes('Symfony\Component\PropertyInfo\Tests\Fixtures\Php81Dummy', 'nothing', [])); + } + /** * @dataProvider defaultValueProvider */ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php new file mode 100644 index 0000000000000..b4e896a434524 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php @@ -0,0 +1,11 @@ +<?php + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +class Php81Dummy +{ + public function getNothing(): never + { + throw new \Exception('Oops'); + } +} diff --git a/src/Symfony/Component/Routing/Loader/Configurator/Traits/RouteTrait.php b/src/Symfony/Component/Routing/Loader/Configurator/Traits/RouteTrait.php index acdffae33beeb..ac05d10e5f37c 100644 --- a/src/Symfony/Component/Routing/Loader/Configurator/Traits/RouteTrait.php +++ b/src/Symfony/Component/Routing/Loader/Configurator/Traits/RouteTrait.php @@ -126,7 +126,7 @@ final public function methods(array $methods): self /** * Adds the "_controller" entry to defaults. * - * @param callable|string $controller a callable or parseable pseudo-callable + * @param callable|string|array $controller a callable or parseable pseudo-callable * * @return $this */ diff --git a/src/Symfony/Component/Routing/RouteCollection.php b/src/Symfony/Component/Routing/RouteCollection.php index 73b6ac47c7a63..afd92da4fa639 100644 --- a/src/Symfony/Component/Routing/RouteCollection.php +++ b/src/Symfony/Component/Routing/RouteCollection.php @@ -56,6 +56,7 @@ public function __clone() * * @return \ArrayIterator|Route[] An \ArrayIterator object for iterating over routes */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->all()); @@ -66,6 +67,7 @@ public function getIterator() * * @return int The number of routes */ + #[\ReturnTypeWillChange] public function count() { return \count($this->routes); diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl.php b/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl.php index e4a1dc618c925..c397b5499b687 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/php_dsl.php @@ -10,7 +10,9 @@ ->options(['utf8' => true]) ->add('buz', 'zub') ->controller('foo:act') - ->stateless(true); + ->stateless(true) + ->add('controller_class', '/controller') + ->controller(['Acme\MyApp\MyController', 'myAction']); $routes->import('php_dsl_sub.php') ->prefix('/sub') diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/php_object_dsl.php b/src/Symfony/Component/Routing/Tests/Fixtures/php_object_dsl.php index c2410831af6b2..8ee12cc8f0c4b 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/php_object_dsl.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/php_object_dsl.php @@ -12,7 +12,9 @@ public function __invoke(RoutingConfigurator $routes) ->options(['utf8' => true]) ->add('buz', 'zub') ->controller('foo:act') - ->stateless(true); + ->stateless(true) + ->add('controller_class', '/controller') + ->controller(['Acme\MyApp\MyController', 'myAction']); $routes->import('php_dsl_sub.php') ->prefix('/sub') diff --git a/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php index 7ed941c711e3b..f5057b39a26b9 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/PhpFileLoaderTest.php @@ -177,6 +177,9 @@ public function testRoutingConfigurator() $expectedCollection->add('buz', (new Route('/zub')) ->setDefaults(['_controller' => 'foo:act', '_stateless' => true]) ); + $expectedCollection->add('controller_class', (new Route('/controller')) + ->setDefaults(['_controller' => ['Acme\MyApp\MyController', 'myAction']]) + ); $expectedCollection->add('c_root', (new Route('/sub/pub/')) ->setRequirements(['id' => '\d+']) ); diff --git a/src/Symfony/Component/Security/Core/Authentication/AuthenticationProviderManager.php b/src/Symfony/Component/Security/Core/Authentication/AuthenticationProviderManager.php index 92a48dc964c2d..76be0f4da328b 100644 --- a/src/Symfony/Component/Security/Core/Authentication/AuthenticationProviderManager.php +++ b/src/Symfony/Component/Security/Core/Authentication/AuthenticationProviderManager.php @@ -110,7 +110,7 @@ public function authenticate(TokenInterface $token) $this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($result), AuthenticationEvents::AUTHENTICATION_SUCCESS); } - // @deprecated since 5.3 + // @deprecated since Symfony 5.3 if ($user = $result->getUser() instanceof UserInterface && !method_exists($result->getUser(), 'getUserIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "getUserIdentifier(): string" in user class "%s" is deprecated. This method will replace "getUsername()" in Symfony 6.0.', get_debug_type($result->getUser())); } diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php index d83c1a0ca77a6..a0e01da4e1a28 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php @@ -120,7 +120,7 @@ protected function retrieveUser(string $userIdentifier, UsernamePasswordToken $t } try { - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 if (method_exists($this->userProvider, 'loadUserByIdentifier')) { $user = $this->userProvider->loadUserByIdentifier($userIdentifier); } else { diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php index 418523e20c6b9..7de715d70a122 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php @@ -70,7 +70,7 @@ protected function retrieveUser(string $userIdentifier, UsernamePasswordToken $t throw new UserNotFoundException('User identifier can not be null.'); } - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 if (method_exists($this->userProvider, 'loadUserByIdentifier')) { return $this->userProvider->loadUserByIdentifier($userIdentifier); } else { @@ -85,7 +85,7 @@ protected function retrieveUser(string $userIdentifier, UsernamePasswordToken $t */ protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token) { - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 $userIdentifier = method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(); $password = $token->getCredentials(); diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php index 4f69f33a23d95..d81e31bb07b75 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/PreAuthenticatedAuthenticationProvider.php @@ -59,7 +59,7 @@ public function authenticate(TokenInterface $token) } $userIdentifier = method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(); - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 if (method_exists($this->userProvider, 'loadUserByIdentifier')) { $user = $this->userProvider->loadUserByIdentifier($userIdentifier); } else { diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php index 0363e18428a37..d2ef93a0e54c3 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php @@ -75,7 +75,7 @@ public function getUserIdentifier(): string } if ($this->user instanceof UserInterface) { - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 return method_exists($this->user, 'getUserIdentifier') ? $this->user->getUserIdentifier() : $this->user->getUsername(); } diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php index 95a4d2d780cb0..ec6d7c7168f56 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/PreAuthenticatedToken.php @@ -50,7 +50,7 @@ public function __construct($user, $credentials, string $firewallName, array $ro * * @return string The provider key * - * @deprecated since 5.2, use getFirewallName() instead + * @deprecated since Symfony 5.2, use getFirewallName() instead */ public function getProviderKey() { diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/RememberMeToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/RememberMeToken.php index c019195eecb62..6ad39c514afe2 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/RememberMeToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/RememberMeToken.php @@ -64,7 +64,7 @@ public function setAuthenticated(bool $authenticated) * * @return string The provider secret * - * @deprecated since 5.2, use getFirewallName() instead + * @deprecated since Symfony 5.2, use getFirewallName() instead */ public function getProviderKey() { diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php index 1fc30bfcd1473..b479324498854 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php @@ -49,7 +49,7 @@ public function setToken(TokenInterface $token = null) // ensure any initializer is called $this->getToken(); - // @deprecated since 5.3 + // @deprecated since Symfony 5.3 if (!method_exists($token, 'getUserIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "getUserIdentifier(): string" in token class "%s" is deprecated. This method will replace "getUsername()" in Symfony 6.0.', get_debug_type($token)); } diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php index 8228f6773955d..a087b082a187c 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php @@ -70,7 +70,7 @@ public function getCredentials() * * @return string The provider key * - * @deprecated since 5.2, use getFirewallName() instead + * @deprecated since Symfony 5.2, use getFirewallName() instead */ public function getProviderKey() { diff --git a/src/Symfony/Component/Security/Core/Exception/AuthenticationException.php b/src/Symfony/Component/Security/Core/Exception/AuthenticationException.php index f64fa77148733..9a06def234ef2 100644 --- a/src/Symfony/Component/Security/Core/Exception/AuthenticationException.php +++ b/src/Symfony/Component/Security/Core/Exception/AuthenticationException.php @@ -112,7 +112,7 @@ public function __sleep(): array /** * @internal */ - public function __wakeup() + public function __wakeup(): void { $this->__unserialize($this->serialized); unset($this->serialized); diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.et.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.et.xlf new file mode 100644 index 0000000000000..cc2b16ae853dc --- /dev/null +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.et.xlf @@ -0,0 +1,83 @@ +<?xml version="1.0"?> +<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2"> + <file source-language="en" datatype="plaintext" original="file.ext"> + <body> + <trans-unit id="1"> + <source>An authentication exception occurred.</source> + <target>Autentimisel juhtus ootamatu viga.</target> + </trans-unit> + <trans-unit id="2"> + <source>Authentication credentials could not be found.</source> + <target>Autentimisandmeid ei leitud.</target> + </trans-unit> + <trans-unit id="3"> + <source>Authentication request could not be processed due to a system problem.</source> + <target>Autentimispäring ei õnnestunud süsteemi probleemi tõttu.</target> + </trans-unit> + <trans-unit id="4"> + <source>Invalid credentials.</source> + <target>Vigased autentimisandmed.</target> + </trans-unit> + <trans-unit id="5"> + <source>Cookie has already been used by someone else.</source> + <target>Küpsis on juba kellegi teise poolt kasutuses.</target> + </trans-unit> + <trans-unit id="6"> + <source>Not privileged to request the resource.</source> + <target>Ressursi pärimiseks pole piisavalt õiguseid.</target> + </trans-unit> + <trans-unit id="7"> + <source>Invalid CSRF token.</source> + <target>Vigane CSRF märgis.</target> + </trans-unit> + <trans-unit id="9"> + <source>No authentication provider found to support the authentication token.</source> + <target>Ei leitud sobivat autentimismeetodit, mis toetaks autentimismärgist.</target> + </trans-unit> + <trans-unit id="10"> + <source>No session available, it either timed out or cookies are not enabled.</source> + <target>Seanss puudub, see on kas aegunud või pole küpsised lubatud.</target> + </trans-unit> + <trans-unit id="11"> + <source>No token could be found.</source> + <target>Identsustõendit ei leitud.</target> + </trans-unit> + <trans-unit id="12"> + <source>Username could not be found.</source> + <target>Kasutajanime ei leitud.</target> + </trans-unit> + <trans-unit id="13"> + <source>Account has expired.</source> + <target>Kasutajakonto on aegunud.</target> + </trans-unit> + <trans-unit id="14"> + <source>Credentials have expired.</source> + <target>Autentimistunnused on aegunud.</target> + </trans-unit> + <trans-unit id="15"> + <source>Account is disabled.</source> + <target>Kasutajakonto on keelatud.</target> + </trans-unit> + <trans-unit id="16"> + <source>Account is locked.</source> + <target>Kasutajakonto on lukustatud.</target> + </trans-unit> + <trans-unit id="17"> + <source>Too many failed login attempts, please try again later.</source> + <target>Liiga palju ebaõnnestunud autentimise katseid, palun proovi hiljem uuesti.</target> + </trans-unit> + <trans-unit id="18"> + <source>Invalid or expired login link.</source> + <target>Vigane või aegunud sisselogimise link.</target> + </trans-unit> + <trans-unit id="19"> + <source>Too many failed login attempts, please try again in %minutes% minute.</source> + <target>Liiga palju ebaõnnestunud autentimise katseid, palun proovi uuesti %minutes% minuti pärast.</target> + </trans-unit> + <trans-unit id="20"> + <source>Too many failed login attempts, please try again in %minutes% minutes.</source> + <target>Liiga palju ebaõnnestunud autentimise katseid, palun proovi uuesti %minutes% minuti pärast.</target> + </trans-unit> + </body> + </file> +</xliff> diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.id.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.id.xlf index 91315bdf1d016..119e2d0cd70fb 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.id.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.id.xlf @@ -64,11 +64,19 @@ </trans-unit> <trans-unit id="17"> <source>Too many failed login attempts, please try again later.</source> - <target>Terlalu banyak percobaan login yang salah, Silahkan coba lagi nanti.</target> + <target>Terlalu banyak percobaan login yang salah, silahkan coba lagi nanti.</target> </trans-unit> <trans-unit id="18"> <source>Invalid or expired login link.</source> - <target>Link login salah atau sudah kadaluwarsa.</target> + <target>Link login salah atau sudah kedaluwarsa.</target> + </trans-unit> + <trans-unit id="19"> + <source>Too many failed login attempts, please try again in %minutes% minute.</source> + <target>Terlalu banyak percobaan login yang salah, silahkan coba lagi dalam %minutes% menit.</target> + </trans-unit> + <trans-unit id="20"> + <source>Too many failed login attempts, please try again in %minutes% minutes.</source> + <target>Terlalu banyak percobaan login yang salah, silahkan coba lagi dalam %minutes% menit.</target> </trans-unit> </body> </file> diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.tl.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.tl.xlf index 5bf5304b5cae1..66547b2a3d1be 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.tl.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.tl.xlf @@ -70,6 +70,14 @@ <source>Invalid or expired login link.</source> <target>Inbalido o nagexpire na ang link para makapaglogin.</target> </trans-unit> + <trans-unit id="19"> + <source>Too many failed login attempts, please try again in %minutes% minute.</source> + <target>Madaming bagsak na pagtatangka, pakisubukan ulit pagkatapos ng %minutes% minuto.</target> + </trans-unit> + <trans-unit id="20"> + <source>Too many failed login attempts, please try again in %minutes% minute.</source> + <target>Madaming bagsak na pagtatangka, pakisubukan ulit pagkatapos ng %minutes% minuto.</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/Symfony/Component/Security/Core/User/ChainUserProvider.php b/src/Symfony/Component/Security/Core/User/ChainUserProvider.php index 30dfeb9b2db1a..7f40abb10ce18 100644 --- a/src/Symfony/Component/Security/Core/User/ChainUserProvider.php +++ b/src/Symfony/Component/Security/Core/User/ChainUserProvider.php @@ -60,7 +60,7 @@ public function loadUserByIdentifier(string $userIdentifier): UserInterface { foreach ($this->providers as $provider) { try { - // @deprecated since 5.3, change to $provider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $provider->loadUserByIdentifier() in 6.0 if (!method_exists($provider, 'loadUserByIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($provider)); @@ -101,7 +101,7 @@ public function refreshUser(UserInterface $user) } if ($supportedUserFound) { - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 $username = method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(); $e = new UserNotFoundException(sprintf('There is no user with name "%s".', $username)); $e->setUserIdentifier($username); diff --git a/src/Symfony/Component/Security/Core/User/InMemoryUserProvider.php b/src/Symfony/Component/Security/Core/User/InMemoryUserProvider.php index 2e9ea5a27f675..d077939471aa5 100644 --- a/src/Symfony/Component/Security/Core/User/InMemoryUserProvider.php +++ b/src/Symfony/Component/Security/Core/User/InMemoryUserProvider.php @@ -51,7 +51,7 @@ public function __construct(array $users = []) */ public function createUser(UserInterface $user) { - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 $userIdentifier = strtolower(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername()); if (isset($this->users[$userIdentifier])) { throw new \LogicException('Another user with the same username already exists.'); @@ -74,7 +74,7 @@ public function loadUserByIdentifier(string $identifier): UserInterface { $user = $this->getUser($identifier); - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 return new InMemoryUser(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), $user->getPassword(), $user->getRoles(), $user->isEnabled()); } @@ -87,7 +87,7 @@ public function refreshUser(UserInterface $user) throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); } - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 $storedUser = $this->getUser(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername()); $userIdentifier = method_exists($storedUser, 'getUserIdentifier') ? $storedUser->getUserIdentifier() : $storedUser->getUsername(); diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index e84a4c6a42ada..4b5c69cf92901 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -117,7 +117,7 @@ private function authenticateViaGuard(AuthenticatorInterface $guardAuthenticator if (null === $user) { $e = new UserNotFoundException(sprintf('Null returned from "%s::getUser()".', get_debug_type($guardAuthenticator))); - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 $e->setUserIdentifier(method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername()); throw $e; diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index e72eb075b163e..6afa8eadf0fa7 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -75,7 +75,7 @@ public function __construct(iterable $authenticators, TokenStorageInterface $tok public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response { // create an authenticated token for the User - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 $token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport(new UserBadge(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), function () use ($user) { return $user; }), $badges), $this->firewallName); // announce the authenticated token @@ -229,7 +229,7 @@ private function executeAuthenticator(AuthenticatorInterface $authenticator, Req private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, PassportInterface $passport, Request $request, AuthenticatorInterface $authenticator): ?Response { - // @deprecated since 5.3 + // @deprecated since Symfony 5.3 $user = $authenticatedToken->getUser(); if ($user instanceof UserInterface && !method_exists($user, 'getUserIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "getUserIdentifier(): string" in user class "%s" is deprecated. This method will replace "getUsername()" in Symfony 6.0.', get_debug_type($authenticatedToken->getUser())); diff --git a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php index a0f6f3ea74ac0..950f19ca6ccd1 100644 --- a/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php +++ b/src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php @@ -30,7 +30,7 @@ class DefaultAuthenticationSuccessHandler implements AuthenticationSuccessHandle protected $httpUtils; protected $options; - /** @deprecated since 5.2, use $firewallName instead */ + /** @deprecated since Symfony 5.2, use $firewallName instead */ protected $providerKey; protected $firewallName; protected $defaultOptions = [ @@ -78,7 +78,7 @@ public function setOptions(array $options) * * @return string * - * @deprecated since 5.2, use getFirewallName() instead + * @deprecated since Symfony 5.2, use getFirewallName() instead */ public function getProviderKey() { diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php index 1202444a1b647..a5736aecd800c 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractPreAuthenticatedAuthenticator.php @@ -86,7 +86,7 @@ public function supports(Request $request): ?bool public function authenticate(Request $request): PassportInterface { - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 $method = 'loadUserByIdentifier'; if (!method_exists($this->userProvider, 'loadUserByIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider)); diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 892f19a9a66bf..aeb84a5799de0 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -81,7 +81,7 @@ public function authenticate(Request $request): PassportInterface { $credentials = $this->getCredentials($request); - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 $method = 'loadUserByIdentifier'; if (!method_exists($this->userProvider, 'loadUserByIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider)); diff --git a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php index 55ef80d80d492..ef213d30a3e34 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/HttpBasicAuthenticator.php @@ -64,7 +64,7 @@ public function authenticate(Request $request): PassportInterface $username = $request->headers->get('PHP_AUTH_USER'); $password = $request->headers->get('PHP_AUTH_PW', ''); - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 $method = 'loadUserByIdentifier'; if (!method_exists($this->userProvider, 'loadUserByIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider)); diff --git a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php index 41e04aef3d763..7605bb7774312 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/JsonLoginAuthenticator.php @@ -91,7 +91,7 @@ public function authenticate(Request $request): PassportInterface throw $e; } - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 $method = 'loadUserByIdentifier'; if (!method_exists($this->userProvider, 'loadUserByIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider)); diff --git a/src/Symfony/Component/Security/Http/EventListener/UserProviderListener.php b/src/Symfony/Component/Security/Http/EventListener/UserProviderListener.php index a6e78f6a1fe79..a3408789d1c71 100644 --- a/src/Symfony/Component/Security/Http/EventListener/UserProviderListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/UserProviderListener.php @@ -45,7 +45,7 @@ public function checkPassport(CheckPassportEvent $event): void return; } - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 if (method_exists($this->userProvider, 'loadUserByIdentifier')) { $badge->setUserLoader([$this->userProvider, 'loadUserByIdentifier']); } else { diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php index 7b8622f22f5a4..33e2c08b64d86 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php @@ -199,7 +199,7 @@ private function onFailure(Request $request, AuthenticationException $failed): R private function onSuccess(Request $request, TokenInterface $token): Response { if (null !== $this->logger) { - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 $this->logger->info('User has been authenticated successfully.', ['username' => method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername()]); } diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php index 9b9851f4ffa14..b698e1e4d7dca 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php @@ -87,7 +87,7 @@ public function authenticate(RequestEvent $event) } if (null !== $token = $this->tokenStorage->getToken()) { - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 if ($token instanceof PreAuthenticatedToken && $this->providerKey == $token->getFirewallName() && $token->isAuthenticated() && (method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername()) === $user) { return; } diff --git a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php index ec6b0b79e1e37..4864403eae0ac 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php @@ -75,7 +75,13 @@ public function authenticate(RequestEvent $event) $attributes = $request->attributes->get('_access_control_attributes'); $request->attributes->remove('_access_control_attributes'); - if (!$attributes || ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] === $attributes && $event instanceof LazyResponseEvent)) { + if ( + !$attributes + || ( + ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] === $attributes || [AuthenticatedVoter::PUBLIC_ACCESS] === $attributes) + && $event instanceof LazyResponseEvent + ) + ) { return; } diff --git a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php index 9469fa8819c0f..2e3a8e5dcab5d 100644 --- a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php @@ -77,7 +77,7 @@ public function authenticate(RequestEvent $event) } if (null !== $token = $this->tokenStorage->getToken()) { - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 if ($token instanceof UsernamePasswordToken && $token->isAuthenticated() && (method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername()) === $username) { return; } diff --git a/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php b/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php index c6e087fb7febd..4c5f1f09b60cf 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php @@ -49,7 +49,7 @@ public function supports(Request $request): ?bool if (null !== $this->logger) { if ('https' === $request->headers->get('X-Forwarded-Proto')) { $this->logger->info('Redirecting to HTTPS. ("X-Forwarded-Proto" header is set to "https" - did you set "trusted_proxies" correctly?)'); - } elseif (str_contains($request->headers->get('Forwarded'), 'proto=https')) { + } elseif (str_contains($request->headers->get('Forwarded', ''), 'proto=https')) { $this->logger->info('Redirecting to HTTPS. ("Forwarded" header is set to "proto=https" - did you set "trusted_proxies" correctly?)'); } else { $this->logger->info('Redirecting to HTTPS.'); diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index 25219dfd48eb3..53e948eb36969 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -234,7 +234,7 @@ protected function refreshUser(TokenInterface $token): ?TokenInterface $userDeauthenticated = true; if (null !== $this->logger) { - // @deprecated since 5.3, change to $refreshedUser->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $refreshedUser->getUserIdentifier() in 6.0 $this->logger->debug('Cannot refresh token because user has changed.', ['username' => method_exists($refreshedUser, 'getUserIdentifier') ? $refreshedUser->getUserIdentifier() : $refreshedUser->getUsername(), 'provider' => \get_class($provider)]); } @@ -244,11 +244,11 @@ protected function refreshUser(TokenInterface $token): ?TokenInterface $token->setUser($refreshedUser); if (null !== $this->logger) { - // @deprecated since 5.3, change to $refreshedUser->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $refreshedUser->getUserIdentifier() in 6.0 $context = ['provider' => \get_class($provider), 'username' => method_exists($refreshedUser, 'getUserIdentifier') ? $refreshedUser->getUserIdentifier() : $refreshedUser->getUsername()]; if ($token instanceof SwitchUserToken) { - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 $context['impersonator_username'] = method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getOriginalToken()->getUsername(); } diff --git a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php index 5930aa6fdff01..69af6d819f3a6 100644 --- a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php @@ -113,7 +113,7 @@ public function authenticate(RequestEvent $event) if (null !== $this->csrfTokenManager) { $csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); - if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) { + if (!\is_string($csrfToken) || false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) { throw new LogoutException('Invalid CSRF token.'); } } diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index b765b89da7d18..2d4be7fcfe004 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -140,7 +140,7 @@ private function attemptSwitchUser(Request $request, string $username): ?TokenIn $originalToken = $this->getOriginalToken($token); if (null !== $originalToken) { - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 if ((method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername()) === $username) { return $token; } @@ -149,7 +149,7 @@ private function attemptSwitchUser(Request $request, string $username): ?TokenIn $token = $this->attemptExitUser($request); } - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 $currentUsername = method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(); $nonExistentUsername = '_'.md5(random_bytes(8).$username); diff --git a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php index 0d13f81e2ec50..ad98464dbe3a2 100644 --- a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordFormAuthenticationListener.php @@ -76,7 +76,7 @@ protected function attemptAuthentication(Request $request) if (null !== $this->csrfTokenManager) { $csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); - if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) { + if (!\is_string($csrfToken) || false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) { throw new InvalidCsrfTokenException('Invalid CSRF token.'); } } diff --git a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php index c1a92cbd58916..9679b33ff92a5 100644 --- a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php @@ -153,7 +153,7 @@ public function authenticate(RequestEvent $event) private function onSuccess(Request $request, TokenInterface $token): ?Response { if (null !== $this->logger) { - // @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $token->getUserIdentifier() in 6.0 $this->logger->info('User has been authenticated successfully.', ['username' => method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername()]); } diff --git a/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php b/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php index 29abec856fecf..b49fd8b1ea31b 100644 --- a/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php +++ b/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php @@ -50,7 +50,7 @@ public function createLoginLink(UserInterface $user, Request $request = null): L $expires = $expiresAt->format('U'); $parameters = [ - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 'user' => method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), 'expires' => $expires, 'hash' => $this->signatureHashUtil->computeSignatureHash($user, $expires), @@ -85,7 +85,7 @@ public function consumeLoginLink(Request $request): UserInterface $userIdentifier = $request->get('user'); try { - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 if (method_exists($this->userProvider, 'loadUserByIdentifier')) { $user = $this->userProvider->loadUserByIdentifier($userIdentifier); } else { diff --git a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeHandler.php b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeHandler.php index 03ccb83649d31..97918c86cb8b4 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeHandler.php +++ b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeHandler.php @@ -63,7 +63,7 @@ abstract protected function processRememberMe(RememberMeDetails $rememberMeDetai public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): UserInterface { try { - // @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0 $method = 'loadUserByIdentifier'; if (!method_exists($this->userProvider, 'loadUserByIdentifier')) { trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider)); diff --git a/src/Symfony/Component/Security/Http/RememberMe/PersistentTokenBasedRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/PersistentTokenBasedRememberMeServices.php index c7376faa910d9..dc60806c4e9eb 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/PersistentTokenBasedRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/PersistentTokenBasedRememberMeServices.php @@ -94,7 +94,7 @@ protected function processAutoLoginCookie(array $cookieParts, Request $request) ); $userProvider = $this->getUserProvider($persistentToken->getClass()); - // @deprecated since 5.3, change to $persistentToken->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $persistentToken->getUserIdentifier() in 6.0 if (method_exists($persistentToken, 'getUserIdentifier')) { $userIdentifier = $persistentToken->getUserIdentifier(); } else { @@ -103,7 +103,7 @@ protected function processAutoLoginCookie(array $cookieParts, Request $request) $userIdentifier = $persistentToken->getUsername(); } - // @deprecated since 5.3, change to $userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $userProvider->loadUserByIdentifier() in 6.0 if (method_exists($userProvider, 'loadUserByIdentifier')) { return $userProvider->loadUserByIdentifier($userIdentifier); } else { @@ -124,7 +124,7 @@ protected function onLoginSuccess(Request $request, Response $response, TokenInt $this->tokenProvider->createNewToken( new PersistentToken( \get_class($user = $token->getUser()), - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), $series, $this->generateHash($tokenValue), diff --git a/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php index a6e79f42e56d7..21725fe7ef00b 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php @@ -41,7 +41,7 @@ protected function processAutoLoginCookie(array $cookieParts, Request $request) } try { $userProvider = $this->getUserProvider($class); - // @deprecated since 5.3, change to $userProvider->loadUserByIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $userProvider->loadUserByIdentifier() in 6.0 if (method_exists($userProvider, 'loadUserByIdentifier')) { $user = $userProvider->loadUserByIdentifier($userIdentifier); } else { @@ -79,7 +79,7 @@ protected function onLoginSuccess(Request $request, Response $response, TokenInt { $user = $token->getUser(); $expires = time() + $this->options['lifetime']; - // @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0 + // @deprecated since Symfony 5.3, change to $user->getUserIdentifier() in 6.0 $value = $this->generateCookieValue(\get_class($user), method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), $expires, $user->getPassword()); $response->headers->setCookie( diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php index aac67379b4e7f..0faa43341ed0d 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php @@ -360,4 +360,41 @@ public function testHandleMWithultipleAttributesShouldBeHandledAsAnd() $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } + + public function testLazyPublicPagesShouldNotAccessTokenStorage() + { + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects($this->never())->method('getToken'); + + $request = new Request(); + $accessMap = $this->createMock(AccessMapInterface::class); + $accessMap->expects($this->any()) + ->method('getPatterns') + ->with($this->equalTo($request)) + ->willReturn([[AuthenticatedVoter::PUBLIC_ACCESS], null]) + ; + + $listener = new AccessListener($tokenStorage, $this->createMock(AccessDecisionManagerInterface::class), $accessMap, $this->createMock(AuthenticationManagerInterface::class), false); + $listener(new LazyResponseEvent(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST))); + } + + /** + * @group legacy + */ + public function testLegacyLazyPublicPagesShouldNotAccessTokenStorage() + { + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects($this->never())->method('getToken'); + + $request = new Request(); + $accessMap = $this->createMock(AccessMapInterface::class); + $accessMap->expects($this->any()) + ->method('getPatterns') + ->with($this->equalTo($request)) + ->willReturn([[AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY], null]) + ; + + $listener = new AccessListener($tokenStorage, $this->createMock(AccessDecisionManagerInterface::class), $accessMap, $this->createMock(AuthenticationManagerInterface::class), false); + $listener(new LazyResponseEvent(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST))); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php index 3c7a05b381d09..d38a03e87fbdb 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ChannelListenerTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\Security\Http\Tests\Firewall; use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use Symfony\Component\HttpFoundation\HeaderBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\RequestEvent; @@ -153,4 +155,29 @@ public function testHandleWithSecuredRequestAndHttpChannel() $this->assertSame($response, $event->getResponse()); } + + public function testSupportsWithoutHeaders() + { + $request = $this->createMock(Request::class); + $request + ->expects($this->any()) + ->method('isSecure') + ->willReturn(false) + ; + $request->headers = new HeaderBag(); + + $accessMap = $this->createMock(AccessMapInterface::class); + $accessMap + ->expects($this->any()) + ->method('getPatterns') + ->with($this->equalTo($request)) + ->willReturn([[], 'https']) + ; + + $entryPoint = $this->createMock(AuthenticationEntryPointInterface::class); + + $listener = new ChannelListener($accessMap, $entryPoint, new NullLogger()); + + $this->assertTrue($listener->supports($request)); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php index 215dcc55804d8..ec9165ffcbb09 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/LogoutListenerTest.php @@ -141,7 +141,10 @@ public function testNoResponseSet() $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST)); } - public function testCsrfValidationFails() + /** + * @dataProvider provideInvalidCsrfTokens + */ + public function testCsrfValidationFails($invalidToken) { $this->expectException(LogoutException::class); $tokenManager = $this->getTokenManager(); @@ -149,14 +152,16 @@ public function testCsrfValidationFails() [$listener, , $httpUtils, $options] = $this->getListener(null, $tokenManager); $request = new Request(); - $request->query->set('_csrf_token', 'token'); + if (null !== $invalidToken) { + $request->query->set('_csrf_token', $invalidToken); + } $httpUtils->expects($this->once()) ->method('checkRequestPath') ->with($request, $options['logout_path']) ->willReturn(true); - $tokenManager->expects($this->once()) + $tokenManager ->method('isTokenValid') ->willReturn(false); @@ -199,6 +204,15 @@ public function testLegacyLogoutHandlers() $this->assertSame($response, $event->getResponse()); } + public function provideInvalidCsrfTokens(): array + { + return [ + ['invalid'], + [['in' => 'valid']], + [null], + ]; + } + private function getTokenManager() { return $this->createMock(CsrfTokenManagerInterface::class); diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php index 54bc144ed093d..6c9ecc5cc28ac 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -30,9 +30,6 @@ use Symfony\Component\Security\Http\SecurityEvents; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -/** - * @group legacy - */ class SwitchUserListenerTest extends TestCase { private $tokenStorage; diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/UsernamePasswordFormAuthenticationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/UsernamePasswordFormAuthenticationListenerTest.php index 6b742c07b3452..36ca126f892fd 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/UsernamePasswordFormAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/UsernamePasswordFormAuthenticationListenerTest.php @@ -24,6 +24,7 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler; use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler; @@ -40,7 +41,7 @@ class UsernamePasswordFormAuthenticationListenerTest extends TestCase /** * @dataProvider getUsernameForLength */ - public function testHandleWhenUsernameLength($username, $ok) + public function testHandleWhenUsernameLength(string $username, bool $ok) { $request = Request::create('/login_check', 'POST', ['_username' => $username, '_password' => 'symfony']); $request->setSession($this->createMock(SessionInterface::class)); @@ -87,10 +88,8 @@ public function testHandleWhenUsernameLength($username, $ok) /** * @dataProvider postOnlyDataProvider */ - public function testHandleNonStringUsernameWithArray($postOnly) + public function testHandleNonStringUsernameWithArray(bool $postOnly) { - $this->expectException(BadRequestHttpException::class); - $this->expectExceptionMessage('The key "_username" must be a string, "array" given.'); $request = Request::create('/login_check', 'POST', ['_username' => []]); $request->setSession($this->createMock(SessionInterface::class)); $listener = new UsernamePasswordFormAuthenticationListener( @@ -104,16 +103,18 @@ public function testHandleNonStringUsernameWithArray($postOnly) ['require_previous_session' => false, 'post_only' => $postOnly] ); $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); + + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "array" given.'); + $listener($event); } /** * @dataProvider postOnlyDataProvider */ - public function testHandleNonStringUsernameWithInt($postOnly) + public function testHandleNonStringUsernameWithInt(bool $postOnly) { - $this->expectException(BadRequestHttpException::class); - $this->expectExceptionMessage('The key "_username" must be a string, "int" given.'); $request = Request::create('/login_check', 'POST', ['_username' => 42]); $request->setSession($this->createMock(SessionInterface::class)); $listener = new UsernamePasswordFormAuthenticationListener( @@ -127,16 +128,18 @@ public function testHandleNonStringUsernameWithInt($postOnly) ['require_previous_session' => false, 'post_only' => $postOnly] ); $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); + + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "int" given.'); + $listener($event); } /** * @dataProvider postOnlyDataProvider */ - public function testHandleNonStringUsernameWithObject($postOnly) + public function testHandleNonStringUsernameWithObject(bool $postOnly) { - $this->expectException(BadRequestHttpException::class); - $this->expectExceptionMessage('The key "_username" must be a string, "stdClass" given.'); $request = Request::create('/login_check', 'POST', ['_username' => new \stdClass()]); $request->setSession($this->createMock(SessionInterface::class)); $listener = new UsernamePasswordFormAuthenticationListener( @@ -150,13 +153,17 @@ public function testHandleNonStringUsernameWithObject($postOnly) ['require_previous_session' => false, 'post_only' => $postOnly] ); $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST); + + $this->expectException(BadRequestHttpException::class); + $this->expectExceptionMessage('The key "_username" must be a string, "stdClass" given.'); + $listener($event); } /** * @dataProvider postOnlyDataProvider */ - public function testHandleNonStringUsernameWith__toString($postOnly) + public function testHandleNonStringUsernameWith__toString(bool $postOnly) { $usernameClass = $this->createMock(DummyUserClass::class); $usernameClass @@ -204,7 +211,63 @@ public function testHandleWhenPasswordAreNull($postOnly) $listener($event); } - public function postOnlyDataProvider() + /** + * @dataProvider provideInvalidCsrfTokens + */ + public function testInvalidCsrfToken($invalidToken) + { + $formBody = ['_username' => 'fabien', '_password' => 'symfony']; + if (null !== $invalidToken) { + $formBody['_csrf_token'] = $invalidToken; + } + + $request = Request::create('/login_check', 'POST', $formBody); + $request->setSession($this->createMock(SessionInterface::class)); + + $httpUtils = $this->createMock(HttpUtils::class); + $httpUtils + ->method('checkRequestPath') + ->willReturn(true) + ; + $httpUtils + ->method('createRedirectResponse') + ->willReturn(new RedirectResponse('/hello')) + ; + + $failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class); + $failureHandler + ->expects($this->once()) + ->method('onAuthenticationFailure') + ->willReturn(new Response()) + ; + + $authenticationManager = $this->createMock(AuthenticationProviderManager::class); + $authenticationManager + ->expects($this->never()) + ->method('authenticate') + ; + + $csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class); + $csrfTokenManager->method('isTokenValid')->willReturn(false); + + $listener = new UsernamePasswordFormAuthenticationListener( + $this->createMock(TokenStorageInterface::class), + $authenticationManager, + $this->createMock(SessionAuthenticationStrategyInterface::class), + $httpUtils, + 'TheProviderKey', + new DefaultAuthenticationSuccessHandler($httpUtils), + $failureHandler, + ['require_previous_session' => false], + null, + null, + $csrfTokenManager + ); + + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); + } + + public function postOnlyDataProvider(): array { return [ [true], @@ -212,13 +275,22 @@ public function postOnlyDataProvider() ]; } - public function getUsernameForLength() + public function getUsernameForLength(): array { return [ [str_repeat('x', Security::MAX_USERNAME_LENGTH + 1), false], [str_repeat('x', Security::MAX_USERNAME_LENGTH - 1), true], ]; } + + public function provideInvalidCsrfTokens(): array + { + return [ + ['invalid'], + [['in' => 'valid']], + [null], + ]; + } } class DummyUserClass diff --git a/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php index 877e1b62bdb57..767f553bf4d55 100644 --- a/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php @@ -53,7 +53,6 @@ protected function setUp(): void /** * @dataProvider provideCreateLoginLinkData - * @group time-sensitive */ public function testCreateLoginLink($user, array $extraProperties, Request $request = null) { @@ -65,8 +64,10 @@ public function testCreateLoginLink($user, array $extraProperties, Request $requ return 'weaverryan' == $parameters['user'] && isset($parameters['expires']) && isset($parameters['hash']) + // allow a small expiration offset to avoid time-sensitivity + && abs(time() + 600 - $parameters['expires']) <= 1 // make sure hash is what we expect - && $parameters['hash'] === $this->createSignatureHash('weaverryan', time() + 600, array_values($extraProperties)); + && $parameters['hash'] === $this->createSignatureHash('weaverryan', $parameters['expires'], array_values($extraProperties)); }), UrlGeneratorInterface::ABSOLUTE_URL ) diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php index 70e4ba311d7a6..d382f82391a6f 100644 --- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php @@ -36,11 +36,13 @@ public function testCreateFromEmptyString() /** * @dataProvider provideBytesAt - * - * @requires extension intl 66.2 */ public function testBytesAt(array $expected, string $string, int $offset, int $form = null) { + if (2 !== grapheme_strlen('च्छे') && 'नमस्ते' === $string) { + $this->markTestSkipped('Skipping due to issue ICU-21661.'); + } + $instance = static::createFromString($string); $instance = $form ? $instance->normalize($form) : $instance; @@ -159,11 +161,13 @@ public static function provideWrap(): array /** * @dataProvider provideLength - * - * @requires extension intl 66.2 */ public function testLength(int $length, string $string) { + if (2 !== grapheme_strlen('च्छे') && 'अनुच्छेद' === $string) { + $this->markTestSkipped('Skipping due to issue ICU-21661.'); + } + $instance = static::createFromString($string); $this->assertSame($length, $instance->length()); diff --git a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php index 0f2a58404c41a..f84942c4ba84b 100644 --- a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php @@ -57,11 +57,13 @@ public static function provideBytesAt(): array /** * @dataProvider provideCodePointsAt - * - * @requires extension intl 66.2 */ public function testCodePointsAt(array $expected, string $string, int $offset, int $form = null) { + if (2 !== grapheme_strlen('च्छे') && 'नमस्ते' === $string) { + $this->markTestSkipped('Skipping due to issue ICU-21661.'); + } + $instance = static::createFromString($string); $instance = $form ? $instance->normalize($form) : $instance; diff --git a/src/Symfony/Component/Templating/PhpEngine.php b/src/Symfony/Component/Templating/PhpEngine.php index 6d89a7f422c4d..73689c15e8df3 100644 --- a/src/Symfony/Component/Templating/PhpEngine.php +++ b/src/Symfony/Component/Templating/PhpEngine.php @@ -171,6 +171,7 @@ protected function evaluate(Storage $template, array $parameters = []) * * @throws \InvalidArgumentException if the helper is not defined */ + #[\ReturnTypeWillChange] public function offsetGet($name) { return $this->get($name); @@ -183,6 +184,7 @@ public function offsetGet($name) * * @return bool true if the helper is defined, false otherwise */ + #[\ReturnTypeWillChange] public function offsetExists($name) { return isset($this->helpers[$name]); @@ -196,6 +198,7 @@ public function offsetExists($name) * * @return void */ + #[\ReturnTypeWillChange] public function offsetSet($name, $value) { $this->set($name, $value); @@ -210,6 +213,7 @@ public function offsetSet($name, $value) * * @throws \LogicException */ + #[\ReturnTypeWillChange] public function offsetUnset($name) { throw new \LogicException(sprintf('You can\'t unset a helper (%s).', $name)); diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php index 7691ed186fd4f..8aca509cf0f3d 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php @@ -92,7 +92,7 @@ public function write(TranslatorBagInterface $translatorBag): void $createdKeysByDomain[$domain] = $this->createKeys($keys, $domain); } - $this->updateTranslations(array_merge($createdKeysByDomain, $existingKeysByDomain), $translatorBag); + $this->updateTranslations(array_merge_recursive($createdKeysByDomain, $existingKeysByDomain), $translatorBag); } public function read(array $domains, array $locales): TranslatorBag @@ -203,7 +203,8 @@ private function createKeys(array $keys, string $domain): array continue; } - $createdKeys = array_reduce($response->toArray(false)['keys'], function ($carry, array $keyItem) { + $keys = $response->toArray(false)['keys'] ?? []; + $createdKeys = array_reduce($keys, static function ($carry, array $keyItem) { $carry[$keyItem['key_name']['web']] = $keyItem['key_id']; return $carry; @@ -232,7 +233,7 @@ private function updateTranslations(array $keysByDomain, TranslatorBagInterface 'android' => null, 'other' => null, ], - 'translations' => array_reduce($translatorBag->getCatalogues(), function ($carry, MessageCatalogueInterface $catalogue) use ($keyName, $domain) { + 'translations' => array_reduce($translatorBag->getCatalogues(), static function ($carry, MessageCatalogueInterface $catalogue) use ($keyName, $domain) { // Message could be not found because the catalogue is empty. // We must not send the key in place of the message to avoid wrong message update on the provider. if ($catalogue->get($keyName, $domain) !== $keyName) { @@ -277,7 +278,7 @@ private function getKeysIds(array $keys, string $domain): array $this->logger->error(sprintf('Unable to get keys ids from Lokalise: "%s".', $response->getContent(false))); } - return array_reduce($response->toArray(false)['keys'], function ($carry, array $keyItem) { + return array_reduce($response->toArray(false)['keys'], static function ($carry, array $keyItem) { $carry[$keyItem['key_name']['web']] = $keyItem['key_id']; return $carry; @@ -287,8 +288,8 @@ private function getKeysIds(array $keys, string $domain): array private function ensureAllLocalesAreCreated(TranslatorBagInterface $translatorBag) { $providerLanguages = $this->getLanguages(); - $missingLanguages = array_reduce($translatorBag->getCatalogues(), function ($carry, $catalogue) use ($providerLanguages) { - if (!\in_array($catalogue->getLocale(), $providerLanguages)) { + $missingLanguages = array_reduce($translatorBag->getCatalogues(), static function ($carry, $catalogue) use ($providerLanguages) { + if (!\in_array($catalogue->getLocale(), $providerLanguages, true)) { $carry[] = $catalogue->getLocale(); } @@ -313,9 +314,7 @@ private function getLanguages(): array $responseContent = $response->toArray(false); if (\array_key_exists('languages', $responseContent)) { - return array_map(function ($language) { - return $language['lang_iso']; - }, $responseContent['languages']); + return array_column($responseContent['languages'], 'lang_iso'); } return []; @@ -325,7 +324,7 @@ private function createLanguages(array $languages): void { $response = $this->client->request('POST', 'languages', [ 'json' => [ - 'languages' => array_map(function ($language) { + 'languages' => array_map(static function ($language) { return ['lang_iso' => $language]; }, $languages), ], diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php index ee4926a5e02e5..9156043de3d0e 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php @@ -69,14 +69,14 @@ public function testCompleteWriteProcess() $this->assertSame('POST', $method); $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); - $this->assertSame($expectedBody, $options['body']); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); return new MockResponse(); }; $getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface { $expectedQuery = [ - 'filter_keys' => 'a', + 'filter_keys' => 'young_dog', 'filter_filenames' => 'messages.xliff', ]; @@ -104,7 +104,7 @@ public function testCompleteWriteProcess() $expectedBody = json_encode([ 'keys' => [ [ - 'key_name' => 'a', + 'key_name' => 'young_dog', 'platforms' => ['web'], 'filenames' => [ 'web' => 'messages.xliff', @@ -117,11 +117,11 @@ public function testCompleteWriteProcess() ]); $this->assertSame('POST', $method); - $this->assertSame($expectedBody, $options['body']); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); return new MockResponse(json_encode(['keys' => [ [ - 'key_name' => ['web' => 'a'], + 'key_name' => ['web' => 'young_dog'], 'key_id' => 29, ], ]])); @@ -144,7 +144,7 @@ public function testCompleteWriteProcess() ]); $this->assertSame('POST', $method); - $this->assertSame($expectedBody, $options['body']); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); return new MockResponse(json_encode(['keys' => [ [ @@ -153,8 +153,8 @@ public function testCompleteWriteProcess() ], ]])); }; - - $updateTranslationsResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $updateProcessed = false; + $updateTranslationsResponse = function (string $method, string $url, array $options = []) use (&$updateProcessed): ResponseInterface { $expectedBody = json_encode([ 'keys' => [ [ @@ -169,11 +169,11 @@ public function testCompleteWriteProcess() 'translations' => [ [ 'language_iso' => 'en', - 'translation' => 'trans_en_a', + 'translation' => 'puppy', ], [ 'language_iso' => 'fr', - 'translation' => 'trans_fr_a', + 'translation' => 'chiot', ], ], ], @@ -200,8 +200,9 @@ public function testCompleteWriteProcess() ], ]); + $updateProcessed = true; $this->assertSame('PUT', $method); - $this->assertSame($expectedBody, $options['body']); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); return new MockResponse(); }; @@ -221,15 +222,16 @@ public function testCompleteWriteProcess() $translatorBag = new TranslatorBag(); $translatorBag->addCatalogue(new MessageCatalogue('en', [ - 'messages' => ['a' => 'trans_en_a'], + 'messages' => ['young_dog' => 'puppy'], 'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'], ])); $translatorBag->addCatalogue(new MessageCatalogue('fr', [ - 'messages' => ['a' => 'trans_fr_a'], + 'messages' => ['young_dog' => 'chiot'], 'validators' => ['post.num_comments' => '{count, plural, one {# commentaire} other {# commentaires}}'], ])); $provider->write($translatorBag); + $this->assertTrue($updateProcessed, 'Translations update was not called.'); } /** @@ -248,7 +250,7 @@ public function testReadForOneLocaleAndOneDomain(string $locale, string $domain, $this->assertSame('POST', $method); $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/files/export', $url); - $this->assertSame($expectedBody, $options['body']); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); return new MockResponse(json_encode([ 'files' => [ diff --git a/src/Symfony/Component/Translation/Resources/bin/translation-status.php b/src/Symfony/Component/Translation/Resources/bin/translation-status.php index 4e0723bb40615..fac8acbadca32 100644 --- a/src/Symfony/Component/Translation/Resources/bin/translation-status.php +++ b/src/Symfony/Component/Translation/Resources/bin/translation-status.php @@ -19,13 +19,16 @@ # show the translation status of all locales $ php translation-status.php - # show the translation status of all locales and all their missing translations + # only show the translation status of incomplete or erroneous locales + $ php translation-status.php --incomplete + + # show the translation status of all locales, all their missing translations and mismatches between trans-unit id and source $ php translation-status.php -v # show the status of a single locale $ php translation-status.php fr - # show the status of a single locale and all its missing translations + # show the status of a single locale, missing translations and mismatches between trans-unit id and source $ php translation-status.php fr -v END; @@ -35,6 +38,8 @@ 'verbose_output' => false, // NULL = analyze all locales 'locale_to_analyze' => null, + // append --incomplete to only show incomplete languages + 'include_completed_languages' => true, // the reference files all the other translations are compared to 'original_files' => [ 'src/Symfony/Component/Form/Resources/translations/validators.en.xlf', @@ -46,13 +51,18 @@ $argc = $_SERVER['argc']; $argv = $_SERVER['argv']; -if ($argc > 3) { +if ($argc > 4) { echo str_replace('translation-status.php', $argv[0], $usageInstructions); exit(1); } foreach (array_slice($argv, 1) as $argumentOrOption) { - if (str_starts_with($argumentOrOption, '-')) { + if ('--incomplete' === $argumentOrOption) { + $config['include_completed_languages'] = false; + continue; + } + + if (0 === strpos($argumentOrOption, '-')) { $config['verbose_output'] = true; } else { $config['locale_to_analyze'] = $argumentOrOption; @@ -67,6 +77,7 @@ } $totalMissingTranslations = 0; +$totalTranslationMismatches = 0; foreach ($config['original_files'] as $originalFilePath) { $translationFilePaths = findTranslationFiles($originalFilePath, $config['locale_to_analyze']); @@ -75,11 +86,14 @@ $totalMissingTranslations += array_sum(array_map(function ($translation) { return count($translation['missingKeys']); }, array_values($translationStatus))); + $totalTranslationMismatches += array_sum(array_map(function ($translation) { + return count($translation['mismatches']); + }, array_values($translationStatus))); - printTranslationStatus($originalFilePath, $translationStatus, $config['verbose_output']); + printTranslationStatus($originalFilePath, $translationStatus, $config['verbose_output'], $config['include_completed_languages']); } -exit($totalMissingTranslations > 0 ? 1 : 0); +exit($totalTranslationMismatches > 0 ? 1 : 0); function findTranslationFiles($originalFilePath, $localeToAnalyze) { @@ -112,21 +126,29 @@ function calculateTranslationStatus($originalFilePath, $translationFilePaths) foreach ($translationFilePaths as $locale => $translationPath) { $translatedKeys = extractTranslationKeys($translationPath); $missingKeys = array_diff_key($allTranslationKeys, $translatedKeys); + $mismatches = findTransUnitMismatches($allTranslationKeys, $translatedKeys); $translationStatus[$locale] = [ 'total' => count($allTranslationKeys), 'translated' => count($translatedKeys), 'missingKeys' => $missingKeys, + 'mismatches' => $mismatches, ]; + $translationStatus[$locale]['is_completed'] = isTranslationCompleted($translationStatus[$locale]); } return $translationStatus; } -function printTranslationStatus($originalFilePath, $translationStatus, $verboseOutput) +function isTranslationCompleted(array $translationStatus): bool +{ + return $translationStatus['total'] === $translationStatus['translated'] && 0 === count($translationStatus['mismatches']); +} + +function printTranslationStatus($originalFilePath, $translationStatus, $verboseOutput, $includeCompletedLanguages) { printTitle($originalFilePath); - printTable($translationStatus, $verboseOutput); + printTable($translationStatus, $verboseOutput, $includeCompletedLanguages); echo \PHP_EOL.\PHP_EOL; } @@ -152,13 +174,35 @@ function extractTranslationKeys($filePath) return $translationKeys; } +/** + * Check whether the trans-unit id and source match with the base translation. + */ +function findTransUnitMismatches(array $baseTranslationKeys, array $translatedKeys): array +{ + $mismatches = []; + + foreach ($baseTranslationKeys as $translationId => $translationKey) { + if (!isset($translatedKeys[$translationId])) { + continue; + } + if ($translatedKeys[$translationId] !== $translationKey) { + $mismatches[$translationId] = [ + 'found' => $translatedKeys[$translationId], + 'expected' => $translationKey, + ]; + } + } + + return $mismatches; +} + function printTitle($title) { echo $title.\PHP_EOL; echo str_repeat('=', strlen($title)).\PHP_EOL.\PHP_EOL; } -function printTable($translations, $verboseOutput) +function printTable($translations, $verboseOutput, bool $includeCompletedLanguages) { if (0 === count($translations)) { echo 'No translations found'; @@ -168,24 +212,47 @@ function printTable($translations, $verboseOutput) $longestLocaleNameLength = max(array_map('strlen', array_keys($translations))); foreach ($translations as $locale => $translation) { + if (!$includeCompletedLanguages && $translation['is_completed']) { + continue; + } + if ($translation['translated'] > $translation['total']) { textColorRed(); - } elseif ($translation['translated'] === $translation['total']) { + } elseif (count($translation['mismatches']) > 0) { + textColorRed(); + } elseif ($translation['is_completed']) { textColorGreen(); } - echo sprintf('| Locale: %-'.$longestLocaleNameLength.'s | Translated: %d/%d', $locale, $translation['translated'], $translation['total']).\PHP_EOL; + echo sprintf( + '| Locale: %-'.$longestLocaleNameLength.'s | Translated: %2d/%2d | Mismatches: %d |', + $locale, + $translation['translated'], + $translation['total'], + count($translation['mismatches']) + ).\PHP_EOL; textColorNormal(); + $shouldBeClosed = false; if (true === $verboseOutput && count($translation['missingKeys']) > 0) { - echo str_repeat('-', 80).\PHP_EOL; - echo '| Missing Translations:'.\PHP_EOL; + echo '| Missing Translations:'.\PHP_EOL; foreach ($translation['missingKeys'] as $id => $content) { - echo sprintf('| (id=%s) %s', $id, $content).\PHP_EOL; + echo sprintf('| (id=%s) %s', $id, $content).\PHP_EOL; } + $shouldBeClosed = true; + } + if (true === $verboseOutput && count($translation['mismatches']) > 0) { + echo '| Mismatches between trans-unit id and source:'.\PHP_EOL; + foreach ($translation['mismatches'] as $id => $content) { + echo sprintf('| (id=%s) Expected: %s', $id, $content['expected']).\PHP_EOL; + echo sprintf('| Found: %s', $content['found']).\PHP_EOL; + } + $shouldBeClosed = true; + } + if ($shouldBeClosed) { echo str_repeat('-', 80).\PHP_EOL; } } diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index 6ea62a0d1c5a8..f4f46788d106c 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -262,11 +262,17 @@ public function testTransWithIcuVariantFallbackLocale() $translator->addResource('array', ['foo' => 'foofoo'], 'en_GB_scouse'); $translator->addResource('array', ['bar' => 'foobar'], 'en_GB'); $translator->addResource('array', ['baz' => 'foobaz'], 'en_001'); - $translator->addResource('array', ['qux' => 'fooqux'], 'en'); + $translator->addResource('array', ['bar' => 'en', 'qux' => 'fooqux'], 'en'); + $translator->addResource('array', ['bar' => 'nl_NL', 'fallback' => 'nl_NL'], 'nl_NL'); + $translator->addResource('array', ['bar' => 'nl', 'fallback' => 'nl'], 'nl'); + + $translator->setFallbackLocales(['nl_NL', 'nl']); + $this->assertSame('foofoo', $translator->trans('foo')); $this->assertSame('foobar', $translator->trans('bar')); $this->assertSame('foobaz', $translator->trans('baz')); $this->assertSame('fooqux', $translator->trans('qux')); + $this->assertSame('nl_NL', $translator->trans('fallback')); } public function testTransWithIcuRootFallbackLocale() @@ -316,7 +322,7 @@ public function testTransWithFallbackLocaleTer() $translator = new Translator('fr_FR'); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', ['foo' => 'foo (en_US)'], 'en_US'); - $translator->addResource('array', ['bar' => 'bar (en)'], 'en'); + $translator->addResource('array', ['foo' => 'foo (en)', 'bar' => 'bar (en)'], 'en'); $translator->setFallbackLocales(['en_US', 'en']); diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 9a639561300cf..c432b8867fd02 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -411,14 +411,8 @@ protected function computeFallbackLocales(string $locale) $this->parentLocales = json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true); } + $originLocale = $locale; $locales = []; - foreach ($this->fallbackLocales as $fallback) { - if ($fallback === $locale) { - continue; - } - - $locales[] = $fallback; - } while ($locale) { $parent = $this->parentLocales[$locale] ?? null; @@ -439,10 +433,18 @@ protected function computeFallbackLocales(string $locale) } if (null !== $locale) { - array_unshift($locales, $locale); + $locales[] = $locale; } } + foreach ($this->fallbackLocales as $fallback) { + if ($fallback === $originLocale) { + continue; + } + + $locales[] = $fallback; + } + return array_unique($locales); } diff --git a/src/Symfony/Component/Validator/ConstraintViolationList.php b/src/Symfony/Component/Validator/ConstraintViolationList.php index df5d4e55a1157..034b554d072d1 100644 --- a/src/Symfony/Component/Validator/ConstraintViolationList.php +++ b/src/Symfony/Component/Validator/ConstraintViolationList.php @@ -110,6 +110,7 @@ public function remove(int $offset) * * @return \ArrayIterator|ConstraintViolationInterface[] */ + #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->violations); @@ -118,6 +119,7 @@ public function getIterator() /** * @return int */ + #[\ReturnTypeWillChange] public function count() { return \count($this->violations); @@ -126,6 +128,7 @@ public function count() /** * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($offset) { return $this->has($offset); @@ -136,6 +139,7 @@ public function offsetExists($offset) * * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->get($offset); @@ -146,6 +150,7 @@ public function offsetGet($offset) * * @return void */ + #[\ReturnTypeWillChange] public function offsetSet($offset, $violation) { if (null === $offset) { @@ -160,6 +165,7 @@ public function offsetSet($offset, $violation) * * @return void */ + #[\ReturnTypeWillChange] public function offsetUnset($offset) { $this->remove($offset); diff --git a/src/Symfony/Component/Validator/Constraints/CardSchemeValidator.php b/src/Symfony/Component/Validator/Constraints/CardSchemeValidator.php index f6ab7acde80e7..faef7ec5ba519 100644 --- a/src/Symfony/Component/Validator/Constraints/CardSchemeValidator.php +++ b/src/Symfony/Component/Validator/Constraints/CardSchemeValidator.php @@ -77,9 +77,9 @@ class CardSchemeValidator extends ConstraintValidator '/^5[1-5][0-9]{14}$/', '/^2(22[1-9][0-9]{12}|2[3-9][0-9]{13}|[3-6][0-9]{14}|7[0-1][0-9]{13}|720[0-9]{12})$/', ], - // Payment system MIR numbers start with 220, then 1 digit from 0 to 4, then 12 digits + // Payment system MIR numbers start with 220, then 1 digit from 0 to 4, then between 12 and 15 digits CardScheme::MIR => [ - '/^220[0-4][0-9]{12}$/', + '/^220[0-4][0-9]{12,15}$/', ], // All UATP card numbers start with a 1 and have a length of 15 digits. CardScheme::UATP => [ diff --git a/src/Symfony/Component/Validator/Constraints/UrlValidator.php b/src/Symfony/Component/Validator/Constraints/UrlValidator.php index b9f3f44efce34..196633336a0fd 100644 --- a/src/Symfony/Component/Validator/Constraints/UrlValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UrlValidator.php @@ -25,7 +25,7 @@ class UrlValidator extends ConstraintValidator (%s):// # protocol (((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+)@)? # basic auth ( - ([\pL\pN\pS\-\_\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name + ([\pL\pN\pS]+\.?[\pL\pN\pS\-\_]+)+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name | # or \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address | # or diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index ef2bf1a5aee97..40228c8760444 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -76,8 +76,8 @@ public function addViolation(string $message, array $params = []); * add the violation when you're done with the configuration: * * $context->buildViolation('Please enter a number between %min% and %max%.') - * ->setParameter('%min%', 3) - * ->setParameter('%max%', 10) + * ->setParameter('%min%', '3') + * ->setParameter('%max%', '10') * ->setTranslationDomain('number_validation') * ->addViolation(); * diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.et.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.et.xlf index 261b8f34e62f9..930b47f82e95a 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.et.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.et.xlf @@ -32,11 +32,11 @@ </trans-unit> <trans-unit id="8"> <source>One or more of the given values is invalid.</source> - <target>One or more of the given values is invalid.</target> + <target>Üks või rohkem väärtustest on vigane.</target> </trans-unit> <trans-unit id="9"> <source>This field was not expected.</source> - <target>See väli ei oodatud.</target> + <target>See väli ei olnud oodatud.</target> </trans-unit> <trans-unit id="10"> <source>This field is missing.</source> @@ -179,7 +179,7 @@ <target>Väärtus peaks olema kasutaja kehtiv salasõna.</target> </trans-unit> <trans-unit id="48"> - <source>This value should have exactly {{ limit }} characters.</source> + <source>This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters.</source> <target>Väärtus peaks olema täpselt {{ limit }} tähemärk pikk.|Väärtus peaks olema täpselt {{ limit }} tähemärki pikk.</target> </trans-unit> <trans-unit id="49"> @@ -203,15 +203,15 @@ <target>PHP laiendi tõttu ebaõnnestus faili üleslaadimine.</target> </trans-unit> <trans-unit id="54"> - <source>This collection should contain {{ limit }} elements or more.</source> + <source>This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more.</source> <target>Kogumikus peaks olema vähemalt {{ limit }} element.|Kogumikus peaks olema vähemalt {{ limit }} elementi.</target> </trans-unit> <trans-unit id="55"> - <source>This collection should contain {{ limit }} elements or less.</source> + <source>This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less.</source> <target>Kogumikus peaks olema ülimalt {{ limit }} element.|Kogumikus peaks olema ülimalt {{ limit }} elementi.</target> </trans-unit> <trans-unit id="56"> - <source>This collection should contain exactly {{ limit }} elements.</source> + <source>This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements.</source> <target>Kogumikus peaks olema täpselt {{ limit }} element.|Kogumikus peaks olema täpselt {{ limit }}|elementi.</target> </trans-unit> <trans-unit id="57"> @@ -304,7 +304,7 @@ </trans-unit> <trans-unit id="79"> <source>The host could not be resolved.</source> - <target>Peremeest ei õnnestunud lahendada.</target> + <target>Sellist domeeni ei õnnestunud leida.</target> </trans-unit> <trans-unit id="80"> <source>This value does not match the expected {{ charset }} charset.</source> @@ -366,6 +366,30 @@ <source>This value should be between {{ min }} and {{ max }}.</source> <target>See väärtus peaks olema vahemikus {{ min }} kuni {{ max }}.</target> </trans-unit> + <trans-unit id="95"> + <source>This value is not a valid hostname.</source> + <target>See väärtus pole korrektne domeeninimi.</target> + </trans-unit> + <trans-unit id="96"> + <source>The number of elements in this collection should be a multiple of {{ compared_value }}.</source> + <target>Selles kogus olevate elementide arv peab olema arvu {{ compared_value }} kordne.</target> + </trans-unit> + <trans-unit id="97"> + <source>This value should satisfy at least one of the following constraints:</source> + <target>See väärtus peab vastama vähemalt ühele järgmistest tingimustest:</target> + </trans-unit> + <trans-unit id="98"> + <source>Each element of this collection should satisfy its own set of constraints.</source> + <target>Kõik väärtused selles kogus peavad vastama oma tingimustele.</target> + </trans-unit> + <trans-unit id="99"> + <source>This value is not a valid International Securities Identification Number (ISIN).</source> + <target>See väärtus pole korrektne ISIN-kood.</target> + </trans-unit> + <trans-unit id="100"> + <source>This value should be a valid expression.</source> + <target>See väärtus pole korrektne avaldis.</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf index 40d07cf57cbb9..4793a16f32032 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.id.xlf @@ -386,6 +386,10 @@ <source>This value is not a valid International Securities Identification Number (ISIN).</source> <target>Nilai ini bukan merupakan International Securities Identification Number (ISIN) yang sah.</target> </trans-unit> + <trans-unit id="100"> + <source>This value should be a valid expression.</source> + <target>Nilai ini harus berupa ekspresi yang valid.</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf index e930e59014191..90fe83bb31cb9 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf @@ -386,6 +386,10 @@ <source>This value is not a valid International Securities Identification Number (ISIN).</source> <target>Ang halagang ito ay hindi wastong International Securities Identification Number (ISIN).</target> </trans-unit> + <trans-unit id="100"> + <source>This value should be a valid expression.</source> + <target>Ang halagang ito ay dapat wastong ekspresyon.</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf index 6c39fac818238..8dc9c70de500d 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf @@ -386,6 +386,10 @@ <source>This value is not a valid International Securities Identification Number (ISIN).</source> <target>Bu değer geçerli bir Uluslararası Menkul Kıymetler Kimlik Numarası değil (ISIN).</target> </trans-unit> + <trans-unit id="100"> + <source>This value should be a valid expression.</source> + <target>Bu değer geçerli bir ifade olmalıdır.</target> + </trans-unit> </body> </file> </xliff> diff --git a/src/Symfony/Component/Validator/Tests/Constraints/CardSchemeValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/CardSchemeValidatorTest.php index 57204fe357fba..100a3811b00ff 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/CardSchemeValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/CardSchemeValidatorTest.php @@ -130,6 +130,9 @@ public function getValidNumbers() ['MASTERCARD', '2709999999999999'], ['MASTERCARD', '2720995105105100'], ['MIR', '2200381427330082'], + ['MIR', '22003814273300821'], + ['MIR', '220038142733008212'], + ['MIR', '2200381427330082123'], ['UATP', '110165309696173'], ['VISA', '4111111111111111'], ['VISA', '4012888888881881'], @@ -162,7 +165,8 @@ public function getInvalidNumbers() ['MASTERCARD', '2721001234567890', CardScheme::INVALID_FORMAT_ERROR], // Not assigned yet ['MASTERCARD', '2220991234567890', CardScheme::INVALID_FORMAT_ERROR], // Not assigned yet ['UATP', '11016530969617', CardScheme::INVALID_FORMAT_ERROR], // invalid length - ['MIR', '22003814273300821', CardScheme::INVALID_FORMAT_ERROR], // invalid length + ['MIR', '220038142733008', CardScheme::INVALID_FORMAT_ERROR], // invalid length + ['MIR', '22003814273300821234', CardScheme::INVALID_FORMAT_ERROR], // invalid length ]; } } diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UrlValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UrlValidatorTest.php index b1f2b1a5047b7..6d32edf4e3b7c 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UrlValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UrlValidatorTest.php @@ -100,6 +100,7 @@ public function getValidUrls() return [ ['http://a.pl'], ['http://www.example.com'], + ['http://tt.example.com'], ['http://www.example.com.'], ['http://www.example.museum'], ['https://example.com/'], @@ -260,6 +261,10 @@ public function getInvalidUrls() ['http://example.com/exploit.html?hel lo'], ['http://example.com/exploit.html?not_a%hex'], ['http://'], + ['http://www..com'], + ['http://www..example.com'], + ['http://wwww.example..com'], + ['http://.www.example.com'], ]; } diff --git a/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php b/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php index 34b208b2bea0c..4ca7f4a99fabe 100644 --- a/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php +++ b/src/Symfony/Component/Validator/Tests/Fixtures/CustomArrayObject.php @@ -29,12 +29,18 @@ public function offsetExists($offset): bool return \array_key_exists($offset, $this->array); } + /** + * @param mixed $offset + * + * @return mixed + */ + #[\ReturnTypeWillChange] public function offsetGet($offset) { return $this->array[$offset]; } - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { if (null === $offset) { $this->array[] = $value; @@ -43,7 +49,7 @@ public function offsetSet($offset, $value) } } - public function offsetUnset($offset) + public function offsetUnset($offset): void { unset($this->array[$offset]); } diff --git a/src/Symfony/Component/VarDumper/Cloner/Data.php b/src/Symfony/Component/VarDumper/Cloner/Data.php index c868862225213..b17dc55e27dba 100644 --- a/src/Symfony/Component/VarDumper/Cloner/Data.php +++ b/src/Symfony/Component/VarDumper/Cloner/Data.php @@ -111,6 +111,7 @@ public function getValue($recursive = false) /** * @return int */ + #[\ReturnTypeWillChange] public function count() { return \count($this->getValue()); @@ -119,6 +120,7 @@ public function count() /** * @return \Traversable */ + #[\ReturnTypeWillChange] public function getIterator() { if (!\is_array($value = $this->getValue())) { @@ -150,6 +152,7 @@ public function __isset(string $key) /** * @return bool */ + #[\ReturnTypeWillChange] public function offsetExists($key) { return $this->__isset($key); @@ -158,6 +161,7 @@ public function offsetExists($key) /** * @return mixed */ + #[\ReturnTypeWillChange] public function offsetGet($key) { return $this->__get($key); @@ -166,6 +170,7 @@ public function offsetGet($key) /** * @return void */ + #[\ReturnTypeWillChange] public function offsetSet($key, $value) { throw new \BadMethodCallException(self::class.' objects are immutable.'); @@ -174,6 +179,7 @@ public function offsetSet($key, $value) /** * @return void */ + #[\ReturnTypeWillChange] public function offsetUnset($key) { throw new \BadMethodCallException(self::class.' objects are immutable.'); diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/CasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/CasterTest.php index f0a1fbbe8b045..c39e82cf6adb0 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/CasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/CasterTest.php @@ -164,7 +164,7 @@ public function testAnonymousClass() , $c ); - $c = eval('return new class implements \Countable { private $foo = "foo"; public function count() { return 0; } };'); + $c = eval('return new class implements \Countable { private $foo = "foo"; public function count(): int { return 0; } };'); $this->assertDumpMatchesFormat( <<<'EOTXT' diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php index e5cc842817a14..cd10c7af37a8a 100644 --- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php +++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php @@ -313,7 +313,7 @@ public function __construct(array $array) parent::__construct($array, 1); } - public function setFlags($flags) + public function setFlags($flags): void { throw new \BadMethodCallException('Calling MyArrayObject::setFlags() is forbidden'); } @@ -346,7 +346,7 @@ public function serialize(): string return serialize([123, parent::serialize()]); } - public function unserialize($data) + public function unserialize($data): void { if ('' === $data) { throw new \InvalidArgumentException('Serialized data is empty.');