diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 5f2d77a453eaf..d4dafb2aa0029 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,6 +1,6 @@
| Q | A
| ------------- | ---
-| Branch? | 7.3 for features / 6.4, and 7.2 for bug fixes
+| Branch? | 7.4 for features / 6.4, 7.2, or 7.3 for bug fixes
| Bug fix? | yes/no
| New feature? | yes/no
| Deprecations? | yes/no
diff --git a/.github/expected-missing-return-types.diff b/.github/expected-missing-return-types.diff
index d48f4ff600dbe..a9b6f3b22ca03 100644
--- a/.github/expected-missing-return-types.diff
+++ b/.github/expected-missing-return-types.diff
@@ -8923,6 +8923,23 @@ diff --git a/src/Symfony/Component/Intl/Data/Bundle/Writer/BundleWriterInterface
- public function write(string $path, string $locale, mixed $data);
+ public function write(string $path, string $locale, mixed $data): void;
}
+diff --git a/src/Symfony/Component/Intl/Transliterator/EmojiTransliterator.php b/src/Symfony/Component/Intl/Transliterator/EmojiTransliterator.php
+--- a/src/Symfony/Component/Intl/Transliterator/EmojiTransliterator.php
++++ b/src/Symfony/Component/Intl/Transliterator/EmojiTransliterator.php
+@@ -74,5 +74,5 @@ if (!class_exists(\Transliterator::class)) {
+ */
+ #[\ReturnTypeWillChange]
+- public function getErrorCode(): int|false
++ public function getErrorCode(): int
+ {
+ return isset($this->transliterator) ? $this->transliterator->getErrorCode() : 0;
+@@ -83,5 +83,5 @@ if (!class_exists(\Transliterator::class)) {
+ */
+ #[\ReturnTypeWillChange]
+- public function getErrorMessage(): string|false
++ public function getErrorMessage(): string
+ {
+ return isset($this->transliterator) ? $this->transliterator->getErrorMessage() : '';
diff --git a/src/Symfony/Component/Intl/Util/IntlTestHelper.php b/src/Symfony/Component/Intl/Util/IntlTestHelper.php
--- a/src/Symfony/Component/Intl/Util/IntlTestHelper.php
+++ b/src/Symfony/Component/Intl/Util/IntlTestHelper.php
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 40da4746f4fbe..677e6e6a30d91 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -6,7 +6,7 @@ on:
schedule:
- cron: '34 4 * * 6'
push:
- branches: [ "7.3" ]
+ branches: [ "7.4" ]
# Declare default permissions as read only.
permissions: read-all
diff --git a/CHANGELOG-6.4.md b/CHANGELOG-6.4.md
index 7eb354e2603a5..78e2a5e01dec1 100644
--- a/CHANGELOG-6.4.md
+++ b/CHANGELOG-6.4.md
@@ -7,6 +7,25 @@ in 6.4 minor versions.
To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash
To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.4.0...v6.4.1
+* 6.4.22 (2025-05-29)
+
+ * bug #60549 [Translation] Add intl-icu fallback for MessageCatalogue metadata (pontus-mp)
+ * bug #60571 [ErrorHandler] Do not transform file to link if it does not exist (lyrixx)
+ * bug #60494 [Messenger] fix: Add argument as integer (overexpOG)
+ * bug #60524 [Notifier] Fix Clicksend transport (BafS)
+ * bug #60478 [Validator] add missing `$extensions` and `$extensionsMessage` to the `Image` constraint (xabbuh)
+ * bug #60423 [DependencyInjection] Make `DefinitionErrorExceptionPass` consider `IGNORE_ON_UNINITIALIZED_REFERENCE` and `RUNTIME_EXCEPTION_ON_INVALID_REFERENCE` the same (MatTheCat)
+ * bug #60421 [VarExporter] Fixed lazy-loading ghost objects generation with property hooks (cheack)
+ * bug #60266 [Security] Exclude remember_me from default login authenticators (santysisi)
+ * bug #60400 [Config] Fix generated comment for multiline "info" (GromNaN)
+ * bug #60260 [Serializer] Prevent `Cannot traverse an already closed generator` error by materializing Traversable input (santysisi)
+ * bug #60292 [HttpFoundation] Encode path in `X-Accel-Redirect` header (Athorcis)
+ * bug #60379 [Security] Avoid failing when PersistentRememberMeHandler handles a malformed cookie (Seldaek)
+ * bug #60373 [FrameworkBundle] Ensure `Email` class exists before using it (Kocal)
+ * bug #60365 [FrameworkBundle] ensure that all supported e-mail validation modes can be configured (xabbuh)
+ * bug #60350 [Security][LoginLink] Throw `InvalidLoginLinkException` on invalid parameters (davidszkiba)
+ * bug #60340 [String] fix EmojiTransliterator return type compatibility with PHP 8.5 (xabbuh)
+
* 6.4.21 (2025-05-02)
* bug #60288 [VarExporter] dump default value for property hooks if present (xabbuh)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ee2cb2a40889b..3e7f5ec2b6e78 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -30,9 +30,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Kris Wallsmith (kriswallsmith)
- Jakub Zalas (jakubzalas)
- Yonel Ceruto (yonelceruto)
+ - HypeMC (hypemc)
- Hugo Hamon (hhamon)
- Tobias Nyholm (tobias)
- - HypeMC (hypemc)
- Jérôme Tamarelle (gromnan)
- Antoine Lamirault (alamirault)
- Samuel ROZE (sroze)
@@ -96,8 +96,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Henrik Bjørnskov (henrikbjorn)
- Ruud Kamphuis (ruudk)
- David Buchmann (dbu)
- - Andrej Hudec (pulzarraider)
- Tomas Norkūnas (norkunas)
+ - Andrej Hudec (pulzarraider)
- Jáchym Toušek (enumag)
- Hubert Lenoir (hubert_lenoir)
- Christian Raue
@@ -160,12 +160,13 @@ The Symfony Connect username in parenthesis allows to get more information
- Włodzimierz Gajda (gajdaw)
- Javier Spagnoletti (phansys)
- Adrien Brault (adrienbrault)
+ - Florent Morselli (spomky_)
+ - soyuka
- Florian Voutzinos (florianv)
- Teoh Han Hui (teohhanhui)
- Przemysław Bogusz (przemyslaw-bogusz)
- Colin Frei
- excelwebzone
- - Florent Morselli (spomky_)
- Paráda József (paradajozsef)
- Maximilian Beckers (maxbeckers)
- Baptiste Clavié (talus)
@@ -175,17 +176,16 @@ The Symfony Connect username in parenthesis allows to get more information
- Dāvis Zālītis (k0d3r1s)
- Gordon Franke (gimler)
- Malte Schlüter (maltemaltesich)
- - soyuka
- jeremyFreeAgent (jeremyfreeagent)
- Michael Babker (mbabker)
- Alexis Lefebvre
+ - Hugo Alliaume (kocal)
- Christopher Hertel (chertel)
- Joshua Thijssen
- Vasilij Dusko
- Daniel Wehner (dawehner)
- Robert Schönthal (digitalkaoz)
- Smaine Milianni (ismail1432)
- - Hugo Alliaume (kocal)
- François-Xavier de Guillebon (de-gui_f)
- Andreas Schempp (aschempp)
- noniagriconomie
@@ -255,6 +255,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Alessandro Lai (jean85)
- 77web
- Gocha Ossinkine (ossinkine)
+ - matlec
- Jesse Rushlow (geeshoe)
- Matthieu Ouellette-Vachon (maoueh)
- Michał Pipa (michal.pipa)
@@ -286,7 +287,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Clément JOBEILI (dator)
- Andreas Möller (localheinz)
- Marek Štípek (maryo)
- - matlec
- Daniel Espendiller
- Arnaud PETITPAS (apetitpa)
- Michael Käfer (michael_kaefer)
@@ -310,6 +310,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Patrick Landolt (scube)
- Karoly Gossler (connorhu)
- Timo Bakx (timobakx)
+ - Quentin Devos
- Giorgio Premi
- Alan Poulain (alanpoulain)
- Ruben Gonzalez (rubenrua)
@@ -337,6 +338,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Nikolay Labinskiy (e-moe)
- Martin Schuhfuß (usefulthink)
- apetitpa
+ - wkania
- Guilliam Xavier
- Pierre Minnieur (pminnieur)
- Dominique Bongiraud
@@ -377,6 +379,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Pascal Montoya
- Julien Brochet
- François Pluchino (francoispluchino)
+ - W0rma
- Tristan Darricau (tristandsensio)
- Jan Sorgalla (jsor)
- henrikbjorn
@@ -401,7 +404,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Zan Baldwin (zanbaldwin)
- Tim Goudriaan (codedmonkey)
- BoShurik
- - Quentin Devos
- Adam Prager (padam87)
- Benoît Burnichon (bburnichon)
- maxime.steinhausser
@@ -428,7 +430,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Uwe Jäger (uwej711)
- javaDeveloperKid
- Chris Smith (cs278)
- - W0rma
- Lynn van der Berg (kjarli)
- Michaël Perrin (michael.perrin)
- Eugene Leonovich (rybakit)
@@ -438,6 +439,7 @@ The Symfony Connect username in parenthesis allows to get more information
- GordonsLondon
- Ray
- Philipp Cordes (corphi)
+ - Fabien S (bafs)
- Chekote
- Thomas Adam
- Anderson Müller
@@ -471,6 +473,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Marcos Sánchez
- Emanuele Panzeri (thepanz)
- Zmey
+ - Santiago San Martin (santysisi)
- Kim Hemsø Rasmussen (kimhemsoe)
- Maximilian Reichel (phramz)
- Samaël Villette (samadu61)
@@ -498,6 +501,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Manuel Kießling (manuelkiessling)
- Alexey Kopytko (sanmai)
- Warxcell (warxcell)
+ - SiD (plbsid)
- Atsuhiro KUBO (iteman)
- rudy onfroy (ronfroy)
- Serkan Yildiz (srknyldz)
@@ -507,7 +511,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Gabor Toth (tgabi333)
- realmfoo
- Joppe De Cuyper (joppedc)
- - Fabien S (bafs)
- Simon Podlipsky (simpod)
- Thomas Tourlourat (armetiz)
- Andrey Esaulov (andremaha)
@@ -612,7 +615,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Alex (aik099)
- Kieran Brahney
- Fabien Villepinte
- - SiD (plbsid)
- Greg Thornton (xdissent)
- Alex Bowers
- Kev
@@ -638,6 +640,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Ivan Sarastov (isarastov)
- flack (flack)
- Shein Alexey
+ - Link1515
- Joe Lencioni
- Daniel Tschinder
- Diego Agulló (aeoris)
@@ -758,6 +761,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jérémy REYNAUD (babeuloula)
- Faizan Akram Dar (faizanakram)
- Arkadius Stefanski (arkadius)
+ - Andy Palmer (andyexeter)
- Jonas Flodén (flojon)
- AnneKir
- Tobias Weichart
@@ -781,6 +785,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Giso Stallenberg (gisostallenberg)
- Rob Bast
- Roberto Espinoza (respinoza)
+ - Steven RENAUX (steven_renaux)
- Marvin Feldmann (breyndotechse)
- Soufian EZ ZANTAR (soezz)
- Marek Zajac
@@ -867,7 +872,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Dariusz Ruminski
- Bahman Mehrdad (bahman)
- Romain Gautier (mykiwi)
- - Link1515
- Matthieu Bontemps
- Erik Trapman
- De Cock Xavier (xdecock)
@@ -1010,7 +1014,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Jonas Elfering
- Mihai Stancu
- Nahuel Cuesta (ncuesta)
- - Santiago San Martin
- Chris Boden (cboden)
- EStyles (insidestyles)
- Christophe Villeger (seragan)
@@ -1065,7 +1068,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Pierrick VIGNAND (pierrick)
- Alex Bogomazov (alebo)
- aaa2000 (aaa2000)
- - Andy Palmer (andyexeter)
- Andrew Neil Forster (krciga22)
- Stefan Warman (warmans)
- Tristan Maindron (tmaindron)
@@ -1865,6 +1867,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Philipp Fritsche
- Léon Gersen
- tarlepp
+ - Giuseppe Arcuti
- Dustin Wilson
- Benjamin Paap (benjaminpaap)
- Claus Due (namelesscoder)
@@ -1958,7 +1961,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Bruno MATEU
- Jeremy Bush
- Lucas Bäuerle
- - Steven RENAUX (steven_renaux)
- Laurens Laman
- Thomason, James
- Dario Savella
@@ -2195,6 +2197,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Tim Ward
- Adiel Cristo (arcristo)
- Christian Flach (cmfcmf)
+ - Dennis Jaschinski (d.jaschinski)
- Fabian Kropfhamer (fabiank)
- Jeffrey Cafferata (jcidnl)
- Junaid Farooq (junaidfarooq)
@@ -2264,6 +2267,7 @@ The Symfony Connect username in parenthesis allows to get more information
- wivaku
- Markus Reinhold
- Jingyu Wang
+ - es
- steveYeah
- Asrorbek (asrorbek)
- Samy D (dinduks)
@@ -2278,6 +2282,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Alan Scott
- Juanmi Rodriguez Cerón
- twifty
+ - David Szkiba
- Andy Raines
- François Poguet
- Anthony Ferrara
@@ -2296,6 +2301,7 @@ The Symfony Connect username in parenthesis allows to get more information
- xdavidwu
- Benjamin RICHARD
- Raphaël Droz
+ - Vladimir Pakhomchik
- pdommelen
- Eric Stern
- ShiraNai7
@@ -2710,6 +2716,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Marcel Siegert
- ryunosuke
- Bruno BOUTAREL
+ - Athorcis
- John Stevenson
- everyx
- Richard Heine
@@ -2767,6 +2774,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Abdouarrahmane FOUAD (fabdouarrahmane)
- Jakub Janata (janatjak)
- Jm Aribau (jmaribau)
+ - Maciej Paprocki (maciekpaprocki)
- Matthew Foster (mfoster)
- Paul Seiffert (seiffert)
- Vasily Khayrulin (sirian)
@@ -3114,6 +3122,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Darryl Hein (xmmedia)
- Vladimir Sadicov (xtech)
- Marcel Berteler
+ - Ruud Seberechts
- sdkawata
- Frederik Schmitt
- Peter van Dommelen
@@ -3151,6 +3160,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Pierre Rineau
- Florian Morello
- Maxim Lovchikov
+ - ivelin vasilev
- adenkejawen
- Florent SEVESTRE (aniki-taicho)
- Ari Pringle (apringle)
@@ -3327,6 +3337,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Kevin Verschaeve (keversc)
- Kevin Herrera (kherge)
- Kubicki Kamil (kubik)
+ - Lauris Binde (laurisb)
- Luis Ramón López López (lrlopez)
- Vladislav Nikolayev (luxemate)
- Martin Mandl (m2mtech)
@@ -3372,7 +3383,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Youpie
- Jason Stephens
- Korvin Szanto
- - wkania
- srsbiz
- Taylan Kasap
- Michael Orlitzky
@@ -3585,6 +3595,7 @@ The Symfony Connect username in parenthesis allows to get more information
- mieszko4
- Steve Preston
- ibasaw
+ - koyolgecen
- Wojciech Skorodecki
- Kevin Frantz
- Neophy7e
@@ -3614,6 +3625,7 @@ The Symfony Connect username in parenthesis allows to get more information
- satalaondrej
- Matthias Dötsch
- jonmldr
+ - Nowfel2501
- Yevgen Kovalienia
- Lebnik
- Shude
@@ -3635,6 +3647,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Egor Gorbachev
- Julian Krzefski
- Derek Stephen McLean
+ - PatrickRedStar
- Norman Soetbeer
- zorn
- Yuriy Potemkin
@@ -3744,6 +3757,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Brandon Kelly (brandonkelly)
- Choong Wei Tjeng (choonge)
- Bermon Clément (chou666)
+ - Chris Shennan (chrisshennan)
- Citia (citia)
- Kousuke Ebihara (co3k)
- Loïc Vernet (coil)
@@ -3906,6 +3920,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Romain
- Xavier REN
- Kevin Meijer
+ - Ignacio Alveal
- max
- Alexander Bauer (abauer)
- Ahmad Mayahi (ahmadmayahi)
diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php
index 93e9818f4383c..6619f911ae1e0 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php
@@ -41,7 +41,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str
throw new ConversionException(sprintf('Expected "%s", got "%s"', 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\Foo', get_debug_type($value)));
}
- return $foo->bar;
+ return $value->bar;
}
public function convertToPHPValue($value, AbstractPlatform $platform): ?Foo
diff --git a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php
index 11eca517c5d69..c8640a4ea5f00 100644
--- a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php
+++ b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php
@@ -46,7 +46,7 @@ public function getFunctions(): array
/**
* Adds a "Link" HTTP header.
*
- * @param string $rel The relation type (e.g. "preload", "prefetch", "prerender" or "dns-prefetch")
+ * @param string $rel The relation type (e.g. "preload", "prefetch", or "dns-prefetch")
* @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]")
*
* @return string The relation URI
@@ -117,7 +117,11 @@ public function prefetch(string $uri, array $attributes = []): string
}
/**
- * Indicates to the client that it should prerender this resource .
+ * Indicates to the client that it should prerender this resource.
+ *
+ * This feature is deprecated and superseded by the Speculation Rules API.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/prerender
*
* @param array $attributes The attributes of this link (e.g. "['as' => true]", "['pr' => 0.5]")
*
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index cb52a0704fd99..bae8967a8b723 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -45,6 +45,7 @@
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Uid\Factory\UuidFactory;
+use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Webhook\Controller\WebhookController;
use Symfony\Component\WebLink\HttpHeaderSerializer;
@@ -1066,7 +1067,7 @@ private function addValidationSection(ArrayNodeDefinition $rootNode, callable $e
->validate()->castToArray()->end()
->end()
->scalarNode('translation_domain')->defaultValue('validators')->end()
- ->enumNode('email_validation_mode')->values(['html5', 'loose', 'strict'])->end()
+ ->enumNode('email_validation_mode')->values((class_exists(Email::class) ? Email::VALIDATION_MODES : ['html5-allow-no-tld', 'html5', 'strict']) + ['loose'])->end()
->arrayNode('mapping')
->addDefaultsIfNotSet()
->fixXmlConfig('path')
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index f585b5bbb784b..68386120e06b1 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -735,7 +735,7 @@ static function (ChildDefinition $definition, AsPeriodicTask|AsCronTask $attribu
$container->getDefinition('config_cache_factory')->setArguments([]);
}
- if (!$config['disallow_search_engine_index'] ?? false) {
+ if (!$config['disallow_search_engine_index']) {
$container->removeDefinition('disallow_search_engine_index_response_listener');
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php
index 7a3a95739b0f2..f1dc560ab76f8 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.php
@@ -45,7 +45,6 @@
tagged_iterator('mailer.transport_factory'),
])
- ->set('mailer.default_transport', TransportInterface::class)
->alias('mailer.default_transport', 'mailer.transports')
->alias(TransportInterface::class, 'mailer.default_transport')
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
index 53268ffd283d8..e5cc8522aafb4 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
@@ -17,6 +17,7 @@
use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
+use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
class PhpFrameworkExtensionTest extends FrameworkExtensionTestCase
@@ -245,4 +246,31 @@ public function testRateLimiterLockFactory()
$container->getDefinition('limiter.without_lock')->getArgument(2);
}
+
+ /**
+ * @dataProvider emailValidationModeProvider
+ */
+ public function testValidatorEmailValidationMode(string $mode)
+ {
+ $this->expectNotToPerformAssertions();
+
+ $this->createContainerFromClosure(function (ContainerBuilder $container) use ($mode) {
+ $container->loadFromExtension('framework', [
+ 'annotations' => false,
+ 'http_method_override' => false,
+ 'handle_all_throwables' => true,
+ 'php_errors' => ['log' => true],
+ 'validation' => [
+ 'email_validation_mode' => $mode,
+ ],
+ ]);
+ });
+ }
+
+ public static function emailValidationModeProvider()
+ {
+ foreach (Email::VALIDATION_MODES as $mode) {
+ yield [$mode];
+ }
+ }
}
diff --git a/src/Symfony/Bundle/SecurityBundle/Security.php b/src/Symfony/Bundle/SecurityBundle/Security.php
index acb30adba8adf..6b5286f2ea868 100644
--- a/src/Symfony/Bundle/SecurityBundle/Security.php
+++ b/src/Symfony/Bundle/SecurityBundle/Security.php
@@ -188,8 +188,7 @@ private function getAuthenticator(?string $authenticatorName, string $firewallNa
$firewallAuthenticatorLocator = $this->authenticators[$firewallName];
if (!$authenticatorName) {
- $authenticatorIds = array_keys($firewallAuthenticatorLocator->getProvidedServices());
-
+ $authenticatorIds = array_filter(array_keys($firewallAuthenticatorLocator->getProvidedServices()), fn (string $authenticatorId) => $authenticatorId !== \sprintf('security.authenticator.remember_me.%s', $firewallName));
if (!$authenticatorIds) {
throw new LogicException(sprintf('No authenticator was found for the firewall "%s".', $firewallName));
}
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php
index 35bd329b2297e..c150730c2a8cb 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityTest.php
@@ -155,7 +155,10 @@ public function testLogin()
$firewallAuthenticatorLocator
->expects($this->once())
->method('getProvidedServices')
- ->willReturn(['security.authenticator.custom.dev' => $authenticator])
+ ->willReturn([
+ 'security.authenticator.custom.dev' => $authenticator,
+ 'security.authenticator.remember_me.main' => $authenticator
+ ])
;
$firewallAuthenticatorLocator
->expects($this->once())
@@ -274,6 +277,49 @@ public function testLoginWithoutRequestContext()
$security->login($user);
}
+ public function testLoginFailsWhenTooManyAuthenticatorsFound()
+ {
+ $request = new Request();
+ $authenticator = $this->createMock(AuthenticatorInterface::class);
+ $requestStack = $this->createMock(RequestStack::class);
+ $firewallMap = $this->createMock(FirewallMap::class);
+ $firewall = new FirewallConfig('main', 'main');
+ $userAuthenticator = $this->createMock(UserAuthenticatorInterface::class);
+ $user = $this->createMock(UserInterface::class);
+ $userChecker = $this->createMock(UserCheckerInterface::class);
+
+ $container = $this->createMock(ContainerInterface::class);
+ $container
+ ->expects($this->atLeastOnce())
+ ->method('get')
+ ->willReturnMap([
+ ['request_stack', $requestStack],
+ ['security.firewall.map', $firewallMap],
+ ['security.authenticator.managers_locator', $this->createContainer('main', $userAuthenticator)],
+ ['security.user_checker_locator', $this->createContainer('main', $userChecker)],
+ ])
+ ;
+
+ $requestStack->expects($this->once())->method('getCurrentRequest')->willReturn($request);
+ $firewallMap->expects($this->once())->method('getFirewallConfig')->willReturn($firewall);
+
+ $firewallAuthenticatorLocator = $this->createMock(ServiceProviderInterface::class);
+ $firewallAuthenticatorLocator
+ ->expects($this->once())
+ ->method('getProvidedServices')
+ ->willReturn([
+ 'security.authenticator.custom.main' => $authenticator,
+ 'security.authenticator.other.main' => $authenticator
+ ])
+ ;
+
+ $security = new Security($container, ['main' => $firewallAuthenticatorLocator]);
+
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('Too many authenticators were found for the current firewall "main". You must provide an instance of "Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface" to login programmatically. The available authenticators for the firewall "main" are "security.authenticator.custom.main" ,"security.authenticator.other.main');
+ $security->login($user);
+ }
+
public function testLogout()
{
$request = new Request();
diff --git a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php
index d43d814ebd38b..9649c8d82cbb9 100644
--- a/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php
+++ b/src/Symfony/Component/Config/Builder/ConfigBuilderGenerator.php
@@ -413,39 +413,39 @@ private function getComment(BaseNode $node): string
{
$comment = '';
if ('' !== $info = (string) $node->getInfo()) {
- $comment .= ' * '.$info."\n";
+ $comment .= $info."\n";
}
if (!$node instanceof ArrayNode) {
foreach ((array) ($node->getExample() ?? []) as $example) {
- $comment .= ' * @example '.$example."\n";
+ $comment .= '@example '.$example."\n";
}
if ('' !== $default = $node->getDefaultValue()) {
- $comment .= ' * @default '.(null === $default ? 'null' : var_export($default, true))."\n";
+ $comment .= '@default '.(null === $default ? 'null' : var_export($default, true))."\n";
}
if ($node instanceof EnumNode) {
- $comment .= sprintf(' * @param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => !$a instanceof \UnitEnum ? var_export($a, true) : '\\'.ltrim(var_export($a, true), '\\'), $node->getValues()))))."\n";
+ $comment .= sprintf('@param ParamConfigurator|%s $value', implode('|', array_unique(array_map(fn ($a) => !$a instanceof \UnitEnum ? var_export($a, true) : '\\'.ltrim(var_export($a, true), '\\'), $node->getValues()))))."\n";
} else {
$parameterTypes = $this->getParameterTypes($node);
- $comment .= ' * @param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n";
+ $comment .= '@param ParamConfigurator|'.implode('|', $parameterTypes).' $value'."\n";
}
} else {
foreach ((array) ($node->getExample() ?? []) as $example) {
- $comment .= ' * @example '.json_encode($example)."\n";
+ $comment .= '@example '.json_encode($example)."\n";
}
if ($node->hasDefaultValue() && [] != $default = $node->getDefaultValue()) {
- $comment .= ' * @default '.json_encode($default)."\n";
+ $comment .= '@default '.json_encode($default)."\n";
}
}
if ($node->isDeprecated()) {
- $comment .= ' * @deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n";
+ $comment .= '@deprecated '.$node->getDeprecation($node->getName(), $node->getParent()->getName())['message']."\n";
}
- return $comment;
+ return $comment ? ' * '.str_replace("\n", "\n * ", rtrim($comment, "\n"))."\n" : '';
}
/**
diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php
index 153af57be9b5b..5c1259c20edd8 100644
--- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php
+++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues.php
@@ -38,7 +38,13 @@ public function getConfigTreeBuilder(): TreeBuilder
->arrayPrototype()
->fixXmlConfig('option')
->children()
- ->scalarNode('dsn')->end()
+ ->scalarNode('dsn')
+ ->info(<<<'INFO'
+ The DSN to use. This is a required option.
+ The info is used to describe the DSN,
+ it can be multi-line.
+ INFO)
+ ->end()
->scalarNode('serializer')->defaultNull()->end()
->arrayNode('options')
->normalizeKeys(false)
diff --git a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php
index b9d8b48db3556..6a98166eccc94 100644
--- a/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php
+++ b/src/Symfony/Component/Config/Tests/Builder/Fixtures/NodeInitialValues/Symfony/Config/NodeInitialValues/Messenger/TransportsConfig.php
@@ -16,6 +16,9 @@ class TransportsConfig
private $_usedProperties = [];
/**
+ * The DSN to use. This is a required option.
+ * The info is used to describe the DSN,
+ * it can be multi-line.
* @default null
* @param ParamConfigurator|mixed $value
* @return $this
diff --git a/src/Symfony/Component/Console/Tests/SignalRegistry/SignalMapTest.php b/src/Symfony/Component/Console/Tests/SignalRegistry/SignalMapTest.php
index f4e320477d4be..3a0c49bb01e21 100644
--- a/src/Symfony/Component/Console/Tests/SignalRegistry/SignalMapTest.php
+++ b/src/Symfony/Component/Console/Tests/SignalRegistry/SignalMapTest.php
@@ -18,15 +18,13 @@ class SignalMapTest extends TestCase
{
/**
* @requires extension pcntl
- *
- * @testWith [2, "SIGINT"]
- * [9, "SIGKILL"]
- * [15, "SIGTERM"]
- * [31, "SIGSYS"]
*/
- public function testSignalExists(int $signal, string $expected)
+ public function testSignalExists()
{
- $this->assertSame($expected, SignalMap::getSignalName($signal));
+ $this->assertSame('SIGINT', SignalMap::getSignalName(\SIGINT));
+ $this->assertSame('SIGKILL', SignalMap::getSignalName(\SIGKILL));
+ $this->assertSame('SIGTERM', SignalMap::getSignalName(\SIGTERM));
+ $this->assertSame('SIGSYS', SignalMap::getSignalName(\SIGSYS));
}
public function testSignalDoesNotExist()
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php b/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php
index b6a2cf907ee9b..204401cd2c8ee 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/DefinitionErrorExceptionPass.php
@@ -65,7 +65,10 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed
}
if ($value instanceof Reference && $this->currentId !== $targetId = (string) $value) {
- if (ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()) {
+ if (
+ ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE === $value->getInvalidBehavior()
+ || ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE === $value->getInvalidBehavior()
+ ) {
$this->sourceReferences[$targetId][$this->currentId] ??= true;
} else {
$this->sourceReferences[$targetId][$this->currentId] = false;
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php
index 9ab5c27fcf763..5ed7be315114a 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionErrorExceptionPassTest.php
@@ -64,6 +64,9 @@ public function testSkipNestedErrors()
$container->register('foo', 'stdClass')
->addArgument(new Reference('bar', ContainerBuilder::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE));
+ $container->register('baz', 'stdClass')
+ ->addArgument(new Reference('bar', ContainerBuilder::IGNORE_ON_UNINITIALIZED_REFERENCE));
+
$pass = new DefinitionErrorExceptionPass();
$pass->process($container);
diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
index 032f194d2f542..2572a8abd6694 100644
--- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
+++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php
@@ -231,6 +231,10 @@ private function formatFile(string $file, int $line, ?string $text = null): stri
$text .= ' at line '.$line;
}
+ if (!file_exists($file)) {
+ return $text;
+ }
+
$link = $this->fileLinkFormat->format($file, $line);
return sprintf('%s', $this->escape($link), $text);
diff --git a/src/Symfony/Component/Form/Extension/Core/Type/WeekType.php b/src/Symfony/Component/Form/Extension/Core/Type/WeekType.php
index 8027a41a99cd8..778cc2aeb0b7b 100644
--- a/src/Symfony/Component/Form/Extension/Core/Type/WeekType.php
+++ b/src/Symfony/Component/Form/Extension/Core/Type/WeekType.php
@@ -42,7 +42,6 @@ public function buildForm(FormBuilderInterface $builder, array $options)
} else {
$yearOptions = $weekOptions = [
'error_bubbling' => true,
- 'empty_data' => '',
];
// when the form is compound the entries of the array are ignored in favor of children data
// so we need to handle the cascade setting here
diff --git a/src/Symfony/Component/Form/Tests/Fixtures/TestExtension.php b/src/Symfony/Component/Form/Tests/Fixtures/TestExtension.php
index 44725a69c71a5..2704ee5303ad2 100644
--- a/src/Symfony/Component/Form/Tests/Fixtures/TestExtension.php
+++ b/src/Symfony/Component/Form/Tests/Fixtures/TestExtension.php
@@ -34,7 +34,7 @@ public function addType(FormTypeInterface $type)
public function getType($name): FormTypeInterface
{
- return $this->types[$name] ?? null;
+ return $this->types[$name];
}
public function hasType($name): bool
diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
index 41a244b818836..c22f283cba444 100644
--- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
+++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
@@ -229,7 +229,7 @@ public function prepare(Request $request): static
$path = $location.substr($path, \strlen($pathPrefix));
// Only set X-Accel-Redirect header if a valid URI can be produced
// as nginx does not serve arbitrary file paths.
- $this->headers->set($type, $path);
+ $this->headers->set($type, rawurlencode($path));
$this->maxlen = 0;
break;
}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
index c7d47a4d70a35..8f298b77f7218 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
@@ -314,7 +314,15 @@ public function testXAccelMapping($realpath, $mapping, $virtual)
$property->setValue($response, $file);
$response->prepare($request);
- $this->assertEquals($virtual, $response->headers->get('X-Accel-Redirect'));
+ $header = $response->headers->get('X-Accel-Redirect');
+
+ if ($virtual) {
+ // Making sure the path doesn't contain characters unsupported by nginx
+ $this->assertMatchesRegularExpression('/^([^?%]|%[0-9A-F]{2})*$/', $header);
+ $header = rawurldecode($header);
+ }
+
+ $this->assertEquals($virtual, $header);
}
public function testDeleteFileAfterSend()
@@ -361,6 +369,7 @@ public static function getSampleXAccelMappings()
['/home/Foo/bar.txt', '/var/www/=/files/,/home/Foo/=/baz/', '/baz/bar.txt'],
['/home/Foo/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', '/baz/bar.txt'],
['/tmp/bar.txt', '"/var/www/"="/files/", "/home/Foo/"="/baz/"', null],
+ ['/var/www/var/www/files/foo%.txt', '/var/www/=/files/', '/files/var/www/files/foo%.txt'],
];
}
diff --git a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
index 7a4807ecf721e..f1aa0ebeab928 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
@@ -604,7 +604,6 @@ public function testGetUri()
$server['REDIRECT_QUERY_STRING'] = 'query=string';
$server['REDIRECT_URL'] = '/path/info';
- $server['SCRIPT_NAME'] = '/index.php';
$server['QUERY_STRING'] = 'query=string';
$server['REQUEST_URI'] = '/path/info?toto=test&1=1';
$server['SCRIPT_NAME'] = '/index.php';
@@ -731,7 +730,6 @@ public function testGetUriForPath()
$server['REDIRECT_QUERY_STRING'] = 'query=string';
$server['REDIRECT_URL'] = '/path/info';
- $server['SCRIPT_NAME'] = '/index.php';
$server['QUERY_STRING'] = 'query=string';
$server['REQUEST_URI'] = '/path/info?toto=test&1=1';
$server['SCRIPT_NAME'] = '/index.php';
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index 3ea14b47ed5e1..bd7589173013e 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '6.4.21';
- public const VERSION_ID = 60421;
+ public const VERSION = '6.4.22';
+ public const VERSION_ID = 60422;
public const MAJOR_VERSION = 6;
public const MINOR_VERSION = 4;
- public const RELEASE_VERSION = 21;
+ public const RELEASE_VERSION = 22;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '11/2026';
diff --git a/src/Symfony/Component/Intl/Tests/Transliterator/EmojiTransliteratorTest.php b/src/Symfony/Component/Intl/Tests/Transliterator/EmojiTransliteratorTest.php
index a01bb0d2f9b8e..38b218db7225b 100644
--- a/src/Symfony/Component/Intl/Tests/Transliterator/EmojiTransliteratorTest.php
+++ b/src/Symfony/Component/Intl/Tests/Transliterator/EmojiTransliteratorTest.php
@@ -189,6 +189,6 @@ public function testGetErrorMessageWithUninitializedTransliterator()
{
$transliterator = EmojiTransliterator::create('emoji-en');
- $this->assertFalse($transliterator->getErrorMessage());
+ $this->assertSame('', $transliterator->getErrorMessage());
}
}
diff --git a/src/Symfony/Component/Intl/Transliterator/EmojiTransliterator.php b/src/Symfony/Component/Intl/Transliterator/EmojiTransliterator.php
index 7b8391ca43e0d..b28f5441c8951 100644
--- a/src/Symfony/Component/Intl/Transliterator/EmojiTransliterator.php
+++ b/src/Symfony/Component/Intl/Transliterator/EmojiTransliterator.php
@@ -70,14 +70,22 @@ public function createInverse(): self
return self::create($this->id, self::REVERSE);
}
+ /**
+ * @return int
+ */
+ #[\ReturnTypeWillChange]
public function getErrorCode(): int|false
{
return isset($this->transliterator) ? $this->transliterator->getErrorCode() : 0;
}
+ /**
+ * @return string
+ */
+ #[\ReturnTypeWillChange]
public function getErrorMessage(): string|false
{
- return isset($this->transliterator) ? $this->transliterator->getErrorMessage() : false;
+ return isset($this->transliterator) ? $this->transliterator->getErrorMessage() : '';
}
public static function listIDs(): array
diff --git a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php
index 0ae1bff21d7c8..061bbcb47d124 100644
--- a/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php
+++ b/src/Symfony/Component/Messenger/Bridge/Amqp/Transport/Connection.php
@@ -31,6 +31,7 @@ class Connection
'x-max-length-bytes',
'x-max-priority',
'x-message-ttl',
+ 'x-delivery-limit',
];
/**
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
index 0e5bd147d3f61..f60e4e23ee3f9 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/ClickSendTransport.php
@@ -87,7 +87,7 @@ protected function doSend(MessageInterface $message): SentMessage
$response = $this->client->request('POST', $endpoint, [
'auth_basic' => [$this->apiUsername, $this->apiKey],
- 'json' => array_filter($options),
+ 'json' => ['messages' => [array_filter($options)]],
]);
try {
diff --git a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
index 166bb10d8a8dc..ba7923a7fa231 100644
--- a/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
+++ b/src/Symfony/Component/Notifier/Bridge/ClickSend/Tests/ClickSendTransportTest.php
@@ -63,10 +63,14 @@ public function testNoInvalidArgumentExceptionIsThrownIfFromIsValid(string $from
$response = $this->createMock(ResponseInterface::class);
$response->expects(self::exactly(2))->method('getStatusCode')->willReturn(200);
$response->expects(self::once())->method('getContent')->willReturn('');
- $client = new MockHttpClient(function (string $method, string $url) use ($response): ResponseInterface {
+ $client = new MockHttpClient(function (string $method, string $url, array $options) use ($response): ResponseInterface {
self::assertSame('POST', $method);
self::assertSame('https://rest.clicksend.com/v3/sms/send', $url);
+ $body = json_decode($options['body'], true);
+ self::assertIsArray($body);
+ self::assertArrayHasKey('messages', $body);
+
return $response;
});
$transport = $this->createTransport($client, $from);
diff --git a/src/Symfony/Component/Routing/Tests/Loader/ObjectLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/ObjectLoaderTest.php
index f3536f1fc56d8..18d8c919a2d73 100644
--- a/src/Symfony/Component/Routing/Tests/Loader/ObjectLoaderTest.php
+++ b/src/Symfony/Component/Routing/Tests/Loader/ObjectLoaderTest.php
@@ -104,7 +104,7 @@ public function supports(mixed $resource, ?string $type = null): bool
protected function getObject(string $id): object
{
- return $this->loaderMap[$id] ?? null;
+ return $this->loaderMap[$id];
}
}
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/application.php b/src/Symfony/Component/Runtime/Tests/phpt/application.php
index 1e1014e9f3e5a..ca2de555edfb7 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/application.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/application.php
@@ -18,8 +18,10 @@
return function (array $context) {
$command = new Command('go');
- $command->setCode(function (InputInterface $input, OutputInterface $output) use ($context) {
+ $command->setCode(function (InputInterface $input, OutputInterface $output) use ($context): int {
$output->write('OK Application '.$context['SOME_VAR']);
+
+ return 0;
});
$app = new Application();
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/command.php b/src/Symfony/Component/Runtime/Tests/phpt/command.php
index 3a5fa11e00000..e307d195b113e 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/command.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/command.php
@@ -19,7 +19,9 @@
return function (Command $command, InputInterface $input, OutputInterface $output, array $context) {
$command->addOption('hello', 'e', InputOption::VALUE_REQUIRED, 'How should I greet?', 'OK');
- return $command->setCode(function () use ($input, $output, $context) {
+ return $command->setCode(function () use ($input, $output, $context): int {
$output->write($input->getOption('hello').' Command '.$context['SOME_VAR']);
+
+ return 0;
});
};
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_debug_exists_0_to_1.php b/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_debug_exists_0_to_1.php
index af6409dda62bc..2968e37ea02f4 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_debug_exists_0_to_1.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_debug_exists_0_to_1.php
@@ -20,6 +20,8 @@
require __DIR__.'/autoload.php';
-return static fn (Command $command, OutputInterface $output, array $context): Command => $command->setCode(static function () use ($output, $context): void {
+return static fn (Command $command, OutputInterface $output, array $context): Command => $command->setCode(static function () use ($output, $context): int {
$output->writeln($context['DEBUG_ENABLED']);
+
+ return 0;
});
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_debug_exists_1_to_0.php b/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_debug_exists_1_to_0.php
index 78a0bf29448b8..1f2fa3590e16f 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_debug_exists_1_to_0.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_debug_exists_1_to_0.php
@@ -20,6 +20,8 @@
require __DIR__.'/autoload.php';
-return static fn (Command $command, OutputInterface $output, array $context): Command => $command->setCode(static function () use ($output, $context): void {
+return static fn (Command $command, OutputInterface $output, array $context): Command => $command->setCode(static function () use ($output, $context): int {
$output->writeln($context['DEBUG_MODE']);
+
+ return 0;
});
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_env_exists.php b/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_env_exists.php
index 3e72372e5af06..8587f20f2382b 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_env_exists.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_env_exists.php
@@ -20,6 +20,8 @@
require __DIR__.'/autoload.php';
-return static fn (Command $command, OutputInterface $output, array $context): Command => $command->setCode(static function () use ($output, $context): void {
+return static fn (Command $command, OutputInterface $output, array $context): Command => $command->setCode(static function () use ($output, $context): int {
$output->writeln($context['ENV_MODE']);
+
+ return 0;
});
diff --git a/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_no_debug.php b/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_no_debug.php
index 3fe4f44d7967b..4ab7694298f95 100644
--- a/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_no_debug.php
+++ b/src/Symfony/Component/Runtime/Tests/phpt/dotenv_overload_command_no_debug.php
@@ -19,6 +19,8 @@
require __DIR__.'/autoload.php';
-return static fn (Command $command, OutputInterface $output, array $context): Command => $command->setCode(static function () use ($output, $context): void {
+return static fn (Command $command, OutputInterface $output, array $context): Command => $command->setCode(static function () use ($output, $context): int {
$output->writeln($context['DEBUG_ENABLED']);
+
+ return 0;
});
diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.be.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.be.xlf
index 194392935fcc1..6478e2a15caf7 100644
--- a/src/Symfony/Component/Security/Core/Resources/translations/security.be.xlf
+++ b/src/Symfony/Component/Security/Core/Resources/translations/security.be.xlf
@@ -76,7 +76,7 @@
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
diff --git a/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php b/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php
index 176d316607506..02ca251106471 100644
--- a/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php
+++ b/src/Symfony/Component/Security/Http/LoginLink/LoginLinkHandler.php
@@ -86,9 +86,16 @@ public function consumeLoginLink(Request $request): UserInterface
if (!$hash = $request->get('hash')) {
throw new InvalidLoginLinkException('Missing "hash" parameter.');
}
+ if (!is_string($hash)) {
+ throw new InvalidLoginLinkException('Invalid "hash" parameter.');
+ }
+
if (!$expires = $request->get('expires')) {
throw new InvalidLoginLinkException('Missing "expires" parameter.');
}
+ if (preg_match('/^\d+$/', $expires) !== 1) {
+ throw new InvalidLoginLinkException('Invalid "expires" parameter.');
+ }
try {
$this->signatureHasher->acceptSignatureHash($userIdentifier, $expires, $hash);
diff --git a/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php b/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php
index 2293666ae7ecb..ad1d990fd74ff 100644
--- a/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php
+++ b/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php
@@ -160,7 +160,12 @@ public function clearRememberMeCookie(): void
return;
}
- $rememberMeDetails = RememberMeDetails::fromRawCookie($cookie);
+ try {
+ $rememberMeDetails = RememberMeDetails::fromRawCookie($cookie);
+ } catch (AuthenticationException) {
+ // malformed cookie should not fail the response and can be simply ignored
+ return;
+ }
[$series] = explode(':', $rememberMeDetails->getValue());
$this->tokenProvider->deleteTokenBySeries($series);
}
diff --git a/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php
index 98ff60d43992c..350ecde4290a0 100644
--- a/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php
@@ -240,6 +240,30 @@ public function testConsumeLoginLinkWithMissingExpiration()
$linker->consumeLoginLink($request);
}
+ public function testConsumeLoginLinkWithInvalidExpiration()
+ {
+ $user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash');
+ $this->userProvider->createUser($user);
+
+ $this->expectException(InvalidLoginLinkException::class);
+ $request = Request::create('/login/verify?user=weaverryan&hash=thehash&expires=%E2%80%AA1000000000%E2%80%AC');
+
+ $linker = $this->createLinker();
+ $linker->consumeLoginLink($request);
+ }
+
+ public function testConsumeLoginLinkWithInvalidHash()
+ {
+ $user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash');
+ $this->userProvider->createUser($user);
+
+ $this->expectException(InvalidLoginLinkException::class);
+ $request = Request::create('/login/verify?user=weaverryan&hash[]=an&hash[]=array&expires=1000000000');
+
+ $linker = $this->createLinker();
+ $linker->consumeLoginLink($request);
+ }
+
private function createSignatureHash(string $username, int $expires, array $extraFields = ['emailProperty' => 'ryan@symfonycasts.com', 'passwordProperty' => 'pwhash']): string
{
$hasher = new SignatureHasher($this->propertyAccessor, array_keys($extraFields), 's3cret');
diff --git a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php
index a5bdac65118d8..bd539341c3f6c 100644
--- a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php
@@ -74,6 +74,22 @@ public function testClearRememberMeCookie()
$this->assertNull($cookie->getValue());
}
+ public function testClearRememberMeCookieMalformedCookie()
+ {
+ $this->tokenProvider->expects($this->exactly(0))
+ ->method('deleteTokenBySeries');
+
+ $this->request->cookies->set('REMEMBERME', 'malformed');
+
+ $this->handler->clearRememberMeCookie();
+
+ $this->assertTrue($this->request->attributes->has(ResponseListener::COOKIE_ATTR_NAME));
+
+ /** @var Cookie $cookie */
+ $cookie = $this->request->attributes->get(ResponseListener::COOKIE_ATTR_NAME);
+ $this->assertNull($cookie->getValue());
+ }
+
public function testConsumeRememberMeCookieValid()
{
$this->tokenProvider->expects($this->any())
diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
index 8f9c3cff0fb3c..07043d946d751 100644
--- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
+++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php
@@ -65,6 +65,10 @@ public function encode(mixed $data, string $format, array $context = []): string
} elseif (empty($data)) {
$data = [[]];
} else {
+ if ($data instanceof \Traversable) {
+ // Generators can only be iterated once — convert to array to allow multiple traversals
+ $data = iterator_to_array($data);
+ }
// Sequential arrays of arrays are considered as collections
$i = 0;
foreach ($data as $key => $value) {
diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php
index c0be73a8bd685..cde6d333e99f0 100644
--- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php
@@ -710,4 +710,28 @@ public function testEndOfLinePassedInConstructor()
$encoder = new CsvEncoder([CsvEncoder::END_OF_LINE => "\r\n"]);
$this->assertSame("foo,bar\r\nhello,test\r\n", $encoder->encode($value, 'csv'));
}
+
+ /** @dataProvider provideIterable */
+ public function testIterable(mixed $data)
+ {
+ $this->assertEquals(<<<'CSV'
+ foo,bar
+ hello,"hey ho"
+ hi,"let's go"
+
+ CSV, $this->encoder->encode($data, 'csv'));
+ }
+
+ public static function provideIterable()
+ {
+ $data = [
+ ['foo' => 'hello', 'bar' => 'hey ho'],
+ ['foo' => 'hi', 'bar' => 'let\'s go'],
+ ];
+
+ yield 'array' => [$data];
+ yield 'array iterator' => [new \ArrayIterator($data)];
+ yield 'iterator aggregate' => [new \IteratorIterator(new \ArrayIterator($data))];
+ yield 'generator' => [(fn (): \Generator => yield from $data)()];
+ }
}
diff --git a/src/Symfony/Component/Translation/MessageCatalogue.php b/src/Symfony/Component/Translation/MessageCatalogue.php
index d56f04393f3e8..17418c9b02fdd 100644
--- a/src/Symfony/Component/Translation/MessageCatalogue.php
+++ b/src/Symfony/Component/Translation/MessageCatalogue.php
@@ -237,6 +237,16 @@ public function getMetadata(string $key = '', string $domain = 'messages'): mixe
return $this->metadata;
}
+ if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX])) {
+ if ('' === $key) {
+ return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX];
+ }
+
+ if (isset($this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key])) {
+ return $this->metadata[$domain.self::INTL_DOMAIN_SUFFIX][$key];
+ }
+ }
+
if (isset($this->metadata[$domain])) {
if ('' == $key) {
return $this->metadata[$domain];
diff --git a/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.php b/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.php
new file mode 100644
index 0000000000000..1ac61673999b2
--- /dev/null
+++ b/src/Symfony/Component/Translation/Tests/Catalogue/MessageCatalogueTest.php
@@ -0,0 +1,27 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Translation\Tests\Catalogue;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Translation\MessageCatalogue;
+
+class MessageCatalogueTest extends TestCase
+{
+ public function testIcuMetadataKept()
+ {
+ $mc = new MessageCatalogue('en', ['messages' => ['a' => 'new_a']]);
+ $metadata = ['metadata' => 'value'];
+ $mc->setMetadata('a', $metadata, 'messages+intl-icu');
+ $this->assertEquals($metadata, $mc->getMetadata('a', 'messages'));
+ $this->assertEquals($metadata, $mc->getMetadata('a', 'messages+intl-icu'));
+ }
+}
diff --git a/src/Symfony/Component/Validator/Constraints/Image.php b/src/Symfony/Component/Validator/Constraints/Image.php
index 43590f4f2425c..8fafa5044f201 100644
--- a/src/Symfony/Component/Validator/Constraints/Image.php
+++ b/src/Symfony/Component/Validator/Constraints/Image.php
@@ -64,7 +64,7 @@ class Image extends File
*/
protected static $errorNames = self::ERROR_NAMES;
- public $mimeTypes = 'image/*';
+ public $mimeTypes;
public $minWidth;
public $maxWidth;
public $maxHeight;
@@ -140,7 +140,9 @@ public function __construct(
?string $allowPortraitMessage = null,
?string $corruptedMessage = null,
?array $groups = null,
- mixed $payload = null
+ mixed $payload = null,
+ array|string|null $extensions = null,
+ ?string $extensionsMessage = null,
) {
parent::__construct(
$options,
@@ -163,7 +165,9 @@ public function __construct(
$uploadExtensionErrorMessage,
$uploadErrorMessage,
$groups,
- $payload
+ $payload,
+ $extensions,
+ $extensionsMessage,
);
$this->minWidth = $minWidth ?? $this->minWidth;
@@ -192,6 +196,10 @@ public function __construct(
$this->allowPortraitMessage = $allowPortraitMessage ?? $this->allowPortraitMessage;
$this->corruptedMessage = $corruptedMessage ?? $this->corruptedMessage;
+ if (null === $this->mimeTypes && [] === $this->extensions) {
+ $this->mimeTypes = 'image/*';
+ }
+
if (!\in_array('image/*', (array) $this->mimeTypes, true) && !\array_key_exists('mimeTypesMessage', $options ?? []) && null === $mimeTypesMessage) {
$this->mimeTypesMessage = 'The mime type of the file is invalid ({{ type }}). Allowed mime types are {{ types }}.';
}
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.af.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.af.xlf
index de23860799dc6..9f53b1afe35c3 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.af.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.af.xlf
@@ -466,13 +466,9 @@