diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 90e51d60536d6..3d21822287b6b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 7.3 for features / 5.4, 6.4, 7.1, and 7.2 for bug fixes +| Branch? | 7.3 for features / 6.4, 7.1, and 7.2 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no diff --git a/CHANGELOG-7.2.md b/CHANGELOG-7.2.md index 11ad8ff6b9959..681d728e832ef 100644 --- a/CHANGELOG-7.2.md +++ b/CHANGELOG-7.2.md @@ -7,6 +7,30 @@ in 7.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/v7.2.0...v7.2.1 +* 7.2.2 (2024-12-31) + + * bug #59304 [PropertyInfo] Remove ``@internal`` from `PropertyReadInfo` and `PropertyWriteInfo` (Dario Guarracino) + * bug #59252 [Stopwatch] bug #54854 undefined key error when trying to fetch a mis… (Alex Niedre) + * bug #59278 [SecurityBundle] Do not replace authenticators service by their traceable version (MatTheCat) + * bug #59228 [HttpFoundation] Avoid mime type guess with temp files in `BinaryFileResponse` (alexandre-daubois) + * bug #59318 [Finder] Fix using `==` as default operator in `DateComparator` (MatTheCat) + * bug #59321 [HtmlSanitizer] reject URLs containing whitespaces (xabbuh) + * bug #59310 [Validator] the "max" option can be zero (xabbuh) + * bug #59271 [TypeInfo] Fix PHPDoc resolving of union with mixed (mtarld) + * bug #59269 [Security/Csrf] Trust "Referer" at the same level as "Origin" (nicolas-grekas) + * bug #59250 [HttpClient] Fix a typo in NoPrivateNetworkHttpClient (Jontsa) + * bug #59103 [Messenger] ensure exception on rollback does not hide previous exception (nikophil) + * bug #59226 [FrameworkBundle] require the writer to implement getFormats() in the translation:extract (xabbuh) + * bug #59213 [FrameworkBundle] don't require fake notifier transports to be installed as non-dev dependencies (xabbuh) + * bug #59113 [FrameworkBundle][Translation] fix translation lint compatibility with the `PseudoLocalizationTranslator` (xabbuh) + * bug #59060 [Validator] set the violation path only if the `errorPath` option is set (xabbuh) + * bug #59066 Fix resolve enum in string type resolver (DavidBadura) + * bug #59156 [PropertyInfo] Fix interface handling in PhpStanTypeHelper (mtarld) + * bug #59160 [BeanstalkMessenger] Round delay to an integer to avoid deprecation warning (plantas) + * bug #59012 [PropertyInfo] Fix interface handling in `PhpStanTypeHelper` (janedbal) + * bug #59134 [HttpKernel] Denormalize request data using the csv format when using "#[MapQueryString]" or "#[MapRequestPayload]" (except for content data) (ovidiuenache) + * bug #59140 [WebProfilerBundle] fix: white-space in highlighted code (chr-hertel) + * 7.2.1 (2024-12-11) * bug #59145 [TypeInfo] Make `Type::nullable` method no-op on every nullable type (mtarld) diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php index e4831557f01db..8e10891b0ba74 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php @@ -27,15 +27,17 @@ class DoctrineTransactionMiddleware extends AbstractDoctrineMiddleware protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope { $entityManager->getConnection()->beginTransaction(); + + $success = false; try { $envelope = $stack->next()->handle($envelope, $stack); $entityManager->flush(); $entityManager->getConnection()->commit(); + $success = true; + return $envelope; } catch (\Throwable $exception) { - $entityManager->getConnection()->rollBack(); - if ($exception instanceof HandlerFailedException) { // Remove all HandledStamp from the envelope so the retry will execute all handlers again. // When a handler fails, the queries of allegedly successful previous handlers just got rolled back. @@ -43,6 +45,12 @@ protected function handleForManager(EntityManagerInterface $entityManager, Envel } throw $exception; + } finally { + $connection = $entityManager->getConnection(); + + if (!$success && $connection->isTransactionActive()) { + $connection->rollBack(); + } } } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php index 977f32e30fa61..05e5dae1b34ac 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php @@ -56,12 +56,9 @@ public function testMiddlewareWrapsInTransactionAndFlushes() public function testTransactionIsRolledBackOnException() { - $this->connection->expects($this->once()) - ->method('beginTransaction') - ; - $this->connection->expects($this->once()) - ->method('rollBack') - ; + $this->connection->expects($this->once())->method('beginTransaction'); + $this->connection->expects($this->once())->method('isTransactionActive')->willReturn(true); + $this->connection->expects($this->once())->method('rollBack'); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Thrown from next middleware.'); @@ -69,6 +66,27 @@ public function testTransactionIsRolledBackOnException() $this->middleware->handle(new Envelope(new \stdClass()), $this->getThrowingStackMock()); } + public function testExceptionInRollBackDoesNotHidePreviousException() + { + $this->connection->expects($this->once())->method('beginTransaction'); + $this->connection->expects($this->once())->method('isTransactionActive')->willReturn(true); + $this->connection->expects($this->once())->method('rollBack')->willThrowException(new \RuntimeException('Thrown from rollBack.')); + + try { + $this->middleware->handle(new Envelope(new \stdClass()), $this->getThrowingStackMock()); + } catch (\Throwable $exception) { + } + + self::assertNotNull($exception); + self::assertInstanceOf(\RuntimeException::class, $exception); + self::assertSame('Thrown from rollBack.', $exception->getMessage()); + + $previous = $exception->getPrevious(); + self::assertNotNull($previous); + self::assertInstanceOf(\RuntimeException::class, $previous); + self::assertSame('Thrown from next middleware.', $previous->getMessage()); + } + public function testInvalidEntityManagerThrowsException() { $managerRegistry = $this->createMock(ManagerRegistry::class); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index d9079b1c7ef17..ccce1de340c02 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -80,7 +80,7 @@ public function testGenerateFragmentUri() ]); $twig->addRuntimeLoader($loader); - $this->assertSame('/_fragment?_hash=XCg0hX8QzSwik8Xuu9aMXhoCeI4oJOob7lUVacyOtyY%3D&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction', $twig->render('index')); + $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction$#', $twig->render('index')); } protected function getFragmentHandler($returnOrException): FragmentHandler diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index b26d3f9ad20dd..194d1c50d25d5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -62,6 +62,10 @@ public function __construct( private array $enabledLocales = [], ) { parent::__construct(); + + if (!method_exists($writer, 'getFormats')) { + throw new \InvalidArgumentException(sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class)); + } } protected function configure(): void diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationLintCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationLintCommandPass.php new file mode 100644 index 0000000000000..4756795d1beff --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationLintCommandPass.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Translation\TranslatorBagInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +final class TranslationLintCommandPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('console.command.translation_lint') || !$container->has('translator')) { + return; + } + + $translatorClass = $container->getParameterBag()->resolveValue($container->findDefinition('translator')->getClass()); + + if (!is_subclass_of($translatorClass, TranslatorInterface::class) || !is_subclass_of($translatorClass, TranslatorBagInterface::class)) { + $container->removeDefinition('console.command.translation_lint'); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php new file mode 100644 index 0000000000000..7542191d0e83e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class TranslationUpdateCommandPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('console.command.translation_extract')) { + return; + } + + $translationWriterClass = $container->getParameterBag()->resolveValue($container->findDefinition('translation.writer')->getClass()); + + if (!method_exists($translationWriterClass, 'getFormats')) { + $container->removeDefinition('console.command.translation_extract'); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index a6d5956033d5e..3106ee30e7b4d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -120,6 +120,8 @@ use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\Mime\MimeTypes; use Symfony\Component\Notifier\Bridge as NotifierBridge; +use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory; +use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory; use Symfony\Component\Notifier\ChatterInterface; use Symfony\Component\Notifier\Notifier; use Symfony\Component\Notifier\Recipient\Recipient; @@ -2833,8 +2835,6 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ NotifierBridge\Engagespot\EngagespotTransportFactory::class => 'notifier.transport_factory.engagespot', NotifierBridge\Esendex\EsendexTransportFactory::class => 'notifier.transport_factory.esendex', NotifierBridge\Expo\ExpoTransportFactory::class => 'notifier.transport_factory.expo', - NotifierBridge\FakeChat\FakeChatTransportFactory::class => 'notifier.transport_factory.fake-chat', - NotifierBridge\FakeSms\FakeSmsTransportFactory::class => 'notifier.transport_factory.fake-sms', NotifierBridge\Firebase\FirebaseTransportFactory::class => 'notifier.transport_factory.firebase', NotifierBridge\FortySixElks\FortySixElksTransportFactory::class => 'notifier.transport_factory.forty-six-elks', NotifierBridge\FreeMobile\FreeMobileTransportFactory::class => 'notifier.transport_factory.free-mobile', @@ -2922,20 +2922,26 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $container->removeDefinition($classToServices[NotifierBridge\Mercure\MercureTransportFactory::class]); } - if (ContainerBuilder::willBeAvailable('symfony/fake-chat-notifier', NotifierBridge\FakeChat\FakeChatTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) { - $container->getDefinition($classToServices[NotifierBridge\FakeChat\FakeChatTransportFactory::class]) - ->replaceArgument(0, new Reference('mailer')) - ->replaceArgument(1, new Reference('logger')) + // don't use ContainerBuilder::willBeAvailable() as these are not needed in production + if (class_exists(FakeChatTransportFactory::class)) { + $container->getDefinition('notifier.transport_factory.fake-chat') + ->replaceArgument(0, new Reference('mailer', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) + ->replaceArgument(1, new Reference('logger', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) ->addArgument(new Reference('event_dispatcher', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) ->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE)); + } else { + $container->removeDefinition('notifier.transport_factory.fake-chat'); } - if (ContainerBuilder::willBeAvailable('symfony/fake-sms-notifier', NotifierBridge\FakeSms\FakeSmsTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) { - $container->getDefinition($classToServices[NotifierBridge\FakeSms\FakeSmsTransportFactory::class]) - ->replaceArgument(0, new Reference('mailer')) - ->replaceArgument(1, new Reference('logger')) + // don't use ContainerBuilder::willBeAvailable() as these are not needed in production + if (class_exists(FakeSmsTransportFactory::class)) { + $container->getDefinition('notifier.transport_factory.fake-sms') + ->replaceArgument(0, new Reference('mailer', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) + ->replaceArgument(1, new Reference('logger', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) ->addArgument(new Reference('event_dispatcher', ContainerBuilder::NULL_ON_INVALID_REFERENCE)) ->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE)); + } else { + $container->removeDefinition('notifier.transport_factory.fake-sms'); } if (ContainerBuilder::willBeAvailable('symfony/bluesky-notifier', NotifierBridge\Bluesky\BlueskyTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index a1eb059bb01ce..e83c4dfe611d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -19,6 +19,8 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationLintCommandPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationUpdateCommandPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\VirtualRequestStackPass; use Symfony\Component\Cache\Adapter\ApcuAdapter; @@ -149,6 +151,8 @@ public function build(ContainerBuilder $container): void $this->addCompilerPassIfExists($container, AddConstraintValidatorsPass::class); $this->addCompilerPassIfExists($container, AddValidatorInitializersPass::class); $this->addCompilerPassIfExists($container, AddConsoleCommandPass::class, PassConfig::TYPE_BEFORE_REMOVING); + // must be registered before the AddConsoleCommandPass + $container->addCompilerPass(new TranslationLintCommandPass(), PassConfig::TYPE_BEFORE_REMOVING, 10); // must be registered as late as possible to get access to all Twig paths registered in // twig.template_iterator definition $this->addCompilerPassIfExists($container, TranslatorPass::class, PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); @@ -181,6 +185,7 @@ public function build(ContainerBuilder $container): void // must be registered after MonologBundle's LoggerChannelPass $container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new VirtualRequestStackPass()); + $container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php index 6d8966a171ba2..48d5c327a3986 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php @@ -50,6 +50,6 @@ public function testGenerateFragmentUri() $client = self::createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]); $client->request('GET', '/fragment_uri'); - $this->assertSame('/_fragment?_hash=CCRGN2D%2FoAJbeGz%2F%2FdoH3bNSPwLCrmwC1zAYCGIKJ0E%3D&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction', $client->getResponse()->getContent()); + $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction$#', $client->getResponse()->getContent()); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 622b853d1d8c6..81c4a8ee6f46f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -643,11 +643,13 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri } if ($container->hasDefinition('debug.security.firewall')) { - foreach ($authenticationProviders as $authenticatorId) { - $container->register('debug.'.$authenticatorId, TraceableAuthenticator::class) - ->setDecoratedService($authenticatorId) - ->setArguments([new Reference('debug.'.$authenticatorId.'.inner')]) + foreach ($authenticationProviders as &$authenticatorId) { + $traceableId = 'debug.'.$authenticatorId; + $container + ->register($traceableId, TraceableAuthenticator::class) + ->setArguments([new Reference($authenticatorId)]) ; + $authenticatorId = $traceableId; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 23aa17b9adb57..c4ae38e65b2da 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -20,7 +20,9 @@ use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass; use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveReferencesToAliasesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\ExpressionLanguage\Expression; @@ -900,6 +902,30 @@ public function testCustomHasherWithMigrateFrom() ]); } + public function testAuthenticatorsDecoration() + { + $container = $this->getRawContainer(); + $container->setParameter('kernel.debug', true); + $container->getCompilerPassConfig()->setOptimizationPasses([ + new ResolveChildDefinitionsPass(), + new DecoratorServicePass(), + new ResolveReferencesToAliasesPass(), + ]); + + $container->register(TestAuthenticator::class); + $container->loadFromExtension('security', [ + 'firewalls' => ['main' => ['custom_authenticator' => TestAuthenticator::class]], + ]); + $container->compile(); + + /** @var Reference[] $managerAuthenticators */ + $managerAuthenticators = $container->getDefinition('security.authenticator.manager.main')->getArgument(0); + $this->assertCount(1, $managerAuthenticators); + $this->assertSame('debug.'.TestAuthenticator::class, (string) reset($managerAuthenticators), 'AuthenticatorManager must be injected traceable authenticators in debug mode.'); + + $this->assertTrue($container->hasDefinition(TestAuthenticator::class), 'Original authenticator must still exist in the container so it can be used outside of the AuthenticatorManager’s context.'); + } + protected function getRawContainer() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig index af9f0a4ceaba3..55589c2945d88 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig @@ -40,6 +40,7 @@ #source .source-content ol li { margin: 0 0 2px 0; padding-left: 5px; + white-space: preserve nowrap; } #source .source-content ol li::marker { color: var(--color-muted); diff --git a/src/Symfony/Component/Finder/Comparator/DateComparator.php b/src/Symfony/Component/Finder/Comparator/DateComparator.php index f3538aaea9566..bcf93cfb6a523 100644 --- a/src/Symfony/Component/Finder/Comparator/DateComparator.php +++ b/src/Symfony/Component/Finder/Comparator/DateComparator.php @@ -36,7 +36,7 @@ public function __construct(string $test) throw new \InvalidArgumentException(\sprintf('"%s" is not a valid date.', $matches[2])); } - $operator = $matches[1] ?? '=='; + $operator = $matches[1] ?: '=='; if ('since' === $operator || 'after' === $operator) { $operator = '>'; } diff --git a/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php b/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php index 47bcc4838bd26..e50b713062638 100644 --- a/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php +++ b/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php @@ -59,6 +59,7 @@ public static function getTestData() ['after 2005-10-10', [strtotime('2005-10-15')], [strtotime('2005-10-09')]], ['since 2005-10-10', [strtotime('2005-10-15')], [strtotime('2005-10-09')]], ['!= 2005-10-10', [strtotime('2005-10-11')], [strtotime('2005-10-10')]], + ['2005-10-10', [strtotime('2005-10-10')], [strtotime('2005-10-11')]], ]; } } diff --git a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php index fe0e0d39cd9d9..c00b8f7dfbfe5 100644 --- a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php +++ b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php @@ -358,10 +358,10 @@ public static function provideParse(): iterable 'non-special://:@untrusted.com/x' => ['scheme' => 'non-special', 'host' => 'untrusted.com'], 'http:foo.com' => ['scheme' => 'http', 'host' => null], " :foo.com \n" => null, - ' foo.com ' => ['scheme' => null, 'host' => null], + ' foo.com ' => null, 'a: foo.com' => null, - 'http://f:21/ b ? d # e ' => ['scheme' => 'http', 'host' => 'f'], - 'lolscheme:x x#x x' => ['scheme' => 'lolscheme', 'host' => null], + 'http://f:21/ b ? d # e ' => null, + 'lolscheme:x x#x x' => null, 'http://f:/c' => ['scheme' => 'http', 'host' => 'f'], 'http://f:0/c' => ['scheme' => 'http', 'host' => 'f'], 'http://f:00000000000000/c' => ['scheme' => 'http', 'host' => 'f'], @@ -434,7 +434,7 @@ public static function provideParse(): iterable 'javascript:example.com/' => ['scheme' => 'javascript', 'host' => null], 'mailto:example.com/' => ['scheme' => 'mailto', 'host' => null], '/a/b/c' => ['scheme' => null, 'host' => null], - '/a/ /c' => ['scheme' => null, 'host' => null], + '/a/ /c' => null, '/a%2fc' => ['scheme' => null, 'host' => null], '/a/%2f/c' => ['scheme' => null, 'host' => null], '#β' => ['scheme' => null, 'host' => null], @@ -495,10 +495,10 @@ public static function provideParse(): iterable 'http://example.com/你好你好' => ['scheme' => 'http', 'host' => 'example.com'], 'http://example.com/‥/foo' => ['scheme' => 'http', 'host' => 'example.com'], "http://example.com/\u{feff}/foo" => ['scheme' => 'http', 'host' => 'example.com'], - "http://example.com\u{002f}\u{202e}\u{002f}\u{0066}\u{006f}\u{006f}\u{002f}\u{202d}\u{002f}\u{0062}\u{0061}\u{0072}\u{0027}\u{0020}" => ['scheme' => 'http', 'host' => 'example.com'], + "http://example.com\u{002f}\u{202e}\u{002f}\u{0066}\u{006f}\u{006f}\u{002f}\u{202d}\u{002f}\u{0062}\u{0061}\u{0072}\u{0027}\u{0020}" => null, 'http://www.google.com/foo?bar=baz#' => ['scheme' => 'http', 'host' => 'www.google.com'], - 'http://www.google.com/foo?bar=baz# »' => ['scheme' => 'http', 'host' => 'www.google.com'], - 'data:test# »' => ['scheme' => 'data', 'host' => null], + 'http://www.google.com/foo?bar=baz# »' => null, + 'data:test# »' => null, 'http://www.google.com' => ['scheme' => 'http', 'host' => 'www.google.com'], 'http://192.0x00A80001' => ['scheme' => 'http', 'host' => '192.0x00A80001'], 'http://www/foo%2Ehtml' => ['scheme' => 'http', 'host' => 'www'], @@ -706,11 +706,11 @@ public static function provideParse(): iterable 'test-a-colon-slash-slash-b.html' => ['scheme' => null, 'host' => null], 'http://example.org/test?a#bc' => ['scheme' => 'http', 'host' => 'example.org'], 'http:\\/\\/f:b\\/c' => ['scheme' => 'http', 'host' => null], - 'http:\\/\\/f: \\/c' => ['scheme' => 'http', 'host' => null], + 'http:\\/\\/f: \\/c' => null, 'http:\\/\\/f:fifty-two\\/c' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/f:999999\\/c' => ['scheme' => 'http', 'host' => null], 'non-special:\\/\\/f:999999\\/c' => ['scheme' => 'non-special', 'host' => null], - 'http:\\/\\/f: 21 \\/ b ? d # e ' => ['scheme' => 'http', 'host' => null], + 'http:\\/\\/f: 21 \\/ b ? d # e ' => null, 'http:\\/\\/[1::2]:3:4' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/2001::1' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/2001::1]' => ['scheme' => 'http', 'host' => null], @@ -734,8 +734,8 @@ public static function provideParse(): iterable 'http:@:www.example.com' => ['scheme' => 'http', 'host' => null], 'http:\\/@:www.example.com' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/@:www.example.com' => ['scheme' => 'http', 'host' => null], - 'http:\\/\\/example example.com' => ['scheme' => 'http', 'host' => null], - 'http:\\/\\/Goo%20 goo%7C|.com' => ['scheme' => 'http', 'host' => null], + 'http:\\/\\/example example.com' => null, + 'http:\\/\\/Goo%20 goo%7C|.com' => null, 'http:\\/\\/[]' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/[:]' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/GOO\\u00a0\\u3000goo.com' => ['scheme' => 'http', 'host' => null], @@ -752,8 +752,8 @@ public static function provideParse(): iterable 'http:\\/\\/hello%00' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/192.168.0.257' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/%3g%78%63%30%2e%30%32%35%30%2E.01' => ['scheme' => 'http', 'host' => null], - 'http:\\/\\/192.168.0.1 hello' => ['scheme' => 'http', 'host' => null], - 'https:\\/\\/x x:12' => ['scheme' => 'https', 'host' => null], + 'http:\\/\\/192.168.0.1 hello' => null, + 'https:\\/\\/x x:12' => null, 'http:\\/\\/[www.google.com]\\/' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/[google.com]' => ['scheme' => 'http', 'host' => null], 'http:\\/\\/[::1.2.3.4x]' => ['scheme' => 'http', 'host' => null], @@ -763,7 +763,7 @@ public static function provideParse(): iterable '..\\/i' => ['scheme' => null, 'host' => null], '\\/i' => ['scheme' => null, 'host' => null], 'sc:\\/\\/\\u0000\\/' => ['scheme' => 'sc', 'host' => null], - 'sc:\\/\\/ \\/' => ['scheme' => 'sc', 'host' => null], + 'sc:\\/\\/ \\/' => null, 'sc:\\/\\/@\\/' => ['scheme' => 'sc', 'host' => null], 'sc:\\/\\/te@s:t@\\/' => ['scheme' => 'sc', 'host' => null], 'sc:\\/\\/:\\/' => ['scheme' => 'sc', 'host' => null], diff --git a/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php b/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php index a806981de770f..05d86ba15da8e 100644 --- a/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php +++ b/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php @@ -94,7 +94,13 @@ public static function parse(string $url): ?array } try { - return UriString::parse($url); + $parsedUrl = UriString::parse($url); + + if (preg_match('/\s/', $url)) { + return null; + } + + return $parsedUrl; } catch (SyntaxError) { return null; } diff --git a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php index 855ed8b2915d2..4fe93645b1b4c 100644 --- a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php +++ b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php @@ -138,7 +138,7 @@ public function request(string $method, string $url, array $options = []): Respo $filterContentHeaders = static function ($h) { return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:'); }; - $options['header'] = array_filter($options['header'], $filterContentHeaders); + $options['headers'] = array_filter($options['headers'], $filterContentHeaders); $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders); $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders); } diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index d3efa896db8ac..c520e593e371b 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -678,4 +678,26 @@ public function testHeadRequestWithClosureBody() $this->assertIsArray($vars); $this->assertSame('HEAD', $vars['REQUEST_METHOD']); } + + /** + * @testWith [301] + * [302] + * [303] + */ + public function testPostToGetRedirect(int $status) + { + $p = TestHttpServer::start(8067); + + try { + $client = $this->getHttpClient(__FUNCTION__); + + $response = $client->request('POST', 'http://localhost:8057/custom?status=' . $status . '&headers[]=Location%3A%20%2F'); + $body = $response->toArray(); + } finally { + $p->stop(); + } + + $this->assertSame('GET', $body['REQUEST_METHOD']); + $this->assertSame('/', $body['REQUEST_URI']); + } } diff --git a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php index c160dff77492a..ddd83c4960cb0 100644 --- a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php @@ -175,6 +175,27 @@ public function testNonCallableOnProgressCallback() $client->request('GET', $url, ['on_progress' => $customCallback]); } + public function testHeadersArePassedOnRedirect() + { + $ipAddr = '104.26.14.6'; + $url = sprintf('http://%s/', $ipAddr); + $content = 'foo'; + + $callback = function ($method, $url, $options) use ($content): MockResponse { + $this->assertArrayHasKey('headers', $options); + $this->assertNotContains('content-type: application/json', $options['headers']); + $this->assertContains('foo: bar', $options['headers']); + return new MockResponse($content); + }; + $responses = [ + new MockResponse('', ['http_code' => 302, 'redirect_url' => 'http://104.26.14.7']), + $callback, + ]; + $client = new NoPrivateNetworkHttpClient(new MockHttpClient($responses)); + $response = $client->request('POST', $url, ['headers' => ['foo' => 'bar', 'content-type' => 'application/json']]); + $this->assertEquals($content, $response->getContent()); + } + private function getMockHttpClient(string $ipAddr, string $content) { return new MockHttpClient(new MockResponse($content, ['primary_ip' => $ipAddr])); diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index 6eddd6c287c85..2c4d831bb9afa 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -189,7 +189,12 @@ public function prepare(Request $request): static } if (!$this->headers->has('Content-Type')) { - $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); + $mimeType = null; + if (!$this->tempFileObject) { + $mimeType = $this->file->getMimeType(); + } + + $this->headers->set('Content-Type', $mimeType ?: 'application/octet-stream'); } parent::prepare($request); diff --git a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php index 2951cdb514471..1263fa39298ff 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php @@ -468,4 +468,15 @@ public function testSetChunkSizeTooSmall() $response->setChunkSize(0); } + + public function testCreateFromTemporaryFileWithoutMimeType() + { + $file = new \SplTempFileObject(); + $file->fwrite('foo,bar'); + + $response = new BinaryFileResponse($file); + $response->prepare(new Request()); + + $this->assertSame('application/octet-stream', $response->headers->get('Content-Type')); + } } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 1f0ff7cc0f053..a196250e8b23b 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -46,11 +46,9 @@ class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscriberInterface { /** - * @see \Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT * @see DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS */ private const CONTEXT_DENORMALIZE = [ - 'disable_type_enforcement' => true, 'collect_denormalization_errors' => true, ]; @@ -190,7 +188,7 @@ private function mapQueryString(Request $request, ArgumentMetadata $argument, Ma return null; } - return $this->serializer->denormalize($data, $argument->getType(), null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]); + return $this->serializer->denormalize($data, $argument->getType(), 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]); } private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): object|array|null @@ -210,7 +208,7 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument, } if ($data = $request->request->all()) { - return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : [])); + return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : [])); } if ('' === ($data = $request->getContent()) && ($argument->isNullable() || $argument->hasDefaultValue())) { diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 82e6dbfbd0c79..387b51c8a4fce 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,11 +73,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.2.1'; - public const VERSION_ID = 70201; + public const VERSION = '7.2.2'; + public const VERSION_ID = 70202; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 1; + public const RELEASE_VERSION = 2; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2025'; diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 8b26767f9ea94..8ed4adfe00567 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php @@ -22,6 +22,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -395,6 +396,38 @@ public function testQueryStringValidationPassed() $this->assertEquals([$payload], $event->getArguments()); } + public function testQueryStringParameterTypeMismatch() + { + $query = ['price' => 'not a float']; + + $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); + $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->never())->method('validate'); + + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ + MapQueryString::class => new MapQueryString(), + ]); + + $request = Request::create('/', 'GET', $query); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + try { + $resolver->onKernelControllerArguments($event); + $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + } catch (HttpException $e) { + $validationFailedException = $e->getPrevious(); + $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); + $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage()); + } + } + public function testRequestInputValidationPassed() { $input = ['price' => '50']; @@ -457,6 +490,38 @@ public function testRequestArrayDenormalization() $this->assertEquals([$payload], $event->getArguments()); } + public function testRequestInputTypeMismatch() + { + $input = ['price' => 'not a float']; + + $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor()); + $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]); + + $validator = $this->createMock(ValidatorInterface::class); + $validator->expects($this->never())->method('validate'); + + $resolver = new RequestPayloadValueResolver($serializer, $validator); + + $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [ + MapRequestPayload::class => new MapRequestPayload(), + ]); + + $request = Request::create('/', 'POST', $input); + + $kernel = $this->createMock(HttpKernelInterface::class); + $arguments = $resolver->resolve($request, $argument); + $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST); + + try { + $resolver->onKernelControllerArguments($event); + $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class)); + } catch (HttpException $e) { + $validationFailedException = $e->getPrevious(); + $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException); + $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage()); + } + } + public function testItThrowsOnMissingAttributeType() { $serializer = new Serializer(); diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php index fa9885d2753cd..43c740ee12b98 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php @@ -60,8 +60,8 @@ public function testRenderControllerReference() $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); - $this->assertEquals( - '', + $this->assertMatchesRegularExpression( + '#^$#', $strategy->render($reference, $request, ['alt' => $altReference])->getContent() ); } @@ -78,8 +78,8 @@ public function testRenderControllerReferenceWithAbsoluteUri() $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); - $this->assertSame( - '', + $this->assertMatchesRegularExpression( + '#^$#', $strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent() ); } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php index f74887ade36f4..8e4b59e5feeb9 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php @@ -32,7 +32,7 @@ public function testRenderWithControllerAndSigner() { $strategy = new HIncludeFragmentRenderer(null, new UriSigner('foo')); - $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent()); + $this->assertMatchesRegularExpression('#^$#', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent()); } public function testRenderWithUri() diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php index 4af00f9f75137..7fd04c5a5b0b7 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php @@ -51,8 +51,8 @@ public function testRenderControllerReference() $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); - $this->assertEquals( - '', + $this->assertMatchesRegularExpression( + '{^$}', $strategy->render($reference, $request, ['alt' => $altReference])->getContent() ); } @@ -69,8 +69,8 @@ public function testRenderControllerReferenceWithAbsoluteUri() $reference = new ControllerReference('main_controller', [], []); $altReference = new ControllerReference('alt_controller', [], []); - $this->assertSame( - '', + $this->assertMatchesRegularExpression( + '{^$}', $strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent() ); } diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php index eaacfa4b60b99..c4b7e904a544e 100644 --- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php @@ -363,4 +363,25 @@ public function testKeepaliveWhenABeanstalkdExceptionOccurs() $this->expectExceptionObject(new TransportException($exception->getMessage(), 0, $exception)); $connection->keepalive((string) $id); } + + public function testSendWithRoundedDelay() + { + $tube = 'xyz'; + $body = 'foo'; + $headers = ['test' => 'bar']; + $delay = 920; + $expectedDelay = 0; + + $client = $this->createMock(PheanstalkInterface::class); + $client->expects($this->once())->method('useTube')->with($tube)->willReturn($client); + $client->expects($this->once())->method('put')->with( + $this->anything(), + $this->anything(), + $expectedDelay, + $this->anything(), + ); + + $connection = new Connection(['tube_name' => $tube], $client); + $connection->send($body, $headers, $delay); + } } diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php index c2c5cd7ee49f8..0f2c2c555a4fb 100644 --- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php @@ -124,7 +124,7 @@ public function send(string $body, array $headers, int $delay = 0): string $job = $this->client->useTube($this->tube)->put( $message, PheanstalkInterface::DEFAULT_PRIORITY, - $delay / 1000, + (int) ($delay / 1000), $this->ttr ); } catch (Exception $exception) { 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 f1f5fbeef8d62..49ebf00dbdd5d 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php @@ -15,7 +15,6 @@ use Doctrine\DBAL\Connection as DBALConnection; use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Platforms\MariaDb1060Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; use Doctrine\DBAL\Platforms\MySQL57Platform; use Doctrine\DBAL\Platforms\MySQL80Platform; @@ -591,9 +590,16 @@ class_exists(MySQLPlatform::class) ? new MySQLPlatform() : new MySQL57Platform() 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE', ]; - if (class_exists(MariaDb1060Platform::class)) { + if (interface_exists(DBALException::class)) { + // DBAL 4+ + $mariaDbPlatformClass = 'Doctrine\DBAL\Platforms\MariaDB1060Platform'; + } else { + $mariaDbPlatformClass = 'Doctrine\DBAL\Platforms\MariaDb1060Platform'; + } + + if (class_exists($mariaDbPlatformClass)) { yield 'MariaDB106' => [ - new MariaDb1060Platform(), + new $mariaDbPlatformClass(), 'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED', ]; } diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php index 8de070dc046c9..d006e32483896 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php +++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php @@ -15,8 +15,6 @@ * The property read info tells how a property can be read. * * @author Joel Wurtz - * - * @internal */ final class PropertyReadInfo { diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php index 6bc7abcdf849e..81ce7eda6d5b0 100644 --- a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php +++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php @@ -15,8 +15,6 @@ * The write mutator defines how a property can be written. * * @author Joel Wurtz - * - * @internal */ final class PropertyWriteInfo { diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 6248e4966dc15..0d77497c2e1da 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -14,15 +14,18 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\Clazz; use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyCollection; +use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyGeneric; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyNamespace; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyPropertyAndGetterWithDifferentTypes; use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyUnionType; +use Symfony\Component\PropertyInfo\Tests\Fixtures\IFace; use Symfony\Component\PropertyInfo\Tests\Fixtures\IntRangeDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; @@ -552,6 +555,77 @@ public static function allowPrivateAccessLegacyProvider(): array ]; } + /** + * @param list $expectedTypes + * + * @dataProvider legacyGenericsProvider + */ + public function testGenericsLegacy(string $property, array $expectedTypes) + { + $this->assertEquals($expectedTypes, $this->extractor->getTypes(DummyGeneric::class, $property)); + } + + /** + * @return iterable}> + */ + public static function legacyGenericsProvider(): iterable + { + yield [ + 'basicClass', + [ + new LegacyType( + builtinType: LegacyType::BUILTIN_TYPE_OBJECT, + class: Clazz::class, + collectionValueType: new LegacyType( + builtinType: LegacyType::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'nullableClass', + [ + new LegacyType( + builtinType: LegacyType::BUILTIN_TYPE_OBJECT, + class: Clazz::class, + nullable: true, + collectionValueType: new LegacyType( + builtinType: LegacyType::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'basicInterface', + [ + new LegacyType( + builtinType: LegacyType::BUILTIN_TYPE_OBJECT, + class: IFace::class, + collectionValueType: new LegacyType( + builtinType: LegacyType::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'nullableInterface', + [ + new LegacyType( + builtinType: LegacyType::BUILTIN_TYPE_OBJECT, + class: IFace::class, + nullable: true, + collectionValueType: new LegacyType( + builtinType: LegacyType::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + } + /** * @dataProvider typesProvider */ @@ -968,7 +1042,41 @@ public static function allowPrivateAccessProvider(): array public function testGenericInterface() { - $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface')); + $this->assertEquals( + Type::generic(Type::enum(\BackedEnum::class), Type::string()), + $this->extractor->getType(Dummy::class, 'genericInterface'), + ); + } + + /** + * @dataProvider genericsProvider + */ + public function testGenerics(string $property, Type $expectedType) + { + $this->assertEquals($expectedType, $this->extractor->getType(DummyGeneric::class, $property)); + } + + /** + * @return iterable + */ + public static function genericsProvider(): iterable + { + yield [ + 'basicClass', + Type::generic(Type::object(Clazz::class), Type::object(Dummy::class)), + ]; + yield [ + 'nullableClass', + Type::nullable(Type::generic(Type::object(Clazz::class), Type::object(Dummy::class))), + ]; + yield [ + 'basicInterface', + Type::generic(Type::object(IFace::class), Type::object(Dummy::class)), + ]; + yield [ + 'nullableInterface', + Type::nullable(Type::generic(Type::object(IFace::class), Type::object(Dummy::class))), + ]; } } diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php new file mode 100644 index 0000000000000..5863fbfc95450 --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +interface IFace {} + +class Clazz {} + +class DummyGeneric +{ + + /** + * @var Clazz + */ + public $basicClass; + + /** + * @var ?Clazz + */ + public $nullableClass; + + /** + * @var IFace + */ + public $basicInterface; + + /** + * @var ?IFace + */ + public $nullableInterface; + +} diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php index 6d06d8b8a9f16..924d74e3aec1a 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php @@ -128,7 +128,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array $collection = $mainType->isCollection() || \is_a($mainType->getClassName(), \Traversable::class, true) || \is_a($mainType->getClassName(), \ArrayAccess::class, true); // it's safer to fall back to other extractors if the generic type is too abstract - if (!$collection && !class_exists($mainType->getClassName())) { + if (!$collection && !class_exists($mainType->getClassName()) && !interface_exists($mainType->getClassName(), false)) { return []; } diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json index 70a57ef06ced0..c033ca51befdc 100644 --- a/src/Symfony/Component/PropertyInfo/composer.json +++ b/src/Symfony/Component/PropertyInfo/composer.json @@ -25,7 +25,7 @@ "require": { "php": ">=8.2", "symfony/string": "^6.4|^7.0", - "symfony/type-info": "^7.1" + "symfony/type-info": "~7.1.9|^7.2.2" }, "require-dev": { "symfony/serializer": "^6.4|^7.0", diff --git a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php index dc10d30307484..f8724e59247c1 100644 --- a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php +++ b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php @@ -105,9 +105,9 @@ public static function provideForToString() /** * @dataProvider providerGetNextRunDates */ - public function testGetNextRunDates(\DateTimeImmutable $from, TriggerInterface $trigger, array $expected, int $count = 0) + public function testGetNextRunDates(\DateTimeImmutable $from, TriggerInterface $trigger, array $expected, int $count) { - $this->assertEquals($expected, $this->getNextRunDates($from, $trigger, $count ?? \count($expected))); + $this->assertEquals($expected, $this->getNextRunDates($from, $trigger, $count)); } public static function providerGetNextRunDates(): iterable diff --git a/src/Symfony/Component/Security/Csrf/SameOriginCsrfTokenManager.php b/src/Symfony/Component/Security/Csrf/SameOriginCsrfTokenManager.php index 9ef61964bfe1e..0c95208c0f580 100644 --- a/src/Symfony/Component/Security/Csrf/SameOriginCsrfTokenManager.php +++ b/src/Symfony/Component/Security/Csrf/SameOriginCsrfTokenManager.php @@ -227,9 +227,21 @@ public function onKernelResponse(ResponseEvent $event): void */ private function isValidOrigin(Request $request): ?bool { - $source = $request->headers->get('Origin') ?? $request->headers->get('Referer') ?? 'null'; + $target = $request->getSchemeAndHttpHost().'/'; + $source = 'null'; - return 'null' === $source ? null : str_starts_with($source.'/', $request->getSchemeAndHttpHost().'/'); + foreach (['Origin', 'Referer'] as $header) { + if (!$request->headers->has($header)) { + continue; + } + $source = $request->headers->get($header); + + if (str_starts_with($source.'/', $target)) { + return true; + } + } + + return 'null' === $source ? null : false; } /** diff --git a/src/Symfony/Component/Security/Csrf/Tests/SameOriginCsrfTokenManagerTest.php b/src/Symfony/Component/Security/Csrf/Tests/SameOriginCsrfTokenManagerTest.php index 1ad17b80e0549..eae31deee379c 100644 --- a/src/Symfony/Component/Security/Csrf/Tests/SameOriginCsrfTokenManagerTest.php +++ b/src/Symfony/Component/Security/Csrf/Tests/SameOriginCsrfTokenManagerTest.php @@ -100,6 +100,20 @@ public function testValidOrigin() $this->assertSame(1 << 8, $request->attributes->get('csrf-token')); } + public function testValidRefererInvalidOrigin() + { + $request = new Request(); + $request->headers->set('Origin', 'http://localhost:1234'); + $request->headers->set('Referer', $request->getSchemeAndHttpHost()); + $this->requestStack->push($request); + + $token = new CsrfToken('test_token', str_repeat('a', 24)); + + $this->logger->expects($this->once())->method('debug')->with('CSRF validation accepted using origin info.'); + $this->assertTrue($this->csrfTokenManager->isTokenValid($token)); + $this->assertSame(1 << 8, $request->attributes->get('csrf-token')); + } + public function testValidOriginAfterDoubleSubmit() { $session = $this->createMock(Session::class); diff --git a/src/Symfony/Component/Stopwatch/Stopwatch.php b/src/Symfony/Component/Stopwatch/Stopwatch.php index 50ac6574fd436..8961507fa07db 100644 --- a/src/Symfony/Component/Stopwatch/Stopwatch.php +++ b/src/Symfony/Component/Stopwatch/Stopwatch.php @@ -140,7 +140,7 @@ public function getEvent(string $name): StopwatchEvent */ public function getSectionEvents(string $id): array { - return $this->sections[$id]->getEvents() ?? []; + return isset($this->sections[$id]) ? $this->sections[$id]->getEvents() : []; } /** diff --git a/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php b/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php index f1e2270018447..f9b532efe1fe4 100644 --- a/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php +++ b/src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php @@ -187,4 +187,9 @@ public function testReset() $this->assertEquals(new Stopwatch(), $stopwatch); } + + public function testShouldReturnEmptyArrayWhenSectionMissing() + { + $this->assertSame([], (new Stopwatch())->getSectionEvents('missing')); + } } diff --git a/src/Symfony/Component/Translation/PseudoLocalizationTranslator.php b/src/Symfony/Component/Translation/PseudoLocalizationTranslator.php index 5d56d2cc11bd6..fe5b0adc25216 100644 --- a/src/Symfony/Component/Translation/PseudoLocalizationTranslator.php +++ b/src/Symfony/Component/Translation/PseudoLocalizationTranslator.php @@ -11,12 +11,13 @@ namespace Symfony\Component\Translation; +use Symfony\Component\Translation\Exception\LogicException; use Symfony\Contracts\Translation\TranslatorInterface; /** * This translator should only be used in a development environment. */ -final class PseudoLocalizationTranslator implements TranslatorInterface +final class PseudoLocalizationTranslator implements TranslatorInterface, TranslatorBagInterface { private const EXPANSION_CHARACTER = '~'; @@ -115,6 +116,24 @@ public function getLocale(): string return $this->translator->getLocale(); } + public function getCatalogue(?string $locale = null): MessageCatalogueInterface + { + if (!$this->translator instanceof TranslatorBagInterface) { + throw new LogicException(\sprintf('The "%s()" method cannot be called as the wrapped translator class "%s" does not implement the "%s".', __METHOD__, $this->translator::class, TranslatorBagInterface::class)); + } + + return $this->translator->getCatalogue($locale); + } + + public function getCatalogues(): array + { + if (!$this->translator instanceof TranslatorBagInterface) { + throw new LogicException(\sprintf('The "%s()" method cannot be called as the wrapped translator class "%s" does not implement the "%s".', __METHOD__, $this->translator::class, TranslatorBagInterface::class)); + } + + return $this->translator->getCatalogues(); + } + private function getParts(string $originalTrans): array { if (!$this->parseHTML) { diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index b0ad600085e14..9320987c6baed 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -16,7 +16,9 @@ use Symfony\Component\TypeInfo\Exception\UnsupportedException; use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy; use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy; +use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection; +use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum; use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeContext\TypeContext; @@ -138,6 +140,8 @@ public static function resolveDataProvider(): iterable yield [Type::object(Dummy::class), 'static', $typeContextFactory->createFromClassName(Dummy::class, AbstractDummy::class)]; yield [Type::object(AbstractDummy::class), 'parent', $typeContextFactory->createFromClassName(Dummy::class)]; yield [Type::object(Dummy::class), 'Dummy', $typeContextFactory->createFromClassName(Dummy::class)]; + yield [Type::enum(DummyEnum::class), 'DummyEnum', $typeContextFactory->createFromClassName(DummyEnum::class)]; + yield [Type::enum(DummyBackedEnum::class), 'DummyBackedEnum', $typeContextFactory->createFromClassName(DummyBackedEnum::class)]; yield [Type::template('T', Type::union(Type::int(), Type::string())), 'T', $typeContextFactory->createFromClassName(DummyWithTemplates::class)]; yield [Type::template('V'), 'V', $typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithTemplates::class, 'getPrice'))]; @@ -151,6 +155,8 @@ public static function resolveDataProvider(): iterable // union yield [Type::union(Type::int(), Type::string()), 'int|string']; + yield [Type::mixed(), 'int|mixed']; + yield [Type::mixed(), 'mixed|int']; // intersection yield [Type::intersection(Type::object(\DateTime::class), Type::object(\Stringable::class)), \DateTime::class.'&'.\Stringable::class]; diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php index 01b32b66f5777..23e6b3a2860a5 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php @@ -25,11 +25,6 @@ */ final class ReflectionTypeResolver implements TypeResolverInterface { - /** - * @var array - */ - private static array $reflectionEnumCache = []; - public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type { if ($subject instanceof \ReflectionUnionType) { @@ -81,11 +76,7 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type default => $identifier, }; - if (is_subclass_of($className, \BackedEnum::class)) { - $reflectionEnum = (self::$reflectionEnumCache[$className] ??= new \ReflectionEnum($className)); - $backingType = $this->resolve($reflectionEnum->getBackingType(), $typeContext); - $type = Type::enum($className, $backingType); - } elseif (is_subclass_of($className, \UnitEnum::class)) { + if (is_subclass_of($className, \UnitEnum::class)) { $type = Type::enum($className); } else { $type = Type::object($className); diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index 4809e9b83bf7e..a172d388a8722 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -223,7 +223,19 @@ private function getTypeFromNode(TypeNode $node, ?TypeContext $typeContext): Typ } if ($node instanceof UnionTypeNode) { - return Type::union(...array_map(fn (TypeNode $t): Type => $this->getTypeFromNode($t, $typeContext), $node->types)); + $types = []; + + foreach ($node->types as $nodeType) { + $type = $this->getTypeFromNode($nodeType, $typeContext); + + if ($type instanceof BuiltinType && TypeIdentifier::MIXED === $type->getTypeIdentifier()) { + return Type::mixed(); + } + + $types[] = $type; + } + + return Type::union(...$types); } if ($node instanceof IntersectionTypeNode) { @@ -246,14 +258,16 @@ private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeC try { new \ReflectionClass($className); self::$classExistCache[$className] = true; - - return Type::object($className); } catch (\Throwable) { } } } if (self::$classExistCache[$className]) { + if (is_subclass_of($className, \UnitEnum::class)) { + return Type::enum($className); + } + return Type::object($className); } diff --git a/src/Symfony/Component/Validator/Constraints/Count.php b/src/Symfony/Component/Validator/Constraints/Count.php index 175e07ebb8fdd..38ea8d6e74e71 100644 --- a/src/Symfony/Component/Validator/Constraints/Count.php +++ b/src/Symfony/Component/Validator/Constraints/Count.php @@ -45,7 +45,7 @@ class Count extends Constraint /** * @param int<0, max>|array|null $exactly The exact expected number of elements * @param int<0, max>|null $min Minimum expected number of elements - * @param positive-int|null $max Maximum expected number of elements + * @param int<0, max>|null $max Maximum expected number of elements * @param positive-int|null $divisibleBy The number the collection count should be divisible by * @param string[]|null $groups * @param array $options diff --git a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php index 408088c7ef709..8977fd2210809 100644 --- a/src/Symfony/Component/Validator/Constraints/UniqueValidator.php +++ b/src/Symfony/Component/Validator/Constraints/UniqueValidator.php @@ -47,11 +47,15 @@ public function validate(mixed $value, Constraint $constraint): void } if (\in_array($element, $collectionElements, true)) { - $this->context->buildViolation($constraint->message) - ->atPath("[$index]".(null !== $constraint->errorPath ? ".{$constraint->errorPath}" : '')) + $violationBuilder = $this->context->buildViolation($constraint->message) ->setParameter('{{ value }}', $this->formatValue($element)) - ->setCode(Unique::IS_NOT_UNIQUE) - ->addViolation(); + ->setCode(Unique::IS_NOT_UNIQUE); + + if (null !== $constraint->errorPath) { + $violationBuilder->atPath("[$index].{$constraint->errorPath}"); + } + + $violationBuilder->addViolation(); return; } diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf index 3977f37433060..485d69add1ee8 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf @@ -444,27 +444,27 @@ This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. - This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words. + این مقدار بسیار کوتاه است. باید حداقل یک کلمه داشته باشد.|این مقدار بسیار کوتاه است. باید حداقل {{ min }} کلمه داشته باشد. This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. - This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less. + این مقدار بیش از حد طولانی است. باید فقط یک کلمه باشد.|این مقدار بیش از حد طولانی است. باید حداکثر {{ max }} کلمه داشته باشد. This value does not represent a valid week in the ISO 8601 format. - This value does not represent a valid week in the ISO 8601 format. + این مقدار یک هفته معتبر در قالب ISO 8601 را نشان نمی‌دهد. This value is not a valid week. - This value is not a valid week. + این مقدار یک هفته معتبر نیست. This value should not be before week "{{ min }}". - This value should not be before week "{{ min }}". + این مقدار نباید قبل از هفته "{{ min }}" باشد. This value should not be after week "{{ max }}". - This value should not be after week "{{ max }}". + این مقدار نباید بعد از هفته "{{ max }}" باشد. diff --git a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php index 76862e73fd278..f81621d652c8f 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/UniqueValidatorTest.php @@ -61,7 +61,7 @@ public static function getValidValues() /** * @dataProvider getInvalidValues */ - public function testInvalidValues($value, $expectedMessageParam, string $expectedErrorPath) + public function testInvalidValues($value, $expectedMessageParam) { $constraint = new Unique([ 'message' => 'myMessage', @@ -71,7 +71,6 @@ public function testInvalidValues($value, $expectedMessageParam, string $expecte $this->buildViolation('myMessage') ->setParameter('{{ value }}', $expectedMessageParam) ->setCode(Unique::IS_NOT_UNIQUE) - ->atPath($expectedErrorPath) ->assertRaised(); } @@ -80,12 +79,12 @@ public static function getInvalidValues() $object = new \stdClass(); return [ - yield 'not unique booleans' => [[true, true], 'true', 'property.path[1]'], - yield 'not unique integers' => [[1, 2, 3, 3], 3, 'property.path[3]'], - yield 'not unique floats' => [[0.1, 0.2, 0.1], 0.1, 'property.path[2]'], - yield 'not unique string' => [['a', 'b', 'a'], '"a"', 'property.path[2]'], - yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]], 'array', 'property.path[2]'], - yield 'not unique objects' => [[$object, $object], 'object', 'property.path[1]'], + yield 'not unique booleans' => [[true, true], 'true'], + yield 'not unique integers' => [[1, 2, 3, 3], 3], + yield 'not unique floats' => [[0.1, 0.2, 0.1], 0.1], + yield 'not unique string' => [['a', 'b', 'a'], '"a"'], + yield 'not unique arrays' => [[[1, 1], [2, 3], [1, 1]], 'array'], + yield 'not unique objects' => [[$object, $object], 'object'], ]; } @@ -97,7 +96,6 @@ public function testInvalidValueNamed() $this->buildViolation('myMessage') ->setParameter('{{ value }}', '3') ->setCode(Unique::IS_NOT_UNIQUE) - ->atPath('property.path[3]') ->assertRaised(); } @@ -154,7 +152,6 @@ public function testExpectsNonUniqueObjects($callback) $this->buildViolation('myMessage') ->setParameter('{{ value }}', 'array') ->setCode(Unique::IS_NOT_UNIQUE) - ->atPath('property.path[2]') ->assertRaised(); } @@ -179,7 +176,6 @@ public function testExpectsInvalidNonStrictComparison() $this->buildViolation('myMessage') ->setParameter('{{ value }}', '1') ->setCode(Unique::IS_NOT_UNIQUE) - ->atPath('property.path[1]') ->assertRaised(); } @@ -206,7 +202,6 @@ public function testExpectsInvalidCaseInsensitiveComparison() $this->buildViolation('myMessage') ->setParameter('{{ value }}', '"hello"') ->setCode(Unique::IS_NOT_UNIQUE) - ->atPath('property.path[1]') ->assertRaised(); } @@ -251,7 +246,7 @@ public static function getInvalidFieldNames(): array /** * @dataProvider getInvalidCollectionValues */ - public function testInvalidCollectionValues(array $value, array $fields, string $expectedMessageParam, string $expectedErrorPath) + public function testInvalidCollectionValues(array $value, array $fields, string $expectedMessageParam) { $this->validator->validate($value, new Unique([ 'message' => 'myMessage', @@ -260,7 +255,6 @@ public function testInvalidCollectionValues(array $value, array $fields, string $this->buildViolation('myMessage') ->setParameter('{{ value }}', $expectedMessageParam) ->setCode(Unique::IS_NOT_UNIQUE) - ->atPath($expectedErrorPath) ->assertRaised(); } @@ -270,27 +264,25 @@ public static function getInvalidCollectionValues(): array 'unique string' => [[ ['lang' => 'eng', 'translation' => 'hi'], ['lang' => 'eng', 'translation' => 'hello'], - ], ['lang'], 'array', 'property.path[1]'], + ], ['lang'], 'array'], 'unique floats' => [[ ['latitude' => 51.509865, 'longitude' => -0.118092, 'poi' => 'capital'], ['latitude' => 52.520008, 'longitude' => 13.404954], ['latitude' => 51.509865, 'longitude' => -0.118092], - ], ['latitude', 'longitude'], 'array', 'property.path[2]'], + ], ['latitude', 'longitude'], 'array'], 'unique int' => [[ ['id' => 1, 'email' => 'bar@email.com'], ['id' => 1, 'email' => 'foo@email.com'], - ], ['id'], 'array', 'property.path[1]'], + ], ['id'], 'array'], 'unique null' => [ [null, null], [], 'null', - 'property.path[1]', ], 'unique field null' => [ [['nullField' => null], ['nullField' => null]], ['nullField'], 'array', - 'property.path[1]', ], ]; } diff --git a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php index 59033d55a0350..399f8bdde17b6 100644 --- a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php +++ b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php @@ -199,6 +199,16 @@ ]); exit; + + case '/custom': + if (isset($_GET['status'])) { + http_response_code((int) $_GET['status']); + } + if (isset($_GET['headers']) && is_array($_GET['headers'])) { + foreach ($_GET['headers'] as $header) { + header($header); + } + } } header('Content-Type: application/json', true);