Version in base suite: 6.4.41+dfsg-0+deb13u1
Version in overlay suite: 6.4.49+dfsg-0+deb13u1
Base version: symfony_6.4.49+dfsg-0+deb13u1
Target version: symfony_6.4.50+dfsg-0+deb13u1
Base file: /srv/ftp-master.debian.org/ftp/pool/main/s/symfony/symfony_6.4.49+dfsg-0+deb13u1.dsc
Target file: /srv/ftp-master.debian.org/policy/pool/main/s/symfony/symfony_6.4.50+dfsg-0+deb13u1.dsc
.agents/skills/pr-review-merge-prep/SKILL.md | 10
.github/composer-config.json | 3
.github/dependabot.yml | 6
.github/expected-missing-return-types.diff | 6
.github/sync-maintained-versions.php | 21
.github/workflows/scorecards.yml | 63
.github/workflows/zizmor.yml | 2
CHANGELOG-6.4.md | 206 ++
debian/changelog | 55
debian/patches/Assume-php-async-aws-sqs-2.patch | 4
debian/patches/Cache-Disable-Test-failing-tests-Error-Class-not-found.patch | 55
debian/patches/Drop-composer-runtime-api-that-confuses-phpabtpl.patch | 2
debian/patches/Filesystem-Drop-currently-failing-tests.patch | 114 +
debian/patches/Group-nophpunit11-for-tests-failing-with-PHPUnit-11.patch | 46
debian/patches/HttpClient-Skip-tests-relying-on-amphp-http-client.patch | 4
debian/patches/Lock-Drop-test-currently-failing-on-buildd.patch | 4
debian/patches/Remove-calls-to-getExpectedException.patch | 2
debian/patches/Yaml-Drop-data-set-currently-failing-on-i386.patch | 6
src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php | 2
src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php | 2
src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php | 48
src/Symfony/Bundle/SecurityBundle/composer.json | 2
src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php | 6
src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php | 5
src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.php | 2
src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php | 46
src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php | 14
src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php | 1
src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php | 6
src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php | 21
src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php | 6
src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php | 4
src/Symfony/Component/AssetMapper/ImportMap/RemotePackageStorage.php | 5
src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php | 13
src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php | 8
src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php | 29
src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php | 17
src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php | 110 +
src/Symfony/Component/BrowserKit/Cookie.php | 2
src/Symfony/Component/BrowserKit/CookieJar.php | 29
src/Symfony/Component/BrowserKit/Tests/CookieJarTest.php | 106 +
src/Symfony/Component/BrowserKit/Tests/CookieTest.php | 7
src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php | 50
src/Symfony/Component/Cache/Adapter/AbstractAdapter.php | 2
src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php | 2
src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php | 2
src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php | 2
src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php | 2
src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php | 16
src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php | 2
src/Symfony/Component/Cache/Adapter/PdoAdapter.php | 2
src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php | 11
src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php | 8
src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php | 40
src/Symfony/Component/Cache/Messenger/EarlyExpirationResult.php | 24
src/Symfony/Component/Cache/Tests/Adapter/AbstractAdapterCreateConnectionTest.php | 59
src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTestCase.php | 18
src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php | 18
src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php | 38
src/Symfony/Component/Cache/Tests/Adapter/FilesystemTagAwareAdapterTest.php | 14
src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php | 18
src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php | 11
src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php | 106 +
src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php | 274 +++
src/Symfony/Component/Cache/Traits/RedisTrait.php | 19
src/Symfony/Component/Cache/Traits/Relay/Relay61Trait.php | 32
src/Symfony/Component/Cache/Traits/RelayProxy.php | 2
src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php | 26
src/Symfony/Component/Config/Util/XmlUtils.php | 1
src/Symfony/Component/Console/Helper/OutputWrapper.php | 4
src/Symfony/Component/Console/Helper/ProcessHelper.php | 3
src/Symfony/Component/Console/Helper/ProgressBar.php | 37
src/Symfony/Component/Console/Helper/QuestionHelper.php | 4
src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php | 4
src/Symfony/Component/Console/Helper/Table.php | 64
src/Symfony/Component/Console/Logger/ConsoleLogger.php | 3
src/Symfony/Component/Console/Style/SymfonyStyle.php | 2
src/Symfony/Component/Console/Tests/Helper/OutputWrapperTest.php | 10
src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php | 2
src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php | 335 ++++
src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php | 25
src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php | 17
src/Symfony/Component/Console/Tests/Helper/TableTest.php | 225 ++
src/Symfony/Component/Console/Tests/Logger/ConsoleLoggerTest.php | 7
src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php | 35
src/Symfony/Component/Filesystem/Filesystem.php | 60
src/Symfony/Component/Filesystem/Path.php | 22
src/Symfony/Component/Filesystem/Tests/FilesystemTest.php | 309 +++
src/Symfony/Component/Filesystem/Tests/Fixtures/remove_collision.php | 32
src/Symfony/Component/Filesystem/Tests/Fixtures/remove_without_rename.php | 29
src/Symfony/Component/Filesystem/Tests/PathTest.php | 63
src/Symfony/Component/Finder/Glob.php | 16
src/Symfony/Component/Finder/Tests/GlobTest.php | 17
src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php | 2
src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php | 9
src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php | 2
src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php | 92 -
src/Symfony/Component/Form/NativeRequestHandler.php | 2
src/Symfony/Component/Form/Resources/translations/validators.da.xlf | 28
src/Symfony/Component/Form/Resources/translations/validators.de.xlf | 26
src/Symfony/Component/Form/Resources/translations/validators.el.xlf | 42
src/Symfony/Component/Form/Resources/translations/validators.et.xlf | 28
src/Symfony/Component/Form/Resources/translations/validators.hr.xlf | 24
src/Symfony/Component/Form/Resources/translations/validators.it.xlf | 24
src/Symfony/Component/Form/Resources/translations/validators.ja.xlf | 22
src/Symfony/Component/Form/Resources/translations/validators.lb.xlf | 26
src/Symfony/Component/Form/Resources/translations/validators.nl.xlf | 24
src/Symfony/Component/Form/Resources/translations/validators.pl.xlf | 24
src/Symfony/Component/Form/Resources/translations/validators.sk.xlf | 30
src/Symfony/Component/Form/Resources/translations/validators.sl.xlf | 28
src/Symfony/Component/Form/Resources/translations/validators.sr_Cyrl.xlf | 24
src/Symfony/Component/Form/Resources/translations/validators.sr_Latn.xlf | 24
src/Symfony/Component/Form/Resources/translations/validators.tr.xlf | 26
src/Symfony/Component/Form/Resources/translations/validators.uk.xlf | 28
src/Symfony/Component/Form/Resources/translations/validators.vi.xlf | 40
src/Symfony/Component/Form/Resources/translations/validators.zh_CN.xlf | 42
src/Symfony/Component/Form/Resources/translations/validators.zh_TW.xlf | 28
src/Symfony/Component/Form/Tests/AbstractRequestHandlerTestCase.php | 18
src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToStringTransformerTest.php | 1
src/Symfony/Component/Form/Tests/Extension/Core/Type/CollectionTypeTest.php | 69
src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php | 28
src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorFunctionalTest.php | 136 +
src/Symfony/Component/Form/Tests/Extension/Validator/Type/CollectionTypeValidatorExtensionTest.php | 28
src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php | 256 +++
src/Symfony/Component/Form/Util/ServerParams.php | 2
src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerCustomTest.php | 339 ++++
src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php | 200 ++
src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php | 21
src/Symfony/Component/HtmlSanitizer/Visitor/AttributeSanitizer/MetaRefreshAttributeSanitizer.php | 65
src/Symfony/Component/HtmlSanitizer/Visitor/AttributeSanitizer/UrlAttributeSanitizer.php | 109 +
src/Symfony/Component/HttpClient/CurlHttpClient.php | 11
src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php | 2
src/Symfony/Component/HttpClient/Response/AmpResponse.php | 19
src/Symfony/Component/HttpClient/Response/AsyncResponse.php | 9
src/Symfony/Component/HttpClient/Response/CommonResponseTrait.php | 30
src/Symfony/Component/HttpClient/Response/CurlResponse.php | 39
src/Symfony/Component/HttpClient/Response/NativeResponse.php | 19
src/Symfony/Component/HttpClient/Response/TraceableResponse.php | 4
src/Symfony/Component/HttpClient/ScopingHttpClient.php | 8
src/Symfony/Component/HttpClient/Tests/AsyncDecoratorTraitTest.php | 26
src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php | 18
src/Symfony/Component/HttpClient/Tests/Fixtures/cancel_from_on_progress.php | 24
src/Symfony/Component/HttpClient/Tests/Fixtures/cancel_from_on_progress_in_stream.php | 33
src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php | 62
src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php | 5
src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php | 23
src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php | 19
src/Symfony/Component/HttpClient/Tests/ScopingHttpClientTest.php | 97 +
src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php | 30
src/Symfony/Component/HttpFoundation/BinaryFileResponse.php | 2
src/Symfony/Component/HttpFoundation/HeaderUtils.php | 2
src/Symfony/Component/HttpFoundation/IpUtils.php | 36
src/Symfony/Component/HttpFoundation/Request.php | 36
src/Symfony/Component/HttpFoundation/RequestMatcher.php | 5
src/Symfony/Component/HttpFoundation/RequestMatcher/MethodRequestMatcher.php | 5
src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php | 4
src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php | 16
src/Symfony/Component/HttpFoundation/Tests/IpUtilsTest.php | 49
src/Symfony/Component/HttpFoundation/Tests/RequestMatcher/MethodRequestMatcherTest.php | 10
src/Symfony/Component/HttpFoundation/Tests/RequestMatcherTest.php | 9
src/Symfony/Component/HttpFoundation/Tests/RequestTest.php | 179 ++
src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php | 32
src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php | 67
src/Symfony/Component/HttpFoundation/UrlHelper.php | 24
src/Symfony/Component/HttpKernel/EventListener/CacheAttributeListener.php | 4
src/Symfony/Component/HttpKernel/EventListener/RouterListener.php | 35
src/Symfony/Component/HttpKernel/Kernel.php | 6
src/Symfony/Component/HttpKernel/Profiler/FileProfilerStorage.php | 2
src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php | 33
src/Symfony/Component/HttpKernel/Tests/EventListener/RouterListenerTest.php | 23
src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php | 56
src/Symfony/Component/HttpKernel/Tests/Profiler/FileProfilerStorageTest.php | 13
src/Symfony/Component/Lock/Store/DoctrineDbalStore.php | 2
src/Symfony/Component/Lock/Store/MongoDbStore.php | 8
src/Symfony/Component/Lock/Store/StoreFactory.php | 2
src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php | 18
src/Symfony/Component/Lock/Tests/Store/MongoDbStoreDsnTest.php | 37
src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php | 12
src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/AmazonSqsTransportFactoryTest.php | 21
src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/ConnectionTest.php | 32
src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/AmazonSqsTransportFactory.php | 4
src/Symfony/Component/Messenger/Bridge/AmazonSqs/Transport/Connection.php | 3
src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpSenderTest.php | 35
src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/AmqpTransportFactoryTest.php | 19
src/Symfony/Component/Messenger/Bridge/Amqp/Tests/Transport/ConnectionTest.php | 22
src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpFactory.php | 2
src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpStamp.php | 7
src/Symfony/Component/Messenger/Bridge/Amqp/Transport/AmqpTransportFactory.php | 4
src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php | 6
src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php | 23
src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php | 4
src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php | 57
src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php | 18
src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/ConnectionTest.php | 112 +
src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisReceiverTest.php | 48
src/Symfony/Component/Messenger/Bridge/Redis/Tests/Transport/RedisTransportFactoryTest.php | 20
src/Symfony/Component/Messenger/Bridge/Redis/Transport/Connection.php | 39
src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisReceiver.php | 6
src/Symfony/Component/Messenger/Bridge/Redis/Transport/RedisTransportFactory.php | 4
src/Symfony/Component/Messenger/Handler/BatchHandlerTrait.php | 13
src/Symfony/Component/Messenger/Middleware/HandleMessageMiddleware.php | 16
src/Symfony/Component/Messenger/Tests/Handler/BatchHandlerTest.php | 39
src/Symfony/Component/Messenger/Tests/Middleware/HandleMessageMiddlewareTest.php | 104 +
src/Symfony/Component/Messenger/Tests/Transport/Serialization/PhpSerializerTest.php | 10
src/Symfony/Component/Messenger/Tests/Transport/Serialization/SerializerTest.php | 39
src/Symfony/Component/Messenger/Tests/Transport/TransportFactoryTest.php | 39
src/Symfony/Component/Messenger/Tests/WorkerTest.php | 791 ++++++++++
src/Symfony/Component/Messenger/Transport/Serialization/PhpSerializer.php | 2
src/Symfony/Component/Messenger/Transport/Serialization/Serializer.php | 21
src/Symfony/Component/Messenger/Transport/TransportFactory.php | 4
src/Symfony/Component/Messenger/Transport/TransportFactoryInterface.php | 4
src/Symfony/Component/Messenger/Worker.php | 191 ++
src/Symfony/Component/Notifier/Bridge/Mailjet/Tests/MailjetTransportFactoryTest.php | 2
src/Symfony/Component/Notifier/Tests/Transport/AbstractTransportFactoryTest.php | 47
src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php | 2
src/Symfony/Component/PropertyAccess/PropertyAccessor.php | 6
src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php | 55
src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php | 11
src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyIntersectionType.php | 45
src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php | 16
src/Symfony/Component/RateLimiter/Policy/FixedWindowLimiter.php | 7
src/Symfony/Component/RateLimiter/Policy/Rate.php | 4
src/Symfony/Component/RateLimiter/Policy/SlidingWindowLimiter.php | 4
src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php | 28
src/Symfony/Component/RateLimiter/Tests/Policy/RateTest.php | 16
src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php | 16
src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherDumper.php | 11
src/Symfony/Component/Routing/Tests/Fixtures/dumper/compiled_url_matcher1.php | 30
src/Symfony/Component/Routing/Tests/Fixtures/dumper/compiled_url_matcher13.php | 10
src/Symfony/Component/Routing/Tests/Fixtures/dumper/compiled_url_matcher2.php | 30
src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php | 64
src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php | 2
src/Symfony/Component/Security/Core/Resources/translations/security.vi.xlf | 4
src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php | 52
src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php | 2
src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationFailureHandler.php | 2
src/Symfony/Component/Security/Http/Authentication/DefaultAuthenticationSuccessHandler.php | 2
src/Symfony/Component/Security/Http/Authenticator/LoginLinkAuthenticator.php | 6
src/Symfony/Component/Security/Http/Authenticator/X509Authenticator.php | 86 -
src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php | 3
src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php | 3
src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php | 26
src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationFailureHandlerTest.php | 23
src/Symfony/Component/Security/Http/Tests/Authentication/DefaultAuthenticationSuccessHandlerTest.php | 27
src/Symfony/Component/Security/Http/Tests/Authenticator/LoginLinkAuthenticatorTest.php | 25
src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php | 150 +
src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php | 33
src/Symfony/Component/Security/Http/Tests/RememberMe/RememberMeDetailsTest.php | 83 +
src/Symfony/Component/Semaphore/Store/StoreFactory.php | 2
src/Symfony/Component/Semaphore/Tests/Store/StoreFactoryTest.php | 12
src/Symfony/Component/Serializer/Encoder/CsvEncoder.php | 26
src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php | 21
src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php | 7
src/Symfony/Component/Serializer/Serializer.php | 2
src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php | 37
src/Symfony/Component/Serializer/Tests/Fixtures/property-path-mapping-without-groups.yaml | 8
src/Symfony/Component/Serializer/Tests/Normalizer/DataUriNormalizerTest.php | 182 ++
src/Symfony/Component/Serializer/Tests/Normalizer/ObjectNormalizerTest.php | 73
src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php | 9
src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php | 58
src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php | 5
src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php | 21
src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php | 5
src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php | 20
src/Symfony/Component/Translation/Bridge/Phrase/PhraseProvider.php | 6
src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php | 50
src/Symfony/Component/Translation/Provider/AbstractProviderFactory.php | 2
src/Symfony/Component/Translation/Tests/Provider/AbstractProviderFactoryTest.php | 47
src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php | 2
src/Symfony/Component/Validator/Constraints/IpValidator.php | 95 +
src/Symfony/Component/Validator/Mapping/Loader/PropertyInfoLoader.php | 9
src/Symfony/Component/Validator/Resources/translations/validators.az.xlf | 4
src/Symfony/Component/Validator/Resources/translations/validators.da.xlf | 38
src/Symfony/Component/Validator/Resources/translations/validators.de.xlf | 34
src/Symfony/Component/Validator/Resources/translations/validators.et.xlf | 36
src/Symfony/Component/Validator/Resources/translations/validators.id.xlf | 4
src/Symfony/Component/Validator/Resources/translations/validators.lb.xlf | 82 -
src/Symfony/Component/Validator/Resources/translations/validators.my.xlf | 4
src/Symfony/Component/Validator/Resources/translations/validators.sk.xlf | 38
src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf | 50
src/Symfony/Component/Validator/Resources/translations/validators.th.xlf | 4
src/Symfony/Component/Validator/Resources/translations/validators.tl.xlf | 4
src/Symfony/Component/Validator/Resources/translations/validators.tr.xlf | 4
src/Symfony/Component/Validator/Resources/translations/validators.uk.xlf | 38
src/Symfony/Component/Validator/Resources/translations/validators.uz.xlf | 4
src/Symfony/Component/Validator/Resources/translations/validators.vi.xlf | 142 -
src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf | 166 +-
src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf | 86 -
src/Symfony/Component/Validator/Tests/Constraints/IpValidatorTest.php | 249 +++
src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php | 59
src/Symfony/Component/Validator/Tests/Resources/TranslationFilesTest.php | 39
src/Symfony/Component/Yaml/Escaper.php | 2
src/Symfony/Component/Yaml/Inline.php | 145 +
src/Symfony/Component/Yaml/Parser.php | 367 ++++
src/Symfony/Component/Yaml/Tests/DumperTest.php | 1
src/Symfony/Component/Yaml/Tests/InlineTest.php | 46
src/Symfony/Component/Yaml/Tests/ParserTest.php | 162 ++
297 files changed, 10259 insertions(+), 1286 deletions(-)
dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmplds8r1cr/symfony_6.4.49+dfsg-0+deb13u1.dsc: no acceptable signature found
dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmplds8r1cr/symfony_6.4.50+dfsg-0+deb13u1.dsc: no acceptable signature found
diff -Nru symfony-6.4.49+dfsg/.agents/skills/pr-review-merge-prep/SKILL.md symfony-6.4.50+dfsg/.agents/skills/pr-review-merge-prep/SKILL.md
--- symfony-6.4.49+dfsg/.agents/skills/pr-review-merge-prep/SKILL.md 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/.agents/skills/pr-review-merge-prep/SKILL.md 2026-11-04 11:23:00.000000000 +0000
@@ -13,13 +13,16 @@
- Report that attempt whichever way it went. When it failed, the report is what justifies the addition, and it is what makes the decision survive the discussion. When it succeeded, close with the working snippet rather than the refusal alone, so the author leaves with a solution instead of a no.
- Give the same scrutiny to changed behavior of existing public API. It is riskier than an addition, because nobody opts into it: establish what depends on the current behavior before accepting the change. A diff that edits existing fixtures or expectations to make them pass is announcing such a change; treat that as the signal rather than as housekeeping.
- Revert-verify every new test: put the changed source back in its base state, keep the tests, and confirm they fail with the expected error, not another one. A test that also passes on base protects existing behavior; it does not prove the fix. Be clear about the role of each test.
+- When a fix has several parts, revert each part on its own and run the tests again. For example, remove the wiring but keep the method, or replace a helper with its simplest wrong version. Each part needs a test that fails without it. A test that fails on base only with "Call to undefined method" proves that the method exists, not that the fix works.
- Undo a revert-verify without destroying the working tree. `git checkout -- ` writes the base version to the index as well, so restoring with `git checkout -- ` reads back from that index and silently discards every fix made during review. Commit or amend first and restore with `git checkout HEAD -- `, or copy the files aside and put them back by hand. Confirm with `git diff --stat` before trusting the next test run.
- Use three-state verification to judge a rework: base (bug present), PR as submitted (what it really does), final (fixed). Run the same checks in the three states. The pattern of failures across the states is the proof.
- Validate the test harness before trusting it: run it on the known-broken state and watch it fail. A check that cannot tell "read the right thing" from "fell back to a default" proves nothing. Give each outcome its own marker.
- Check the branch target in both directions. Find the commit that introduced the flaw and probe the older maintained branches directly. Bugfixes go to the oldest affected maintained branch. Behavior changes and features go to the dev branch, and a released X.Y is not the dev branch anymore. The reported symptom and the underlying flaw can have different oldest branches: check both.
+- During a feature freeze, a new feature opened on the frozen branch gets the milestone of the next version and no in-depth review. The exception is a feature that completes another feature new in the frozen branch. Performance work and refactors are not features.
- A config tree must not depend on anything external. Reject a `Configuration` class, a `configure()` method or a security factory `addConfiguration()` that reads container parameters (`kernel.debug`), env vars, another extension's raw config or machine state (`extension_loaded()`, `isSupported()`) into node defaults, normalization or validation: the same tree must come out whatever the environment or the machine, because `config:dump-reference`, `config/reference.php` and `config/schema.json` are generated from it. Such a default belongs in the extension, behind a `defaultNull()` node whose `info()` names the fallback, or behind a static `'%kernel.debug%'` default that the extension resolves.
- Generated files must be reproducible. The same sources at the same path must give the same bytes, whatever the machine, the clock or what else the process did before. This covers the dumped container and its service ids, the output of cache warmers, compiled templates and `.meta` files. Reject a change that puts any of the following into them, or into a hash used as an id or a file name: the current time, random bytes, `spl_object_id()`, a static counter, a file mtime or inode, or an order that comes from the filesystem (a `Finder` without a sort, `RecursiveDirectoryIterator`, `glob()` with `GLOB_NOSORT`). Take the build time from the `container.build_time` parameter, never from the clock. Keep absolute paths out of hashed ids, and make generated paths relative to the project or build directory where the generated code allows it, as the container dumper does. When a freshness check needs filesystem state, keep that state out of what is written without debug, and out of what is written when `SOURCE_DATE_EPOCH` is set. To settle a doubt, build the same app twice, once from a copy at another path, and diff the two `var/cache/prod` directories.
- List the edge cases and run them. Do not accept plausible reasoning when a probe can answer: nullable, union and interface types, empty input, recursion, encodings, interaction with sibling state.
+- Check state that outlives a request. Long-running runtimes, such as FrankenPHP worker mode, RoadRunner or Swoole, keep services between requests. A service that changes its own properties while handling a request must be reset before the next request, usually with the `kernel.reset` tag. Check when the reset runs: it must not happen in the middle of a request. Look for other services with the same flaw. A counter updated by an enter/leave pair without `try`/`finally` stays wrong after an exception.
- Check every symbol the change borrows from another package against that package's declared version constraint. A symbol added in the current release cannot be satisfied by the oldest version the constraint allows, so the call site needs an existence guard, or the test belongs in the package that owns the code. The lowest-dependency job is only the symptom: the same code is broken at runtime for anyone on that older version.
- Test composition: rebase onto the current base tip and rerun. When sibling PRs touch the same area, also run the combined result.
- Account for every failing CI job: reproduce it when possible and classify it as caused by the PR or pre-existing. A red job nobody can explain is a finding. When comparing against a baseline run, check its date and not only its conclusion: a branch tip that saw no push for hours keeps a stale green run, while CI installs current dependencies on every run, which makes an unrelated change look like the regressor.
@@ -41,7 +44,8 @@
- Preserve the contributor's authorship: amend into their commit, or rebuild their commits with the same author. Keep a commit structure that has meaning, such as a tests-then-fix pair. Update the commit message to cover what was added; a message must always match its diff.
- Authorship is two fields. Amending keeps the author and rewrites the committer, and the committer is what the forge shows next to the commit and what stays in public history. Never pass a placeholder identity to `git commit` or `git rebase`: the repository's configured identity is already the right one. Check with `git log -1 --format='%an <%ae> / %cn <%ce>'` before every push, because merged history cannot be fixed.
- Fixes added during review follow the same TDD rule as the original bug: failing test first, then the fix.
-- Retargeting to another branch is a rewrite, not a copy: express the fix in the target branch's code shape and conventions (available APIs, test style, annotations or attributes, tool versions). Rebuild tests that depend on things the target branch lacks. Record the known merge-up resolution for the higher branch in the pull request description, under a `Merge-up to :` paragraph, so the merger does not have to rediscover it.
+- When review finds a bug in what the pull request changes, amend the fix into the pull request. A bug that was there before, in code the pull request does not touch, gets its own pull request on its oldest affected branch. Start its description with "Found while reviewing #NNNNN". Do not make the reviewed pull request bigger. Several such bugs in the same file go in one pull request, one commit each.
+- Retargeting to another branch is a rewrite, not a copy: express the fix in the target branch's code shape and conventions (available APIs, test style, annotations or attributes, tool versions). Rebuild tests that depend on things the target branch lacks. Record the known merge-up resolution for the higher branch in the pull request description, under a `Merge-up to :` paragraph, so the merger does not have to rediscover it. When the notes are long, put them at the end of the description in a `Merge-up notes` block. Notes are long when they cover several branches or include a patch to apply after the merge-up. Put such a patch in a `diff` code block.
- When the rework sits in local commits on top of the contributor's, squash before rebasing. Otherwise the rebase replays the original commit first and raises conflicts against content the rework already replaced.
- A conflict-free rebase can still produce broken code: an import the source branch happened to have, a test runner that ignores newer test syntax. The test run decides, not the rebase.
- Rebase onto the current base tip and confirm the parent commit equals it. Check again just before pushing: active repositories move. Run rebases and merges non-interactively (`GIT_EDITOR=true`).
@@ -50,7 +54,7 @@
- Run scoped tests while iterating and the full suite of each touched component before calling it done; never run the whole monorepo at once. Suites are addressed by path: `./phpunit src/Symfony/Component/`, and the same form for `src/Symfony/Bridge/` and `src/Symfony/Bundle/`, while everything under `src/Symfony/Contracts` is a single suite. Check static analysis and lowest-dependency jobs when the change can affect them. If an assertion depends on the installed dependency version, use feature detection instead of assuming one version.
- Retarget before pushing, never after. CI reads the base branch from the pull request event at push time, so pushing a branch rebased onto a new base while the PR still points at the old one makes every job diff against the wrong branch: a diff of thousands of files, style checks on code the PR never touched, and a wall of red that a three-file change cannot explain. Rerunning does not help, since the event is unchanged.
- Push to the PR's real head with an explicit `HEAD:` refspec, since the work happens on a detached HEAD. The fork's repository name can differ from the upstream one; query it.
-- Update the PR's metadata to match what it has become: title and component prefix, header table, description, labels, base and milestone. On a PR that sat, the `Branch?` row names a branch that has since shipped, the description keeps wording the review already corrected, and the component prefix can name the wrong package. Leave exactly one status label, and none the diff no longer justifies.
+- Update the PR's metadata to match what it has become: title and component prefix, header table, description, labels, base and milestone. On a PR that sat, the `Branch?` row names a branch that has since shipped, the description keeps wording the review already corrected, and the component prefix can name the wrong package. Leave exactly one status label, and none the diff no longer justifies. A change that only touches tests is not a bug fix, even when it fixes a flaky test. Its `Bug fix?` row says `no`, and it gets no `Bug` label. Keep the milestone in sync with the base branch, including after a retarget. Carsonbot sets the milestone from the base branch when the pull request is opened, and replaces a milestone set at that moment. Set the milestone after the bot has labeled the pull request, then check it again.
- Treat the description as the first draft of the documentation, not as a review log. Write it for someone who never saw the discussion: what the feature does, the public API it adds, the options and their defaults, which choice to make when, and the traps. For a feature, end with the points the documentation must carry, so whoever opens the symfony-docs pull request does not have to reconstruct them from the diff.
- Apply corrections instead of suggesting them. The branch is being rewritten anyway, so a review comment asking for a one-line change is a pointless round trip. This covers the contributor's own code comments and test names when they describe something the code does not do.
- Routine merge prep needs no comment at all: a rebase, a changelog fix, a constraint bump and a style fix are all visible in the diff. Comment when the reader has to decide or react.
@@ -58,6 +62,7 @@
- Keep changelog entries to one line each, naming the new public surface and nothing more. Reasoning, comparison tables and usage guidance belong in the documentation. A changelog entry that has to explain itself is a sign the documentation entry is missing.
- Apply the house rules to inherited content too: when amending someone else's commit, remove what the rules forbid instead of keeping it. Scan the added diff lines for violations before pushing.
- Finish on GitHub: post review feedback as a real review with evidence, close replaced issues with a factual explanation, and correct wrong claims left in threads.
+- Fetch the thread and the live description again right before posting a comment or replacing the description. The author or another maintainer may have answered or edited it while you worked, and a description rebuilt from an old copy drops their edits.
- When closing on the merits, state the decision and the reason and stop. Do not invite a fresh pull request, since that reopens the same discussion under a new number. When closing a stale pull request nobody rejected, a short line welcoming a fresh take is the right tone instead.
## House rules
@@ -70,6 +75,7 @@
- Keep a factual tone in everything published: findings and evidence, no self-promotion, no filler.
- Never apologize for a late or missing review. Thank the author for their patience and stop there. Do not editorialize about the project's failure to review; credit what the author did well instead.
- Comments are published under the maintainer's own account, so write their past review comments in the first person and everyone else's in the third.
+- Refer to earlier changes by their pull request number, not by a commit sha. The number leads to the discussion.
- Private methods go at the end of the class, after all public and protected ones. Do not move existing ones in the same patch.
- Do not call a Symfony or third-party method or constructor with PHP named arguments, in a patch you write or in one you review. Parameter names are outside the backward compatibility promise except for the constructors of attribute classes (note 10 of https://symfony.com/doc/current/contributing/code/bc.html), so a renamed parameter breaks such a call in a minor release. Pass the intermediate defaults explicitly to reach a later optional parameter. Named arguments stay fine for attribute constructors and for PHP's own functions, whose parameter names are part of the language.
- Use plain English: common words, short sentences, one idea per sentence. Avoid idioms, cultural references and rare vocabulary. Most readers are not native speakers.
diff -Nru symfony-6.4.49+dfsg/.github/composer-config.json symfony-6.4.50+dfsg/.github/composer-config.json
--- symfony-6.4.49+dfsg/.github/composer-config.json 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/.github/composer-config.json 2026-11-04 11:23:00.000000000 +0000
@@ -14,6 +14,9 @@
},
"allow-plugins": {
"symfony/flex": true
+ },
+ "audit": {
+ "block-insecure": false
}
}
}
diff -Nru symfony-6.4.49+dfsg/.github/dependabot.yml symfony-6.4.50+dfsg/.github/dependabot.yml
--- symfony-6.4.49+dfsg/.github/dependabot.yml 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/.github/dependabot.yml 2026-11-04 11:23:00.000000000 +0000
@@ -1,6 +1,6 @@
# https://docs.github.com/en/code-security/reference/supply-chain-security/dependabot-options-reference
-# Generated by .github/sync-maintained-versions.php from https://symfony.com/maintained-versions.json
+# Generated by .github/sync-maintained-versions.php from https://symfony.com/releases.json
# One entry per maintained branch; cooldown increases in merge-up order so the lowest branch surfaces first.
version: 2
@@ -40,7 +40,7 @@
patterns:
- '*'
cooldown:
- default-days: 10
+ default-days: 9
- package-ecosystem: 'github-actions'
directory: '/'
@@ -52,4 +52,4 @@
patterns:
- '*'
cooldown:
- default-days: 11
+ default-days: 10
diff -Nru symfony-6.4.49+dfsg/.github/expected-missing-return-types.diff symfony-6.4.50+dfsg/.github/expected-missing-return-types.diff
--- symfony-6.4.49+dfsg/.github/expected-missing-return-types.diff 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/.github/expected-missing-return-types.diff 2026-11-04 11:23:00.000000000 +0000
@@ -1428,7 +1428,7 @@
--- a/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php
+++ b/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php
@@ -38,5 +38,5 @@ class EarlyExpirationDispatcher
- * @return mixed
+ * @return mixed The computed value, or the cached value when recomputation is deferred or skipped
*/
- public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null, ?float $beta = null)
+ public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null, ?float $beta = null): mixed
@@ -1438,10 +1438,10 @@
--- a/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php
+++ b/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php
@@ -33,5 +33,5 @@ class EarlyExpirationHandler
- * @return void
+ * @return EarlyExpirationResult|null
*/
- public function __invoke(EarlyExpirationMessage $message)
-+ public function __invoke(EarlyExpirationMessage $message): void
++ public function __invoke(EarlyExpirationMessage $message): ?EarlyExpirationResult
{
$item = $message->getItem();
diff --git a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php
diff -Nru symfony-6.4.49+dfsg/.github/sync-maintained-versions.php symfony-6.4.50+dfsg/.github/sync-maintained-versions.php
--- symfony-6.4.49+dfsg/.github/sync-maintained-versions.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/.github/sync-maintained-versions.php 2026-11-04 11:23:00.000000000 +0000
@@ -1,7 +1,7 @@
high).
+// https://symfony.com/releases.json (ordered low -> high).
//
// Standalone helper: run it by hand whenever the maintained branches change.
//
@@ -10,7 +10,6 @@
// branch; cooldown.default-days grows by one
// in merge-up order so the lowest branch
// surfaces first and the review queue is staggered.
-// - .github/workflows/scorecards.yml push trigger pinned to the default (highest) branch.
// - .github/workflows/unit-tests.yml LTS test versions for the current branch: cross-test
// against the latest LTS at or below it, low-deps floor
// against the LTS one major below that.
@@ -25,8 +24,8 @@
}
$self = basename(__FILE__);
-$source = 'https://symfony.com/maintained-versions.json';
-$branches = json_decode(file_get_contents($source), true, 512, \JSON_THROW_ON_ERROR);
+$source = 'https://symfony.com/releases.json';
+$branches = json_decode(file_get_contents($source), true, 512, \JSON_THROW_ON_ERROR)['maintained_versions'] ?? null;
if (!\is_array($branches) || !$branches) {
fwrite(\STDERR, "No maintained versions found at $source\n");
@@ -34,7 +33,6 @@
}
$branches = array_values($branches);
-$default = end($branches); // highest branch == dev == default
$check = \in_array('--check', $argv, true);
// Identify the checked-out branch from the source tree itself (robust to detached
@@ -98,19 +96,6 @@
$dirty = $sync(__DIR__.'/dependabot.yml', rtrim($dependabot)."\n");
-// --- .github/workflows/scorecards.yml (push trigger only) -------------------
-
-$scorecards = __DIR__.'/workflows/scorecards.yml';
-$yaml = file_get_contents($scorecards);
-$patched = preg_replace('/^(\s*branches:\s*\[\s*")[^"]*("\s*\])/m', '${1}'.$default.'${2}', $yaml, 1, $count);
-
-if (1 !== $count) {
- fwrite(\STDERR, "Could not find the push branch trigger in scorecards.yml\n");
- exit(1);
-}
-
-$dirty = $sync($scorecards, $patched) || $dirty;
-
// --- .github/workflows/unit-tests.yml (LTS test versions) -------------------
// LTS releases are the *.4 maintained versions. Cross-test against the latest one
diff -Nru symfony-6.4.49+dfsg/.github/workflows/scorecards.yml symfony-6.4.50+dfsg/.github/workflows/scorecards.yml
--- symfony-6.4.49+dfsg/.github/workflows/scorecards.yml 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/.github/workflows/scorecards.yml 1970-01-01 00:00:00.000000000 +0000
@@ -1,63 +0,0 @@
-name: Scorecards supply-chain security
-
-on:
- # Only the default branch is supported.
- branch_protection_rule:
- schedule:
- - cron: '34 4 * * 6'
- push:
- branches: [ "8.2" ]
-
-# Declare default permissions as read only.
-permissions: read-all
-
-jobs:
- analysis:
- name: Scorecards analysis
- runs-on: ubuntu-24.04
- permissions:
- # Needed to upload the results to code-scanning dashboard.
- security-events: write
- # Used to receive a badge. (Upcoming feature)
- id-token: write
- # Needs for private repositories.
- contents: read
- actions: read
-
- steps:
- - name: "Checkout code"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- with:
- persist-credentials: false
-
- - name: "Run analysis"
- uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
- with:
- results_file: results.sarif
- results_format: sarif
- # (Optional) Read-only PAT token. Uncomment the `repo_token` line below if:
- # - you want to enable the Branch-Protection check on a *public* repository, or
- # - you are installing Scorecards on a *private* repository
- # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
- # repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
-
- # Publish the results for public repositories to enable scorecard badges. For more details, see
- # https://github.com/ossf/scorecard-action#publishing-results.
- # For private repositories, `publish_results` will automatically be set to `false`, regardless
- # of the value entered here.
- publish_results: true
-
- # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
- # format to the repository Actions tab.
- - name: "Upload artifact"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
- with:
- name: SARIF file
- path: results.sarif
- retention-days: 5
-
- # Upload the results to GitHub's code scanning dashboard.
- - name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
- with:
- sarif_file: results.sarif
diff -Nru symfony-6.4.49+dfsg/.github/workflows/zizmor.yml symfony-6.4.50+dfsg/.github/workflows/zizmor.yml
--- symfony-6.4.49+dfsg/.github/workflows/zizmor.yml 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/.github/workflows/zizmor.yml 2026-11-04 11:23:00.000000000 +0000
@@ -28,7 +28,7 @@
persist-credentials: false
- name: Run zizmor
- uses: zizmorcore/zizmor-action@85b2b9fb8e3325e7a23d7d0a9d7ba5ae0f999e3e # v0.7.1
+ uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
with:
advanced-security: false
annotations: true
diff -Nru symfony-6.4.49+dfsg/CHANGELOG-6.4.md symfony-6.4.50+dfsg/CHANGELOG-6.4.md
--- symfony-6.4.49+dfsg/CHANGELOG-6.4.md 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/CHANGELOG-6.4.md 2026-11-04 11:23:00.000000000 +0000
@@ -7,6 +7,212 @@
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/v6.4.0...v6.4.1
+## 6.4.50 (2026-11-04)
+
+### AssetMapper
+
+ * bug #67802 [AssetMapper] Fix the resolution of relative paths in downloaded packages (nicolas-grekas)
+ * bug #67750 [AssetMapper] Decode the version of a jsDelivr import before resolving it (mathieudz)
+ * bug #67621 [AssetMapper] Fix `importmap:remove` deleting the files of local entries (nicolas-grekas)
+
+### BrowserKit
+
+ * bug #67685 [BrowserKit] Match cookies across equivalent IPv6 addresses (X-Coder264)
+ * bug #67684 [BrowserKit] Expire cookies dated at the Unix epoch (X-Coder264)
+
+### Cache
+
+ * bug #67759 [Cache] Fix FilesystemTagAwareAdapter::deleteItems() returning true when some items could not be deleted (nicolas-grekas)
+ * bug #67741 [Cache] Fix Redis deleteItems() returning true when some items could not be deleted (nicolas-grekas)
+ * bug #67736 [Cache] Fix tag-aware deleteItems() returning false when the per-item retry succeeds (nicolas-grekas)
+ * bug #67720 [Cache] Retry failed early expiration callbacks (nicolas-grekas)
+ * bug #67703 [Cache] Honor the $save flag set by the callback in EarlyExpirationHandler (BenMacha)
+ * bug #67714 [Cache] Preserve null results from synchronous early recomputation (nicolas-grekas)
+ * bug #67681 [Cache] Add support for relay 0.61 (nicolas-grekas)
+ * minor #67744 [Cache] Mark connection options as #[\SensitiveParameter] (nicolas-grekas)
+
+### Config
+
+ * security #ghsa-5jff-8h8q-r5qx [Config] Do not validate XML documents on parse (nicolas-grekas)
+
+### Console
+
+ * bug #67831 [Console] Fix ProgressBar's percent after finish() on undecorated output without a max (nicolas-grekas)
+ * bug #67830 [Console] Fix ProgressBar::finish() with a lowered max and restarting a finished bar without a max (jack-worman)
+ * bug #67823 [Console] Fix ProgressBar frames on undecorated output when resuming, restarting or finishing (jack-worman)
+ * bug #67811 [Console] Fix ProgressBar::setMaxSteps() not updating the percentage (nicolas-grekas)
+ * bug #67805 [Console] Fix ProgressBar::start() computing the percentage against the previous max (jack-worman)
+ * bug #67769 [Console] Fix text ending with a backslash escaping the closing tag that follows it (nicolas-grekas)
+ * bug #67762 [Console] Keep an escaped tag delimiter with its backslash when wrapping text (initd-it)
+ * bug #67634 [Console] Fix `setRow()`, `setColumnMaxWidth()` and consecutive rowspans in `Table` (nicolas-grekas)
+ * bug #67633 [Console] Fix `Table` cells that span rows when they follow a cell that spans columns (ybai08)
+
+### Filesystem
+
+ * security #ghsa-6prc-h956-hxjq [Filesystem] Only treat a valid URI scheme as the root of a path (nicolas-grekas)
+ * bug #67786 [Filesystem] Fix failure handling in remove() (nicolas-grekas)
+ * bug #67778 [Filesystem] Reserve temporary removal directories atomically (X-Coder264)
+
+### Finder
+
+ * bug #67785 [Finder] Fix Glob::toRegex() producing an invalid regex for bracket expressions containing a slash (0xABCD01)
+
+### Form
+
+ * bug #67776 [Form] Map data validation errors to the sibling form that has the field (nicolas-grekas)
+ * bug #67772 [Form] Keep validation errors on their originating form (X-Coder264)
+ * bug #67731 [Form] Fix `allow_delete` removing collection entries submitted as null (nicolas-grekas)
+ * bug #67700 [Form] Ignore collection entries with invalid names when allow_add is enabled (BenMacha)
+ * bug #67702 [Form] Fix submitting "0" to a non-compound root form (BenMacha)
+ * bug #67713 [Form] Skip unsubmitted collection entries with `delete_empty` (nicolas-grekas)
+ * bug #67701 [Form] Fix the post_max_size check when it is set to a negative value (BenMacha)
+ * bug #67650 [Form] Preserve the Unix epoch in date string transformation (ahbaqdadi)
+ * minor #67795 [Form] Review Greek translations (sogav21)
+ * minor #67773 [Form] Review Croatian translations (HypeMC)
+ * minor #67674 [Form] Review Polish translations (lukaszkrakowiak)
+ * minor #67660 [Form] Review Japanese translations (OTA-EITA)
+ * minor #67623 [Form][Validator] Review Slovak (sk) translations (tito10047)
+ * minor #67642 [Form][Validator] Review and improve Chinese (zh_CN) translations (siganushka)
+ * minor #67635 [Form][Validator] Review and fix Luxembourgish (lb) translations (mweimerskirch)
+ * minor #67632 [Form] Fix Dutch translations for validators (mdalikadar)
+ * minor #67631 [Form] Fix Italian translations for validators (mdalikadar)
+ * minor #67626 [Form][Security][Validator] Review Vietnamese translations (HongTin2104)
+ * minor #67622 [Form][Validator] review translations for Ukrainian (uk) (qconer)
+ * minor #67627 [Form][Validator] Review translations for Chinese (zh_TW) (HsukqiLee)
+ * minor #67447 [Form] Review Turkish (tr) validator translations (erenbati)
+ * minor #67617 [Form][Validator] Review German translations (Rommmoa)
+
+### HtmlSanitizer
+
+ * security #ghsa-q8jq-xrf4-v5j8 [HtmlSanitizer] Apply the URL policy to srcset and imagesrcset (nicolas-grekas)
+ * security #cve-2026-118517 [HtmlSanitizer] Require special schemes and scheme-relative URLs to carry a non-empty authority (nicolas-grekas)
+ * security #ghsa-wm2w-4mgj-vw33 [HtmlSanitizer] Fix meta refresh URL sanitization bypass (nicolas-grekas)
+
+### HttpClient
+
+ * security #ghsa-c7hm-wqwp-f9v8 [HttpClient] Pin the IP of cached hosts in NoPrivateNetworkHttpClient (nicolas-grekas)
+ * security #ghsa-gq53-v8w2-gjhj [HttpClient] Don't send the options of the default scope to other origins in ScopingHttpClient (nicolas-grekas)
+ * bug #67619 [HttpClient] Let PHP destruct the response decorated by TraceableResponse (nicolas-grekas)
+ * bug #67612 [HttpClient] Fix AsyncResponse throwing a LogicException when it is canceled while streaming (nicolas-grekas)
+ * bug #67610 [HttpClient] Fix getContent() returning a truncated body when the response is canceled while it reads it (nicolas-grekas)
+ * bug #67599 [HttpClient] Fix reading a traced CurlResponse as a stream after its decorator is destructed (PhilETaylor)
+ * bug #67609 [HttpClient] Fix CurlHttpClient writing the body of a response to stdout when it is canceled during stream() (nicolas-grekas)
+ * bug #67605 [HttpClient] Fix clearing the DNS cache of pending requests when a traced response is destructed (nicolas-grekas)
+
+### HttpFoundation
+
+ * security #ghsa-xg2q-4rrq-657q [HttpFoundation] Add the list of private ranges usable as trusted proxies (nicolas-grekas)
+ * security #cve-2026-118514 [HttpFoundation] Match HEAD requests when GET is listed in a MethodRequestMatcher (nicolas-grekas)
+ * security #cve-2026-118514 [HttpFoundation] Match HEAD requests when GET is listed in a RequestMatcher (nicolas-grekas)
+ * security #ghsa-5mx8-mwrp-5693 [HttpFoundation] Don't let Request::getBaseUrl() start with a host chosen by the client (nicolas-grekas)
+ * security #ghsa-3h27-hjv7-fqjx [HttpFoundation] Don't match the script name in Request::getBaseUrl() when it's only the start of a path segment (jack-worman)
+ * bug #67829 [HttpFoundation] Fix Request::getBaseUrl() for a percent-encoded or wrong-case script name after a prefix, and behind an alias (jack-worman)
+ * bug #67826 [HttpFoundation] Don't use the script name as Request::getBaseUrl() when it's not in the URI path (jack-worman)
+ * bug #67804 [HttpFoundation] Don't match the script name in Request::getBaseUrl() when it's only the start of a path segment (jack-worman)
+ * bug #67814 [HttpFoundation] Fix UrlHelper for fragments behind a trusted X-Forwarded-Prefix and for an empty path (nicolas-grekas)
+ * bug #67800 [HttpFoundation] Preserve the base URL for query and fragment references (ahbaqdadi)
+ * minor #67760 [HttpFoundation][Lock][Semaphore] Do not expose the DSN in the exception thrown for unsupported connections (nicolas-grekas)
+ * minor #67746 [HttpFoundation] Mark the DSN and options of SessionHandlerFactory as #[\SensitiveParameter] (nicolas-grekas)
+
+### HttpKernel
+
+ * security #ghsa-wg5q-m2ww-f3c9 [HttpKernel] Restore the router context of the parent request after a sub-request (nicolas-grekas)
+ * bug #67683 [HttpKernel] Compute #[Cache] durations without daylight saving time (nicolas-grekas)
+ * bug #67607 [HttpKernel] Fix searching profiles by status code 0 (chr-hertel)
+
+### Lock
+
+ * minor #67745 [Lock] Mark the DSNs and options of the DBAL and MongoDB stores as #[\SensitiveParameter] (nicolas-grekas)
+
+### Messenger
+
+ * bug #67792 [Messenger] Don't reuse a serialized body after replacing the message (dalerazimov)
+ * bug #67767 [Messenger] Fix the Redis transport throwing "Redis server went away" when connecting fails (nicolas-grekas)
+ * bug #67753 [Messenger] Treat Beanstalkd jobs that aren't Messenger envelopes as decoding failures (nicolas-grekas)
+ * bug #67724 [Messenger] Fix Redis DSN credential decoding and exception traces (nicolas-grekas)
+ * bug #67711 [Messenger] Do not resend the stamp headers of a received AMQP message (BenMacha)
+ * bug #67709 [Messenger] Restore the schema assets filter when the Doctrine transport setup fails (BenMacha)
+ * bug #67710 [Messenger] Do not expose the Redis DSN in the invalid host query parameter exception (BenMacha)
+ * bug #67654 [Messenger] Wait for all batch handlers before acknowledging a message (andreybolonin)
+ * bug #67655 [Messenger] Retry the messages whose batch was flushed during a failed dispatch (nicolas-grekas)
+ * bug #67651 [Messenger] Let the worker flush the batches that contain a failed message (nicolas-grekas)
+ * bug #67647 [Messenger] Don't hide handler failures when a batch handler acks the same message (nicolas-grekas)
+ * bug #67643 [Messenger] Preserve handled stamps when a batch handler nacks (dalerazimov)
+ * minor #67763 [Messenger] Treat encoded envelopes whose body or type isn't a string as decoding failures (nicolas-grekas)
+ * minor #67766 [Messenger] Ignore the "X-Symfony-Messenger" attribute of an SQS message when it isn't a JSON object (nicolas-grekas)
+ * minor #67754 [Messenger] Treat Redis stream entries that aren't Messenger envelopes as decoding failures (nicolas-grekas)
+ * minor #67739 [Messenger] Mark transport options as #[\SensitiveParameter] (nicolas-grekas)
+ * minor #67722 [Messenger] Ignore ids that are not integers when finding a message of the Doctrine transport (BenMacha)
+
+### Notifier
+
+ * minor #67761 [Notifier][Translation] Do not expose the DSN in the exception thrown when its password is missing (nicolas-grekas)
+
+### PropertyAccess
+
+ * bug #67733 [PropertyAccess] Fix parsing type errors thrown by anonymous classes (nicolas-grekas)
+ * bug #67682 [PropertyAccess] Handle uninitialized properties of anonymous classes on PHP 8.6 (nicolas-grekas)
+
+### PropertyInfo
+
+ * bug #67788 [PropertyInfo] Don't turn intersection types into class names in PhpDocExtractor (nicolas-grekas)
+
+### RateLimiter
+
+ * bug #67705 [RateLimiter] Validate the refill amount, the fixed window interval and the sliding window limit (BenMacha)
+
+### Routing
+
+ * security #cve-2026-118516 [Routing] Prevent host placeholders from matching the path in compiled matchers (nicolas-grekas)
+
+### Security
+
+ * security #cve-2026-118513 [Security] Parse the DN into RDNs before extracting the user identifier (nicolas-grekas)
+ * security #ghsa-8f57-m8wj-x65w [Security] Run checkPostAuth() of user checkers on programmatic logins (nicolas-grekas)
+ * minor #67738 [Security] Fix `Voter` with anonymous classes and nested `supports()` calls (nicolas-grekas)
+ * minor #67707 [Security] Reject login links with an out of range "expires" parameter (BenMacha)
+ * minor #67708 [Security] Ignore target and failure paths that are neither paths nor absolute URLs (BenMacha)
+ * minor #67716 [Security] Reject malformed login link user identifiers (nicolas-grekas)
+ * minor #67706 [Security] Reject remember-me cookies with an invalid expiration time (BenMacha)
+
+### Serializer
+
+ * security #ghsa-hqp2-7rr8-hf26 [Serializer] Ignore undeclared property paths when denormalizing (nicolas-grekas)
+ * security #cve-2026-118515 [Serializer] Don't build UploadedFile and other file objects from denormalized data (nicolas-grekas)
+ * security #ghsa-pxj9-jh84-vhm5 [Serializer] Lowercase the scheme of data URIs before opening them (nicolas-grekas)
+ * bug #67752 [Serializer] Skip empty lines when decoding multi-column CSV documents (0xABCD01)
+ * bug #67665 [Serializer] Fix denormalizing a data URI ending with slashes to a File (ahmed-bhs)
+
+### Translation
+
+ * bug #67669 [Translation] Don't read provider responses as file paths (nicolas-grekas)
+ * minor #67625 [Translation] Reviewed the Danish translation in issue #65554 (mcardle)
+
+### Validator
+
+ * security #ghsa-4qpf-57w4-m3wc [Validator] Reject non-public IP addresses in the no-priv, no-res and only-public modes (nicolas-grekas)
+ * bug #67787 [Validator] Only extract property types when auto-mapping applies (paulferrett)
+ * minor #67644 [Validator] Show the count in the word count messages of locales with a single plural form (nicolas-grekas)
+
+### WebProfilerBundle
+
+ * bug #67618 [WebProfilerBundle] Reset the CSP handler between requests (norkunas)
+ * bug #67620 [WebProfilerBundle] Reset the profiler Twig extension between requests (nicolas-grekas)
+ * bug #67606 [WebProfilerBundle] Return a 404 when opening the profiler file viewer without a file (kevinsleegerslameco, nicolas-grekas)
+
+### Yaml
+
+ * security #ghsa-hwfg-qwvh-2wfx [Yaml] Fix exponential and quadratic parse times (nicolas-grekas)
+ * security #ghsa-hwfg-qwvh-2wfx [Yaml] Fix quadratic parse times (nicolas-grekas)
+ * bug #67735 [Yaml] Strip the byte order mark at the start of a stream (0xABCD01)
+
+### Other
+
+ * bug #67614 Reject trailing newlines in Content-Disposition fallbacks (ahbaqdadi)
+ * minor #67675 translation: checked proposed serbian translations (Trysha-rbrn)
+ * minor #67676 review(translation): [#64516] reviewed and updated the translation (mikocevar)
+ * minor #67649 Review translations for Estonian (et) (erkia)
+
## 6.4.49 (2026-10-28)
### AssetMapper
diff -Nru symfony-6.4.49+dfsg/debian/changelog symfony-6.4.50+dfsg/debian/changelog
--- symfony-6.4.49+dfsg/debian/changelog 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/changelog 2026-11-04 15:33:54.000000000 +0000
@@ -1,3 +1,58 @@
+symfony (6.4.50+dfsg-0+deb13u1) trixie-security; urgency=medium
+
+ [ Fabien Potencier ]
+ * Update VERSION for 6.4.50
+
+ [ Nicolas Grekas ]
+ * [Config] Do not validate XML documents on parse
+ (GHSA-5jff-8h8q-r5qx) [CVE-2026-123824]
+ * [HtmlSanitizer] Fix meta refresh URL sanitization bypass
+ (GHSA-wm2w-4mgj-vw33) [CVE-2026-121373]
+ * [Validator] Reject non-public IP addresses in the no-priv, no-res
+ and only-public modes (GHSA-4qpf-57w4-m3wc) [CVE-2026-123125]
+ * [HtmlSanitizer] Require special schemes and scheme-relative URLs to
+ carry a non-empty authority [CVE-2026-118517]
+ * [HttpClient] Don't send the options of the default scope to other
+ origins in ScopingHttpClient (GHSA-gq53-v8w2-gjhj) [CVE-2026-121971]
+ * [Security] Run the checkPostAuth() method of user checkers when
+ authenticating users programmatically
+ (GHSA-8f57-m8wj-x65w) [CVE-2026-123746]
+ * [HttpClient] Pin the IP of cached hosts in NoPrivateNetworkHttpClient
+ (GHSA-c7hm-wqwp-f9v8) [CVE-2026-123119]
+ * [Serializer] Lowercase the scheme of data URIs before opening them
+ (GHSA-pxj9-jh84-vhm5) [CVE-2026-121623]
+ * [Security] Parse the DN into RDNs before extracting the user identifier
+ [CVE-2026-118513]
+ * [Serializer] Don't build UploadedFile and other file objects from
+ denormalized data [CVE-2026-118515]
+ * [HtmlSanitizer] Apply the URL policy to srcset and imagesrcset
+ (GHSA-q8jq-xrf4-v5j8) [CVE-2026-121949]
+ * [HttpFoundation] Add the list of private ranges usable as trusted proxies
+ (GHSA-xg2q-4rrq-657q)
+ * [HttpKernel] Restore the router context of the parent request after a
+ sub-request (GHSA-wg5q-m2ww-f3c9)
+ * [Routing] Prevent host placeholders from matching the path in compiled
+ matchers [CVE-2026-118516]
+ * [Filesystem] Only treat a valid URI scheme as the root of a path
+ (GHSA-6prc-h956-hxjq)
+ * [Yaml] Fix exponential and quadratic parse times
+ (GHSA-hwfg-qwvh-2wfx) [CVE-2026-123792]
+ * [Serializer] Ignore undeclared property paths when denormalizing
+ (GHSA-hqp2-7rr8-hf26) [CVE-2026-122053]
+ * [HttpFoundation] Match HEAD requests when GET is listed in a RequestMatcher
+ [CVE-2026-118514]
+ * [HttpFoundation] Don't let Request::getBaseUrl() start with a host chosen by
+ the client (GHSA-5mx8-mwrp-5693)
+
+ [ Jack Worman ]
+ * [HttpFoundation] Don't match the script name in Request::getBaseUrl() when
+ it's only the start of a path segment (GHSA-3h27-hjv7-fqjx)
+
+ [ David Prévot ]
+ * Refresh patches
+
+ -- David Prévot Wed, 04 Nov 2026 16:33:54 +0100
+
symfony (6.4.49+dfsg-0+deb13u1) trixie-security; urgency=medium
[ Fabien Potencier ]
diff -Nru symfony-6.4.49+dfsg/debian/patches/Assume-php-async-aws-sqs-2.patch symfony-6.4.50+dfsg/debian/patches/Assume-php-async-aws-sqs-2.patch
--- symfony-6.4.49+dfsg/debian/patches/Assume-php-async-aws-sqs-2.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/Assume-php-async-aws-sqs-2.patch 2026-11-04 15:33:54.000000000 +0000
@@ -10,10 +10,10 @@
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/ConnectionTest.php
-index b147f32..6699a36 100644
+index d9add5b..b45553d 100644
--- a/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/ConnectionTest.php
+++ b/src/Symfony/Component/Messenger/Bridge/AmazonSqs/Tests/Transport/ConnectionTest.php
-@@ -599,8 +599,6 @@ class ConnectionTest extends TestCase
+@@ -631,8 +631,6 @@ class ConnectionTest extends TestCase
private function isAsyncAwsSqsVersion2Installed(): bool
{
diff -Nru symfony-6.4.49+dfsg/debian/patches/Cache-Disable-Test-failing-tests-Error-Class-not-found.patch symfony-6.4.50+dfsg/debian/patches/Cache-Disable-Test-failing-tests-Error-Class-not-found.patch
--- symfony-6.4.49+dfsg/debian/patches/Cache-Disable-Test-failing-tests-Error-Class-not-found.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/Cache-Disable-Test-failing-tests-Error-Class-not-found.patch 2026-11-04 15:33:54.000000000 +0000
@@ -9,8 +9,9 @@
.../Cache/Tests/Adapter/TagAwareAdapterTest.php | 272 ---------------------
.../TagAwareAndProxyAdapterIntegrationTest.php | 1 +
.../Cache/Tests/Adapter/TagAwareTestTrait.php | 9 +
- .../Tests/Messenger/EarlyExpirationHandlerTest.php | 1 +
- 5 files changed, 23 insertions(+), 272 deletions(-)
+ .../Messenger/EarlyExpirationDispatcherTest.php | 1 +
+ .../Tests/Messenger/EarlyExpirationHandlerTest.php | 5 +
+ 6 files changed, 28 insertions(+), 272 deletions(-)
delete mode 100644 src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php
diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php b/src/Symfony/Component/Cache/Tests/Adapter/AdapterTestCase.php
@@ -402,15 +403,59 @@
$pool = $this->createCachePool();
$pool->clear();
+diff --git a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php
+index fc61143..e4a7eac 100644
+--- a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php
++++ b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php
+@@ -145,6 +145,7 @@ class EarlyExpirationDispatcherTest extends TestCase
+ */
+ public function testSynchronousEarlyExpirationReturnsNullAndSkipsDuplicates()
+ {
++ $this->markTestSkipped('Test failing on current Debian (Error: Class "�" not found).');
+ $pool = new class extends FilesystemAdapter {
+ public bool $failSave = false;
+
diff --git a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php
-index 963215d..d4c3a82 100644
+index 76fdb31..49bba02 100644
--- a/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php
+++ b/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php
-@@ -34,6 +34,7 @@ class EarlyExpirationHandlerTest extends TestCase
- */
+@@ -53,6 +53,7 @@ class EarlyExpirationHandlerTest extends TestCase
+
public function testHandle()
{
+ $this->markTestSkipped('Test failing on current Debian (Error: Class "�" not found).');
$pool = new FilesystemAdapter();
$item = $pool->getItem('foo');
$item->set(234);
+@@ -122,6 +123,7 @@ class EarlyExpirationHandlerTest extends TestCase
+
+ public function testWorkerRetriesFailedCallbacks()
+ {
++ $this->markTestSkipped('Test failing on current Debian (Error: Class "�" not found).');
+ foreach ([new \RuntimeException('Failed computation.'), new \Error('Failed computation.')] as $failure) {
+ $calls = 0;
+ $currentRetry = 0;
+@@ -184,6 +186,7 @@ class EarlyExpirationHandlerTest extends TestCase
+
+ public function testFailureDoesNotClearReplacementComputation()
+ {
++ $this->markTestSkipped('Test failing on current Debian (Error: Class "�" not found).');
+ foreach ([false, true] as $evictOriginal) {
+ $calls = 0;
+ $handler = $replacement = null;
+@@ -262,6 +265,7 @@ class EarlyExpirationHandlerTest extends TestCase
+
+ public function testExpiredMessageIsSkippedAfterFailure()
+ {
++ $this->markTestSkipped('Test failing on current Debian (Error: Class "�" not found).');
+ $calls = 0;
+ $callback = static function () use (&$calls) {
+ if (1 === ++$calls) {
+@@ -287,6 +291,7 @@ class EarlyExpirationHandlerTest extends TestCase
+
+ public function testFailureWithoutMetadataCanBeRetried()
+ {
++ $this->markTestSkipped('Test failing on current Debian (Error: Class "�" not found).');
+ $calls = 0;
+ $callback = static function () use (&$calls) {
+ if (1 === ++$calls) {
diff -Nru symfony-6.4.49+dfsg/debian/patches/Drop-composer-runtime-api-that-confuses-phpabtpl.patch symfony-6.4.50+dfsg/debian/patches/Drop-composer-runtime-api-that-confuses-phpabtpl.patch
--- symfony-6.4.49+dfsg/debian/patches/Drop-composer-runtime-api-that-confuses-phpabtpl.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/Drop-composer-runtime-api-that-confuses-phpabtpl.patch 2026-11-04 15:33:54.000000000 +0000
@@ -34,7 +34,7 @@
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/config": "^6.1|^7.0",
diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json
-index cd7d7b4..c55d9db 100644
+index 511e2cc..835249e 100644
--- a/src/Symfony/Bundle/SecurityBundle/composer.json
+++ b/src/Symfony/Bundle/SecurityBundle/composer.json
@@ -17,7 +17,6 @@
diff -Nru symfony-6.4.49+dfsg/debian/patches/Filesystem-Drop-currently-failing-tests.patch symfony-6.4.50+dfsg/debian/patches/Filesystem-Drop-currently-failing-tests.patch
--- symfony-6.4.49+dfsg/debian/patches/Filesystem-Drop-currently-failing-tests.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/Filesystem-Drop-currently-failing-tests.patch 2026-11-04 15:33:54.000000000 +0000
@@ -4,12 +4,12 @@
TODO: investigate
---
- src/Symfony/Component/Filesystem/Tests/FilesystemTest.php | 12 ++++++++++++
- src/Symfony/Component/Filesystem/Tests/PathTest.php | 11 ++---------
- 2 files changed, 14 insertions(+), 9 deletions(-)
+ .../Component/Filesystem/Tests/FilesystemTest.php | 24 ++++++++++++++++++++++
+ .../Component/Filesystem/Tests/PathTest.php | 22 ++++++--------------
+ 2 files changed, 30 insertions(+), 16 deletions(-)
diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
-index f73c58a..edcfa68 100644
+index fab133c..6c4b0af 100644
--- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
+++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
@@ -253,6 +253,8 @@ class FilesystemTest extends FilesystemTestCase
@@ -21,7 +21,61 @@
$file = $this->workspace.\DIRECTORY_SEPARATOR.'1';
$this->filesystem->touch($file, 0);
-@@ -1229,6 +1231,8 @@ class FilesystemTest extends FilesystemTestCase
+@@ -365,6 +367,8 @@ class FilesystemTest extends FilesystemTestCase
+ */
+ public function testRemoveDoesNotDeleteExistingTemporaryDirectories()
+ {
++ $this->markTestSkipped('Test currently failing on Debian');
++
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $directory = $this->workspace.'/directory';
+@@ -409,6 +413,8 @@ class FilesystemTest extends FilesystemTestCase
+ */
+ public function testRemoveRetriesTemporaryDirectoryCollisions()
+ {
++ $this->markTestSkipped('Test currently failing on Debian');
++
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $directory = $this->workspace.'/directory';
+@@ -448,6 +454,8 @@ class FilesystemTest extends FilesystemTestCase
+ */
+ public function testRemoveDoesNotInterfereWithConcurrentRemoval()
+ {
++ $this->markTestSkipped('Test currently failing on Debian');
++
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $first = $this->workspace.'/first';
+@@ -538,6 +546,8 @@ class FilesystemTest extends FilesystemTestCase
+ */
+ public function testRemoveIgnoresDirectoryRemovedConcurrently()
+ {
++ $this->markTestSkipped('Test currently failing on Debian');
++
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $directory = $this->workspace.'/directory';
+@@ -646,6 +656,8 @@ class FilesystemTest extends FilesystemTestCase
+
+ public function testRemoveRestoresDirectoryWhenNestedRemovalFails()
+ {
++ $this->markTestSkipped('Test currently failing on Debian');
++
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $directory = $this->workspace.'/directory';
+@@ -669,6 +681,8 @@ class FilesystemTest extends FilesystemTestCase
+
+ public function testRemoveThrowsExceptionOnUnreadableDirectory()
+ {
++ $this->markTestSkipped('Test currently failing on Debian');
++
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $directory = $this->workspace.'/directory';
+@@ -1538,6 +1552,8 @@ class FilesystemTest extends FilesystemTestCase
public function testMakePathRelativeWithExistingFile()
{
@@ -30,7 +84,7 @@
$dir = $this->workspace.\DIRECTORY_SEPARATOR.'foo'.\DIRECTORY_SEPARATOR.'bar';
mkdir($dir, 0o777, true);
$file = $dir.\DIRECTORY_SEPARATOR.'test.txt';
-@@ -1282,6 +1286,8 @@ class FilesystemTest extends FilesystemTestCase
+@@ -1591,6 +1607,8 @@ class FilesystemTest extends FilesystemTestCase
public function testMirrorWithCustomIteratorAndDeleteOption()
{
@@ -39,7 +93,7 @@
$sourcePath = $this->workspace.\DIRECTORY_SEPARATOR.'source-with-a-longer-name'.\DIRECTORY_SEPARATOR;
$targetPath = $this->workspace.\DIRECTORY_SEPARATOR.'target'.\DIRECTORY_SEPARATOR;
-@@ -1505,6 +1511,8 @@ class FilesystemTest extends FilesystemTestCase
+@@ -1814,6 +1832,8 @@ class FilesystemTest extends FilesystemTestCase
public function testTempnamWithSuffixIsPrivate()
{
@@ -48,7 +102,7 @@
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('This test cannot run on Windows.');
}
-@@ -1523,6 +1531,8 @@ class FilesystemTest extends FilesystemTestCase
+@@ -1832,6 +1852,8 @@ class FilesystemTest extends FilesystemTestCase
public function testTempnamTrimsTrailingWhitespaceFromTruncatedPrefix()
{
@@ -57,7 +111,7 @@
// PHP's tempnam() truncates the prefix to 63 characters; if that leaves a
// trailing space, the file creation fails on Windows. See #64722.
$prefix = str_repeat('a', 62).' suffix';
-@@ -1904,6 +1914,7 @@ class FilesystemTest extends FilesystemTestCase
+@@ -2213,6 +2235,7 @@ class FilesystemTest extends FilesystemTestCase
public function testCopyShouldKeepExecutionPermission()
{
$this->markAsSkippedIfChmodIsMissing();
@@ -65,7 +119,7 @@
$sourceFilePath = $this->workspace.\DIRECTORY_SEPARATOR.'copy_source_file';
$targetFilePath = $this->workspace.\DIRECTORY_SEPARATOR.'copy_target_file';
-@@ -1919,6 +1930,7 @@ class FilesystemTest extends FilesystemTestCase
+@@ -2228,6 +2251,7 @@ class FilesystemTest extends FilesystemTestCase
public function testCopyShouldKeepRestrictivePermissions()
{
$this->markAsSkippedIfChmodIsMissing();
@@ -74,7 +128,7 @@
$sourceFilePath = $this->workspace.\DIRECTORY_SEPARATOR.'copy_source_file';
$targetFilePath = $this->workspace.\DIRECTORY_SEPARATOR.'copy_target_file';
diff --git a/src/Symfony/Component/Filesystem/Tests/PathTest.php b/src/Symfony/Component/Filesystem/Tests/PathTest.php
-index 8363c22..638c860 100644
+index a705802..7794aa2 100644
--- a/src/Symfony/Component/Filesystem/Tests/PathTest.php
+++ b/src/Symfony/Component/Filesystem/Tests/PathTest.php
@@ -379,9 +379,6 @@ class PathTest extends TestCase
@@ -100,7 +154,43 @@
yield ['..', '/', ''];
yield ['../..', '/', ''];
yield ['..', 'phar:///', ''];
-@@ -1151,6 +1142,8 @@ class PathTest extends TestCase
+@@ -1127,6 +1118,8 @@ class PathTest extends TestCase
+
+ public function testInvalidSchemeIsNotARoot()
+ {
++ $this->markTestSkipped('Test currently failing on Debian');
++
+ $this->assertFalse(Path::isBasePath('/base', '/base/../../etc/x://../passwd'));
+ $this->assertFalse(Path::isBasePath('/base', '/base/x://../../etc/passwd'));
+ $this->assertSame('/etc/passwd', Path::canonicalize('/base/../../etc/x://../passwd'));
+@@ -1148,24 +1141,19 @@ class PathTest extends TestCase
+ $this->assertFalse(Path::isAbsolute('a/b:///c'));
+ $this->assertFalse(Path::isAbsolute('a_b:///c'));
+ $this->assertFalse(Path::isAbsolute(':///c'));
+-
+- $this->assertSame('/', Path::getRoot('/etc/x://y'));
+- $this->assertSame('', Path::getRoot('a/b:///c'));
+-
+- $this->assertSame('/a/b:/x/c', Path::makeAbsolute('c', '/a/b://x'));
+ }
+
+ public function testIsLocalIgnoresInvalidScheme()
+ {
++ $this->markTestSkipped('Test currently failing on Debian');
++
+ $this->assertTrue(Path::isLocal('/etc/x://y'));
+ $this->assertTrue(Path::isLocal('a/b://c'));
+ }
+
+ public function testSingleLetterBeforeDoubleSlashIsNotAScheme()
+ {
+- $this->assertSame(Path::canonicalize('x:/../y'), Path::canonicalize('x://../y'));
+ $this->assertSame(Path::getRoot('x:/y'), Path::getRoot('x://y'));
+- $this->assertTrue(Path::isLocal('x://y'));
+ }
+
+ public function testValidSchemeIsARoot()
+@@ -1214,6 +1202,8 @@ class PathTest extends TestCase
public function testNormalize()
{
diff -Nru symfony-6.4.49+dfsg/debian/patches/Group-nophpunit11-for-tests-failing-with-PHPUnit-11.patch symfony-6.4.50+dfsg/debian/patches/Group-nophpunit11-for-tests-failing-with-PHPUnit-11.patch
--- symfony-6.4.49+dfsg/debian/patches/Group-nophpunit11-for-tests-failing-with-PHPUnit-11.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/Group-nophpunit11-for-tests-failing-with-PHPUnit-11.patch 2026-11-04 15:33:54.000000000 +0000
@@ -27,7 +27,7 @@
.../Finder/Tests/FinderOpenBasedirTest.php | 9 -----
.../Form/Test/FormPerformanceTestCase.php | 4 ---
.../Component/Form/Tests/VersionAwareTest.php | 24 -------------
- .../Tests/NoPrivateNetworkHttpClientTest.php | 2 ++
+ .../Tests/NoPrivateNetworkHttpClientTest.php | 3 ++
.../Constraint/RequestAttributeValueSameTest.php | 3 ++
.../Constraint/ResponseCookieValueSameTest.php | 3 ++
.../Test/Constraint/ResponseFormatSameTest.php | 6 ++++
@@ -62,7 +62,7 @@
.../Tests/Constraints/RangeValidatorTest.php | 2 +-
.../Mapping/Loader/PropertyInfoLoaderTest.php | 3 ++
.../VarDumper/Tests/Caster/DoctrineCasterTest.php | 3 ++
- 59 files changed, 179 insertions(+), 165 deletions(-)
+ 59 files changed, 180 insertions(+), 165 deletions(-)
delete mode 100644 src/Symfony/Bridge/PhpUnit/Tests/expectdeprecationfail.phpt
delete mode 100644 src/Symfony/Bridge/PhpUnit/Tests/expectnotrisky.phpt
delete mode 100644 src/Symfony/Bridge/PhpUnit/Tests/expectrisky.phpt
@@ -277,7 +277,7 @@
*/
public function testMapRequestPayload(string $format, array $parameters, ?string $content, callable $responseAssertion, int $expectedStatusCode)
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php
-index 6093348..479efc3 100644
+index 3952317..0c656d0 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php
@@ -41,6 +41,9 @@ class RemotePackageStorageTest extends TestCase
@@ -570,7 +570,7 @@
- }
-}
diff --git a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
-index dd60a40..a286a62 100644
+index 43eff8f..6e0fb2a 100644
--- a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
@@ -71,6 +71,7 @@ class NoPrivateNetworkHttpClientTest extends TestCase
@@ -589,6 +589,14 @@
* @group dns-sensitive
*/
public function testExcludeByHost(string $ipAddr, $subnets, bool $mustThrow)
+@@ -145,6 +147,7 @@ class NoPrivateNetworkHttpClientTest extends TestCase
+ }
+
+ /**
++ * @group nophpunit11
+ * @group dns-sensitive
+ */
+ public function testCachedHostIsResolvedToTheCheckedIp()
diff --git a/src/Symfony/Component/HttpFoundation/Tests/Test/Constraint/RequestAttributeValueSameTest.php b/src/Symfony/Component/HttpFoundation/Tests/Test/Constraint/RequestAttributeValueSameTest.php
index c7ee239..7951bd6 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/Test/Constraint/RequestAttributeValueSameTest.php
@@ -953,10 +961,10 @@
*/
public function testThrowsAnErrorIfTokenIsInvalid(string $token)
diff --git a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php
-index 3004e8d..7fc73ea 100644
+index 1020934..47918a7 100644
--- a/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Authentication/AuthenticatorManagerTest.php
-@@ -329,6 +329,9 @@ class AuthenticatorManagerTest extends TestCase
+@@ -355,6 +355,9 @@ class AuthenticatorManagerTest extends TestCase
$this->assertSame($this->response, $response);
}
@@ -1054,10 +1062,10 @@
/**
diff --git a/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php b/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php
-index d781993..a312229 100644
+index b2d8c69..c3cfd68 100644
--- a/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php
+++ b/src/Symfony/Component/Translation/Bridge/Phrase/Tests/PhraseProviderTest.php
-@@ -60,6 +60,7 @@ class PhraseProviderTest extends TestCase
+@@ -62,6 +62,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider readProvider
@@ -1065,7 +1073,7 @@
*/
public function testRead(string $locale, string $localeId, string $domain, string $responseContent, TranslatorBag $expectedTranslatorBag)
{
-@@ -110,6 +111,7 @@ class PhraseProviderTest extends TestCase
+@@ -112,6 +113,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider readProvider
@@ -1073,7 +1081,7 @@
*/
public function testReadCached(string $locale, string $localeId, string $domain, string $responseContent, TranslatorBag $expectedTranslatorBag)
{
-@@ -262,6 +264,7 @@ class PhraseProviderTest extends TestCase
+@@ -294,6 +296,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider cacheKeyProvider
@@ -1081,7 +1089,7 @@
*/
public function testCacheKeyOptionsSort(array $options, string $expectedKey)
{
-@@ -294,6 +297,7 @@ class PhraseProviderTest extends TestCase
+@@ -326,6 +329,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider cacheItemProvider
@@ -1089,7 +1097,7 @@
*/
public function testGetCacheItem(mixed $cachedValue, bool $hasMatchHeader)
{
-@@ -378,6 +382,7 @@ class PhraseProviderTest extends TestCase
+@@ -410,6 +414,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider readProviderExceptionsProvider
@@ -1097,7 +1105,7 @@
*/
public function testReadProviderExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage)
{
-@@ -415,6 +420,7 @@ class PhraseProviderTest extends TestCase
+@@ -447,6 +452,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider initLocalesExceptionsProvider
@@ -1105,7 +1113,7 @@
*/
public function testInitLocalesExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage)
{
-@@ -664,6 +670,7 @@ class PhraseProviderTest extends TestCase
+@@ -696,6 +702,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider createLocalesExceptionsProvider
@@ -1113,7 +1121,7 @@
*/
public function testCreateLocaleExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage)
{
-@@ -753,6 +760,7 @@ class PhraseProviderTest extends TestCase
+@@ -785,6 +792,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider deleteExceptionsProvider
@@ -1121,7 +1129,7 @@
*/
public function testDeleteProviderExceptions(int $statusCode, string $expectedExceptionMessage, string $expectedLoggerMessage)
{
-@@ -796,6 +804,7 @@ class PhraseProviderTest extends TestCase
+@@ -828,6 +836,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider writeProvider
@@ -1129,7 +1137,7 @@
*/
public function testWrite(string $locale, string $localeId, string $domain, string $content, TranslatorBag $bag)
{
-@@ -944,6 +953,7 @@ class PhraseProviderTest extends TestCase
+@@ -976,6 +985,7 @@ class PhraseProviderTest extends TestCase
/**
* @dataProvider writeExceptionsProvider
@@ -1250,10 +1258,10 @@
public function testThrowsOnInvalidStringDates($expectedMessage, $value, $min, $max)
{
diff --git a/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php b/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php
-index 1ffb6be..7a0a55f 100644
+index 55e1505..ccb4714 100644
--- a/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php
+++ b/src/Symfony/Component/Validator/Tests/Mapping/Loader/PropertyInfoLoaderTest.php
-@@ -34,6 +34,9 @@ use Symfony\Component\Validator\Validation;
+@@ -37,6 +37,9 @@ use Symfony\Component\Validator\Validation;
*/
class PropertyInfoLoaderTest extends TestCase
{
diff -Nru symfony-6.4.49+dfsg/debian/patches/HttpClient-Skip-tests-relying-on-amphp-http-client.patch symfony-6.4.50+dfsg/debian/patches/HttpClient-Skip-tests-relying-on-amphp-http-client.patch
--- symfony-6.4.49+dfsg/debian/patches/HttpClient-Skip-tests-relying-on-amphp-http-client.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/HttpClient-Skip-tests-relying-on-amphp-http-client.patch 2026-11-04 15:33:54.000000000 +0000
@@ -8,10 +8,10 @@
2 files changed, 14 insertions(+)
diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
-index 5add806..e91eda8 100644
+index 91c7205..f8f4fa9 100644
--- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
+++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
-@@ -35,6 +35,13 @@ The vulcain binary can be found at https://github.com/symfony/binary-utils/relea
+@@ -36,6 +36,13 @@ The vulcain binary can be found at https://github.com/symfony/binary-utils/relea
abstract class HttpClientTestCase extends BaseHttpClientTestCase
{
diff -Nru symfony-6.4.49+dfsg/debian/patches/Lock-Drop-test-currently-failing-on-buildd.patch symfony-6.4.50+dfsg/debian/patches/Lock-Drop-test-currently-failing-on-buildd.patch
--- symfony-6.4.49+dfsg/debian/patches/Lock-Drop-test-currently-failing-on-buildd.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/Lock-Drop-test-currently-failing-on-buildd.patch 2026-11-04 15:33:54.000000000 +0000
@@ -8,10 +8,10 @@
1 file changed, 1 deletion(-)
diff --git a/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php
-index 7d71810..2390963 100644
+index 8ad7265..74bc3e0 100644
--- a/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php
+++ b/src/Symfony/Component/Lock/Tests/Store/DoctrineDbalStoreTest.php
-@@ -272,7 +272,6 @@ class DoctrineDbalStoreTest extends AbstractStoreTestCase
+@@ -273,7 +273,6 @@ class DoctrineDbalStoreTest extends AbstractStoreTestCase
yield [\Doctrine\DBAL\Platforms\PostgreSQL94Platform::class];
}
diff -Nru symfony-6.4.49+dfsg/debian/patches/Remove-calls-to-getExpectedException.patch symfony-6.4.50+dfsg/debian/patches/Remove-calls-to-getExpectedException.patch
--- symfony-6.4.49+dfsg/debian/patches/Remove-calls-to-getExpectedException.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/Remove-calls-to-getExpectedException.patch 2026-11-04 15:33:54.000000000 +0000
@@ -8,7 +8,7 @@
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
-index 769bcad..25d5491 100644
+index a8bfc17..7c5f8c8 100644
--- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
+++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php
@@ -445,7 +445,7 @@ class UrlMatcherTest extends TestCase
diff -Nru symfony-6.4.49+dfsg/debian/patches/Yaml-Drop-data-set-currently-failing-on-i386.patch symfony-6.4.50+dfsg/debian/patches/Yaml-Drop-data-set-currently-failing-on-i386.patch
--- symfony-6.4.49+dfsg/debian/patches/Yaml-Drop-data-set-currently-failing-on-i386.patch 2026-10-28 19:28:35.000000000 +0000
+++ symfony-6.4.50+dfsg/debian/patches/Yaml-Drop-data-set-currently-failing-on-i386.patch 2026-11-04 15:33:54.000000000 +0000
@@ -7,10 +7,10 @@
1 file changed, 2 deletions(-)
diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php
-index e19a740..da4b28d 100644
+index 28ab52f..925efd6 100644
--- a/src/Symfony/Component/Yaml/Tests/InlineTest.php
+++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php
-@@ -386,7 +386,6 @@ class InlineTest extends TestCase
+@@ -425,7 +425,6 @@ class InlineTest extends TestCase
['2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)],
['2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)],
['1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)],
@@ -18,7 +18,7 @@
['"a \\"string\\" with \'quoted strings inside\'"', 'a "string" with \'quoted strings inside\''],
["'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''],
-@@ -464,7 +463,6 @@ class InlineTest extends TestCase
+@@ -510,7 +509,6 @@ class InlineTest extends TestCase
['2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)],
['2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)],
['1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)],
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php symfony-6.4.50+dfsg/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -99,7 +99,6 @@
$request->attributes->set('_controller', TestAbstractController::class.'::fooAction');
$this->assertSame([$controller, 'fooAction'], $resolver->getController($request));
- $this->assertSame($container, $controller->setContainer($container));
}
public function testAbstractControllerServiceWithFqcnIdGetsContainerWhenNotSet()
@@ -120,7 +119,6 @@
$request->attributes->set('_controller', DummyController::class.'::fooAction');
$this->assertSame([$controller, 'fooAction'], $resolver->getController($request));
- $this->assertSame($container, $controller->getContainer());
}
public function testAbstractControllerGetsNoContainerWhenSet()
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php symfony-6.4.50+dfsg/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -30,7 +30,7 @@
protected function setUp(): void
{
- $this->tmpDir = sys_get_temp_dir().'/sf_translation';
+ $this->tmpDir = sys_get_temp_dir().'/sf_translator_test';
$this->deleteTmpDir();
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php symfony-6.4.50+dfsg/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -28,6 +28,7 @@
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher\PathRequestMatcher;
+use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Ldap\Ldap;
@@ -549,6 +550,53 @@
$container->compile();
}
+ public function testAccessControlRuleRestrictedToGetAppliesToHeadRequests()
+ {
+ $container = $this->getRawContainer();
+ $container->loadFromExtension('security', [
+ 'providers' => [
+ 'default' => ['id' => 'foo'],
+ ],
+ 'firewalls' => [
+ 'main' => ['pattern' => '^/', 'http_basic' => []],
+ ],
+ 'access_control' => [
+ ['path' => '^/admin', 'roles' => 'ROLE_ADMIN', 'methods' => ['GET']],
+ ['path' => '^/upload', 'roles' => 'ROLE_USER', 'methods' => ['POST']],
+ ],
+ ]);
+ $container->compile();
+
+ [$adminRule, $uploadRule] = $container->getDefinition('security.access_map')->getMethodCalls();
+
+ $matcher = $this->createRequestMatcher($container, $adminRule[1][0]);
+ $this->assertTrue($matcher->matches(Request::create('/admin', 'GET')));
+ $this->assertTrue($matcher->matches(Request::create('/admin', 'HEAD')));
+
+ $matcher = $this->createRequestMatcher($container, $uploadRule[1][0]);
+ $this->assertFalse($matcher->matches(Request::create('/upload', 'HEAD')));
+ $this->assertTrue($matcher->matches(Request::create('/upload', 'POST')));
+ }
+
+ private function createRequestMatcher(ContainerBuilder $container, $reference): RequestMatcherInterface
+ {
+ $definition = $container->getDefinition((string) $reference);
+ $class = $definition->getClass();
+ $arguments = [];
+
+ foreach ($definition->getArguments() as $argument) {
+ if (\is_array($argument)) {
+ $argument = array_map(fn ($child) => $child instanceof Reference ? $this->createRequestMatcher($container, $child) : $child, $argument);
+ } elseif ($argument instanceof Reference) {
+ $argument = $this->createRequestMatcher($container, $argument);
+ }
+
+ $arguments[] = $argument;
+ }
+
+ return new $class(...$arguments);
+ }
+
public function testRemovesExpressionCacheWarmerDefinitionIfNoExpressions()
{
$container = $this->getRawContainer();
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/SecurityBundle/composer.json symfony-6.4.50+dfsg/src/Symfony/Bundle/SecurityBundle/composer.json
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/SecurityBundle/composer.json 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/SecurityBundle/composer.json 2026-11-04 11:23:00.000000000 +0000
@@ -25,7 +25,7 @@
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/event-dispatcher": "^5.4|^6.0|^7.0",
"symfony/http-kernel": "^6.2",
- "symfony/http-foundation": "^6.2|^7.0",
+ "symfony/http-foundation": "^6.4.50|^7.4.23",
"symfony/password-hasher": "^5.4|^6.0|^7.0",
"symfony/security-core": "^6.4.49|^7.4.22",
"symfony/security-csrf": "^5.4|^6.0|^7.0",
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php 2026-11-04 11:23:00.000000000 +0000
@@ -369,12 +369,12 @@
throw new NotFoundHttpException('The base dir should be set.');
}
- $file = $request->query->get('file');
- $line = $request->query->get('line');
+ $file = $request->query->getString('file');
+ $line = $request->query->getInt('line');
$filename = $this->baseDir.\DIRECTORY_SEPARATOR.$file;
- if (preg_match("'(^|[/\\\\])\.'", $file) || !is_readable($filename)) {
+ if ('' === $file || preg_match("'(^|[/\\\\])\.'", $file) || !is_file($filename) || !is_readable($filename)) {
throw new NotFoundHttpException(\sprintf('The file "%s" cannot be opened.', $file));
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php 2026-11-04 11:23:00.000000000 +0000
@@ -76,6 +76,11 @@
$this->cspDisabled = true;
}
+ public function reset(): void
+ {
+ $this->cspDisabled = false;
+ }
+
/**
* Cleanup temporary headers and updates Content-Security-Policy headers.
*
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.php 2026-11-04 11:23:00.000000000 +0000
@@ -55,6 +55,7 @@
->args([
inline_service(NonceGenerator::class),
])
+ ->tag('kernel.reset', ['method' => 'reset'])
->set('twig.extension.webprofiler', WebProfilerExtension::class)
->args([
@@ -63,6 +64,7 @@
->call('setDisplayOptions', [['maxStringLength' => 4096, 'fileLinkFormat' => service('debug.file_link_formatter')]]),
])
->tag('twig.extension')
+ ->tag('kernel.reset', ['method' => 'reset'])
->set('debug.file_link_formatter', FileLinkFormatter::class)
->args([
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -98,6 +98,18 @@
$this->assertSame(404, $client->getResponse()->getStatusCode());
}
+ public function testProfilerPageDoesNotDisableCspOfLaterRequests()
+ {
+ $kernel = new WebProfilerBundleKernel();
+ $client = new KernelBrowser($kernel);
+ $client->disableReboot();
+
+ $client->request('GET', '/_profiler/latest');
+ $client->request('GET', '/csp');
+
+ $this->assertTrue($client->getResponse()->headers->has('Content-Security-Policy'));
+ }
+
public function testPanelActionWithValidPanelAndToken()
{
$kernel = new WebProfilerBundleKernel();
@@ -186,9 +198,43 @@
['../TwigBundle/README.md', false],
['Controller/../README.md', false],
['Controller/./ProfilerController.php', false],
+ ['', false],
+ ['Controller', false],
];
}
+ public function testOpeningWithoutFile()
+ {
+ $urlGenerator = $this->createStub(UrlGeneratorInterface::class);
+ $twig = $this->createStub(Environment::class);
+ $profiler = $this->createStub(Profiler::class);
+
+ $controller = new ProfilerController($urlGenerator, $profiler, $twig, [], null, __DIR__.'/../..');
+
+ $this->expectException(NotFoundHttpException::class);
+
+ $controller->openAction(Request::create('/_profiler/open'));
+ }
+
+ public function testOpeningPassesTheLineAsAnInteger()
+ {
+ $urlGenerator = $this->createStub(UrlGeneratorInterface::class);
+ $profiler = $this->createStub(Profiler::class);
+ $lines = [];
+ $twig = $this->createStub(Environment::class);
+ $twig->method('render')->willReturnCallback(static function (string $template, array $context) use (&$lines): string {
+ $lines[] = $context['line'];
+
+ return '';
+ });
+
+ $controller = new ProfilerController($urlGenerator, $profiler, $twig, [], null, __DIR__.'/../..');
+ $controller->openAction(Request::create('/_profiler/open', Request::METHOD_GET, ['file' => 'README.md', 'line' => '12']));
+ $controller->openAction(Request::create('/_profiler/open', Request::METHOD_GET, ['file' => 'README.md']));
+
+ $this->assertSame([12, 0], $lines);
+ }
+
/**
* @dataProvider provideCspVariants
*/
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -219,4 +219,18 @@
return $generator;
}
+
+ public function testResetReEnablesCsp()
+ {
+ $cspHandler = new ContentSecurityPolicyHandler($this->createMock(NonceGenerator::class));
+
+ $cspHandler->disableCsp();
+ $cspHandler->reset();
+
+ $response = new Response();
+ $response->headers->set('Content-Security-Policy', "default-src 'self'");
+ $cspHandler->updateResponseHeaders(new Request(), $response);
+
+ $this->assertTrue($response->headers->has('Content-Security-Policy'));
+ }
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -103,6 +103,7 @@
$this->container->removeDefinition('web_profiler.controller.exception');
$this->assertFalse($this->container->has('web_profiler.debug_toolbar'));
+ $this->assertSame([['method' => 'reset']], $this->container->getDefinition('twig.extension.webprofiler')->getTag('kernel.reset'));
self::assertSaneContainer($this->getCompiledContainer());
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php 2026-11-04 11:23:00.000000000 +0000
@@ -46,6 +46,7 @@
$routes->import(__DIR__.'/../../Resources/config/routing/profiler.xml')->prefix('/_profiler');
$routes->import(__DIR__.'/../../Resources/config/routing/wdt.xml')->prefix('/_wdt');
$routes->add('_', '/')->controller('kernel::homepageController');
+ $routes->add('csp', '/csp')->controller('kernel::cspController');
}
protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
@@ -92,4 +93,9 @@
{
return new Response('Homepage Controller.');
}
+
+ public function cspController(): Response
+ {
+ return new Response('CSP Controller.', 200, ['Content-Security-Policy' => "default-src 'self'"]);
+ }
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Tests/Twig/WebProfilerExtensionTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -15,6 +15,7 @@
use Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Twig\Environment;
+use Twig\Error\LoaderError;
use Twig\Loader\ArrayLoader;
class WebProfilerExtensionTest extends TestCase
@@ -38,6 +39,26 @@
self::assertSame($dump2HasHeader, str_contains($dump2, $needle));
}
+ public function testDumpHeaderIsDisplayedAfterResetFollowingATemplateError()
+ {
+ $extension = new WebProfilerExtension();
+ $twig = new Environment(new ArrayLoader([
+ 'dump' => '{{ profiler_dump(data) }}',
+ 'error' => '{{ include("missing.html.twig") }}',
+ ]));
+ $twig->addExtension($extension);
+ $data = (new VarCloner())->cloneVar([]);
+
+ try {
+ $twig->render('error');
+ } catch (LoaderError) {
+ }
+ $twig->render('dump', ['data' => $data]);
+ $extension->reset();
+
+ $this->assertStringContainsString('window.Sfdump', $twig->render('dump', ['data' => $data]));
+ }
+
public static function provideMessages(): iterable
{
yield ['Some message', ['foo' => 'foo', 'bar' => 'bar'], false, true];
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php
--- symfony-6.4.49+dfsg/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php 2026-11-04 11:23:00.000000000 +0000
@@ -60,6 +60,12 @@
}
}
+ public function reset(): void
+ {
+ $this->stackLevel = 0;
+ $this->dumper->setOutput($this->output = fopen('php://memory', 'r+'));
+ }
+
public function getFunctions(): array
{
return [
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php 2026-11-04 11:23:00.000000000 +0000
@@ -205,6 +205,10 @@
private function cleanupPackageFiles(ImportMapEntry $entry): void
{
+ if (!$entry->isRemotePackage()) {
+ return;
+ }
+
$asset = $this->findAsset($entry->path);
if ($asset && is_file($asset->sourcePath)) {
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageStorage.php symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageStorage.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageStorage.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageStorage.php 2026-11-04 11:23:00.000000000 +0000
@@ -12,6 +12,7 @@
namespace Symfony\Component\AssetMapper\ImportMap;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
+use Symfony\Component\Filesystem\Path;
/**
* Manages the local storage of remote/vendor importmap packages.
@@ -93,11 +94,11 @@
$filename .= '.'.$importMapType->value;
}
- return $this->vendorDir.'/'.$filename;
+ return $this->vendorDir.'/'.ltrim(Path::canonicalize('/'.$filename), '/');
}
private function getExtraFileDownloadPath(ImportMapEntry $entry, string $extraFilename): string
{
- return $this->vendorDir.'/'.$entry->getPackageName().'/'.ltrim($extraFilename, '/');
+ return $this->vendorDir.'/'.$entry->getPackageName().'/'.ltrim(Path::canonicalize('/'.$extraFilename), '/');
}
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php 2026-11-04 11:23:00.000000000 +0000
@@ -284,9 +284,10 @@
preg_match_all(self::IMPORT_REGEX, $content, $matches);
$dependencies = [];
foreach ($matches[2] as $index => $packageName) {
- $version = $matches[3][$index] ?: null;
+ $version = $matches[3][$index] ? rawurldecode($matches[3][$index]) : null;
$packageName .= $matches[4][$index]; // add the path if any
+ self::checkPackagePath($packageName);
$dependencies[] = new PackageRequireOptions($packageName, $version);
}
@@ -303,6 +304,7 @@
if (ImportMapType::JS === $type) {
$content = preg_replace_callback(self::IMPORT_REGEX, static function ($matches) use (&$dependencies) {
$packageName = $matches[2].$matches[4]; // add the path if any
+ self::checkPackagePath($packageName);
$dependencies[] = $packageName;
// replace the "/npm/package@version/+esm" with "package@version"
@@ -328,12 +330,19 @@
continue;
}
- $extraFiles[] = Path::join(\dirname($sourceFilePath), $path);
+ $extraFiles[] = Path::join(\dirname('/'.$sourceFilePath), $path);
}
return preg_replace('{/\*# sourceMappingURL=[^ ]*+ \*/}', '', $content);
}
+ private static function checkPackagePath(string $packageName): void
+ {
+ if (preg_match('{(?:^|/)\.\.?(?:/|$)}', $packageName)) {
+ throw new RuntimeException(\sprintf('Invalid import "%s": "." and ".." path segments are not allowed.', $packageName));
+ }
+ }
+
/**
* Determine the URL pattern to be used by the HTTP Client.
*/
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -470,16 +470,16 @@
if ('\\' !== \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('Must be on windows where dirname() understands backslashes');
}
- $inputAsset = new MappedAsset('app.js', 'C:\\\\project\\assets\\app.js', publicPathWithoutDigest: '/assets/app.js');
+ $inputAsset = new MappedAsset('app.js', 'C:\\project\\assets\\app.js', publicPathWithoutDigest: '/assets/app.js');
$assetMapper = $this->createStub(AssetMapperInterface::class);
$assetMapper
->method('getAssetFromSourcePath')
->willReturnCallback(static function ($path) {
return match ($path) {
- 'C://project/assets/other.js' => new MappedAsset('other.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/other.js'),
- 'C://project/assets/subdir/foo.js' => new MappedAsset('subdir/foo.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/subdir/foo.js'),
- 'C://project/root_asset.js' => new MappedAsset('root_asset.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/root_asset.js'),
+ 'C:/project/assets/other.js' => new MappedAsset('other.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/other.js'),
+ 'C:/project/assets/subdir/foo.js' => new MappedAsset('subdir/foo.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/subdir/foo.js'),
+ 'C:/project/root_asset.js' => new MappedAsset('root_asset.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/root_asset.js'),
default => throw new \RuntimeException(\sprintf('Unexpected source path "%s"', $path)),
};
});
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -299,6 +299,35 @@
yield 'package listed twice' => [['lodash', 'lodash'], 'lodash'];
}
+ public function testRemoveOnlyDeletesDownloadedFiles()
+ {
+ $manager = $this->createImportMapManager();
+ $this->writeFile('assets/app.js', 'app contents');
+ $this->writeFile('vendor/symfony/stimulus-bundle/assets/dist/loader.js', 'loader contents');
+ $this->writeFile('assets/vendor/lodash.js', 'lodash contents');
+ $this->assetMapper
+ ->method('getAssetFromSourcePath')
+ ->willReturnCallback(static fn (string $sourcePath) => new MappedAsset(basename($sourcePath), $sourcePath))
+ ;
+ $this->configReader
+ ->method('convertPathToFilesystemPath')
+ ->willReturnCallback(static fn (string $path) => str_starts_with($path, './') ? self::$writableRoot.substr($path, 1) : $path)
+ ;
+ $this->mockImportMap([
+ self::createLocalEntry('app', './assets/app.js'),
+ self::createLocalEntry('@symfony/stimulus-bundle', './vendor/symfony/stimulus-bundle/assets/dist/loader.js'),
+ self::createRemoteEntry('lodash', '1.2.3', self::$writableRoot.'/assets/vendor/lodash.js'),
+ ]);
+
+ $this->configReader->expects($this->once())->method('writeEntries');
+
+ $manager->remove(['app', '@symfony/stimulus-bundle', 'lodash']);
+
+ $this->assertFileExists(self::$writableRoot.'/assets/app.js');
+ $this->assertFileExists(self::$writableRoot.'/vendor/symfony/stimulus-bundle/assets/dist/loader.js');
+ $this->assertFileDoesNotExist(self::$writableRoot.'/assets/vendor/lodash.js');
+ }
+
public function testUpdateAll()
{
$this->packageResolver = $this->createMock(PackageResolverInterface::class);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -105,6 +105,17 @@
$this->assertEquals('any content', file_get_contents($targetPath));
}
+ public function testSaveExtraFileKeepsItInThePackageDirectory()
+ {
+ $storage = new RemotePackageStorage(self::$writableRoot.'/assets/vendor');
+ $entry = ImportMapEntry::createRemote('foo', ImportMapType::JS, '/does/not/matter', '1.0.0', 'module_specifier', false);
+ $storage->saveExtraFile($entry, '../../extra-file.woff2', 'any content');
+ $targetPath = self::$writableRoot.'/assets/vendor/module_specifier/extra-file.woff2';
+ $this->assertFileExists($targetPath);
+ $this->assertTrue($storage->isExtraFileDownloaded($entry, '../../extra-file.woff2'));
+ $this->assertFileDoesNotExist(self::$writableRoot.'/extra-file.woff2');
+ }
+
/**
* @dataProvider getDownloadPathTests
*/
@@ -139,5 +150,11 @@
'importMapType' => ImportMapType::CSS,
'expectedPath' => '/assets/vendor/foo/bar.css',
];
+
+ yield 'javascript package with dot segments in its path' => [
+ 'packageModuleSpecifier' => 'foo/../../../bar@1.0.0/bar.js',
+ 'importMapType' => ImportMapType::JS,
+ 'expectedPath' => '/assets/vendor/bar@1.0.0/bar.js',
+ ];
}
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -12,6 +12,7 @@
namespace Symfony\Component\AssetMapper\Tests\ImportMap\Resolver;
use PHPUnit\Framework\TestCase;
+use Symfony\Component\AssetMapper\Exception\RuntimeException;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions;
@@ -192,6 +193,43 @@
],
];
+ yield 'require package that imports another with an encoded version constraint' => [
+ 'packages' => [new PackageRequireOptions('prosemirror-model', '^1.25')],
+ 'expectedRequests' => [
+ [
+ 'url' => '/v1/packages/npm/prosemirror-model/resolved?specifier=%5E1.25',
+ 'response' => ['body' => ['version' => '1.25.12']],
+ ],
+ [
+ 'url' => '/prosemirror-model@1.25.12/+esm',
+ 'response' => ['body' => 'import t from"/npm/orderedmap@%5E2.0.0/+esm";function e(){}'],
+ ],
+ [
+ 'url' => '/v1/packages/npm/prosemirror-model@1.25.12/entrypoints',
+ 'response' => ['body' => ['entrypoints' => []]],
+ ],
+ [
+ 'url' => '/v1/packages/npm/orderedmap/resolved?specifier=%5E2.0.0',
+ 'response' => ['body' => ['version' => '2.1.1']],
+ ],
+ [
+ 'url' => '/orderedmap@2.1.1/+esm',
+ ],
+ [
+ 'url' => '/v1/packages/npm/orderedmap@2.1.1/entrypoints',
+ 'response' => ['body' => ['entrypoints' => []]],
+ ],
+ ],
+ 'expectedResolvedPackages' => [
+ 'prosemirror-model' => [
+ 'version' => '1.25.12',
+ ],
+ 'orderedmap' => [
+ 'version' => '2.1.1',
+ ],
+ ],
+ ];
+
yield 'require single CSS package' => [
'packages' => [new PackageRequireOptions('bootstrap/dist/css/bootstrap.min.css')],
'expectedRequests' => [
@@ -539,6 +577,78 @@
]);
}
+ public function testDownloadCssFileAtPackageRootWithUrlReferences()
+ {
+ $expectedRequests = [
+ [
+ 'url' => '/npm/some-css-package@1.0.0',
+ 'body' => '@font-face{src:url("fonts/a.woff2") format("woff2"),url("../b.woff2") format("woff2")}',
+ ],
+ [
+ 'url' => '/npm/some-css-package@1.0.0/fonts/a.woff2',
+ 'body' => 'a font contents',
+ ],
+ [
+ 'url' => '/npm/some-css-package@1.0.0/b.woff2',
+ 'body' => 'b font contents',
+ ],
+ ];
+ $responses = [];
+ foreach ($expectedRequests as $expectedRequest) {
+ $responses[] = function ($method, $url) use ($expectedRequest) {
+ $this->assertSame('GET', $method);
+ $this->assertStringEndsWith($expectedRequest['url'], $url);
+
+ return new MockResponse($expectedRequest['body']);
+ };
+ }
+
+ $httpClient = new MockHttpClient($responses);
+
+ $provider = new JsDelivrEsmResolver($httpClient);
+ $actualReturn = $provider->downloadPackages([
+ 'some-css-package' => self::createRemoteEntry('some-css-package', version: '1.0.0', type: ImportMapType::CSS),
+ ]);
+ $this->assertSame(\count($responses), $httpClient->getRequestsCount());
+
+ $this->assertSame([
+ '/fonts/a.woff2' => 'a font contents',
+ '/b.woff2' => 'b font contents',
+ ], $actualReturn['some-css-package']['extraFiles']);
+ }
+
+ public function testResolvePackagesRejectsDotSegmentsInImports()
+ {
+ $httpClient = new MockHttpClient(static fn ($method, $url) => match (true) {
+ str_contains($url, '/resolved') => new MockResponse(json_encode(['version' => '1.0.0'])),
+ str_contains($url, '/entrypoints') => new MockResponse(json_encode(['entrypoints' => []])),
+ default => new MockResponse('import "/npm/foo@1.0.0/../../bar@1.0.0/bar.js/+esm";'),
+ });
+
+ $provider = new JsDelivrEsmResolver($httpClient);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Invalid import "foo/../../bar@1.0.0/bar.js": "." and ".." path segments are not allowed.');
+
+ $provider->resolvePackages([new PackageRequireOptions('foo')]);
+ }
+
+ public function testDownloadPackagesRejectsDotSegmentsInImports()
+ {
+ $httpClient = new MockHttpClient([
+ new MockResponse('import "/npm/foo@1.0.0/../../bar@1.0.0/bar.js/+esm";'),
+ ]);
+
+ $provider = new JsDelivrEsmResolver($httpClient);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage('Invalid import "foo/../../bar@1.0.0/bar.js": "." and ".." path segments are not allowed.');
+
+ $provider->downloadPackages([
+ 'foo' => self::createRemoteEntry('foo', version: '1.0.0'),
+ ]);
+ }
+
public function testDownloadCssRecursivelyDownloadsUrlCss()
{
$expectedRequests = [
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/Cookie.php symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/Cookie.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/Cookie.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/Cookie.php 2026-11-04 11:23:00.000000000 +0000
@@ -205,6 +205,8 @@
$now = time();
// 1 is the earliest expiration time, 0 means a session cookie
$values['expires'] = 0 < $maxAge ? (\PHP_INT_MAX - $now > $maxAge ? $now + $maxAge : \PHP_INT_MAX) : 1;
+ } elseif ('0' === $values['expires']) {
+ $values['expires'] = 1;
}
return new static(
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/CookieJar.php symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/CookieJar.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/CookieJar.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/CookieJar.php 2026-11-04 11:23:00.000000000 +0000
@@ -27,7 +27,7 @@
*/
public function set(Cookie $cookie)
{
- $this->cookieJar[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
+ $this->cookieJar[self::normalizeDomain($cookie->getDomain())][$cookie->getPath()][$cookie->getName()] = $cookie;
}
/**
@@ -44,6 +44,10 @@
{
$this->flushExpiredCookies();
+ if (null !== $domain) {
+ $domain = self::normalizeDomain($domain);
+ }
+
$match = null;
foreach ($this->cookieJar as $cookieDomain => $pathCookies) {
@@ -85,7 +89,7 @@
// this should never happen but it allows for a better BC
$domains = array_keys($this->cookieJar);
} else {
- $domains = [$domain];
+ $domains = [self::normalizeDomain($domain)];
}
foreach ($domains as $domain) {
@@ -180,6 +184,9 @@
$this->flushExpiredCookies();
$parts = array_replace(['path' => '/'], parse_url($uri));
+ if (isset($parts['host'])) {
+ $parts['host'] = self::normalizeDomain($parts['host']);
+ }
$cookies = [];
foreach ($this->cookieJar as $domain => $pathCookies) {
if ($domain) {
@@ -257,4 +264,22 @@
return str_starts_with($requestPath, $cookiePath) && (str_ends_with($cookiePath, '/') || '/' === $requestPath[\strlen($cookiePath)]);
}
+
+ private static function normalizeDomain(string $domain): string
+ {
+ if (!str_contains($domain, ':') || str_contains($domain, "\0")) {
+ return $domain;
+ }
+
+ $ip = ltrim($domain, '.');
+ $prefix = substr($domain, 0, \strlen($domain) - \strlen($ip));
+ $bracketed = str_starts_with($ip, '[') && str_ends_with($ip, ']');
+ if (false !== $address = @inet_pton($bracketed ? substr($ip, 1, -1) : $ip)) {
+ if (false !== $address = inet_ntop($address)) {
+ return $prefix.($bracketed ? '['.$address.']' : $address);
+ }
+ }
+
+ return $domain;
+ }
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/Tests/CookieJarTest.php symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/Tests/CookieJarTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/Tests/CookieJarTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/Tests/CookieJarTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -18,6 +18,102 @@
class CookieJarTest extends TestCase
{
+ public function testCookiesMatchEquivalentIpv6Hosts()
+ {
+ foreach ([
+ ['[0:0:0:0:0:0:0:1]', '[::1]'],
+ ['[2001:0DB8:0:0:0:0:0:1]', '[2001:db8::1]'],
+ ['[0:0:0:0:0:ffff:c000:280]', '[::ffff:192.0.2.128]'],
+ ] as [$firstHost, $secondHost]) {
+ $cookieJar = new CookieJar();
+ $cookieJar->updateFromSetCookie(['sid=old'], 'https://'.$firstHost.'/');
+
+ $this->assertSame(['sid' => 'old'], $cookieJar->allValues('https://'.$secondHost.'/'));
+ $this->assertSame('old', $cookieJar->get('sid', '/', $secondHost)->getValue());
+
+ $cookieJar->updateFromSetCookie(['sid=new'], 'https://'.$secondHost.'/');
+
+ $this->assertCount(1, $cookieJar->all());
+ $this->assertSame(['sid' => 'new'], $cookieJar->allValues('https://'.$firstHost.'/'));
+
+ $cookieJar->expire('sid', '/', $firstHost);
+
+ $this->assertSame([], $cookieJar->all());
+ }
+ }
+
+ public function testCookiesWithLeadingDotIpv6Domains()
+ {
+ foreach (['.', '..'] as $prefix) {
+ $cookieJar = new CookieJar();
+ $cookieJar->set($cookie = new Cookie('sid', 'old', null, '/', $prefix.'[0:0:0:0:0:0:0:1]'));
+
+ $this->assertSame($cookie, $cookieJar->get('sid', '/', '[0:0:0:0:0:0:0:1]'));
+ $this->assertSame(['sid' => 'old'], $cookieJar->allValues('https://[0:0:0:0:0:0:0:1]/'));
+ $this->assertSame(['sid' => 'old'], $cookieJar->allValues('https://[::1]/'));
+ $this->assertSame($prefix.'[0:0:0:0:0:0:0:1]', $cookie->getDomain());
+
+ $cookieJar->set(new Cookie('sid', 'new', null, '/', $prefix.'[::1]'));
+
+ $this->assertCount(1, $cookieJar->all());
+ $this->assertSame('new', $cookieJar->get('sid', '/', '[::1]')->getValue());
+
+ $cookieJar->expire('sid', '/', $prefix.'[0:0:0:0:0:0:0:1]');
+
+ $this->assertSame([], $cookieJar->all());
+ }
+ }
+
+ public function testCookieIpv6DomainPrefixesStaySeparate()
+ {
+ $cookieJar = new CookieJar();
+ foreach (['', '.', '..'] as $prefix) {
+ $cookieJar->set(new Cookie('sid', 'value', null, '/', $prefix.'[0:0:0:0:0:0:0:1]'));
+ }
+
+ $this->assertCount(3, $cookieJar->all());
+
+ $cookieJar->expire('sid', '/', '[::1]');
+ $this->assertCount(2, $cookieJar->all());
+
+ $cookieJar->expire('sid', '/', '.[::1]');
+ $this->assertCount(1, $cookieJar->all());
+ $this->assertSame('..[0:0:0:0:0:0:0:1]', $cookieJar->all()[0]->getDomain());
+ }
+
+ public function testManuallySetCookiesMatchEquivalentIpv6Domains()
+ {
+ $cookieJar = new CookieJar();
+ $cookieJar->set($cookie = new Cookie('sid', 'old', null, '/', '0:0:0:0:0:0:0:1'));
+
+ $this->assertSame($cookie, $cookieJar->get('sid', '/', '::1'));
+ $this->assertSame('0:0:0:0:0:0:0:1', $cookie->getDomain());
+
+ $cookieJar->set(new Cookie('sid', 'new', null, '/', '::1'));
+
+ $this->assertCount(1, $cookieJar->all());
+ $this->assertSame('new', $cookieJar->get('sid', '/', '0:0:0:0:0:0:0:1')->getValue());
+
+ $cookieJar->expire('sid', '/', '0:0:0:0:0:0:0:1');
+
+ $this->assertSame([], $cookieJar->all());
+ }
+
+ public function testCookieIpv6DomainsStaySeparate()
+ {
+ $cookieJar = new CookieJar();
+ $cookieJar->set(new Cookie('sid', 'bare', null, '/', '::1'));
+ $cookieJar->set(new Cookie('sid', 'bracketed', null, '/', '[::1]'));
+ $cookieJar->set(new Cookie('sid', 'other', null, '/', '[::2]'));
+ $cookieJar->set(new Cookie('sid', 'invalid', null, '/', '[invalid:address]'));
+
+ $this->assertCount(4, $cookieJar->all());
+ $this->assertSame('bare', $cookieJar->get('sid', '/', '::1')->getValue());
+ $this->assertSame(['sid' => 'bracketed'], $cookieJar->allValues('https://[::1]/'));
+ $this->assertSame(['sid' => 'other'], $cookieJar->allValues('https://[::2]/'));
+ $this->assertSame('invalid', $cookieJar->get('sid', '/', '[invalid:address]')->getValue());
+ }
+
public function testSetGet()
{
$cookieJar = new CookieJar();
@@ -93,6 +189,16 @@
$this->assertNull($cookieJar->get('foo'));
}
+ public function testUpdateFromSetCookieDeletesCookiesAtTheUnixEpoch()
+ {
+ $cookieJar = new CookieJar();
+ $cookieJar->updateFromSetCookie(['foo=bar'], 'https://example.com/');
+ $cookieJar->updateFromSetCookie(['foo=bar; Expires=Thu, 01 Jan 1970 00:00:00 GMT'], 'https://example.com/');
+
+ $this->assertNull($cookieJar->get('foo'));
+ $this->assertSame([], $cookieJar->allValues('https://example.com/'));
+ }
+
public function testUpdateFromSetCookieIgnoresAnEmptyDomainAttribute()
{
$cookieJar = new CookieJar();
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/Tests/CookieTest.php symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/Tests/CookieTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/Tests/CookieTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/Tests/CookieTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -158,6 +158,13 @@
$this->assertSame((string) \PHP_INT_MAX, Cookie::fromString('foo=bar; Max-Age=99999999999999999999')->getExpiresTime());
}
+ public function testFromStringExpiresCookiesAtTheUnixEpoch()
+ {
+ $this->assertTrue(Cookie::fromString('foo=bar; Expires=Thu, 01 Jan 1970 00:00:00 GMT')->isExpired());
+ $this->assertFalse(Cookie::fromString('foo=bar; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=60')->isExpired());
+ $this->assertFalse((new Cookie('foo', 'bar', 0))->isExpired());
+ }
+
public function testFromStringExpiresCookiesWithNonPositiveMaxAge()
{
$this->assertTrue(Cookie::fromString('foo=bar; Max-Age=0')->isExpired());
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/BrowserKit/Tests/HttpBrowserTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -22,6 +22,56 @@
class HttpBrowserTest extends AbstractBrowserTest
{
+ public function testDoesNotReplayCookiesExpiredAtTheUnixEpochAfterRedirect()
+ {
+ $client = new MockHttpClient([
+ new MockResponse('', ['http_code' => 302, 'response_headers' => [
+ 'Location: https://example.com/next',
+ 'Set-Cookie: sid=value; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/',
+ ]]),
+ function ($method, $url, $options) {
+ $this->assertArrayNotHasKey('cookie', $options['normalized_headers']);
+
+ return new MockResponse();
+ },
+ ]);
+ $browser = new HttpBrowser($client);
+ $browser->getCookieJar()->updateFromSetCookie(['sid=old'], 'https://example.com/');
+ $browser->request('GET', 'https://example.com/');
+ }
+
+ public function testCookieDomainsWithNullBytesDoNotThrow()
+ {
+ $client = new MockHttpClient([
+ new MockResponse('', ['response_headers' => ["Set-Cookie: sid=value; Domain=[x:\0y]"]]),
+ function ($method, $url, $options) {
+ $this->assertArrayNotHasKey('cookie', $options['normalized_headers']);
+
+ return new MockResponse();
+ },
+ ]);
+ $browser = new HttpBrowser($client);
+ $browser->request('GET', 'https://[::1]/');
+ $browser->request('GET', 'https://[::1]/');
+ }
+
+ public function testReplaysCookiesAfterRedirectToEquivalentIpv6Host()
+ {
+ $client = new MockHttpClient([
+ new MockResponse('', ['http_code' => 302, 'response_headers' => [
+ 'Location: https://[::1]/next',
+ 'Set-Cookie: sid=value; Path=/',
+ ]]),
+ function ($method, $url, $options) {
+ $this->assertSame(['cookie: sid=value'], $options['normalized_headers']['cookie'] ?? []);
+
+ return new MockResponse();
+ },
+ ]);
+ $browser = new HttpBrowser($client);
+ $browser->request('GET', 'https://[0:0:0:0:0:0:0:1]/');
+ }
+
public function getBrowser(array $server = [], ?History $history = null, ?CookieJar $cookieJar = null)
{
return new TestHttpClient($server, $history, $cookieJar);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/AbstractAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -109,7 +109,7 @@
return new ChainAdapter([$apcu, $opcache]);
}
- public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed
+ public static function createConnection(#[\SensitiveParameter] string $dsn, #[\SensitiveParameter] array $options = []): mixed
{
if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
return RedisAdapter::createConnection($dsn, $options);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -258,6 +258,8 @@
} catch (\Exception) {
}
+ $ok = true;
+
// When bulk-delete failed, retry each item individually
foreach ($ids as $key => $id) {
try {
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -54,7 +54,7 @@
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
- public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \CouchbaseBucket
+ public static function createConnection(#[\SensitiveParameter] array|string $servers, #[\SensitiveParameter] array $options = []): \CouchbaseBucket
{
if (\is_string($servers)) {
$servers = [$servers];
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/CouchbaseCollectionAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -47,7 +47,7 @@
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
- public static function createConnection(#[\SensitiveParameter] array|string $dsn, array $options = []): Bucket|Collection
+ public static function createConnection(#[\SensitiveParameter] array|string $dsn, #[\SensitiveParameter] array $options = []): Bucket|Collection
{
if (\is_string($dsn)) {
$dsn = [$dsn];
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/DoctrineDbalAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -66,7 +66,7 @@
*
* @throws InvalidArgumentException When namespace contains invalid characters
*/
- public function __construct(Connection|string $connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null)
+ public function __construct(#[\SensitiveParameter] Connection|string $connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null)
{
if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(\sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/FilesystemTagAwareAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -11,6 +11,7 @@
namespace Symfony\Component\Cache\Adapter;
+use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
use Symfony\Component\Cache\PruneableInterface;
@@ -170,14 +171,22 @@
protected function doDeleteYieldTags(array $ids): iterable
{
+ $failed = false;
+
foreach ($ids as $id) {
$file = $this->getFile($id);
- if (!is_file($file) || !$h = @fopen($file, 'r')) {
+ if (!is_file($file)) {
+ continue;
+ }
+
+ if (!$h = @fopen($file, 'r')) {
+ $failed = true;
continue;
}
if (!@unlink($file)) {
fclose($h);
+ $failed = true;
continue;
}
@@ -202,6 +211,11 @@
fclose($h);
}
+
+ if ($failed) {
+ // Make deleteItems() retry deleting the items one by one
+ throw new CacheException('Failed to delete some keys.');
+ }
}
protected function doDeleteTagRelations(array $tagData): bool
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/MemcachedAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -89,7 +89,7 @@
*
* @throws \ErrorException When invalid options or servers are provided
*/
- public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \Memcached
+ public static function createConnection(#[\SensitiveParameter] array|string $servers, #[\SensitiveParameter] array $options = []): \Memcached
{
if (\is_string($servers)) {
$servers = [$servers];
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/PdoAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/PdoAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/PdoAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/PdoAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -54,7 +54,7 @@
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
* @throws InvalidArgumentException When namespace contains invalid characters
*/
- public function __construct(#[\SensitiveParameter] \PDO|string $connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null)
+ public function __construct(#[\SensitiveParameter] \PDO|string $connOrDsn, string $namespace = '', int $defaultLifetime = 0, #[\SensitiveParameter] array $options = [], ?MarshallerInterface $marshaller = null)
{
if (\is_string($connOrDsn) && str_contains($connOrDsn, '://')) {
throw new InvalidArgumentException(\sprintf('Usage of Doctrine DBAL URL with "%s" is not supported. Use a PDO DSN or "%s" instead.', __CLASS__, DoctrineDbalAdapter::class));
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php 2026-11-04 11:23:00.000000000 +0000
@@ -19,6 +19,7 @@
use Predis\Response\Status;
use Relay\Relay;
use Symfony\Component\Cache\CacheItem;
+use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Exception\LogicException;
use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
@@ -156,9 +157,10 @@
}
});
+ $failed = false;
foreach ($results as $id => $result) {
- if ($result instanceof \RedisException || $result instanceof \Relay\Exception || $result instanceof ErrorInterface) {
- CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]);
+ if (false === $result || $result instanceof \RedisException || $result instanceof \Relay\Exception || $result instanceof ErrorInterface) {
+ $failed = true;
continue;
}
@@ -169,6 +171,11 @@
yield $id => [];
}
}
+
+ if ($failed) {
+ // Make deleteItems() retry deleting the items one by one
+ throw new CacheException('Failed to delete some keys.');
+ }
}
protected function doDeleteTagRelations(array $tagData): bool
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationDispatcher.php 2026-11-04 11:23:00.000000000 +0000
@@ -35,7 +35,7 @@
}
/**
- * @return mixed
+ * @return mixed The computed value, or the cached value when recomputation is deferred or skipped
*/
public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null, ?float $beta = null)
{
@@ -59,6 +59,12 @@
// The item's value is not stale, no need to write it to the backend
$save = false;
+ foreach ($envelope->all(HandledStamp::class) as $stamp) {
+ if (($result = $stamp->getResult()) instanceof EarlyExpirationResult && $message->getItem() === $result->item) {
+ return $message->getItem()->get();
+ }
+ }
+
return $message->getItem()->get() ?? $item->get();
}
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationHandler.php 2026-11-04 11:23:00.000000000 +0000
@@ -30,7 +30,7 @@
}
/**
- * @return void
+ * @return EarlyExpirationResult|null
*/
public function __invoke(EarlyExpirationMessage $message)
{
@@ -42,16 +42,16 @@
if ($expiry && $ctime) {
// skip duplicate or expired messages
- $processingNonce = [$expiry, $ctime];
+ $processingNonce = (object) ['value' => [$expiry, $ctime]];
$pool = $message->getPool();
$key = $item->getKey();
- if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) {
- return;
+ if (($this->processedNonces[$pool][$key]->value ?? null) === $processingNonce->value) {
+ return null;
}
if (microtime(true) >= $expiry) {
- return;
+ return null;
}
$this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []);
@@ -74,12 +74,28 @@
CacheItem::class
);
- $startTime = microtime(true);
- $pool = $message->findPool($this->reverseContainer);
- $callback = $message->findCallback($this->reverseContainer);
- $save = true;
- $value = $callback($item, $save);
- $setMetadata($item, $startTime);
- $pool->save($item->set($value));
+ try {
+ $startTime = microtime(true);
+ $pool = $message->findPool($this->reverseContainer);
+ $callback = $message->findCallback($this->reverseContainer);
+ $save = true;
+ $item->set($callback($item, $save));
+
+ if ($save) {
+ $setMetadata($item, $startTime);
+ $pool->save($item);
+ }
+
+ return new EarlyExpirationResult($item);
+ } catch (\Throwable $e) {
+ $pool = $message->getPool();
+ $key = $item->getKey();
+
+ if (isset($processingNonce) && ($this->processedNonces[$pool][$key] ?? null) === $processingNonce) {
+ unset($this->processedNonces[$pool][$key]);
+ }
+
+ throw $e;
+ }
}
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationResult.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationResult.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationResult.php 1970-01-01 00:00:00.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Messenger/EarlyExpirationResult.php 2026-11-04 11:23:00.000000000 +0000
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Messenger;
+
+use Symfony\Component\Cache\CacheItem;
+
+/**
+ * @internal
+ */
+final class EarlyExpirationResult
+{
+ public function __construct(public readonly CacheItem $item)
+ {
+ }
+}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/AbstractAdapterCreateConnectionTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/AbstractAdapterCreateConnectionTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/AbstractAdapterCreateConnectionTest.php 1970-01-01 00:00:00.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/AbstractAdapterCreateConnectionTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -0,0 +1,59 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Tests\Adapter;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Cache\Adapter\AbstractAdapter;
+use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter;
+use Symfony\Component\Cache\Exception\CacheException;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
+
+class AbstractAdapterCreateConnectionTest extends TestCase
+{
+ public function testOptionsAreNotExposedInTrace()
+ {
+ if (!class_exists(\SensitiveParameterValue::class)) {
+ $this->markTestSkipped('Sensitive parameter support requires PHP 8.2.');
+ }
+
+ $previous = ini_set('zend.exception_ignore_args', '0');
+ try {
+ foreach (['redis://localhost:999999', 'memcached://localhost:999999', 'couchbase://localhost:999999/cache'] as $dsn) {
+ try {
+ AbstractAdapter::createConnection($dsn, ['auth' => 's3cr3t', 'password' => 's3cr3t']);
+ $this->fail('An exception should have been thrown.');
+ } catch (CacheException|InvalidArgumentException $e) {
+ $this->assertStringNotContainsString('s3cr3t', json_encode($e->getTrace(), \JSON_THROW_ON_ERROR));
+ }
+ }
+ } finally {
+ ini_set('zend.exception_ignore_args', $previous);
+ }
+ }
+
+ public function testCouchbaseBucketOptionsAreNotExposedInTrace()
+ {
+ if (!class_exists(\SensitiveParameterValue::class)) {
+ $this->markTestSkipped('Sensitive parameter support requires PHP 8.2.');
+ }
+
+ $previous = ini_set('zend.exception_ignore_args', '0');
+ try {
+ CouchbaseBucketAdapter::createConnection('couchbase://localhost:999999/cache', ['password' => 's3cr3t']);
+ $this->fail('An exception should have been thrown.');
+ } catch (CacheException|InvalidArgumentException $e) {
+ $this->assertStringNotContainsString('s3cr3t', json_encode($e->getTrace(), \JSON_THROW_ON_ERROR));
+ } finally {
+ ini_set('zend.exception_ignore_args', $previous);
+ }
+ }
+}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTestCase.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTestCase.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTestCase.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTestCase.php 2026-11-04 11:23:00.000000000 +0000
@@ -14,6 +14,7 @@
use Psr\Cache\CacheItemPoolInterface;
use Relay\Relay;
use Symfony\Component\Cache\Adapter\RedisAdapter;
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
abstract class AbstractRedisAdapterTestCase extends AdapterTestCase
{
@@ -55,4 +56,21 @@
$cache->clear();
$this->assertFalse($cache->hasItem('foo'));
}
+
+ public function testDeleteItemsWhenKeyHoldsAnotherType()
+ {
+ $cache = $this->createCachePool();
+
+ if (!$cache instanceof RedisAdapter && !$cache instanceof RedisTagAwareAdapter) {
+ $this->markTestSkipped('The Redis key of the item is not known.');
+ }
+
+ $cache->save($cache->getItem('foo')->set('foo'));
+ self::$redis->sAdd((new \ReflectionMethod($cache, 'getId'))->invoke($cache, 'bar'), 'bar');
+
+ $cache->deleteItems(['foo', 'bar']);
+
+ $this->assertFalse($cache->hasItem('foo'));
+ $this->assertFalse($cache->hasItem('bar'));
+ }
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/DoctrineDbalAdapterTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -22,6 +22,7 @@
use Doctrine\DBAL\Schema\Table;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\DoctrineDbalAdapter;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Tests\Fixtures\DriverWrapper;
/**
@@ -215,6 +216,23 @@
$doSave->invoke($adapter, ['key' => 'value'], 0);
}
+ public function testDsnIsNotExposedInTrace()
+ {
+ if (!class_exists(\SensitiveParameterValue::class)) {
+ $this->markTestSkipped('Sensitive parameter support requires PHP 8.2.');
+ }
+
+ $previous = ini_set('zend.exception_ignore_args', '0');
+ try {
+ new DoctrineDbalAdapter('pgsql://user:s3cr3t@localhost/db', 'invalid:namespace');
+ $this->fail('An exception should have been thrown.');
+ } catch (InvalidArgumentException $e) {
+ $this->assertStringNotContainsString('s3cr3t', json_encode($e->getTrace(), \JSON_THROW_ON_ERROR));
+ } finally {
+ ini_set('zend.exception_ignore_args', $previous);
+ }
+ }
+
protected function isPruned(DoctrineDbalAdapter $cache, string $name): bool
{
$o = new \ReflectionObject($cache);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/FilesystemAdapterTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -30,6 +30,44 @@
(new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache');
}
+ public function testDeleteItemsWhenUnlinkFails()
+ {
+ $cache = $this->createCachePool();
+ $cache->save($cache->getItem('foo')->set('foo'));
+ $cache->save($cache->getItem('bar')->set('bar'));
+
+ $file = (new \ReflectionMethod($cache, 'getFile'))->invoke($cache, (new \ReflectionMethod($cache, 'getId'))->invoke($cache, 'foo'));
+
+ // Windows doesn't delete read-only files, other systems don't delete files in read-only directories
+ chmod($file, 0o444);
+ chmod(\dirname($file), 0o555);
+
+ try {
+ $this->assertFalse($cache->deleteItems(['foo', 'bar']));
+ $this->assertTrue($cache->hasItem('foo'));
+ $this->assertFalse($cache->hasItem('bar'));
+ } finally {
+ chmod(\dirname($file), 0o777);
+ chmod($file, 0o666);
+ }
+ }
+
+ public function testDeleteItemsWhenFileIsNotReadable()
+ {
+ if ('\\' === \DIRECTORY_SEPARATOR) {
+ $this->markTestSkipped('Files cannot be made unreadable on Windows.');
+ }
+
+ $cache = $this->createCachePool();
+ $cache->save($cache->getItem('foo')->set('foo'));
+
+ $file = (new \ReflectionMethod($cache, 'getFile'))->invoke($cache, (new \ReflectionMethod($cache, 'getId'))->invoke($cache, 'foo'));
+ chmod($file, 0o000);
+
+ $this->assertTrue($cache->deleteItems(['foo']));
+ $this->assertFalse($cache->hasItem('foo'));
+ }
+
protected function isPruned(CacheItemPoolInterface $cache, string $name): bool
{
$getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile');
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/FilesystemTagAwareAdapterTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/FilesystemTagAwareAdapterTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/FilesystemTagAwareAdapterTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/FilesystemTagAwareAdapterTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -38,4 +38,18 @@
$this->assertFalse($adapter->commit(), 'Commit should return false when tag save fails');
}
+
+ public function testDeleteItemsWhenFetchingTagsFails()
+ {
+ $pool = new class extends FilesystemTagAwareAdapter {
+ protected function doDeleteYieldTags(array $ids): iterable
+ {
+ throw new \RuntimeException('Fetching tags failed.');
+ }
+ };
+ $pool->save($pool->getItem('foo')->set('bar'));
+
+ $this->assertTrue($pool->deleteItem('foo'));
+ $this->assertFalse($pool->hasItem('foo'));
+ }
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/PdoAdapterTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -13,6 +13,7 @@
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\PdoAdapter;
+use Symfony\Component\Cache\Exception\InvalidArgumentException;
/**
* @requires extension pdo_sqlite
@@ -115,6 +116,23 @@
}
}
+ public function testOptionsAreNotExposedInTrace()
+ {
+ if (!class_exists(\SensitiveParameterValue::class)) {
+ $this->markTestSkipped('Sensitive parameter support requires PHP 8.2.');
+ }
+
+ $previous = ini_set('zend.exception_ignore_args', '0');
+ try {
+ new PdoAdapter('mysql://localhost/db', '', 0, ['db_password' => 's3cr3t']);
+ $this->fail('An exception should have been thrown.');
+ } catch (InvalidArgumentException $e) {
+ $this->assertStringNotContainsString('s3cr3t', json_encode($e->getTrace(), \JSON_THROW_ON_ERROR));
+ } finally {
+ ini_set('zend.exception_ignore_args', $previous);
+ }
+ }
+
protected function isPruned(PdoAdapter $cache, string $name): bool
{
$o = new \ReflectionObject($cache);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Adapter/PredisReplicationAdapterTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -11,6 +11,9 @@
namespace Symfony\Component\Cache\Tests\Adapter;
+use Symfony\Component\Cache\Adapter\RedisAdapter;
+use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
+
/**
* @group integration
*/
@@ -37,4 +40,12 @@
self::$redis = new \Predis\Client($hostTable, ['replication' => 'predis', 'prefix' => 'prefix_']);
}
+
+ public function testDeleteItemsOnReadOnlyReplica()
+ {
+ $replica = new \Predis\Client(array_combine(['host', 'port'], explode(':', explode(' ', getenv('REDIS_REPLICATION_HOSTS'))[0])));
+
+ $this->assertFalse((new RedisAdapter($replica))->deleteItems(['foo', 'bar']));
+ $this->assertFalse((new RedisTagAwareAdapter($replica))->deleteItems(['foo', 'bar']));
+ }
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationDispatcherTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -12,20 +12,32 @@
namespace Symfony\Component\Cache\Tests\Messenger;
use PHPUnit\Framework\TestCase;
+use Psr\Cache\CacheItemInterface;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher;
+use Symfony\Component\Cache\Messenger\EarlyExpirationHandler;
use Symfony\Component\Cache\Messenger\EarlyExpirationMessage;
+use Symfony\Component\Cache\Messenger\EarlyExpirationResult;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ReverseContainer;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Messenger\Envelope;
+use Symfony\Component\Messenger\Handler\HandlerDescriptor;
+use Symfony\Component\Messenger\Handler\HandlersLocator;
use Symfony\Component\Messenger\MessageBus;
use Symfony\Component\Messenger\MessageBusInterface;
+use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
+use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
+use Symfony\Component\Messenger\Stamp\ReceivedStamp;
+use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport;
+use Symfony\Component\Messenger\Transport\InMemoryTransport as LegacyInMemoryTransport;
+use Symfony\Component\Messenger\Transport\Sender\SendersLocator;
+use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
class EarlyExpirationDispatcherTest extends TestCase
{
@@ -127,6 +139,100 @@
];
$this->assertSame($expected, $logger->records);
}
+
+ /**
+ * @group time-sensitive
+ */
+ public function testSynchronousEarlyExpirationReturnsNullAndSkipsDuplicates()
+ {
+ $pool = new class extends FilesystemAdapter {
+ public bool $failSave = false;
+
+ public function save(CacheItemInterface $item): bool
+ {
+ return !$this->failSave && parent::save($item);
+ }
+ };
+ $pool->get('null-sync', static function (CacheItem $item) {
+ $item->expiresAfter(3600);
+ usleep(1000);
+
+ return 234;
+ });
+ $pool->failSave = true;
+
+ $callback = new class {
+ public int $calls = 0;
+
+ public function __invoke()
+ {
+ ++$this->calls;
+
+ return null;
+ }
+ };
+
+ $container = new Container();
+ $container->set('callback', $callback);
+ $container->set('pool', $pool);
+ $reverseContainer = new ReverseContainer($container, new ServiceLocator([]));
+ $handler = new EarlyExpirationHandler($reverseContainer);
+ $observer = new HandlerDescriptor(static fn (EarlyExpirationMessage $message) => $message->getItem(), ['alias' => 'observer']);
+ $tupleObserver = new HandlerDescriptor(static fn (EarlyExpirationMessage $message) => [$message->getItem()], ['alias' => 'tuple']);
+ $otherItem = $pool->getItem('unrelated');
+ $foreignObserver = new HandlerDescriptor(static fn () => new EarlyExpirationResult($otherItem), ['alias' => 'foreign']);
+ $bus = new MessageBus([new HandleMessageMiddleware(new HandlersLocator([EarlyExpirationMessage::class => [$handler, $observer, $tupleObserver, $foreignObserver]]))]);
+ $pool->setCallbackWrapper(new EarlyExpirationDispatcher($bus, $reverseContainer));
+
+ $this->assertNull($pool->get('null-sync', $callback, \INF));
+ $this->assertSame(234, $pool->getItem('null-sync')->get());
+ $this->assertSame(234, $pool->get('null-sync', $callback, \INF));
+ $this->assertSame(1, $callback->calls);
+ }
+
+ public function testAsynchronousEarlyExpirationPreservesCurrentValue()
+ {
+ $pool = new FilesystemAdapter();
+ $pool->save($pool->getItem('null-async')->set(234));
+ $callback = new class {
+ public int $calls = 0;
+
+ public function __invoke()
+ {
+ ++$this->calls;
+
+ return null;
+ }
+ };
+
+ $container = new Container();
+ $container->set('callback', $callback);
+ $container->set('pool', $pool);
+ $reverseContainer = new ReverseContainer($container, new ServiceLocator([]));
+ $pool->setCallbackWrapper(new EarlyExpirationDispatcher(new MessageBus(), $reverseContainer));
+ $this->assertSame(234, $pool->get('null-async', $callback, \INF));
+ $this->assertSame(0, $callback->calls);
+
+ $transportClass = class_exists(InMemoryTransport::class) ? InMemoryTransport::class : LegacyInMemoryTransport::class;
+ $transport = new $transportClass(new PhpSerializer());
+ $container->set('async', $transport);
+ $handler = new EarlyExpirationHandler($reverseContainer);
+ $bus = new MessageBus([
+ new SendMessageMiddleware(new SendersLocator([EarlyExpirationMessage::class => ['async']], $container)),
+ new HandleMessageMiddleware(new HandlersLocator([EarlyExpirationMessage::class => [$handler]])),
+ ]);
+ $pool->setCallbackWrapper(new EarlyExpirationDispatcher($bus, $reverseContainer));
+
+ $this->assertSame(234, $pool->get('null-async', $callback, \INF));
+ $this->assertSame(0, $callback->calls);
+ $this->assertCount(1, $transport->getSent());
+
+ $bus->dispatch($transport->get()[0]->with(new ReceivedStamp('async')));
+
+ $this->assertSame(1, $callback->calls);
+ $this->assertTrue($pool->hasItem('null-async'));
+ $this->assertNull($pool->get('null-async', $this->fail(...), 0));
+ }
}
final class TestLogger extends AbstractLogger
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Tests/Messenger/EarlyExpirationHandlerTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -14,14 +14,36 @@
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
+use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher;
use Symfony\Component\Cache\Messenger\EarlyExpirationHandler;
use Symfony\Component\Cache\Messenger\EarlyExpirationMessage;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ReverseContainer;
use Symfony\Component\DependencyInjection\ServiceLocator;
+use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Filesystem\Filesystem;
+use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
+use Symfony\Component\Messenger\Event\WorkerRunningEvent;
+use Symfony\Component\Messenger\EventListener\SendFailedMessageForRetryListener;
+use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
+use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener;
+use Symfony\Component\Messenger\Handler\HandlersLocator;
+use Symfony\Component\Messenger\MessageBus;
+use Symfony\Component\Messenger\Middleware\HandleMessageMiddleware;
+use Symfony\Component\Messenger\Middleware\SendMessageMiddleware;
+use Symfony\Component\Messenger\Retry\MultiplierRetryStrategy;
+use Symfony\Component\Messenger\Stamp\ReceivedStamp;
+use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
+use Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport;
+use Symfony\Component\Messenger\Transport\InMemoryTransport as LegacyInMemoryTransport;
+use Symfony\Component\Messenger\Transport\Sender\SendersLocator;
+use Symfony\Component\Messenger\Transport\Serialization\PhpSerializer;
+use Symfony\Component\Messenger\Worker;
use Symfony\Contracts\Cache\CallbackInterface;
+/**
+ * @group time-sensitive
+ */
class EarlyExpirationHandlerTest extends TestCase
{
public static function tearDownAfterClass(): void
@@ -29,9 +51,6 @@
(new Filesystem())->remove(sys_get_temp_dir().'/symfony-cache');
}
- /**
- * @group time-sensitive
- */
public function testHandle()
{
$pool = new FilesystemAdapter();
@@ -65,4 +84,253 @@
$this->assertGreaterThan(25, $metadata['ctime']);
$this->assertGreaterThan(time(), $metadata['expiry']);
}
+
+ public function testHandleReturnsValueWithoutSavingWhenCallbackDisablesSaving()
+ {
+ $pool = new FilesystemAdapter();
+ $item = $pool->getItem('bar');
+ $pool->save($item->set(234));
+
+ $computationService = new class implements CallbackInterface {
+ public bool $called = false;
+
+ public function __invoke(CacheItemInterface $item, bool &$save): mixed
+ {
+ $this->called = true;
+ $save = false;
+
+ return 123;
+ }
+ };
+
+ $container = new Container();
+ $container->set('computation_service', $computationService);
+ $container->set('cache_pool', $pool);
+
+ $reverseContainer = new ReverseContainer($container, new ServiceLocator([]));
+
+ $handler = new EarlyExpirationHandler($reverseContainer);
+ $bus = new MessageBus([new HandleMessageMiddleware(new HandlersLocator([EarlyExpirationMessage::class => [$handler]]))]);
+ $pool->setCallbackWrapper(new EarlyExpirationDispatcher($bus, $reverseContainer));
+
+ $value = $pool->get('bar', $computationService, \INF);
+
+ $this->assertTrue($computationService->called);
+ $this->assertSame(234, $pool->getItem('bar')->get());
+ $this->assertSame(123, $value);
+ }
+
+ public function testWorkerRetriesFailedCallbacks()
+ {
+ foreach ([new \RuntimeException('Failed computation.'), new \Error('Failed computation.')] as $failure) {
+ $calls = 0;
+ $currentRetry = 0;
+ $callbackRetries = [];
+ $whileComputing = null;
+ $callback = static function (CacheItemInterface $item) use (&$calls, &$currentRetry, &$callbackRetries, &$whileComputing, $failure) {
+ if (3 < ++$calls) {
+ throw new \LogicException('Too many callback calls.');
+ }
+ $callbackRetries[] = $currentRetry;
+ if (1 === $calls) {
+ $whileComputing();
+
+ throw $failure;
+ }
+
+ $item->expiresAfter(3600);
+
+ return 'recovered';
+ };
+ [$pool, $container, $reverseContainer, $handler] = $this->createHandler($callback);
+ $transportClass = class_exists(InMemoryTransport::class) ? InMemoryTransport::class : LegacyInMemoryTransport::class;
+ $transport = new $transportClass(new PhpSerializer());
+ $container->set('async', $transport);
+ $bus = new MessageBus([
+ new SendMessageMiddleware(new SendersLocator([EarlyExpirationMessage::class => ['async']], $container)),
+ new HandleMessageMiddleware(new HandlersLocator([EarlyExpirationMessage::class => [$handler]])),
+ ]);
+ $pool->setCallbackWrapper(new EarlyExpirationDispatcher($bus, $reverseContainer));
+
+ $this->assertSame(234, $pool->get('foo', $callback, \INF));
+ $originalEnvelope = $transport->getSent()[0];
+ $whileComputing = static fn () => $bus->dispatch($originalEnvelope->with(new ReceivedStamp('async')));
+
+ $dispatcher = new EventDispatcher();
+ $senders = new ServiceLocator(['async' => static fn () => $transport]);
+ $strategies = new ServiceLocator(['async' => static fn () => new MultiplierRetryStrategy(1, 0)]);
+ $dispatcher->addSubscriber(new SendFailedMessageForRetryListener($senders, $strategies));
+ $dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(2));
+ $dispatcher->addSubscriber(new StopWorkerOnTimeLimitListener(1));
+ $dispatcher->addListener(WorkerMessageReceivedEvent::class, static function (WorkerMessageReceivedEvent $event) use (&$currentRetry) {
+ $currentRetry = RedeliveryStamp::getRetryCountFromEnvelope($event->getEnvelope());
+ });
+ $dispatcher->addListener(WorkerRunningEvent::class, static function (WorkerRunningEvent $event) use ($pool) {
+ if (!$event->isWorkerIdle()) {
+ $pool->reset();
+ }
+ });
+
+ $worker = new Worker(['async' => $transport], $bus, $dispatcher);
+ $worker->run(['sleep' => 0]);
+
+ $this->assertSame([0, 1], $callbackRetries);
+ $this->assertSame(2, $calls);
+ $this->assertSame('recovered', $pool->getItem('foo')->get());
+ $bus->dispatch($originalEnvelope->with(new ReceivedStamp('async')));
+ $this->assertSame(2, $calls);
+ }
+ }
+
+ public function testFailureDoesNotClearReplacementComputation()
+ {
+ foreach ([false, true] as $evictOriginal) {
+ $calls = 0;
+ $handler = $replacement = null;
+ $otherMessages = [];
+ $failure = new \RuntimeException('Failed computation.');
+ $callback = static function () use (&$calls, &$handler, &$replacement, &$otherMessages, $failure) {
+ if (3 < ++$calls) {
+ throw new \LogicException('Too many callback calls.');
+ }
+ if (1 === $calls) {
+ foreach ($otherMessages as $message) {
+ $handler($message);
+ }
+ $handler($replacement);
+
+ throw $failure;
+ }
+
+ return 'recovered';
+ };
+ [$pool, $container, $reverseContainer, $handler, $message] = $this->createHandler($callback);
+
+ if ($evictOriginal) {
+ $otherCalls = 0;
+ $otherCallback = static function () use (&$otherCalls) {
+ if (102 < ++$otherCalls) {
+ throw new \LogicException('Too many callback calls.');
+ }
+
+ return 0;
+ };
+ $container->set('other_callback', $otherCallback);
+
+ for ($i = 0; $i < 100; ++$i) {
+ $pool->get('other-'.$i, static function (CacheItemInterface $item) {
+ $item->expiresAfter(3600);
+ usleep(1000);
+
+ return 0;
+ });
+ $otherMessages[] = EarlyExpirationMessage::create($reverseContainer, $otherCallback, $pool->getItem('other-'.$i), $pool);
+ }
+
+ $replacement = EarlyExpirationMessage::create($reverseContainer, $callback, $message->getItem(), $pool);
+ } else {
+ $pool->delete('foo');
+ $pool->get('foo', static function (CacheItemInterface $item) {
+ $item->expiresAfter(7200);
+ usleep(2000);
+
+ return 345;
+ });
+ $replacement = EarlyExpirationMessage::create($reverseContainer, $callback, $pool->getItem('foo'), $pool);
+ }
+
+ try {
+ $handler($message);
+ $this->fail('An exception should have been thrown.');
+ } catch (\RuntimeException $e) {
+ $this->assertSame($failure, $e);
+ }
+
+ $this->assertSame('recovered', $pool->getItem('foo')->get());
+ $handler($replacement);
+ $this->assertSame(2, $calls);
+
+ if ($evictOriginal) {
+ $this->assertSame(100, $otherCalls);
+ $handler($otherMessages[99]);
+ $this->assertSame(100, $otherCalls);
+ $handler($otherMessages[0]);
+ $this->assertSame(101, $otherCalls);
+ }
+ }
+ }
+
+ public function testExpiredMessageIsSkippedAfterFailure()
+ {
+ $calls = 0;
+ $callback = static function () use (&$calls) {
+ if (1 === ++$calls) {
+ throw new \RuntimeException('Failed computation.');
+ }
+
+ return 'recovered';
+ };
+ [$pool, , , $handler, $message] = $this->createHandler($callback, 1);
+
+ try {
+ $handler($message);
+ $this->fail('An exception should have been thrown.');
+ } catch (\RuntimeException) {
+ }
+
+ sleep(2);
+ $handler($message);
+
+ $this->assertSame(1, $calls);
+ $this->assertFalse($pool->hasItem('foo'));
+ }
+
+ public function testFailureWithoutMetadataCanBeRetried()
+ {
+ $calls = 0;
+ $callback = static function () use (&$calls) {
+ if (1 === ++$calls) {
+ throw new \RuntimeException('Failed computation.');
+ }
+
+ return 'recovered';
+ };
+ [$pool, , $reverseContainer, $handler] = $this->createHandler($callback);
+ $message = EarlyExpirationMessage::create($reverseContainer, $callback, $pool->getItem('uncached'), $pool);
+
+ try {
+ $handler($message);
+ $this->fail('An exception should have been thrown.');
+ } catch (\RuntimeException) {
+ }
+
+ $handler($message);
+
+ $this->assertSame(2, $calls);
+ $this->assertSame('recovered', $pool->getItem('uncached')->get());
+ }
+
+ private function createHandler(\Closure $callback, int $lifetime = 3600): array
+ {
+ $pool = new FilesystemAdapter();
+ $pool->clear();
+ $pool->get('foo', static function (CacheItemInterface $item) use ($lifetime) {
+ $item->expiresAfter($lifetime);
+ usleep(1000);
+
+ return 234;
+ });
+ $container = new Container();
+ $container->set('callback', $callback);
+ $container->set('cache_pool', $pool);
+ $reverseContainer = new ReverseContainer($container, new ServiceLocator([]));
+
+ return [
+ $pool,
+ $container,
+ $reverseContainer,
+ new EarlyExpirationHandler($reverseContainer),
+ EarlyExpirationMessage::create($reverseContainer, $callback, $pool->getItem('foo'), $pool),
+ ];
+ }
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Traits/RedisTrait.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Traits/RedisTrait.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Traits/RedisTrait.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Traits/RedisTrait.php 2026-11-04 11:23:00.000000000 +0000
@@ -86,7 +86,7 @@
*
* @throws InvalidArgumentException when the DSN is invalid
*/
- public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay
+ public static function createConnection(#[\SensitiveParameter] string $dsn, #[\SensitiveParameter] array $options = []): \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|Relay
{
if (str_starts_with($dsn, 'redis:')) {
$scheme = 'redis';
@@ -575,25 +575,34 @@
static $del;
$del ??= (class_exists(UNLINK::class) ? 'unlink' : 'del');
- $this->pipeline(static function () use ($ids, $del) {
+ $results = $this->pipeline(static function () use ($ids, $del) {
foreach ($ids as $id) {
yield $del => [$id];
}
- })->rewind();
+ });
+
+ foreach ($results as $result) {
+ if ($result instanceof ErrorInterface) {
+ return false;
+ }
+ }
} else {
static $unlink = true;
if ($unlink) {
try {
- $unlink = false !== $this->redis->unlink($ids);
+ $result = $this->redis->unlink($ids);
+ $unlink = false !== $result;
} catch (\Throwable) {
$unlink = false;
}
}
if (!$unlink) {
- $this->redis->del($ids);
+ $result = $this->redis->del($ids);
}
+
+ return false !== $result && !$result instanceof ErrorInterface;
}
return true;
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Traits/Relay/Relay61Trait.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Traits/Relay/Relay61Trait.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Traits/Relay/Relay61Trait.php 1970-01-01 00:00:00.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Traits/Relay/Relay61Trait.php 2026-11-04 11:23:00.000000000 +0000
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Cache\Traits\Relay;
+
+if (version_compare(phpversion('relay'), '0.61.0', '>=')) {
+ /**
+ * @internal
+ */
+ trait Relay61Trait
+ {
+ public function reconnect(): bool
+ {
+ return ($this->lazyObjectState->realInstance ??= ($this->lazyObjectState->initializer)())->reconnect(...\func_get_args());
+ }
+ }
+} else {
+ /**
+ * @internal
+ */
+ trait Relay61Trait
+ {
+ }
+}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Traits/RelayProxy.php symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Traits/RelayProxy.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Cache/Traits/RelayProxy.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Cache/Traits/RelayProxy.php 2026-11-04 11:23:00.000000000 +0000
@@ -32,6 +32,7 @@
use Symfony\Component\Cache\Traits\Relay\Relay40Trait;
use Symfony\Component\Cache\Traits\Relay\Relay50Trait;
use Symfony\Component\Cache\Traits\Relay\Relay502Trait;
+use Symfony\Component\Cache\Traits\Relay\Relay61Trait;
use Symfony\Component\Cache\Traits\Relay\SwapdbTrait;
use Symfony\Component\VarExporter\LazyObjectInterface;
use Symfony\Component\VarExporter\LazyProxyTrait;
@@ -72,6 +73,7 @@
use Relay40Trait;
use Relay50Trait;
use Relay502Trait;
+ use Relay61Trait;
use SwapdbTrait;
private const LAZY_OBJECT_PROPERTY_SCOPES = [];
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Config/Tests/Util/XmlUtilsTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -13,6 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Config\Util\Exception\InvalidXmlException;
+use Symfony\Component\Config\Util\Exception\XmlParsingException;
use Symfony\Component\Config\Util\XmlUtils;
class XmlUtilsTest extends TestCase
@@ -226,6 +227,31 @@
// should not throw an exception
XmlUtils::loadFile(__DIR__.'/../Fixtures/Util/valid.xml', __DIR__.'/../Fixtures/Util/schema.xsd');
}
+
+ public function testParseDoesNotResolveExternalEntities()
+ {
+ // the target is not well-formed on its own, so expanding it would abort the parsing
+ $target = __DIR__.'/../Fixtures/Util/invalid.xml';
+ $parts = explode('/', ltrim(str_replace('\\', '/', $target), '/'));
+ $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : '';
+ $uri = 'file:///'.$drive.implode('/', array_map('rawurlencode', $parts));
+ $content = ']>&xxe;';
+
+ $internalErrors = libxml_use_internal_errors(true);
+
+ try {
+ XmlUtils::parse($content);
+ $this->fail('An exception should have been raised.');
+ } catch (XmlParsingException $e) {
+ // the document is rejected for carrying a doctype, not for failing DTD validation,
+ // which means the doctype was never processed and the entity was never resolved
+ $this->assertSame('Document types are not allowed.', $e->getMessage());
+ $this->assertTrue(!libxml_get_errors(), 'The external entity should not have been resolved.');
+ } finally {
+ libxml_clear_errors();
+ libxml_use_internal_errors($internalErrors);
+ }
+ }
}
interface Validator
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Config/Util/XmlUtils.php symfony-6.4.50+dfsg/src/Symfony/Component/Config/Util/XmlUtils.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Config/Util/XmlUtils.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Config/Util/XmlUtils.php 2026-11-04 11:23:00.000000000 +0000
@@ -52,7 +52,6 @@
libxml_clear_errors();
$dom = new \DOMDocument();
- $dom->validateOnParse = true;
if (!$dom->loadXML($content, \LIBXML_NONET | \LIBXML_COMPACT)) {
throw new XmlParsingException(implode("\n", static::getXmlErrors($internalErrors)));
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/OutputWrapper.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/OutputWrapper.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/OutputWrapper.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/OutputWrapper.php 2026-11-04 11:23:00.000000000 +0000
@@ -71,7 +71,9 @@
// the u modifier counts characters instead of bytes, but preg_replace() returns null
// on malformed UTF-8, which would empty the text
$modifiers = preg_match('//u', $text) ? 'imux' : 'imx';
- $pattern = \sprintf('#(?:((?>(%1$s)((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(%1$s))(?:\r?\n)?|(?:\r?\n|$))#%2$s', $rowPattern, $modifiers);
+ // a line cut inside a word never ends between a backslash and the "<" or ">" it escapes,
+ // or the escaped character would start the next line as a tag delimiter
+ $pattern = \sprintf('#(?:((?>(%1$s)((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(%1$s)(?!(?<=\\\\)[<>]))(?:\r?\n)?|(?:\r?\n|$))#%2$s', $rowPattern, $modifiers);
$output = rtrim(preg_replace($pattern, '\\1'.$break, $text), $break);
return str_replace(' '.$break, $break, $output);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/ProcessHelper.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/ProcessHelper.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/ProcessHelper.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/ProcessHelper.php 2026-11-04 11:23:00.000000000 +0000
@@ -11,6 +11,7 @@
namespace Symfony\Component\Console\Helper;
+use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Exception\ProcessFailedException;
@@ -127,7 +128,7 @@
private function escapeString(string $str): string
{
- return str_replace('<', '\\<', $str);
+ return OutputFormatter::escapeTrailingBackslash(str_replace('<', '\\<', $str));
}
public function getName(): string
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/ProgressBar.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/ProgressBar.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/ProgressBar.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/ProgressBar.php 2026-11-04 11:23:00.000000000 +0000
@@ -58,6 +58,9 @@
private bool $overwrite = true;
private Terminal $terminal;
private ?string $previousMessage = null;
+ private int $lastDrawnStep = 0;
+ private int $lastDrawnMax = 0;
+ private bool $finishedWithoutMax = false;
private Cursor $cursor;
private array $placeholders = [];
@@ -334,16 +337,24 @@
*/
public function start(?int $max = null, int $startAt = 0): void
{
+ $startAt = max(0, $startAt);
$this->startTime = time();
$this->step = $startAt;
$this->startingStep = $startAt;
- $startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0;
-
if (null !== $max) {
$this->setMaxSteps($max);
+ } elseif ($this->finishedWithoutMax) {
+ // drop the max that finish() set to draw a full bar
+ $this->setMaxSteps(0);
+ }
+
+ if ($this->max && $startAt > $this->max) {
+ $this->max = $startAt;
}
+ $this->percent = $this->max ? (float) $startAt / $this->max : 0.0;
+
$this->display();
}
@@ -401,8 +412,10 @@
public function setMaxSteps(int $max): void
{
$this->format = null;
+ $this->finishedWithoutMax = false;
$this->max = max(0, $max);
$this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4;
+ $this->percent = $this->max ? min(1, (float) $this->step / $this->max) : 0;
}
/**
@@ -410,12 +423,24 @@
*/
public function finish(): void
{
+ if ($this->max && $this->step > $this->max) {
+ // the max was lowered below the step with setMaxSteps(), raise it like setProgress() does
+ $this->max = $this->step;
+ }
+
+ $max = $this->max;
+
if (!$this->max) {
$this->max = $this->step;
+ $this->percent = $this->max ? 1.0 : 0.0;
+ $this->finishedWithoutMax = true;
}
- if ($this->step === $this->max && !$this->overwrite) {
- // prevent double 100% output
+ if ($this->step === $this->max && !$this->overwrite && $this->lastDrawnStep === $this->step && \in_array($this->lastDrawnMax, [$max, $this->max], true)) {
+ // prevent double output when the last frame already shows this step, against the max it had before finishing or this one
+ // remember this max as drawn, so that calling finish() again does not draw the frame either
+ $this->lastDrawnMax = $this->max;
+
return;
}
@@ -436,6 +461,8 @@
}
$this->overwrite($this->buildLine());
+ $this->lastDrawnStep = $this->step;
+ $this->lastDrawnMax = $this->max;
}
/**
@@ -507,7 +534,7 @@
$this->cursor->clearLine();
}
}
- } elseif ($this->step > 0) {
+ } elseif (null !== $this->previousMessage) {
$message = \PHP_EOL.$message;
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/QuestionHelper.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/QuestionHelper.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/QuestionHelper.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/QuestionHelper.php 2026-11-04 11:23:00.000000000 +0000
@@ -220,7 +220,7 @@
foreach ($choices as $key => $value) {
$padding = str_repeat(' ', $maxWidth - self::width($key));
- $messages[] = \sprintf(" [<$tag>%s$padding$tag>] %s", $key, $value);
+ $messages[] = \sprintf(" [<$tag>%s$padding$tag>] %s", OutputFormatter::escapeTrailingBackslash($key), $value);
}
return $messages;
@@ -236,7 +236,7 @@
if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) {
$message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error');
} else {
- $message = ''.$error->getMessage().'';
+ $message = ''.OutputFormatter::escapeTrailingBackslash($error->getMessage()).'';
}
$output->writeln($message);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/SymfonyQuestionHelper.php 2026-11-04 11:23:00.000000000 +0000
@@ -56,13 +56,13 @@
$default[$key] = $choices[trim($value)];
}
- $text = \sprintf(' %s [%s]:', $text, implode(', ', $default));
+ $text = \sprintf(' %s [%s]:', $text, OutputFormatter::escapeTrailingBackslash(implode(', ', $default)));
break;
case $question instanceof ChoiceQuestion:
$choices = $question->getChoices();
- $text = \sprintf(' %s [%s]:', $text, $choices[$default] ?? $default);
+ $text = \sprintf(' %s [%s]:', $text, OutputFormatter::escapeTrailingBackslash($choices[$default] ?? $default));
break;
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/Table.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/Table.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Helper/Table.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Helper/Table.php 2026-11-04 11:23:00.000000000 +0000
@@ -285,7 +285,7 @@
*/
public function setRow(int|string $column, array $row): static
{
- $this->rows[$column] = $row;
+ $this->rows[$column] = array_values($row);
return $this;
}
@@ -412,7 +412,7 @@
$rows[] = [\sprintf(
'%s%s>: %s',
str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))),
- $headers[$i] ?? '',
+ OutputFormatter::escapeTrailingBackslash($headers[$i] ?? ''),
$part
)];
} else {
@@ -668,7 +668,7 @@
$padType = $cell->getStyle()->getPadByAlign();
}
- return \sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType));
+ return \sprintf($cellFormat, OutputFormatter::escapeTrailingBackslash(str_pad($content, $width, $style->getPaddingChar(), $padType)));
}
/**
@@ -697,13 +697,15 @@
$rows = $this->fillNextRows($rows, $rowKey);
// Remove any new line breaks and replace it with a new line
+ $firstColumn = $colspan = 0;
foreach ($rows[$rowKey] as $column => $cell) {
+ $firstColumn += $colspan;
$colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
$minWrappedWidth = 0;
$widthApplied = [];
$lengthColumnBorder = $this->getColumnSeparatorWidth() + Helper::width($this->style->getCellRowContentFormat()) - 2;
- for ($i = $column; $i < ($column + $colspan); ++$i) {
+ for ($i = $firstColumn; $i < ($firstColumn + $colspan); ++$i) {
if (isset($this->columnMaxWidths[$i])) {
$minWrappedWidth += $this->columnMaxWidths[$i];
$widthApplied[] = ['type' => 'max', 'column' => $i];
@@ -741,7 +743,7 @@
$cellWidth -= $this->configuredColumnWidths[$item['column']] + $lengthColumnBorder;
}
}
- for ($i = $column; $i < ($column + $colspan); ++$i) {
+ for ($i = $firstColumn; $i < ($firstColumn + $colspan); ++$i) {
if (isset($columnsMinWidthProcessed[$i])) {
continue;
}
@@ -794,49 +796,44 @@
{
$this->rowspanLines ??= new \WeakMap();
$unmergedRows = [];
- foreach ($rows[$line] as $column => $cell) {
+ $column = 0;
+ foreach ($rows[$line] as $cellKey => $cell) {
if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) {
throw new InvalidArgumentException(\sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
}
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
$nbLines = $cell->getRowspan() - 1;
- $lines = [$cell];
+ $value = '';
if (str_contains($cell, "\n")) {
$eol = str_contains($cell, "\r\n") ? "\r\n" : "\n";
$lines = explode($eol, $this->closeStyles($cell, $eol));
$nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines;
- $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
- $this->rowspanLines[$rows[$line][$column]] = true;
+ $rows[$line][$cellKey] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
+ $this->rowspanLines[$rows[$line][$cellKey]] = true;
unset($lines[0]);
+ $value = implode($eol, $lines);
}
- // create a two dimensional array (rowspan x colspan)
- $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows);
- foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
- $value = $lines[$unmergedRowKey - $line] ?? '';
- $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
- $this->rowspanLines[$unmergedRows[$unmergedRowKey][$column]] = true;
- if ($nbLines === $unmergedRowKey - $line) {
- break;
- }
- }
+ // only fill the next row, the rows below are filled from that row after its own cells that span rows
+ $unmergedRows[$line + 1][$column] = new TableCell($value, ['rowspan' => $nbLines, 'colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]);
+ $this->rowspanLines[$unmergedRows[$line + 1][$column]] = true;
}
+
+ $column += $cell instanceof TableCell ? $cell->getColspan() : 1;
}
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
// we need to know if $unmergedRow will be merged or inserted into $rows
if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
- foreach ($unmergedRow as $cellKey => $cell) {
- // insert cell into row at cellKey position
- array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]);
+ foreach ($unmergedRow as $column => $cell) {
+ // insert cell into row at column position
+ array_splice($rows[$unmergedRowKey], $this->getCellPosition($rows[$unmergedRowKey], $column), 0, [$cell]);
}
} else {
$row = $this->copyRow($rows, $unmergedRowKey - 1);
foreach ($unmergedRow as $column => $cell) {
- if (!empty($cell)) {
- $row[$column] = $unmergedRow[$column];
- }
+ array_splice($row, $this->getCellPosition($row, $column), 1, [$cell]);
}
array_splice($rows, $unmergedRowKey, 0, [$row]);
}
@@ -963,6 +960,23 @@
}
/**
+ * Gets the position in the given row of the cell that starts at the given column.
+ */
+ private function getCellPosition(array $row, int $column): int
+ {
+ $position = 0;
+ foreach ($row as $cell) {
+ if ($column <= 0) {
+ break;
+ }
+ $column -= $cell instanceof TableCell ? $cell->getColspan() : 1;
+ ++$position;
+ }
+
+ return $position;
+ }
+
+ /**
* Gets list of columns for the given row.
*/
private function getRowColumns(array $row): array
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Logger/ConsoleLogger.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Logger/ConsoleLogger.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Logger/ConsoleLogger.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Logger/ConsoleLogger.php 2026-11-04 11:23:00.000000000 +0000
@@ -14,6 +14,7 @@
use Psr\Log\AbstractLogger;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LogLevel;
+use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -78,7 +79,7 @@
// the if condition check isn't necessary -- it's the same one that $output will do internally anyway.
// We only do it for efficiency here as the message formatting is relatively expensive.
if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) {
- $output->writeln(\sprintf('<%1$s>[%2$s] %3$s%1$s>', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]);
+ $output->writeln(\sprintf('<%1$s>[%2$s] %3$s%1$s>', $this->formatLevelMap[$level], $level, OutputFormatter::escapeTrailingBackslash($this->interpolate($message, $context))), $this->verbosityLevelMap[$level]);
}
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Style/SymfonyStyle.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Style/SymfonyStyle.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Style/SymfonyStyle.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Style/SymfonyStyle.php 2026-11-04 11:23:00.000000000 +0000
@@ -512,7 +512,7 @@
$line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0));
if ($style) {
- $line = \sprintf('<%s>%s>', $style, $line);
+ $line = \sprintf('<%s>%s>', $style, OutputFormatter::escapeTrailingBackslash($line));
}
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/OutputWrapperTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/OutputWrapperTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/OutputWrapperTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/OutputWrapperTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -42,6 +42,16 @@
$this->assertSame($wrapper->wrap('Lorem X ipsum', 5), str_replace("\xB3", 'X', $wrapper->wrap("Lorem \xB3 ipsum", 5)));
}
+ public function testWrapKeepsEscapedTagDelimitersWithTheirBackslash()
+ {
+ $wrapper = new OutputWrapper();
+
+ $this->assertSame("aaaa\n\\wrap('aaaa\assertSame("aaaa\n\\>b", $wrapper->wrap('aaaa\>b', 5));
+ $this->assertSame("aaa\\<\nb", $wrapper->wrap('aaa\assertSame("aaaa\n\\c", $wrapper->wrap('aaaa\c', 5));
+ }
+
public function testWrapDoesNotDropBytesOfMalformedUtf8()
{
$text = "Cannot read the log file: \xB3 check the permissions";
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/ProcessHelperTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -116,6 +116,8 @@
[[' OUT 42'], true, Process::fromShellCommandline('php -r "echo 42;"'), StreamOutput::VERBOSITY_DEBUG, null],
[[' OUT 42'], true, [new Process(['php', '-r', 'echo 42;'])], StreamOutput::VERBOSITY_DEBUG, null],
[[' OUT 42'], true, [Process::fromShellCommandline('php -r '.$PHP), 'PHP' => 'echo 42;'], StreamOutput::VERBOSITY_DEBUG, null],
+ [[], true, 'php -r "echo 42;" C:\\\\', StreamOutput::VERBOSITY_VERY_VERBOSE, null],
+ [[], false, 'php -r "exit(252);"', StreamOutput::VERBOSITY_VERBOSE, 'Cannot write to C:\\'],
];
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -94,6 +94,141 @@
);
}
+ public function testResumeWithNewMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
+ $bar->start(200, 50);
+
+ rewind($output->getStream());
+
+ $this->assertEquals(
+ ' 50/200 [=======>--------------------] 25%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testResumeWithNewMaxAfterSmallerMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 10, 0);
+ $bar->start(100, 50);
+
+ rewind($output->getStream());
+
+ $this->assertEquals(
+ ' 50/100 [==============>-------------] 50%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testResumeBeyondNewMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
+ $bar->start(10, 50);
+
+ rewind($output->getStream());
+
+ $this->assertSame(50, $bar->getMaxSteps());
+ $this->assertEquals(
+ ' 50/50 [============================] 100%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testResumeWithNonDecoratedOutput()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 200, 0);
+ $bar->start(null, 50);
+ $bar->advance(20);
+
+ rewind($output->getStream());
+
+ $this->assertEquals(
+ ' 50/200 [=======>--------------------] 25%'.\PHP_EOL.
+ ' 70/200 [=========>------------------] 35%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testResumeNoMaxWithNonDecoratedOutput()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 0, 0);
+ $bar->start(null, 15);
+
+ rewind($output->getStream());
+
+ $this->assertEquals(
+ ' 15 [>---------------------------]',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testRestartWithNonDecoratedOutput()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 2, 0);
+ $bar->start();
+ $bar->finish();
+ $bar->start();
+
+ rewind($output->getStream());
+
+ $this->assertEquals(
+ ' 0/2 [>---------------------------] 0%'.\PHP_EOL.
+ ' 2/2 [============================] 100%'.\PHP_EOL.
+ ' 0/2 [>---------------------------] 0%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testRestartNoMaxAfterFinish()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
+ $bar->start();
+ $bar->advance(3);
+ $bar->finish();
+ $bar->start();
+ $bar->advance(5);
+
+ rewind($output->getStream());
+
+ $this->assertSame(0, $bar->getMaxSteps());
+ $this->assertEquals(
+ ' 0 [>---------------------------]'.
+ $this->generateOutput(' 3 [--->------------------------]').
+ $this->generateOutput(' 3 [============================]').
+ $this->generateOutput(' 0 [>---------------------------]').
+ $this->generateOutput(' 5 [----->----------------------]'),
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testRestartWithMaxSetAfterFinishWithoutMax()
+ {
+ $bar = new ProgressBar($this->getOutputStream(), 0, 0);
+ $bar->start();
+ $bar->advance(3);
+ $bar->finish();
+ $bar->setMaxSteps(10);
+ $bar->start();
+
+ $this->assertSame(10, $bar->getMaxSteps());
+ }
+
+ public function testResumeAtNegativeStep()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 0, 0);
+ $bar->start(null, -5);
+ $bar->start(100, -5);
+
+ rewind($output->getStream());
+
+ $this->assertSame(0, $bar->getProgress());
+ $this->assertEquals(
+ ' 0 [>---------------------------]'.
+ $this->generateOutput(' 0/100 [>---------------------------] 0%'),
+ stream_get_contents($output->getStream())
+ );
+ }
+
public function testRegularTimeEstimation()
{
$bar = new ProgressBar($output = $this->getOutputStream(), 1_200, 0);
@@ -843,6 +978,146 @@
);
}
+ public function testFinishNonDecoratedOutputWithoutMaxWhenLastStepIsNotDrawnYet()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 0, 10);
+ $bar->start();
+ $bar->advance(15);
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(' 0 [>---------------------------]'.\PHP_EOL.' 15 [============================]', stream_get_contents($output->getStream()));
+ }
+
+ public function testFinishNonDecoratedOutputWithoutMaxWhenLastStepIsAlreadyDrawn()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 0, 0);
+ $bar->start();
+ $bar->advance(15);
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(' 0 [>---------------------------]'.\PHP_EOL.' 15 [->--------------------------]', stream_get_contents($output->getStream()));
+ }
+
+ public function testFinishTwiceNonDecoratedOutputWithoutMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 0, 0);
+ $bar->start();
+ $bar->advance(15);
+ $bar->finish();
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(' 0 [>---------------------------]'.\PHP_EOL.' 15 [->--------------------------]', stream_get_contents($output->getStream()));
+ }
+
+ public function testDisplayAfterFinishNonDecoratedOutputWithoutMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 0, 0);
+ $bar->start();
+ $bar->advance(15);
+ $bar->finish();
+
+ $this->assertSame(1.0, $bar->getProgressPercent());
+
+ $bar->display();
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0 [>---------------------------]'.\PHP_EOL.
+ ' 15 [->--------------------------]'.\PHP_EOL.
+ ' 15 [============================]',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testFinishEmptyNonDecoratedOutputWithoutMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 0, 0);
+ $bar->start();
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(' 0 [>---------------------------]', stream_get_contents($output->getStream()));
+ }
+
+ public function testFinishUnstartedNonDecoratedOutputWithoutMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 0, 0);
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertSame('', stream_get_contents($output->getStream()));
+ }
+
+ public function testFinishNonDecoratedOutputAfterSettingMaxStepsToCurrentStep()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 100, 0);
+ $bar->start();
+ $bar->setProgress(50);
+ $bar->setMaxSteps(50);
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0/100 [>---------------------------] 0%'.\PHP_EOL.
+ ' 50/100 [==============>-------------] 50%'.\PHP_EOL.
+ ' 50/50 [============================] 100%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testFinishNonDecoratedOutputWithoutMaxAfterSettingMaxStepsToCurrentStep()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 0, 0);
+ $bar->start();
+ $bar->setProgress(15);
+ $bar->setMaxSteps(15);
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0 [>---------------------------]'.\PHP_EOL.
+ ' 15 [->--------------------------]'.\PHP_EOL.
+ ' 15/15 [============================] 100%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testFinishNonDecoratedOutputAfterReachingMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 2, 0);
+ $bar->setFormat('verbose');
+ $bar->start();
+ $bar->advance(2);
+ sleep(1);
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0/2 [>---------------------------] 0% < 1 sec'.\PHP_EOL.
+ ' 2/2 [============================] 100% < 1 sec',
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testFinishNonDecoratedOutputAgainAfterRemovingMax()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 10, 0);
+ $bar->start();
+ $bar->finish();
+ $bar->setMaxSteps(0);
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0/10 [>---------------------------] 0%'.\PHP_EOL.
+ ' 10/10 [============================] 100%',
+ stream_get_contents($output->getStream())
+ );
+ }
+
public function testParallelBars()
{
$output = $this->getOutputStream();
@@ -946,6 +1221,66 @@
stream_get_contents($output->getStream())
);
}
+
+ public function testSettingMaxStepsUpdatesPercent()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 100, 0);
+ $bar->start();
+ $bar->setProgress(50);
+ $bar->setMaxSteps(200);
+ $bar->display();
+ $bar->setMaxSteps(10);
+ $bar->display();
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0/100 [>---------------------------] 0%'.
+ $this->generateOutput(' 50/100 [==============>-------------] 50%').
+ $this->generateOutput(' 50/200 [=======>--------------------] 25%').
+ $this->generateOutput(' 50/10 [============================] 100%'),
+ stream_get_contents($output->getStream())
+ );
+
+ $bar->setMaxSteps(0);
+ $this->assertSame(0.0, $bar->getProgressPercent());
+ }
+
+ public function testFinishAfterSettingMaxStepsBelowProgress()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(), 100, 0);
+ $bar->start();
+ $bar->setProgress(80);
+ $bar->setMaxSteps(50);
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertSame(80, $bar->getProgress());
+ $this->assertEquals(
+ ' 0/100 [>---------------------------] 0%'.
+ $this->generateOutput(' 80/100 [======================>-----] 80%').
+ $this->generateOutput(' 80/80 [============================] 100%'),
+ stream_get_contents($output->getStream())
+ );
+ }
+
+ public function testFinishNonDecoratedOutputAfterDrawingMaxStepsBelowProgress()
+ {
+ $bar = new ProgressBar($output = $this->getOutputStream(false), 100, 0);
+ $bar->start();
+ $bar->setProgress(80);
+ $bar->setMaxSteps(50);
+ $bar->display();
+ $bar->finish();
+
+ rewind($output->getStream());
+ $this->assertEquals(
+ ' 0/100 [>---------------------------] 0%'.\PHP_EOL.
+ ' 80/100 [======================>-----] 80%'.\PHP_EOL.
+ ' 80/50 [============================] 100%'.\PHP_EOL.
+ ' 80/80 [============================] 100%',
+ stream_get_contents($output->getStream())
+ );
+ }
public function testWithSmallScreen()
{
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -782,6 +782,31 @@
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("\n")), $output, $question);
}
+ public function testChoiceKeysWithATrailingBackslash()
+ {
+ $question = new ChoiceQuestion('Which drive?', ['C:\\' => 'first', 'D:\\' => 'second']);
+ (new QuestionHelper())->ask($this->createStreamableInputInterfaceMock($this->getInputStream("C:\\\n")), $output = $this->createOutputInterface(), $question);
+
+ rewind($output->getStream());
+ $this->assertStringStartsWith("Which drive?\n [C:\\] first\n [D:\\] second\n > ", str_replace(\PHP_EOL, "\n", stream_get_contents($output->getStream())));
+ }
+
+ public function testErrorWithATrailingBackslash()
+ {
+ $question = new Question('Which path?');
+ $question->setValidator(static function ($value) {
+ if ('/' !== $value) {
+ throw new \InvalidArgumentException('The path must not end with \\');
+ }
+
+ return $value;
+ });
+ (new QuestionHelper())->ask($this->createStreamableInputInterfaceMock($this->getInputStream("a\\\n/\n")), $output = $this->createOutputInterface(), $question);
+
+ rewind($output->getStream());
+ $this->assertSame("Which path?The path must not end with \\\nWhich path?", str_replace(\PHP_EOL, "\n", stream_get_contents($output->getStream())));
+ }
+
public function testAskThrowsExceptionOnMissingInput()
{
$this->expectException(MissingInputException::class);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -138,6 +138,23 @@
$this->assertOutputNotContains('[Add new tag, hello]', $output);
}
+ public function testAskChoiceDefaultWithATrailingBackslash()
+ {
+ $question = new ChoiceQuestion('Which drive?', ['C:\\', 'D:\\'], 0);
+ (new SymfonyQuestionHelper())->ask($this->createStreamableInputInterfaceMock($this->getInputStream("\n")), $output = $this->createOutputInterface(), $question);
+
+ $this->assertOutputContains('Which drive? [C:\]:', $output);
+ }
+
+ public function testAskMultiselectChoiceDefaultWithATrailingBackslash()
+ {
+ $question = new ChoiceQuestion('Which drives?', ['C:\\', 'D:\\'], '0,1');
+ $question->setMultiselect(true);
+ (new SymfonyQuestionHelper())->ask($this->createStreamableInputInterfaceMock($this->getInputStream("\n")), $output = $this->createOutputInterface(), $question);
+
+ $this->assertOutputContains('Which drives? [C:\, D:\]:', $output);
+ }
+
public function testAskReturnsNullIfValidatorAllowsIt()
{
$questionHelper = new SymfonyQuestionHelper();
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/TableTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/TableTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Helper/TableTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Helper/TableTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -581,6 +581,104 @@
TABLE,
],
+ 'Cell with rowspan after a cell with colspan' => [
+ ['ISBN', 'Title', 'Author'],
+ [
+ [
+ new TableCell('Divine Comedy', ['colspan' => 2]),
+ new TableCell('Dante Alighieri', ['rowspan' => 2]),
+ ],
+ ['9971-5-0210-0', 'Inferno'],
+ ],
+ 'default',
+ <<<'TABLE'
+ +---------------+---------+-----------------+
+ | ISBN | Title | Author |
+ +---------------+---------+-----------------+
+ | Divine Comedy | Dante Alighieri |
+ | 9971-5-0210-0 | Inferno | |
+ +---------------+---------+-----------------+
+
+ TABLE,
+ ],
+ 'Cell with rowspan and line breaks after a cell with colspan' => [
+ ['ISBN', 'Title', 'Author'],
+ [
+ [
+ new TableCell('Divine Comedy', ['colspan' => 2]),
+ new TableCell("Dante\nAlighieri\n1265-1321", ['rowspan' => 2]),
+ ],
+ ['9971-5-0210-0', 'Inferno'],
+ ],
+ 'default',
+ <<<'TABLE'
+ +---------------+---------+-----------+
+ | ISBN | Title | Author |
+ +---------------+---------+-----------+
+ | Divine Comedy | Dante |
+ | 9971-5-0210-0 | Inferno | Alighieri |
+ | | | 1265-1321 |
+ +---------------+---------+-----------+
+
+ TABLE,
+ ],
+ 'Cells with rowspan around a cell with colspan' => [
+ ['ISBN', 'Title', 'Part', 'Author'],
+ [
+ [
+ new TableCell('9971-5-0210-0', ['rowspan' => 2]),
+ new TableCell('Divine Comedy', ['colspan' => 2]),
+ new TableCell('Dante Alighieri', ['rowspan' => 2]),
+ ],
+ ['Inferno', 'Canto I'],
+ ],
+ 'default',
+ <<<'TABLE'
+ +---------------+---------+---------+-----------------+
+ | ISBN | Title | Part | Author |
+ +---------------+---------+---------+-----------------+
+ | 9971-5-0210-0 | Divine Comedy | Dante Alighieri |
+ | | Inferno | Canto I | |
+ +---------------+---------+---------+-----------------+
+
+ TABLE,
+ ],
+ 'Cell with rowspan above a cell with colspan' => [
+ ['ISBN', 'Title', 'Author', 'Year'],
+ [
+ ['9971-5-0210-0', 'Divine Comedy', new TableCell('Dante Alighieri', ['rowspan' => 2]), '1320'],
+ [new TableCell('La Vita Nuova', ['colspan' => 2]), '1294'],
+ ],
+ 'default',
+ <<<'TABLE'
+ +---------------+---------------+-----------------+------+
+ | ISBN | Title | Author | Year |
+ +---------------+---------------+-----------------+------+
+ | 9971-5-0210-0 | Divine Comedy | Dante Alighieri | 1320 |
+ | La Vita Nuova | | 1294 |
+ +-------------------------------+-----------------+------+
+
+ TABLE,
+ ],
+ 'Cells with rowspan that start in consecutive rows' => [
+ ['ISBN', 'Title', 'Author'],
+ [
+ ['99921-58-10-7', new TableCell('Divine Comedy', ['rowspan' => 3]), 'Dante Alighieri'],
+ [new TableCell('9971-5-0210-0', ['rowspan' => 2]), 'Charles Dickens'],
+ ['J. R. R. Tolkien'],
+ ],
+ 'default',
+ <<<'TABLE'
+ +---------------+---------------+------------------+
+ | ISBN | Title | Author |
+ +---------------+---------------+------------------+
+ | 99921-58-10-7 | Divine Comedy | Dante Alighieri |
+ | 9971-5-0210-0 | | Charles Dickens |
+ | | | J. R. R. Tolkien |
+ +---------------+---------------+------------------+
+
+ TABLE,
+ ],
'Cell with rowspan and colspan with separator inside a rowspan' => [
['ISBN', 'Author'],
[
@@ -988,6 +1086,27 @@
$this->assertEquals($expected, $this->getOutputContent($output));
}
+ public function testStyleWithoutPaddingAfterACellEndingWithABackslash()
+ {
+ $style = new TableStyle();
+ $style
+ ->setHorizontalBorderChars('')
+ ->setVerticalBorderChars('')
+ ->setDefaultCrossingChar('')
+ ->setCellRowContentFormat('%s')
+ ->setPadType(\STR_PAD_LEFT)
+ ;
+
+ $table = new Table($output = $this->getOutputStream());
+ $table
+ ->setHeaders(['C:\\', 'D:\\'])
+ ->setRows([['abcd', 'ef']])
+ ->setStyle($style);
+ $table->render();
+
+ $this->assertSame(" C:\\D:\\\nabcd ef\n", $this->getOutputContent($output));
+ }
+
public function testRowSeparator()
{
$table = new Table($output = $this->getOutputStream());
@@ -1048,6 +1167,30 @@
$this->assertEquals($expected, $this->getOutputContent($output));
}
+ public function testRenderHeaderCellWithRowspanWithNonListKeys()
+ {
+ $table = new Table($output = $this->getOutputStream());
+ $table
+ ->setHeaders([[1 => 'ISBN', 2 => new TableCell("Title\n(original)", ['rowspan' => 2]), 3 => 'Author']])
+ ->setRows([['9971-5-0210-0', 'Divine Comedy', 'Dante Alighieri']])
+ ;
+ $table->render();
+
+ $expected =
+ <<<'TABLE'
+ +---------------+---------------+-----------------+
+ | ISBN | Title | Author |
+ +---------------+---------------+-----------------+
+ | | (original) | |
+ +---------------+---------------+-----------------+
+ | 9971-5-0210-0 | Divine Comedy | Dante Alighieri |
+ +---------------+---------------+-----------------+
+
+ TABLE;
+
+ $this->assertEquals($expected, $this->getOutputContent($output));
+ }
+
public function testColumnStyle()
{
$table = new Table($output = $this->getOutputStream());
@@ -1092,6 +1235,28 @@
$table->render();
}
+ public function testSetRowWithStringKeys()
+ {
+ $table = new Table($output = $this->getOutputStream());
+ $table
+ ->setHeaders(['ISBN', 'Title'])
+ ->setRow(0, ['isbn' => '9971-5-0210-0', 'title' => 'Divine Comedy'])
+ ;
+ $table->render();
+
+ $expected =
+ <<<'TABLE'
+ +---------------+---------------+
+ | ISBN | Title |
+ +---------------+---------------+
+ | 9971-5-0210-0 | Divine Comedy |
+ +---------------+---------------+
+
+ TABLE;
+
+ $this->assertEquals($expected, $this->getOutputContent($output));
+ }
+
public function testColumnWidth()
{
$table = new Table($output = $this->getOutputStream());
@@ -2179,6 +2344,54 @@
$this->assertSame($expected, $this->getOutputContent($output));
}
+ public function testColumnMaxWidthOfCellAfterColspan()
+ {
+ $table = new Table($output = $this->getOutputStream());
+ $table
+ ->setHeaders(['ISBN', 'Title', 'Author'])
+ ->setRows([[new TableCell('Divine Comedy', ['colspan' => 2]), 'Dante Alighieri']])
+ ->setColumnMaxWidth(2, 9)
+ ;
+ $table->render();
+
+ $expected =
+ <<<'TABLE'
+ +---------+--------+-----------+
+ | ISBN | Title | Author |
+ +---------+--------+-----------+
+ | Divine Comedy | Dante |
+ | | Alighieri |
+ +------------------+-----------+
+
+ TABLE;
+
+ $this->assertSame($expected, $this->getOutputContent($output));
+ }
+
+ public function testColumnMaxWidthOfCellWithColspanAfterColspan()
+ {
+ $table = new Table($output = $this->getOutputStream());
+ $table
+ ->setHeaders(['ISBN', 'Title', 'Author', 'Year'])
+ ->setRows([[new TableCell('Divine Comedy', ['colspan' => 2]), new TableCell('Dante Alighieri, 1265-1321', ['colspan' => 2])]])
+ ->setColumnMaxWidth(2, 8)
+ ;
+ $table->render();
+
+ $expected =
+ <<<'TABLE'
+ +---------+--------+----------+----------+
+ | ISBN | Title | Author | Year |
+ +---------+--------+----------+----------+
+ | Divine Comedy | Dante Alighieri, |
+ | | 1265-1321 |
+ +------------------+---------------------+
+
+ TABLE;
+
+ $this->assertSame($expected, $this->getOutputContent($output));
+ }
+
public static function provideRenderVerticalTests(): \Traversable
{
$books = [
@@ -2529,6 +2742,18 @@
'Books',
'Page 1/2',
];
+
+ yield 'With a trailing backslash in a header' => [
+ <<assertEquals($expected, $this->getLogs());
}
+ public function testMessageWithATrailingBackslash()
+ {
+ $this->getLogger()->error('Cannot write to {dir}', ['dir' => 'C:\\']);
+
+ $this->assertSame(['error Cannot write to C:\\'], $this->getLogs());
+ }
+
public function testObjectCastToString()
{
$this->getLogger()->warning(new DummyTest());
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -130,6 +130,23 @@
$this->assertCount(1, array_unique(array_map('strlen', $lines)), 'Every line of the block is padded to the same width.');
}
+ public function testErrorKeepsAnEscapedTagThatIsWrappedAsText()
+ {
+ // the escaped "<" falls right where the block wraps the message
+ $message = str_repeat('a', 110).'y';
+ $this->command->setCode(static function (InputInterface $input, OutputInterface $output) use ($message) {
+ (new SymfonyStyle($input, $output))->error($message);
+
+ return Command::SUCCESS;
+ });
+
+ $this->tester->execute([], ['interactive' => false, 'decorated' => false]);
+
+ $lines = array_filter(explode("\n", $this->tester->getDisplay(true)), static fn (string $line): bool => '' !== trim($line));
+
+ $this->assertSame($message, implode('', array_map(static fn (string $line): string => trim(substr($line, 9)), $lines)));
+ }
+
public function testGetErrorStyle()
{
$input = $this->createStub(InputInterface::class);
@@ -215,6 +232,24 @@
$this->assertSame(0, memory_get_usage() - $start);
}
+ public function testBlockKeepsTheBackslashThatEndsAFullLine()
+ {
+ $this->command->setCode(static function (InputInterface $input, OutputInterface $output) {
+ $io = new SymfonyStyle($input, $output);
+ $io->error(str_repeat('a', 110).'\b');
+ $io->writeln('after');
+
+ return Command::SUCCESS;
+ });
+
+ $this->tester->execute([], ['interactive' => false, 'decorated' => true]);
+
+ $lines = explode("\n", $this->tester->getDisplay(true));
+
+ $this->assertSame("\e[37;41m [ERROR] ".str_repeat('a', 110)."\\\e[39;49m", $lines[2]);
+ $this->assertSame('after', $lines[6]);
+ }
+
public function testAskAndClearExpectFullSectionCleared()
{
$answer = 'Answer';
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Filesystem.php symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Filesystem.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Filesystem.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Filesystem.php 2026-11-04 11:23:00.000000000 +0000
@@ -171,35 +171,55 @@
throw new IOException(\sprintf('Failed to remove symlink "%s": ', $file).self::$lastError);
}
} elseif (is_dir($file)) {
- if (!$isRecursive) {
- $tmpName = \dirname(realpath($file)).'/.!'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-!'));
+ $tmpDir = null;
+ $origFile = null;
- if (file_exists($tmpName)) {
- try {
- self::doRemove([$tmpName], true);
- } catch (IOException) {
+ try {
+ if (!$isRecursive && false !== $realPath = realpath($file)) {
+ for ($i = 0; $i < 10; ++$i) {
+ // Keep the temporary suffix within six characters to preserve room for descendant paths.
+ $tmpName = \dirname($realPath).'/.'.strtr(base64_encode(random_bytes(2)), ['/' => '-', '+' => '_', '=' => '']);
+
+ if (!self::box('mkdir', $tmpName, 0o700)) {
+ continue;
+ }
+
+ $tmpDir = $tmpName;
+
+ if (self::box('rename', $file, $tmpDir.'/d')) {
+ $origFile = $file;
+ $file = $tmpDir.'/d';
+ }
+
+ break;
}
}
- if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) {
- $origFile = $file;
- $file = $tmpName;
- } else {
- $origFile = null;
- }
- }
+ try {
+ $filesystemIterator = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS);
+ } catch (\UnexpectedValueException $e) {
+ if (!file_exists($file)) {
+ continue;
+ }
- $filesystemIterator = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS);
- self::doRemove(iterator_to_array($filesystemIterator, true), true);
+ throw new IOException(\sprintf('Failed to remove directory "%s": ', $file).$e->getMessage(), 0, $e);
+ }
- if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) {
- $lastError = self::$lastError;
+ self::doRemove(iterator_to_array($filesystemIterator, true), true);
- if (null !== $origFile && self::box('rename', $file, $origFile)) {
- $file = $origFile;
+ if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) {
+ throw new IOException(\sprintf('Failed to remove directory "%s": ', $file).self::$lastError);
+ }
+ } catch (IOException $e) {
+ if (null === $origFile || !self::box('rename', $file, $origFile)) {
+ throw $e;
}
- throw new IOException(\sprintf('Failed to remove directory "%s": ', $file).$lastError);
+ throw new IOException(str_replace($file, $origFile, $e->getMessage()), 0, $e);
+ } finally {
+ if (null !== $tmpDir) {
+ self::box('rmdir', $tmpDir);
+ }
}
} elseif (!self::box('unlink', $file) && ((self::$lastError && str_contains(self::$lastError, 'Permission denied')) || file_exists($file))) {
throw new IOException(\sprintf('Failed to remove file "%s": ', $file).self::$lastError);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Path.php symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Path.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Path.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Path.php 2026-11-04 11:23:00.000000000 +0000
@@ -213,9 +213,9 @@
}
// Maintain scheme
- if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
- $scheme = substr($path, 0, $schemeSeparatorPosition + 3);
- $path = substr($path, $schemeSeparatorPosition + 3);
+ if (preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $path, $m)) {
+ $scheme = $m[0];
+ $path = substr($path, \strlen($scheme));
} else {
$scheme = '';
}
@@ -372,7 +372,7 @@
}
// URLs and stream wrappers are considered absolute
- if (str_contains($path, '://') && null !== parse_url($path, \PHP_URL_SCHEME)) {
+ if (preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $path)) {
return true;
}
@@ -460,9 +460,9 @@
return self::canonicalize($path);
}
- if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) {
- $scheme = substr($basePath, 0, $schemeSeparatorPosition + 3);
- $basePath = substr($basePath, $schemeSeparatorPosition + 3);
+ if (preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $basePath, $m)) {
+ $scheme = $m[0];
+ $basePath = substr($basePath, \strlen($scheme));
} else {
$scheme = '';
}
@@ -593,7 +593,7 @@
*/
public static function isLocal(string $path): bool
{
- return '' !== $path && !str_contains($path, '://');
+ return '' !== $path && !preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $path);
}
/**
@@ -795,9 +795,9 @@
}
// Remember scheme as part of the root, if any
- if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
- $root = substr($path, 0, $schemeSeparatorPosition + 3);
- $path = substr($path, $schemeSeparatorPosition + 3);
+ if (preg_match('{^[a-zA-Z0-9+.-]{2,}://}', $path, $m)) {
+ $root = $m[0];
+ $path = substr($path, \strlen($root));
} else {
$root = '';
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -358,6 +358,270 @@
$this->assertFileDoesNotExist($basePath.'dir');
}
+ /**
+ * @runInSeparateProcess
+ *
+ * @preserveGlobalState disabled
+ */
+ public function testRemoveDoesNotDeleteExistingTemporaryDirectories()
+ {
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $directory = $this->workspace.'/directory';
+ mkdir($directory);
+ file_put_contents($directory.'/cache', 'cache');
+ $temporaryDirectories = [$this->workspace.'/.AAA'];
+
+ foreach ([2, 8] as $length) {
+ $temporaryDirectories[] = $this->workspace.'/.!'.strrev(strtr(base64_encode(str_repeat("\0", $length)), '/=', '-!'));
+ }
+
+ foreach ($temporaryDirectories as $temporaryDirectory) {
+ mkdir($temporaryDirectory);
+ file_put_contents($temporaryDirectory.'/foreign', 'foreign');
+ }
+
+ $attempts = 0;
+ $GLOBALS['filesystemRemoveCollision'] = static function (int $length) use (&$attempts): string {
+ ++$attempts;
+
+ return str_repeat("\0", $length);
+ };
+
+ try {
+ $this->filesystem->remove($directory);
+ } finally {
+ unset($GLOBALS['filesystemRemoveCollision']);
+ }
+
+ $this->assertDirectoryDoesNotExist($directory);
+ $this->assertSame(10, $attempts);
+
+ foreach ($temporaryDirectories as $temporaryDirectory) {
+ $this->assertStringEqualsFile($temporaryDirectory.'/foreign', 'foreign');
+ }
+ }
+
+ /**
+ * @runInSeparateProcess
+ *
+ * @preserveGlobalState disabled
+ */
+ public function testRemoveRetriesTemporaryDirectoryCollisions()
+ {
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $directory = $this->workspace.'/directory';
+ $temporaryDirectory = $this->workspace.'/.AAA';
+ mkdir($directory);
+ mkdir($temporaryDirectory);
+ file_put_contents($directory.'/cache', 'cache');
+ file_put_contents($temporaryDirectory.'/foreign', 'foreign');
+ $attempts = 0;
+ $removedDirectory = null;
+ $GLOBALS['filesystemRemoveCollision'] = static function (int $length) use (&$attempts): string {
+ return str_repeat(\chr($attempts++), $length);
+ };
+ $GLOBALS['filesystemRemoveIterator'] = static function (\FilesystemIterator $iterator, bool $preserveKeys) use (&$removedDirectory): array {
+ $removedDirectory ??= $iterator->getPath();
+
+ return iterator_to_array($iterator, $preserveKeys);
+ };
+
+ try {
+ $this->filesystem->remove($directory);
+ } finally {
+ unset($GLOBALS['filesystemRemoveCollision'], $GLOBALS['filesystemRemoveIterator']);
+ }
+
+ $this->assertSame(2, $attempts);
+ $this->assertSame($this->workspace.'/.AQE/d', $removedDirectory);
+ $this->assertDirectoryDoesNotExist($directory);
+ $this->assertDirectoryDoesNotExist($this->workspace.'/.AQE');
+ $this->assertStringEqualsFile($temporaryDirectory.'/foreign', 'foreign');
+ }
+
+ /**
+ * @runInSeparateProcess
+ *
+ * @preserveGlobalState disabled
+ */
+ public function testRemoveDoesNotInterfereWithConcurrentRemoval()
+ {
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $first = $this->workspace.'/first';
+ $second = $this->workspace.'/second';
+ mkdir($first);
+ mkdir($second);
+ file_put_contents($first.'/first-cache', 'first');
+ file_put_contents($second.'/second-cache', 'second');
+ $phase = 0;
+ $activeDirectory = null;
+ $filesystem = $this->filesystem;
+ $secondRemoval = new \Fiber(static fn () => $filesystem->remove($second));
+ $GLOBALS['filesystemRemoveCollision'] = static fn (int $length): string => str_repeat("\0", $length);
+ $GLOBALS['filesystemRemoveIterator'] = static function (\FilesystemIterator $iterator, bool $preserveKeys) use (&$phase, &$activeDirectory, $secondRemoval): array {
+ if (0 === $phase) {
+ $activeDirectory = $iterator->getPath();
+ $snapshot = iterator_to_array($iterator, $preserveKeys);
+ $phase = 1;
+ $secondRemoval->start();
+
+ return $snapshot;
+ }
+
+ if (1 === $phase && $iterator->getPath() === $activeDirectory && is_file($activeDirectory.'/second-cache')) {
+ $phase = 2;
+ \Fiber::suspend();
+ }
+
+ return iterator_to_array($iterator, $preserveKeys);
+ };
+
+ try {
+ $filesystem->remove($first);
+ } finally {
+ unset($GLOBALS['filesystemRemoveCollision'], $GLOBALS['filesystemRemoveIterator']);
+
+ if ($secondRemoval->isSuspended()) {
+ $secondRemoval->resume();
+ }
+ }
+
+ $this->assertTrue($secondRemoval->isTerminated());
+ $this->assertDirectoryDoesNotExist($first);
+ $this->assertDirectoryDoesNotExist($second);
+ $this->assertCount(0, new \FilesystemIterator($this->workspace));
+ }
+
+ /**
+ * @runInSeparateProcess
+ *
+ * @preserveGlobalState disabled
+ */
+ public function testRemoveRestoresDirectoryWhenConcurrentWritePreventsRemoval()
+ {
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $directory = $this->workspace.'/directory';
+ mkdir($directory);
+ file_put_contents($directory.'/cache', 'cache');
+ $written = false;
+ $GLOBALS['filesystemRemoveIterator'] = static function (\FilesystemIterator $iterator, bool $preserveKeys) use (&$written): array {
+ $snapshot = iterator_to_array($iterator, $preserveKeys);
+
+ if (!$written) {
+ $written = true;
+ file_put_contents($iterator->getPath().'/concurrent', 'concurrent');
+ }
+
+ return $snapshot;
+ };
+
+ try {
+ $this->filesystem->remove($directory);
+ $this->fail('Expected removal to fail.');
+ } catch (IOException $e) {
+ $this->assertStringContainsString('Failed to remove directory "'.$directory.'": ', $e->getMessage());
+ $this->assertStringEqualsFile($directory.'/concurrent', 'concurrent');
+ $this->assertCount(1, new \FilesystemIterator($this->workspace));
+ } finally {
+ unset($GLOBALS['filesystemRemoveIterator']);
+ }
+ }
+
+ /**
+ * @runInSeparateProcess
+ *
+ * @preserveGlobalState disabled
+ */
+ public function testRemoveIgnoresDirectoryRemovedConcurrently()
+ {
+ require __DIR__.'/Fixtures/remove_collision.php';
+
+ $directory = $this->workspace.'/directory';
+ mkdir($directory);
+ $attempts = 0;
+ $GLOBALS['filesystemRemoveRealpath'] = static function (string $path): string|false {
+ if (is_dir($path)) {
+ rmdir($path);
+ }
+
+ return realpath($path);
+ };
+ $GLOBALS['filesystemRemoveCollision'] = static function (int $length) use (&$attempts): string {
+ ++$attempts;
+
+ return random_bytes($length);
+ };
+
+ try {
+ $this->filesystem->remove($directory);
+ } finally {
+ unset($GLOBALS['filesystemRemoveRealpath'], $GLOBALS['filesystemRemoveCollision']);
+ }
+
+ $this->assertSame(0, $attempts);
+ $this->assertCount(0, new \FilesystemIterator($this->workspace));
+ }
+
+ public function testRemoveWithRestrictiveUmask()
+ {
+ $directory = $this->workspace.'/directory';
+ mkdir($directory);
+ file_put_contents($directory.'/cache', 'cache');
+ $umask = umask(0o777);
+
+ try {
+ $this->filesystem->remove($directory);
+ } finally {
+ umask($umask);
+ }
+
+ $this->assertCount(0, new \FilesystemIterator($this->workspace));
+ }
+
+ public function testRemoveTreeWithPathsNearLengthLimit()
+ {
+ if ('\\' === \DIRECTORY_SEPARATOR) {
+ $this->markTestSkipped('Windows path limits depend on long-path support.');
+ }
+
+ $directory = $this->workspace.'/d';
+ mkdir($directory);
+ $path = $directory;
+ $maxLength = \PHP_MAXPATHLEN - 8;
+
+ while (\strlen($path) < $maxLength - 1) {
+ $path .= '/'.str_repeat('x', min(200, $maxLength - \strlen($path) - 1));
+ mkdir($path);
+ }
+
+ file_put_contents($path.'/f', 'contents');
+ $this->assertGreaterThanOrEqual(\PHP_MAXPATHLEN - 7, \strlen($path.'/f'));
+
+ $this->filesystem->remove($directory);
+
+ $this->assertDirectoryDoesNotExist($directory);
+ $this->assertCount(0, new \FilesystemIterator($this->workspace));
+ }
+
+ public function testRemoveCleansReservationWhenRenameIsDisabled()
+ {
+ $directory = $this->workspace.'/directory';
+ mkdir($directory);
+ file_put_contents($directory.'/cache', 'cache');
+ $finder = new PhpExecutableFinder();
+ $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-ddisable_functions=rename', __DIR__.'/Fixtures/remove_without_rename.php', $directory]));
+ $process->run();
+
+ $this->assertTrue($process->isSuccessful(), $process->getErrorOutput());
+ $this->assertSame('Unable to perform filesystem operation because the "rename()" function has been disabled.'.\PHP_EOL, $process->getOutput());
+ $this->assertStringEqualsFile($directory.'/cache', 'cache');
+ $this->assertCount(1, new \FilesystemIterator($this->workspace));
+ }
+
public function testRemoveThrowsExceptionOnPermissionDenied()
{
$this->markAsSkippedIfChmodIsMissing();
@@ -380,6 +644,51 @@
}
}
+ public function testRemoveRestoresDirectoryWhenNestedRemovalFails()
+ {
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $directory = $this->workspace.'/directory';
+ $file = $directory.'/sub/file';
+ mkdir($directory.'/sub', 0o777, true);
+ touch($file);
+ chmod($directory.'/sub', 0o500);
+
+ try {
+ $this->filesystem->remove($directory);
+ $this->fail('Filesystem::remove() should throw an exception');
+ } catch (IOException $e) {
+ $this->assertStringStartsWith('Failed to remove file "'.$file.'": unlink(', $e->getMessage());
+ $this->assertStringEndsWith('): Permission denied', $e->getMessage());
+ $this->assertFileExists($file);
+ $this->assertCount(1, new \FilesystemIterator($this->workspace));
+ } finally {
+ chmod($directory.'/sub', 0o777);
+ }
+ }
+
+ public function testRemoveThrowsExceptionOnUnreadableDirectory()
+ {
+ $this->markAsSkippedIfChmodIsMissing();
+
+ $directory = $this->workspace.'/directory';
+ mkdir($directory);
+ touch($directory.'/file');
+ chmod($directory, 0o300);
+
+ try {
+ $this->filesystem->remove($directory);
+ $this->fail('Filesystem::remove() should throw an exception');
+ } catch (IOException $e) {
+ $this->assertStringStartsWith('Failed to remove directory "'.$directory.'": FilesystemIterator::__construct(', $e->getMessage());
+ $this->assertStringEndsWith('): Failed to open directory: Permission denied', $e->getMessage());
+ $this->assertFileExists($directory.'/file');
+ $this->assertCount(1, new \FilesystemIterator($this->workspace));
+ } finally {
+ chmod($directory, 0o777);
+ }
+ }
+
public function testRemoveCleansInvalidLinks()
{
$this->markAsSkippedIfSymlinkIsMissing();
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Tests/Fixtures/remove_collision.php symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Tests/Fixtures/remove_collision.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Tests/Fixtures/remove_collision.php 1970-01-01 00:00:00.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Tests/Fixtures/remove_collision.php 2026-11-04 11:23:00.000000000 +0000
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Filesystem;
+
+function random_bytes(int $length): string
+{
+ return isset($GLOBALS['filesystemRemoveCollision']) ? ($GLOBALS['filesystemRemoveCollision'])($length) : \random_bytes($length);
+}
+
+/**
+ * @param \Traversable $iterator
+ *
+ * @return array
+ */
+function iterator_to_array(\Traversable $iterator, bool $preserveKeys = true): array
+{
+ return isset($GLOBALS['filesystemRemoveIterator']) ? ($GLOBALS['filesystemRemoveIterator'])($iterator, $preserveKeys) : \iterator_to_array($iterator, $preserveKeys);
+}
+
+function realpath(string $path): string|false
+{
+ return isset($GLOBALS['filesystemRemoveRealpath']) ? ($GLOBALS['filesystemRemoveRealpath'])($path) : \realpath($path);
+}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Tests/Fixtures/remove_without_rename.php symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Tests/Fixtures/remove_without_rename.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Tests/Fixtures/remove_without_rename.php 1970-01-01 00:00:00.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Tests/Fixtures/remove_without_rename.php 2026-11-04 11:23:00.000000000 +0000
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Component\Filesystem\Exception\IOException;
+use Symfony\Component\Filesystem\Filesystem;
+
+require __DIR__.'/../../Exception/ExceptionInterface.php';
+require __DIR__.'/../../Exception/IOExceptionInterface.php';
+require __DIR__.'/../../Exception/IOException.php';
+require __DIR__.'/../../Filesystem.php';
+
+if (\function_exists('rename')) {
+ throw new \LogicException('The rename function must be disabled.');
+}
+
+try {
+ (new Filesystem())->remove($argv[1]);
+ throw new \LogicException('Removal must fail when rename() is disabled.');
+} catch (IOException $e) {
+ echo $e->getMessage().\PHP_EOL;
+}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Tests/PathTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Tests/PathTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Filesystem/Tests/PathTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Filesystem/Tests/PathTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -1125,6 +1125,69 @@
$this->assertSame('/path/to/test/subdir', Path::join('/path', 'to', '/test', 'subdir/'));
}
+ public function testInvalidSchemeIsNotARoot()
+ {
+ $this->assertFalse(Path::isBasePath('/base', '/base/../../etc/x://../passwd'));
+ $this->assertFalse(Path::isBasePath('/base', '/base/x://../../etc/passwd'));
+ $this->assertSame('/etc/passwd', Path::canonicalize('/base/../../etc/x://../passwd'));
+ $this->assertSame('/etc/passwd', Path::join('/base', '../../etc/x://../passwd'));
+ $this->assertSame('/etc/passwd', Path::makeAbsolute('../../etc/x://../passwd', '/base'));
+ $this->assertSame('../etc/passwd', Path::makeRelative('/base/../../etc/x://../passwd', '/base'));
+
+ $this->assertSame('b:/c', Path::canonicalize('a/../b://c'));
+ $this->assertSame('x', Path::canonicalize('.://../x'));
+ $this->assertSame('x', Path::canonicalize('a_b://../x'));
+ $this->assertSame('x', Path::canonicalize('://../x'));
+ $this->assertSame('a/c', Path::canonicalize('a/b://../c'));
+ $this->assertSame('../b', Path::canonicalize('../a://../b'));
+ }
+
+ public function testIsAbsoluteAndGetRootIgnoreInvalidScheme()
+ {
+ $this->assertTrue(Path::isAbsolute('/etc/x://y'));
+ $this->assertFalse(Path::isAbsolute('a/b:///c'));
+ $this->assertFalse(Path::isAbsolute('a_b:///c'));
+ $this->assertFalse(Path::isAbsolute(':///c'));
+
+ $this->assertSame('/', Path::getRoot('/etc/x://y'));
+ $this->assertSame('', Path::getRoot('a/b:///c'));
+
+ $this->assertSame('/a/b:/x/c', Path::makeAbsolute('c', '/a/b://x'));
+ }
+
+ public function testIsLocalIgnoresInvalidScheme()
+ {
+ $this->assertTrue(Path::isLocal('/etc/x://y'));
+ $this->assertTrue(Path::isLocal('a/b://c'));
+ }
+
+ public function testSingleLetterBeforeDoubleSlashIsNotAScheme()
+ {
+ $this->assertSame(Path::canonicalize('x:/../y'), Path::canonicalize('x://../y'));
+ $this->assertSame(Path::getRoot('x:/y'), Path::getRoot('x://y'));
+ $this->assertTrue(Path::isLocal('x://y'));
+ }
+
+ public function testValidSchemeIsARoot()
+ {
+ $this->assertSame('phar:///style.css', Path::canonicalize('phar:///../style.css'));
+ $this->assertSame('PHAR:///style.css', Path::canonicalize('PHAR:///../style.css'));
+ $this->assertSame('file:///etc/passwd', Path::canonicalize('file:///../etc/passwd'));
+ $this->assertSame('vfs://root/file', Path::canonicalize('vfs://root/dir/../file'));
+ $this->assertSame('s3://file', Path::canonicalize('s3://../file'));
+ $this->assertSame('compress.zlib:///file.gz', Path::canonicalize('compress.zlib:///../file.gz'));
+ $this->assertSame('1a://x', Path::canonicalize('1a://../x'));
+ $this->assertSame('+a://x', Path::canonicalize('+a://../x'));
+
+ $this->assertTrue(Path::isAbsolute('PHAR:///c'));
+ $this->assertTrue(Path::isAbsolute('compress.zlib:///c'));
+ $this->assertTrue(Path::isAbsolute('1a:///c'));
+ $this->assertSame('compress.zlib:///', Path::getRoot('compress.zlib:///c'));
+ $this->assertFalse(Path::isLocal('compress.zlib:///c'));
+ $this->assertFalse(Path::isLocal('s3://bucket/c'));
+ $this->assertFalse(Path::isLocal('1a://x'));
+ }
+
public function testGetHomeDirectoryFailsIfNotSupportedOperatingSystem()
{
$this->expectException(\RuntimeException::class);
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Finder/Glob.php symfony-6.4.50+dfsg/src/Symfony/Component/Finder/Glob.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Finder/Glob.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Finder/Glob.php 2026-11-04 11:23:00.000000000 +0000
@@ -77,8 +77,8 @@
if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) {
$regex .= "\\$car";
- } elseif ('[' === $car && !$escaping && null !== $class = self::bracketExpressionToRegex($glob, $i, $delimiter, $bracketScanEnd)) {
- $regex .= $class;
+ } elseif ('[' === $car && !$escaping) {
+ $regex .= self::bracketExpressionToRegex($glob, $i, $delimiter, $bracketScanEnd);
} elseif ('*' === $car) {
if ($escaping || $strictWildcardSlash || !str_ends_with($regex, '.*')) {
// ".*.*" matches the same as ".*" but backtracks quadratically
@@ -135,16 +135,16 @@
}
/**
- * Returns null when a "/" comes before the closing "]", and "\[" when no "]" closes the "[" at position $i.
+ * Returns "\[" when no "]" closes the "[" at position $i, or when a "/" comes before the closing "]".
*
* $scanEnd is where the last scan stopped without finding a "]": every "[" before that position gets the same result.
*/
- private static function bracketExpressionToRegex(string $glob, int &$i, string $delimiter, int &$scanEnd): ?string
+ private static function bracketExpressionToRegex(string $glob, int &$i, string $delimiter, int &$scanEnd): string
{
$sizeGlob = \strlen($glob);
if ($i < $scanEnd) {
- return $scanEnd < $sizeGlob ? null : '\\[';
+ return '\\[';
}
$class = '[';
@@ -159,9 +159,7 @@
}
if ('/' === $car || ('\\' === $car && '/' === ($glob[$j + 1] ?? ''))) {
- $scanEnd = $j;
-
- return null;
+ break;
}
if ('[' === $car && preg_match('/\[:[a-z]++:\]/A', $glob, $m, 0, $j)) {
@@ -178,7 +176,7 @@
$class .= $car;
}
}
- $scanEnd = $sizeGlob;
+ $scanEnd = $j;
return '\\[';
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Finder/Tests/GlobTest.php symfony-6.4.50+dfsg/src/Symfony/Component/Finder/Tests/GlobTest.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Finder/Tests/GlobTest.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Finder/Tests/GlobTest.php 2026-11-04 11:23:00.000000000 +0000
@@ -174,6 +174,23 @@
$this->assertSame('#^(?=[^\.])[a[]$#', Glob::toRegex('[a[]'));
}
+ public function testGlobToRegexBracketWithSlash()
+ {
+ $this->assertSame(1, preg_match(Glob::toRegex('a[/]b'), 'a[/]b'));
+ $this->assertSame(0, preg_match(Glob::toRegex('a[/]b'), 'a/b'));
+ $this->assertSame(1, preg_match(Glob::toRegex('[/]'), '[/]'));
+ $this->assertSame(1, preg_match(Glob::toRegex('[a/b]'), '[a/b]'));
+ $this->assertSame(1, preg_match(Glob::toRegex('x[y/z]w'), 'x[y/z]w'));
+ $this->assertSame(0, preg_match(Glob::toRegex('x[y/z]w'), 'xzw'));
+ $this->assertSame(1, preg_match(Glob::toRegex('[a[/]'), '[a[/]'));
+
+ $this->assertSame(1, preg_match(Glob::toRegex('[a/b][c]'), '[a/b]c'));
+ $this->assertSame(0, preg_match(Glob::toRegex('[a/b][c]'), '[a/b]d'));
+
+ $this->assertSame(1, preg_match(Glob::toRegex('[*/]x'), '[foo/]x'));
+ $this->assertSame(0, preg_match(Glob::toRegex('[*/]x'), '[foo/bar/]x'));
+ }
+
public function testGlobToRegexBracketsAfterAnUnclosedOne()
{
$this->assertSame('#^(?=[^\.])\[\[:alpha:]$#', Glob::toRegex('[[:alpha:]'));
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php symfony-6.4.50+dfsg/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToStringTransformer.php 2026-11-04 11:23:00.000000000 +0000
@@ -101,7 +101,7 @@
*/
public function reverseTransform(mixed $value): ?\DateTime
{
- if (empty($value)) {
+ if (null === $value || '' === $value) {
return null;
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php symfony-6.4.50+dfsg/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Form/Extension/Core/EventListener/ResizeFormListener.php 2026-11-04 11:23:00.000000000 +0000
@@ -13,6 +13,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
+use Symfony\Component\Form\FormConfigBuilder;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
@@ -101,10 +102,10 @@
$data = [];
}
- // Remove all empty rows
+ // Remove all rows missing from the submitted data
if ($this->allowDelete) {
foreach ($form as $name => $child) {
- if (!isset($data[$name])) {
+ if (!\array_key_exists($name, $data)) {
$form->remove($name);
}
}
@@ -113,7 +114,7 @@
// Add all additional rows
if ($this->allowAdd) {
foreach ($data as $name => $value) {
- if (!$form->has($name)) {
+ if (!$form->has($name) && '' !== $name && FormConfigBuilder::isValidName((string) $name)) {
$form->add($name, $this->type, array_replace([
'property_path' => '['.$name.']',
], $this->prototypeOptions));
@@ -142,7 +143,7 @@
$initialKeys = $this->initialKeys[$form] ?? [];
/** @var FormInterface $child */
foreach ($form as $name => $child) {
- if (!$child->isValid() || !$child->isSynchronized()) {
+ if (!$child->isSubmitted() || !$child->isValid() || !$child->isSynchronized()) {
continue;
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php symfony-6.4.50+dfsg/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php 2026-11-04 11:23:00.000000000 +0000
@@ -97,7 +97,7 @@
if (\is_array($params) && \is_array($files)) {
$data = FormUtil::mergeParamsAndFiles($params, $files);
} else {
- $data = $params ?: $files;
+ $data = null === $params || '' === $params || [] === $params ? $files : $params;
}
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php symfony-6.4.50+dfsg/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php 2026-11-04 11:23:00.000000000 +0000
@@ -56,12 +56,13 @@
$violationPath = null;
$relativePath = null;
+ $originForms = [];
$match = false;
// Don't create a ViolationPath instance for empty property paths
if ('' !== $violation->getPropertyPath()) {
$violationPath = new ViolationPath($violation->getPropertyPath());
- $relativePath = $this->reconstructPath($violationPath, $form);
+ $relativePath = $this->reconstructPath($violationPath, $form, $originForms);
}
// This case happens if the violation path is empty and thus
@@ -90,7 +91,7 @@
$scope = $relativePath->getRoot();
$it = new PropertyPathIterator($relativePath);
- while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it))) {
+ while ($this->acceptsErrors($scope) && null !== ($child = $this->matchChild($scope, $it, $originForms))) {
$scope = $child;
$it->next();
$match = true;
@@ -231,10 +232,12 @@
*
* If a matching child is found, it is returned. Otherwise
* null is returned.
+ *
+ * @param array $originForms The originating forms indexed by their offset in the reconstructed property path
*/
- private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it): ?FormInterface
+ private function matchChild(FormInterface $form, PropertyPathIteratorInterface $it, array $originForms): ?FormInterface
{
- $target = null;
+ $candidates = [];
$chunk = '';
$foundAtIndex = null;
@@ -251,6 +254,7 @@
$children = iterator_to_array(new \RecursiveIteratorIterator(new InheritDataAwareIterator($form)), false);
$allChildren = $children;
$startIndex = $it->valid() ? $it->key() : null;
+ $origin = null !== $startIndex ? ($originForms[$startIndex] ?? null) : null;
while ($it->valid()) {
if ($it->isIndex()) {
@@ -278,8 +282,11 @@
foreach ($children as $i => $child) {
$childPath = (string) $child->getPropertyPath();
if ($childPath === $chunk) {
- $target = $child;
- $foundAtIndex = $it->key();
+ if ($foundAtIndex !== $index = $it->key()) {
+ $candidates = [];
+ $foundAtIndex = $index;
+ }
+ $candidates[] = $child;
} elseif (str_starts_with($childPath, $chunk)) {
continue;
}
@@ -291,15 +298,15 @@
}
// No child matched the violation path verbatim: retry with camelized paths.
- if (null === $target && null !== $startIndex) {
- return $this->matchCamelizedChild($allChildren, $it, $startIndex);
+ if (null === $foundAtIndex) {
+ return null !== $startIndex ? $this->matchCamelizedChild($allChildren, $it, $startIndex, $originForms) : null;
}
- if (null !== $foundAtIndex) {
- $it->seek($foundAtIndex);
+ if (\in_array($origin, $candidates, true)) {
+ $candidates = [$origin];
}
- return $target;
+ return $this->selectCandidate($candidates, $it, $foundAtIndex, $originForms);
}
/**
@@ -310,11 +317,12 @@
* setters, which makes a child named "discount_price" read and write the
* "discountPrice" property the violation points at.
*
- * @param list $children
+ * @param list $children
+ * @param array $originForms
*/
- private function matchCamelizedChild(array $children, PropertyPathIteratorInterface $it, int $startIndex): ?FormInterface
+ private function matchCamelizedChild(array $children, PropertyPathIteratorInterface $it, int $startIndex, array $originForms): ?FormInterface
{
- $target = null;
+ $candidates = [];
$chunk = '';
$foundAtIndex = null;
$childPaths = [];
@@ -332,8 +340,11 @@
foreach ($childPaths as $i => $childPath) {
if ($childPath === $chunk) {
- $target = $children[$i];
- $foundAtIndex = $it->key();
+ if ($foundAtIndex !== $index = $it->key()) {
+ $candidates = [];
+ $foundAtIndex = $index;
+ }
+ $candidates[] = $children[$i];
} elseif (str_starts_with($childPath, $chunk)) {
continue;
}
@@ -342,17 +353,59 @@
}
}
- if (null !== $foundAtIndex) {
- $it->seek($foundAtIndex);
+ return null !== $foundAtIndex ? $this->selectCandidate($candidates, $it, $foundAtIndex, $originForms) : null;
+ }
+
+ /**
+ * Selects the candidate through which the longest part of the property path can be matched, the last one on a tie.
+ *
+ * @param non-empty-list $candidates The children whose property path matches the path up to $foundAtIndex
+ * @param array $originForms
+ */
+ private function selectCandidate(array $candidates, PropertyPathIteratorInterface $it, int $foundAtIndex, array $originForms): FormInterface
+ {
+ $target = end($candidates);
+
+ if (1 < \count($candidates)) {
+ $deepestIndex = -1;
+
+ foreach ($candidates as $candidate) {
+ $it->seek($foundAtIndex);
+
+ if ($deepestIndex <= $index = $this->matchDescendants($candidate, $it, $originForms)) {
+ $target = $candidate;
+ $deepestIndex = $index;
+ }
+ }
}
+ $it->seek($foundAtIndex);
+
return $target;
}
/**
+ * Returns the index of the last element of the property path that the mapping can reach through the descendants of the form.
+ *
+ * @param array $originForms
+ */
+ private function matchDescendants(FormInterface $form, PropertyPathIteratorInterface $it, array $originForms): int
+ {
+ $index = $it->key();
+
+ for ($it->next(); $this->acceptsErrors($form) && $it->valid() && null !== $form = $this->matchChild($form, $it, $originForms); $it->next()) {
+ $index = $it->key();
+ }
+
+ return $index;
+ }
+
+ /**
* Reconstructs a property path from a violation path and a form tree.
+ *
+ * @param array $originForms The originating forms indexed by their offset in the reconstructed property path
*/
- private function reconstructPath(ViolationPath $violationPath, FormInterface $origin): ?RelativePath
+ private function reconstructPath(ViolationPath $violationPath, FormInterface $origin, array &$originForms): ?RelativePath
{
$propertyPathBuilder = new PropertyPathBuilder($violationPath);
$it = $violationPath->getIterator();
@@ -386,6 +439,7 @@
break;
}
+ $originForms[$i] = $scope;
$propertyPathBuilder->replace($i, 1, $propertyPath);
$i += $propertyPath->getLength();
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Form/NativeRequestHandler.php symfony-6.4.50+dfsg/src/Symfony/Component/Form/NativeRequestHandler.php
--- symfony-6.4.49+dfsg/src/Symfony/Component/Form/NativeRequestHandler.php 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Form/NativeRequestHandler.php 2026-11-04 11:23:00.000000000 +0000
@@ -108,7 +108,7 @@
if (\is_array($params) && \is_array($files)) {
$data = FormUtil::mergeParamsAndFiles($params, $files);
} else {
- $data = $params ?: $files;
+ $data = null === $params || '' === $params || [] === $params ? $files : $params;
}
}
diff -Nru symfony-6.4.49+dfsg/src/Symfony/Component/Form/Resources/translations/validators.da.xlf symfony-6.4.50+dfsg/src/Symfony/Component/Form/Resources/translations/validators.da.xlf
--- symfony-6.4.49+dfsg/src/Symfony/Component/Form/Resources/translations/validators.da.xlf 2026-10-28 18:15:18.000000000 +0000
+++ symfony-6.4.50+dfsg/src/Symfony/Component/Form/Resources/translations/validators.da.xlf 2026-11-04 11:23:00.000000000 +0000
@@ -20,7 +20,7 @@
This value is not a valid birthdate.
- Værdien er ikke en gyldig fødselsdato.
+ Værdien er ikke en gyldig fødselsdato.The selected choice is invalid.
@@ -32,7 +32,7 @@
This value is not a valid color.
- Værdien er ikke en gyldig farve.
+ Værdien er ikke en gyldig farve.This value is not a valid country.
@@ -44,7 +44,7 @@
This value is not a valid date interval.
- Værdien er ikke et gyldigt datointerval.
+ Værdien er ikke et gyldigt datointerval.This value is not a valid date and time.
@@ -56,7 +56,7 @@
This value is not a valid file.
- Værdien er ikke en gyldig fil.
+ Værdien er ikke en gyldig fil.The hidden field is invalid.
@@ -64,7 +64,7 @@
This value is not a valid integer.
- Værdien er ikke et gyldigt heltal.
+ Værdien er ikke et gyldigt heltal.This value is not a valid language.
@@ -72,15 +72,15 @@
This value is not a valid locale.
- Værdien er ikke en gyldig sprogkode.
+ Værdien er ikke en gyldig sprogkode.This value is not a valid money amount.
- Værdien er ikke et gyldigt beløb.
+ Værdien er ikke et gyldigt beløb.This value is not a valid number.
- Værdien er ikke et gyldigt tal.
+ Værdien er ikke et gyldigt tal.The password is invalid.
@@ -88,7 +88,7 @@
This value is not a valid percentage.
- Værdien er ikke en gyldig procentværdi.
+ Værdien er ikke en gyldig procentværdi.The values do not match.
@@ -108,11 +108,11 @@
This value is not a valid search term.
- Værdien er ikke et gyldigt søgeord.
+ Værdien er ikke et gyldigt søgeord.This value is not a valid phone number.
- Værdien er ikke et gyldigt telefonnummer.
+ Værdien er ikke et gyldigt telefonnummer.The checkbox has an invalid value.
@@ -124,11 +124,11 @@
This value is not a valid option.
- Værdien er ikke en gyldig mulighed.
+ Værdien er ikke en gyldig mulighed.This value is not a valid range.
- Værdien er ikke et gyldigt interval.
+ Værdien er ikke et gyldigt interval.This value is not a valid week.
@@ -140,7 +140,7 @@
The lower bound must not be greater than the upper bound.
- Den nedre grænse må ikke være større end den øvre grænse.
+ Den nedre grænse må ikke være større end den øvre grænse.
This form should not contain extra fields.
- Mẫu này không nên chứa trường mở rộng.
+ Biểu mẫu này không được chứa trường thừa.The uploaded file was too large. Please try to upload a smaller file.
@@ -20,7 +20,7 @@
This value is not a valid birthdate.
- Giá trị này không phải là ngày sinh hợp lệ.
+ Giá trị này không phải là ngày sinh hợp lệ.The selected choice is invalid.
@@ -32,7 +32,7 @@
This value is not a valid color.
- Giá trị này không phải là màu hợp lệ.
+ Giá trị này không phải là màu hợp lệ.This value is not a valid country.
@@ -44,11 +44,11 @@
This value is not a valid date interval.
- Giá trị này không phải là khoảng thời gian hợp lệ.
+ Giá trị này không phải là khoảng thời gian hợp lệ.This value is not a valid date and time.
- Giá trị này không phải là ngày và thời gian hợp lệ.
+ Giá trị này không phải là ngày giờ hợp lệ.This value is not a valid date.
@@ -56,15 +56,15 @@
This value is not a valid file.
- Giá trị này không phải là tệp hợp lệ.
+ Giá trị này không phải là tệp hợp lệ.The hidden field is invalid.
- Phạm vi ẩn không hợp lệ.
+ Trường ẩn không hợp lệ.This value is not a valid integer.
- Giá trị này không phải là số nguyên hợp lệ.
+ Giá trị này không phải là số nguyên hợp lệ.This value is not a valid language.
@@ -72,15 +72,15 @@
This value is not a valid locale.
- Giá trị này không phải là miền hợp lệ.
+ Giá trị này không phải là mã ngôn ngữ và khu vực hợp lệ.This value is not a valid money amount.
- Giá trị này không phải là khoản tiền hợp lệ.
+ Giá trị này không phải là số tiền hợp lệ.This value is not a valid number.
- Giá trị này không phải là số hợp lệ.
+ Giá trị này không phải là số hợp lệ.The password is invalid.
@@ -88,7 +88,7 @@
This value is not a valid percentage.
- Giá trị này không phải là phần trăm hợp lệ.
+ Giá trị này không phải là tỷ lệ phần trăm hợp lệ.The values do not match.
@@ -108,11 +108,11 @@
This value is not a valid search term.
- Giá trị này không phải là chuỗi tìm kiếm hợp lệ.
+ Giá trị này không phải là từ khóa tìm kiếm hợp lệ.This value is not a valid phone number.
- Giá trị này không phải là số điện thoại hợp lệ.
+ Giá trị này không phải là số điện thoại hợp lệ.The checkbox has an invalid value.
@@ -120,27 +120,27 @@
This value is not a valid email address.
- Giá trị này không phải là địa chỉ email hợp lệ.
+ Giá trị này không phải là địa chỉ email hợp lệ.This value is not a valid option.
- Giá trị này không phải là phương án hợp lệ.
+ Giá trị này không phải là lựa chọn hợp lệ.This value is not a valid range.
- Giá trị này không phải là phạm vi hợp lệ.
+ Giá trị này không phải là khoảng giá trị hợp lệ.This value is not a valid week.
- Giá trị này không phải là tuần hợp lệ.
+ Giá trị này không phải là tuần hợp lệ.This value is not a valid UUID.
- Giá trị này không phải là UUID hợp lệ.
+ Giá trị này không phải là UUID hợp lệ.The lower bound must not be greater than the upper bound.
- Giới hạn dưới không được lớn hơn giới hạn trên.
+ Giới hạn dưới không được lớn hơn giới hạn trên.This form should not contain extra fields.
- 该表单中不可有额外字段.
+ 该表单中不应包含额外字段。The uploaded file was too large. Please try to upload a smaller file.
- 上传文件太大, 请重新尝试上传一个较小的文件.
+ 上传文件太大,请尝试上传较小的文件。The CSRF token is invalid. Please try to resubmit the form.
- CSRF 验证符无效, 请重新提交.
+ CSRF 验证无效,请重新提交。This value is not a valid HTML5 color.
- 该数值不是个有效的 HTML5 颜色。
+ 该值不是一个有效的 HTML5 颜色。This value is not a valid birthdate.
- 该数值不是个有效的生日日期。
+ 该值不是一个有效的出生日期。The selected choice is invalid.
@@ -32,7 +32,7 @@
This value is not a valid color.
- 该数值不是个有效的颜色。
+ 该值不是一个有效的颜色值。This value is not a valid country.
@@ -44,11 +44,11 @@
This value is not a valid date interval.
- 该数值不是个有效的日期间隔。
+ 该值不是一个有效的日期间隔。This value is not a valid date and time.
- 该值不是一个有效的日期时间(datetime)。
+ 该值不是一个有效的日期和时间(datetime)。This value is not a valid date.
@@ -56,7 +56,7 @@
This value is not a valid file.
- 该数值不是个有效的文件。
+ 该值不是一个有效的文件。The hidden field is invalid.
@@ -64,7 +64,7 @@
This value is not a valid integer.
- 该数值不是个有效的整数。
+ 该值不是一个有效的整数。This value is not a valid language.
@@ -76,11 +76,11 @@
This value is not a valid money amount.
- 该数值不是个有效的金额。
+ 该值不是一个有效的金额。This value is not a valid number.
- 该数值不是个有效的数字。
+ 该值不是一个有效的数字。The password is invalid.
@@ -88,7 +88,7 @@
This value is not a valid percentage.
- 该数值不是个有效的百分比。
+ 该值不是一个有效的百分比。The values do not match.
@@ -104,19 +104,19 @@
This value is not a valid URL.
- 该值不是一个有效的 URL 。
+ 该值不是一个有效的 URL。This value is not a valid search term.
- 该数值不是个有效的搜索词。
+ 该值不是一个有效的搜索词。This value is not a valid phone number.
- 该数值不是个有效的手机号码。
+ 该值不是一个有效的手机号码。The checkbox has an invalid value.
- 无效的选框值。
+ 复选框的值无效。This value is not a valid email address.
@@ -124,11 +124,11 @@
This value is not a valid option.
- 该数值不是个有效的选项。
+ 该值不是一个有效的选项。This value is not a valid range.
- 该数值不是个有效的范围。
+ 该值不是一个有效的范围。This value is not a valid week.
@@ -136,11 +136,11 @@
This value is not a valid UUID.
- 该值不是有效的 UUID 。
+ 该值不是有效的 UUID。The lower bound must not be greater than the upper bound.
- 下限不得大于上限。
+ 下限不得大于上限。