From 88cf73e7eefd54ed4877b258df1c2c5cd554a4e6 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 2 Dec 2024 10:54:37 +0100 Subject: [PATCH 01/32] set the violation path only if the "errorPath" option is set --- .../Validator/Constraints/UniqueValidator.php | 12 +++++--- .../Tests/Constraints/UniqueValidatorTest.php | 30 +++++++------------ 2 files changed, 19 insertions(+), 23 deletions(-) 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/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]', ], ]; } From 2695871378e98bc89c24d171dd854dd840418278 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 7 Dec 2024 08:58:47 +0100 Subject: [PATCH 02/32] fix translation lint compatibility with the PseudoLocalizationTranslator --- .../Compiler/TranslationLintCommandPass.php | 33 +++++++++++++++++++ .../FrameworkBundle/FrameworkBundle.php | 3 ++ .../PseudoLocalizationTranslator.php | 21 +++++++++++- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationLintCommandPass.php 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/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index a1eb059bb01ce..dc708089212da 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -19,6 +19,7 @@ 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\UnusedTagsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\VirtualRequestStackPass; use Symfony\Component\Cache\Adapter\ApcuAdapter; @@ -149,6 +150,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); 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) { From ac08b9a5c41c9ccff2892d085bed4de660d582c6 Mon Sep 17 00:00:00 2001 From: Christopher Hertel Date: Mon, 9 Dec 2024 00:00:41 +0100 Subject: [PATCH 03/32] fix: preserve and nowrap in profiler code highlighting --- .../WebProfilerBundle/Resources/views/Profiler/open.css.twig | 1 + 1 file changed, 1 insertion(+) 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); From e0957a0b33bf44814914810aa412180554ea4c64 Mon Sep 17 00:00:00 2001 From: Ionut Enache Date: Sat, 7 Dec 2024 22:19:39 +0200 Subject: [PATCH 04/32] [HttpKernel] Denormalize request data using the csv format when using "#[MapQueryString]" or "#[MapRequestPayload]" (except for content data) --- .../RequestPayloadValueResolver.php | 6 +- .../RequestPayloadValueResolverTest.php | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php index 444be1b3fe7d2..542297c035e3b 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php @@ -40,11 +40,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, ]; @@ -161,7 +159,7 @@ private function mapQueryString(Request $request, string $type, MapQueryString $ return null; } - return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE); + return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE); } private function mapRequestPayload(Request $request, string $type, MapRequestPayload $attribute): ?object @@ -175,7 +173,7 @@ private function mapRequestPayload(Request $request, string $type, MapRequestPay } if ($data = $request->request->all()) { - return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE); + return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE); } if ('' === $data = $request->getContent()) { diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php index 331c2a529d8c0..7c5c946560737 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\HttpKernelInterface; use Symfony\Component\PropertyAccess\Exception\InvalidTypeException; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Encoder\XmlEncoder; use Symfony\Component\Serializer\Exception\PartialDenormalizationException; @@ -363,6 +364,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']; @@ -391,6 +424,38 @@ public function testRequestInputValidationPassed() $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 testItThrowsOnVariadicArgument() { $serializer = new Serializer(); From a96cfa400f5395435930e9225d66e2e37e86927c Mon Sep 17 00:00:00 2001 From: Kurt Thiemann Date: Thu, 5 Dec 2024 14:35:19 +0100 Subject: [PATCH 05/32] [HttpClient] Test POST to GET redirects --- .../HttpClient/Tests/HttpClientTestCase.php | 22 +++++++++++++++++++ .../HttpClient/Test/Fixtures/web/index.php | 10 +++++++++ 2 files changed, 32 insertions(+) diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index f572908c1535d..79763bc1019f3 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -673,4 +673,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/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); From 88e7a7226c0bb0ee211799a4a1fa4eb5e9f0e9af Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 27 Nov 2024 11:52:54 +0100 Subject: [PATCH 06/32] [PropertyInfo] Fix interface handling in `PhpStanTypeHelper` --- .../Tests/Extractor/PhpStanExtractorTest.php | 86 ++++++++++++++++++- .../Tests/Fixtures/DummyGeneric.php | 41 +++++++++ .../PropertyInfo/Util/PhpStanTypeHelper.php | 2 +- 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index b97032574ab86..b7987668b4f8f 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -14,10 +14,13 @@ 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\ConstructorDummyWithoutDocBlock; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; 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\IFace; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80PromotedDummy; @@ -482,7 +485,88 @@ public static function php80TypesProvider() public function testGenericInterface() { - $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface')); + $this->assertEquals( + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: \BackedEnum::class, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_STRING, + ) + ), + ], + $this->extractor->getTypes(Dummy::class, 'genericInterface') + ); + } + + /** + * @param list $expectedTypes + * @dataProvider genericsProvider + */ + public function testGenericsLegacy(string $property, array $expectedTypes) + { + $this->assertEquals($expectedTypes, $this->extractor->getTypes(DummyGeneric::class, $property)); + } + + /** + * @return iterable}> + */ + public static function genericsProvider(): iterable + { + yield [ + 'basicClass', + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Clazz::class, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'nullableClass', + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Clazz::class, + nullable: true, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'basicInterface', + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: IFace::class, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: Dummy::class, + ) + ), + ], + ]; + yield [ + 'nullableInterface', + [ + new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: IFace::class, + nullable: true, + collectionValueType: new Type( + builtinType: Type::BUILTIN_TYPE_OBJECT, + class: 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 7d439f55660dd..56a6b509172c7 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 []; } From 99aae50d516ad4d14b3d5c6e551c5e8ad1dae80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20Planta=C5=A1?= Date: Tue, 10 Dec 2024 15:49:24 +0100 Subject: [PATCH 07/32] [BeanstalkMessenger] Round delay to an integer to avoid deprecation warning --- .../Tests/Transport/ConnectionTest.php | 21 +++++++++++++++++++ .../Beanstalkd/Transport/Connection.php | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) 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 f4cc745846584..bf54ac1272483 100644 --- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php @@ -330,4 +330,25 @@ public function testSendWhenABeanstalkdExceptionOccurs() $connection->send($body, $headers, $delay); } + + 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 98ab37c875d18..ff520ddb3071f 100644 --- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php @@ -123,7 +123,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) { From 65b370e63ea15607c495a538ebc266df7fd59adf Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Tue, 10 Dec 2024 12:14:43 +0100 Subject: [PATCH 08/32] [PropertyInfo] Upmerge #59012 --- .../Tests/Extractor/PhpStanExtractorTest.php | 142 ++++++++++-------- 1 file changed, 83 insertions(+), 59 deletions(-) diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index 6543a0b5a2963..ba378812e3677 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -14,20 +14,20 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; -use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy; 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\DummyGeneric; -use Symfony\Component\PropertyInfo\Tests\Fixtures\IFace; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\Php80PromotedDummy; @@ -555,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 */ @@ -972,86 +1043,39 @@ public static function allowPrivateAccessProvider(): array public function testGenericInterface() { $this->assertEquals( - [ - new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: \BackedEnum::class, - collectionValueType: new Type( - builtinType: Type::BUILTIN_TYPE_STRING, - ) - ), - ], - $this->extractor->getTypes(Dummy::class, 'genericInterface') + Type::generic(Type::object(\BackedEnum::class), Type::string()), + $this->extractor->getType(Dummy::class, 'genericInterface'), ); } /** - * @param list $expectedTypes * @dataProvider genericsProvider */ - public function testGenericsLegacy(string $property, array $expectedTypes) + public function testGenerics(string $property, Type $expectedType) { - $this->assertEquals($expectedTypes, $this->extractor->getTypes(DummyGeneric::class, $property)); + $this->assertEquals($expectedType, $this->extractor->getType(DummyGeneric::class, $property)); } /** - * @return iterable}> + * @return iterable */ public static function genericsProvider(): iterable { yield [ 'basicClass', - [ - new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: Clazz::class, - collectionValueType: new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: Dummy::class, - ) - ), - ], + Type::generic(Type::object(Clazz::class), Type::object(Dummy::class)), ]; yield [ 'nullableClass', - [ - new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: Clazz::class, - nullable: true, - collectionValueType: new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: Dummy::class, - ) - ), - ], + Type::nullable(Type::generic(Type::object(Clazz::class), Type::object(Dummy::class))), ]; yield [ 'basicInterface', - [ - new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: IFace::class, - collectionValueType: new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: Dummy::class, - ) - ), - ], + Type::generic(Type::object(IFace::class), Type::object(Dummy::class)), ]; yield [ 'nullableInterface', - [ - new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: IFace::class, - nullable: true, - collectionValueType: new Type( - builtinType: Type::BUILTIN_TYPE_OBJECT, - class: Dummy::class, - ) - ), - ], + Type::nullable(Type::generic(Type::object(IFace::class), Type::object(Dummy::class))), ]; } } From f4c44497b1ff7ab4d6c30778c7b10842f2e1c836 Mon Sep 17 00:00:00 2001 From: David Badura Date: Tue, 3 Dec 2024 15:35:23 +0100 Subject: [PATCH 09/32] Fix resolve enum in string type resolver --- .../Tests/TypeResolver/StringTypeResolverTest.php | 4 ++++ .../TypeInfo/TypeResolver/ReflectionTypeResolver.php | 11 +---------- .../TypeInfo/TypeResolver/StringTypeResolver.php | 6 ++++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index 22812267b6466..c8b22e7d717a0 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'))]; diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php index 6af8feb370ef2..39672559365b2 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php @@ -27,11 +27,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) { @@ -83,11 +78,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 a8d8c600cdbee..a20946424f15d 100644 --- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php +++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php @@ -243,14 +243,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); } From fb5a0f37decee0cd39ed41576bea4c6aea125281 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Wed, 11 Dec 2024 13:15:11 +0100 Subject: [PATCH 10/32] Bump Symfony version to 7.2.2 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 82e6dbfbd0c79..66644f2cbccfe 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,12 +73,12 @@ 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-DEV'; + public const VERSION_ID = 70202; public const MAJOR_VERSION = 7; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 1; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 2; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '07/2025'; public const END_OF_LIFE = '07/2025'; From 9005f2594448acc2711355f9074ed4f566085b3f Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Wed, 11 Dec 2024 16:52:17 +0100 Subject: [PATCH 11/32] [PropertyInfo] Fix generic enum test --- .../PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index ba378812e3677..0d77497c2e1da 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -1043,7 +1043,7 @@ public static function allowPrivateAccessProvider(): array public function testGenericInterface() { $this->assertEquals( - Type::generic(Type::object(\BackedEnum::class), Type::string()), + Type::generic(Type::enum(\BackedEnum::class), Type::string()), $this->extractor->getType(Dummy::class, 'genericInterface'), ); } From 080f39e6a1c7ac23d2975cb6bb902b04cc303f53 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 12 Dec 2024 09:52:07 +0100 Subject: [PATCH 12/32] bump lowest required TypeInfo component version --- src/Symfony/Component/PropertyInfo/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From d8fa076a63d4cb4db7e58c7f15ac2e6b2bb1ebbc Mon Sep 17 00:00:00 2001 From: Tim Goudriaan Date: Sat, 7 Dec 2024 12:26:50 +0100 Subject: [PATCH 13/32] [Scheduler] Fix optional count variable in testGetNextRunDates --- .../Scheduler/Tests/Trigger/PeriodicalTriggerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php index 22162e792fa24..8f1ce90fee72a 100644 --- a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php +++ b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php @@ -108,9 +108,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 From 0332f5ea46ded09fd8530bfb483fe4252e7a1028 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Sat, 14 Dec 2024 16:09:22 +0100 Subject: [PATCH 14/32] Remove 5.4 branch from PR template --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From cf64a866f7323c2a2643f3bea5eeffd7994356db Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 15 Dec 2024 11:26:42 +0100 Subject: [PATCH 15/32] don't require fake notifier transports to be installed as non-dev dependencies --- .../FrameworkExtension.php | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 36984e7398528..aed6bdefa2c6f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -122,6 +122,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; @@ -2812,8 +2814,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', @@ -2891,20 +2891,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 (isset($config['admin_recipients'])) { From fd4e4930f21b6bd69e86e052492c3110633edd47 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 16 Dec 2024 14:21:39 +0100 Subject: [PATCH 16/32] choose the correctly cased class name for the MariaDB platform --- .../Doctrine/Tests/Transport/ConnectionTest.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 5e99e9d82579a..4ff2d53fc9b2a 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php @@ -14,7 +14,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; @@ -590,9 +589,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', ]; } From 8448227905e52e35fcdb6a777445abedb53c2e32 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 16 Dec 2024 16:09:00 +0100 Subject: [PATCH 17/32] require the writer to implement getFormats() in the translation:extract --- .../Command/TranslationUpdateCommand.php | 4 +++ .../Compiler/TranslationUpdateCommandPass.php | 31 +++++++++++++++++++ .../FrameworkBundle/FrameworkBundle.php | 2 ++ 3 files changed, 37 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index 91e7e33491d97..0ffe6a949d472 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -64,6 +64,10 @@ public function __construct(TranslationWriterInterface $writer, TranslationReade { parent::__construct(); + if (!method_exists($writer, 'getFormats')) { + throw new \InvalidArgumentException(sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class)); + } + $this->writer = $writer; $this->reader = $reader; $this->extractor = $extractor; 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/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 0da10da9d77f8..c371d10dbc684 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -20,6 +20,7 @@ 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\TranslationUpdateCommandPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\VirtualRequestStackPass; use Symfony\Component\Cache\Adapter\ApcuAdapter; @@ -193,6 +194,7 @@ public function build(ContainerBuilder $container) // 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); From 223dcd1af264e0b3fa07e8280548968dbc3aa17a Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 16 Dec 2024 17:04:34 +0100 Subject: [PATCH 18/32] [HttpFoundation] Avoid mime type guess with temp files in `BinaryFileResponse` --- .../Component/HttpFoundation/BinaryFileResponse.php | 7 ++++++- .../HttpFoundation/Tests/BinaryFileResponseTest.php | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index a2b160f8a2cc7..0f5b3fca55d45 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 77bc32e8c5abc..d4d84b305b7fe 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php @@ -458,4 +458,15 @@ public function testCreateFromTemporaryFile() $string = ob_get_clean(); $this->assertSame('foo,bar', $string); } + + 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')); + } } From f38d4b07fafcf9b198597ef07ec971512dcf7eaa Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Thu, 5 Dec 2024 18:36:22 +0100 Subject: [PATCH 19/32] [Messenger] ensure exception on rollback does not hide previous exception --- .../DoctrineTransactionMiddleware.php | 12 ++++++-- .../DoctrineTransactionMiddlewareTest.php | 30 +++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) 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); From e6ec2126c8c74b7690883a16ea7fe410eb671b6a Mon Sep 17 00:00:00 2001 From: Jontsa Date: Wed, 18 Dec 2024 14:18:31 +0200 Subject: [PATCH 20/32] [HttpClient] Fix a typo in NoPrivateNetworkHttpClient --- .../HttpClient/NoPrivateNetworkHttpClient.php | 2 +- .../Tests/NoPrivateNetworkHttpClientTest.php | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php index 1a81155e76811..4094f98806323 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/NoPrivateNetworkHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php index fb940790b0b3f..06ffc128187cf 100644 --- a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php @@ -173,6 +173,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])); From c3cf57755ce7a3a163dc9445631b0d3711156511 Mon Sep 17 00:00:00 2001 From: Alex Niedre Date: Wed, 18 Dec 2024 15:16:32 +0100 Subject: [PATCH 21/32] bug #54854 [Stopwatch] undefined key error when trying to fetch a missing section --- src/Symfony/Component/Stopwatch/Stopwatch.php | 2 +- src/Symfony/Component/Stopwatch/Tests/StopwatchTest.php | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) 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')); + } } From 85a5d13d6bbb74a796b0432f9baab0c3028070e4 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Thu, 19 Dec 2024 14:05:52 +0100 Subject: [PATCH 22/32] relax assertions on generated hashes --- .../Twig/Tests/Extension/HttpKernelExtensionTest.php | 2 +- .../FrameworkBundle/Tests/Functional/FragmentTest.php | 2 +- .../HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php | 8 ++++---- .../Tests/Fragment/HIncludeFragmentRendererTest.php | 2 +- .../HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index 9b7eb0b1165c8..a7057fda57d88 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -81,7 +81,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/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/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() ); } From 6cd974bcdc778f0f77e90ae5b182c276bf631e36 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 20 Dec 2024 10:56:48 +0100 Subject: [PATCH 23/32] [Security/Csrf] Trust "Referer" at the same level as "Origin" --- .../Security/Csrf/SameOriginCsrfTokenManager.php | 16 ++++++++++++++-- .../Tests/SameOriginCsrfTokenManagerTest.php | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) 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); From cc8670d3b75d1f7f948d06e93450fb010c95095b Mon Sep 17 00:00:00 2001 From: Mathias Arlaud Date: Fri, 20 Dec 2024 12:24:04 +0100 Subject: [PATCH 24/32] [TypeInfo] Fix PHPDoc resolving of union with mixed --- .../Tests/TypeResolver/StringTypeResolverTest.php | 2 ++ .../TypeInfo/TypeResolver/StringTypeResolver.php | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index c2c7eb03615e7..9320987c6baed 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -155,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/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php index 15fff64aebb93..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) { From 7f251dafdc9e29fa8f4f70d9a20296cb4f01c026 Mon Sep 17 00:00:00 2001 From: Dario Guarracino Date: Thu, 26 Dec 2024 20:01:29 +0100 Subject: [PATCH 25/32] [PropertyInfo] Remove `@internal` directives to allow extensions with no static-analysis errors --- src/Symfony/Component/PropertyInfo/PropertyReadInfo.php | 2 -- src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php | 2 -- 2 files changed, 4 deletions(-) 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 { From a5d95be04de972aadbf773b501a1eca5f8b522a0 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 27 Dec 2024 12:59:17 +0100 Subject: [PATCH 26/32] the "max" option can be zero --- src/Symfony/Component/Validator/Constraints/Count.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 399524f1de662d536b18c8f374f9c831a1bf86d7 Mon Sep 17 00:00:00 2001 From: BahmanMD Date: Mon, 23 Dec 2024 00:32:24 +0330 Subject: [PATCH 27/32] Update validators.fa.xlf --- .../Resources/translations/validators.fa.xlf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 }}" باشد. From a247d5851922578c182d46886f976725e961cf47 Mon Sep 17 00:00:00 2001 From: matlec Date: Sun, 29 Dec 2024 14:51:37 +0100 Subject: [PATCH 28/32] [Finder] Fix using `==` as default operator in `DateComparator` --- src/Symfony/Component/Finder/Comparator/DateComparator.php | 2 +- .../Component/Finder/Tests/Comparator/DateComparatorTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Finder/Comparator/DateComparator.php b/src/Symfony/Component/Finder/Comparator/DateComparator.php index e0c523d05523b..f7c27de677fb1 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')]], ]; } } From a90f6e72e90ef1bb1f4c9041a14d877f0487842a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 29 Dec 2024 21:27:14 +0100 Subject: [PATCH 29/32] reject URLs containing whitespaces --- .../Tests/TextSanitizer/UrlSanitizerTest.php | 28 +++++++++---------- .../TextSanitizer/UrlSanitizer.php | 8 +++++- 2 files changed, 21 insertions(+), 15 deletions(-) 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; } From d44b7afa5b5dfc84c7e1ab76ecb159d100bbbd6d Mon Sep 17 00:00:00 2001 From: MatTheCat Date: Sat, 21 Dec 2024 20:27:11 +0100 Subject: [PATCH 30/32] [SecurityBundle] Do not replace authenticators service by their traceable version --- .../DependencyInjection/SecurityExtension.php | 10 ++++--- .../SecurityExtensionTest.php | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) 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(); From e9e6074354d4a977d08c9d8f0fbca93d7149438c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 31 Dec 2024 15:59:37 +0100 Subject: [PATCH 31/32] Update CHANGELOG for 7.2.2 --- CHANGELOG-7.2.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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) From 9a930949516a90703810891fc85795ea1a910066 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 31 Dec 2024 15:59:40 +0100 Subject: [PATCH 32/32] Update VERSION for 7.2.2 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 66644f2cbccfe..387b51c8a4fce 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,12 +73,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '7.2.2-DEV'; + 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 = 2; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2025'; public const END_OF_LIFE = '07/2025';