8000
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 72ea7cfa3e9c0..f7b87a1ce8cd4 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,6 +1,6 @@
| Q | A
| ------------- | ---
-| Branch? | 6.3 for features / 5.4 or 6.2 for bug fixes
+| Branch? | 6.4 for features / 5.4, 6.2, or 6.3 for bug fixes
| Bug fix? | yes/no
| New feature? | yes/no
| Deprecations? | yes/no
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 69192939d066a..3757f2523e928 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -65,7 +65,7 @@ jobs:
echo COLUMNS=120 >> $GITHUB_ENV
echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration" >> $GITHUB_ENV
- echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.php }}" = "8.2" ]] && echo ' --ignore-platform-req=php+') >> $GITHUB_ENV
+ echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.mode }}" = experimental ]] && echo ' --ignore-platform-req=php+') >> $GITHUB_ENV
SYMFONY_VERSIONS=$(git ls-remote -q --heads | cut -f2 | grep -o '/[1-9][0-9]*\.[0-9].*' | sort -V)
SYMFONY_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | cut -d "'" -f2 | cut -d '.' -f 1-2)
diff --git a/CHANGELOG-5.4.md b/CHANGELOG-5.4.md
index 8241220390c11..dca25ff5b0d50 100644
--- a/CHANGELOG-5.4.md
+++ b/CHANGELOG-5.4.md
@@ -7,6 +7,34 @@ in 5.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/v5.4.0...v5.4.1
+* 5.4.24 (2023-05-27)
+
+ * bug #50429 [Console] block input stream if needed (joelwurtz)
+ * bug #50315 [Translation] Fix handling of null messages in `ArrayLoader` (rob006)
+ * bug #50338 [Console] Remove ``exec`` and replace it by ``shell_exec`` (maxbeckers)
+ * bug #50362 [FrameworkBundle] Fix Workflow without a marking store definition uses marking store definition of previously defined workflow (krciga22)
+ * bug #50309 [HttpFoundation] UrlHelper is now aware of RequestContext changes (giosh94mhz)
+ * bug #50309 [HttpFoundation] UrlHelper is now aware of RequestContext changes (giosh94mhz)
+ * bug #50354 [Process] Stop the process correctly even if underlying input stream is not closed (joelwurtz)
+ * bug #50332 [PropertyInfo] Fix `PhpStanExtractor` when constructor has no docblock (HypeMC)
+ * bug #50253 [FrameworkBundle] Generate caches consistently on successive run of `cache:clear` command (Okhoshi)
+ * bug #49063 [Messenger] Respect `isRetryable` decision of the retry strategy for re-delivery (FlyingDR)
+ * bug #50251 [Serializer] Handle datetime deserialization in U format (tugmaks)
+ * bug #50266 [HttpFoundation] Fix file streaming after connection aborted (rlshukhov)
+ * bug #50269 Fix param type annotation (l-vo)
+ * bug #50256 [HttpClient] Fix setting duplicate-name headers when redirecting with AmpHttpClient (nicolas-grekas)
+ * bug #50214 [WebProfilerBundle] Remove legacy filters remnants (MatTheCat)
+ * bug #50235 [HttpClient] Fix getting through proxies via CONNECT (nicolas-grekas)
+ * bug #50244 [HttpKernel] Fix restoring surrogate content from cache (nicolas-grekas)
+ * bug #50246 [DependencyInjection] Do not check errored definitions’ type (MatTheCat)
+ * bug #49557 [PropertyInfo] Fix phpDocExtractor nullable array value type (fabpot)
+ * bug #50213 [ErrorHandler] Prevent conflicts with WebProfilerBundle’s JavaScript (MatTheCat)
+ * bug #50192 [Serializer] backed enum throw notNormalizableValueException outside construct method (alli83)
+ * bug #50238 [HttpKernel] Don't use eval() to render ESI/SSI (nicolas-grekas)
+ * bug #50226 [HttpClient] Ensure HttplugClient ignores invalid HTTP headers (nicolas-grekas)
+ * bug #50203 [Messenger] Fix registering message handlers (nicolas-grekas)
+ * bug #50204 [ErrorHandler] Skip Httplug deprecations for HttplugClient (nicolas-grekas)
+
* 5.4.23 (2023-04-28)
* bug #50143 [Console] trim(): Argument #1 () must be of type string, bool given (danepowell)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 24afd64907140..cf5f960f7a101 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -21,9 +21,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Jordi Boggiano (seldaek)
- Roland Franssen (ro0)
- Victor Berchet (victor)
+ - Javier Eguiluz (javier.eguiluz)
- Yonel Ceruto (yonelceruto)
- Tobias Nyholm (tobias)
- - Javier Eguiluz (javier.eguiluz)
- Oskar Stark (oskarstark)
- Ryan Weaver (weaverryan)
- Johannes S (johannes)
@@ -40,8 +40,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Abdellatif Ait boudad (aitboudad)
- Jan Schädlich (jschaedl)
- Lukas Kahwe Smith (lsmith)
- - Jérôme Tamarelle (gromnan)
- Kevin Bond (kbond)
+ - Jérôme Tamarelle (gromnan)
- Martin Hasoň (hason)
- Jeremy Mikola (jmikola)
- Jean-François Simon (jfsimon)
@@ -52,21 +52,21 @@ The Symfony Connect username in parenthesis allows to get more information
- Valentin Udaltsov (vudaltsov)
- Vasilij Duško (staff)
- Matthias Pigulla (mpdude)
+ - Antoine Lamirault (alamirault)
- Gabriel Ostrolucký (gadelat)
- - Antoine Makdessi (amakdessi)
- Laurent VOULLEMIER (lvo)
+ - Antoine Makdessi (amakdessi)
- Pierre du Plessis (pierredup)
- - Antoine Lamirault (alamirault)
- Grégoire Paris (greg0ire)
- Jonathan Wage (jwage)
+ - Mathieu Lechat (mat_the_cat)
- Titouan Galopin (tgalopin)
- David Maicher (dmaicher)
+ - Alexander Schranz (alexander-schranz)
- Gábor Egyed (1ed)
- Mathieu Santostefano (welcomattic)
- - Alexander Schranz (alexander-schranz)
- Alexandre Salomé (alexandresalome)
- William DURAND
- - Mathieu Lechat (mat_the_cat)
- ornicar
- Dany Maillard (maidmaid)
- Eriksen Costa
@@ -76,10 +76,10 @@ The Symfony Connect username in parenthesis allows to get more information
- Francis Besset (francisbesset)
- Vasilij Dusko | CREATION
- Bulat Shakirzyanov (avalanche123)
+ - Vincent Langlet (deviling)
- Iltar van der Berg
- Miha Vrhovnik (mvrhov)
- Mathieu Piot (mpiot)
- - Vincent Langlet (deviling)
- Saša Stamenković (umpirsky)
- Alex Pott
- Guilhem N (guilhemn)
@@ -95,14 +95,14 @@ The Symfony Connect username in parenthesis allows to get more information
- Ruud Kamphuis (ruudk)
- Henrik Bjørnskov (henrikbjorn)
- David Buchmann (dbu)
+ - Massimiliano Arione (garak)
- Andrej Hudec (pulzarraider)
- Julien Falque (julienfalque)
- - Massimiliano Arione (garak)
- Jáchym Toušek (enumag)
- Douglas Greenshields (shieldo)
+ - Mathias Arlaud (mtarld)
- Christian Raue
- Fran Moreno (franmomu)
- - Mathias Arlaud (mtarld)
- Graham Campbell (graham)
- Michel Weimerskirch (mweimerskirch)
- Eric Clemmons (ericclemmons)
@@ -117,18 +117,18 @@ The Symfony Connect username in parenthesis allows to get more information
- Dariusz Górecki (canni)
- Maxime Helias (maxhelias)
- Ener-Getick
- - Sebastiaan Stok (sstok)
- Tugdual Saunier (tucksaun)
+ - Sebastiaan Stok (sstok)
- Jérôme Vasseur (jvasseur)
- Ion Bazan (ionbazan)
- Rokas Mikalkėnas (rokasm)
+ - Yanick Witschi (toflar)
- Lee McDermott
- Brandon Turner
- Luis Cordova (cordoval)
- Daniel Holmes (dholmes)
- Toni Uebernickel (havvg)
- Bart van den Burg (burgov)
- - Yanick Witschi (toflar)
- Jordan Alliot (jalliot)
- Smaine Milianni (ismail1432)
- John Wards (johnwards)
@@ -137,6 +137,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Antoine Hérault (herzult)
- Konstantin.Myakshin
- Arman Hosseini (arman)
+ - gnito-org
- Saif Eddin Gmati (azjezz)
- Simon Berger
- Arnaud Le Blanc (arnaud-lb)
@@ -152,6 +153,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Chris Wilkinson (thewilkybarkid)
- Brice BERNARD (brikou)
- Roman Martinuk (a2a4)
+ - Joel Wurtz (brouznouf)
- Gregor Harlan (gharlan)
- Baptiste Clavié (talus)
- Adrien Brault (adrienbrault)
@@ -168,7 +170,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Guillaume (guill)
- Christopher Hertel (chertel)
- Jacob Dreesen (jdreesen)
- - Joel Wurtz (brouznouf)
- Olivier Dolbeau (odolbeau)
- Florian Voutzinos (florianv)
- zairig imad (zairigimad)
@@ -180,6 +181,7 @@ The Symfony Connect username in parenthesis allows to get more information
- HeahDude
- Richard van Laak (rvanlaak)
- Paráda József (paradajozsef)
+ - Hubert Lenoir (hubert_lenoir)
- Alessandro Lai (jean85)
- Alexander Schwenn (xelaris)
- Fabien Pennequin (fabienpennequin)
@@ -203,7 +205,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Tigran Azatyan (tigranazatyan)
- Eric GELOEN (gelo)
- Matthieu Napoli (mnapoli)
- - Hubert Lenoir (hubert_lenoir)
- Tomáš Votruba (tomas_votruba)
- Joshua Thijssen
- Stefano Sala (stefano.sala)
@@ -295,7 +296,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Samuel NELA (snela)
- Romain Monteil (ker0x)
- dFayet
- - gnito-org
- Karoly Gossler (connorhu)
- Vincent AUBERT (vincent)
- Sebastien Morel (plopix)
@@ -304,7 +304,9 @@ The Symfony Connect username in parenthesis allows to get more information
- Timothée Barray (tyx)
- Sébastien Alfaiate (seb33300)
- James Halsall (jaitsu)
+ - Maximilian Beckers (maxbeckers)
- Mikael Pajunen
+ - Marcin Sikoń (marphi)
- Warnar Boekkooi (boekkooi)
- Marco Petersen (ocrampete16)
- Benjamin Leveque (benji07)
@@ -335,6 +337,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Urinbayev Shakhobiddin (shokhaa)
- Ahmed Raafat
- Philippe Segatori
+ - Allison Guilhem (a_guilhem)
- Thibaut Cheymol (tcheymol)
- Julien Pauli
- Islam Israfilov (islam93)
@@ -346,7 +349,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Ruben Gonzalez (rubenrua)
- Benjamin Dulau (dbenjamin)
- Pavel Kirpitsov (pavel-kirpichyov)
- - Maximilian Beckers (maxbeckers)
- Mathieu Lemoine (lemoinem)
- Christian Schmidt
- Andreas Hucks (meandmymonkey)
@@ -357,7 +359,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Clara van Miert
- Martin Auswöger
- Alexander Menshchikov
- - Marcin Sikoń (marphi)
- Stepan Anchugov (kix)
- bronze1man
- sun (sun)
@@ -375,11 +376,13 @@ The Symfony Connect username in parenthesis allows to get more information
- Kyle
- Dominique Bongiraud
- Hidde Wieringa (hiddewie)
+ - Dane Powell
- Christopher Davis (chrisguitarguy)
- Lukáš Holeczy (holicz)
- Michael Lee (zerustech)
- Florian Lonqueu-Brochard (florianlb)
- Leszek Prabucki (l3l0)
+ - Giorgio Premi
- Emanuele Panzeri (thepanz)
- Matthew Smeets
- François Zaninotto (fzaninotto)
@@ -415,14 +418,12 @@ The Symfony Connect username in parenthesis allows to get more information
- Mantis Development
- Pablo Lozano (arkadis)
- quentin neyrat (qneyrat)
- - Dane Powell
- Antonio Jose Cerezo (ajcerezo)
- Marcin Szepczynski (czepol)
- Lescot Edouard (idetox)
- Loïc Frémont (loic425)
- Rob Frawley 2nd (robfrawley)
- Mohammad Emran Hasan (phpfour)
- - Allison Guilhem (a_guilhem)
- Dmitriy Mamontov (mamontovdmitriy)
- Kévin THERAGE (kevin_therage)
- Nikita Konstantinov (unkind)
@@ -430,7 +431,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Francois Zaninotto
- Laurent Masforné (heisenberg)
- Claude Khedhiri (ck-developer)
- - Giorgio Premi
- Daniel Tschinder
- Christian Schmidt
- Alexander Kotynia (olden)
@@ -576,6 +576,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Grégoire Passault (gregwar)
- Jerzy Zawadzki (jzawadzki)
- Ismael Ambrosi (iambrosi)
+ - Samaël Villette (samadu61)
- Saif Eddin G
- Emmanuel BORGES (eborges78)
- siganushka (siganushka)
@@ -691,6 +692,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Ruben Jacobs (rubenj)
- Arkadius Stefanski (arkadius)
- Jérémy M (th3mouk)
+ - Tristan Pouliquen
- Terje Bråten
- Pierre Rineau
- Renan Gonçalves (renan_saddam)
@@ -720,6 +722,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Eric COURTIAL
- Xesxen
- ShinDarth
+ - Phil E. Taylor (philetaylor)
- Arun Philip
- Stéphane PY (steph_py)
- Philipp Kräutli (pkraeutli)
@@ -752,10 +755,10 @@ The Symfony Connect username in parenthesis allows to get more information
- Hassan Amouhzi
- Antonin CLAUZIER (0x346e3730)
- Andrei C. (moldman)
- - Samaël Villette (samadu61)
- Tamas Szijarto
- stlrnz
- Adrien Wilmet (adrienfr)
+ - Mathieu Rochette (mathroc)
- Alex Bacart
- hugovms
- Michele Locati
@@ -898,6 +901,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jonas Flodén (flojon)
- Adrien Lucas (adrienlucas)
- Dominik Zogg
+ - Quentin Devos
- Kai Dederichs
- Luc Vieillescazes (iamluc)
- Thomas Nunninger
@@ -956,7 +960,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Gigino Chianese (sajito)
- Xav` (xavismeh)
- Remi Collet
- - Mathieu Rochette (mathroc)
- Vicent Soria Durá (vicentgodella)
- Michael Moravec
- Anthony Ferrara
@@ -1026,6 +1029,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Loïc Faugeron
- Aurélien Fredouelle
- Pavel Campr (pcampr)
+ - Markus Staab
- Forfarle (forfarle)
- Johnny Robeson (johnny)
- Kai Eichinger (kai_eichinger)
@@ -1267,7 +1271,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Szijarto Tamas
- Arend Hummeling
- Makdessi Alex
- - Phil E. Taylor (philetaylor)
- Juan Miguel Besada Vidal (soutlink)
- dlorek
- Stuart Fyfe
@@ -1295,7 +1298,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Simon Schick (simonsimcity)
- Victor Macko (victor_m)
- Tristan Roussel
- - Quentin Devos
- Jorge Vahldick (jvahldick)
- Vladimir Mantulo (mantulo)
- aim8604
@@ -1303,6 +1305,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Maciej Zgadzaj
- David Legatt (dlegatt)
- Maarten de Boer (mdeboer)
+ - Alexandre parent
- Cameron Porter
- Hossein Bukhamsin
- Oliver Hoff
@@ -1449,7 +1452,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Michael Olšavský
- Benny Born
- Emirald Mateli
- - Tristan Pouliquen
- Jose Gonzalez
- Claudio Zizza
- Ivo Valchev
@@ -1841,7 +1843,6 @@ The Symfony Connect username in parenthesis allows to get more information
- vladyslavstartsev
- Kévin
- Marc Abramowitz
- - Markus Staab
- michal
- Martijn Evers
- Sjoerd Adema
@@ -2220,11 +2221,13 @@ The Symfony Connect username in parenthesis allows to get more information
- Andrew Tch
- Alexander Cheprasov
- Rodrigo Díez Villamuera (rodrigodiez)
+ - Brad Treloar
- Stephen Clouse
- e-ivanov
- Abderrahman DAIF (death_maker)
- Yann Rabiller (einenlum)
- Jochen Bayer (jocl)
+ - Constantine Shtompel
- VAN DER PUTTE Guillaume (guillaume_vdp)
- Patrick Carlo-Hickman
- Bruno MATEU
@@ -2235,6 +2238,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Viacheslav Sychov
- Nicolas Sauveur (baishu)
- Helmut Hummel (helhum)
+ - Andrew Neil Forster (krciga22)
- Matt Brunt
- Carlos Ortega Huetos
- Péter Buri (burci)
@@ -2285,7 +2289,6 @@ The Symfony Connect username in parenthesis allows to get more information
- John Espiritu (johnillo)
- Oxan van Leeuwen
- pkowalczyk
- - Alexandre parent
- Soner Sayakci
- Max Voloshin (maxvoloshin)
- Nicolas Fabre (nfabre)
@@ -2400,6 +2403,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Claudiu Cristea
- Zacharias Luiten
- Sebastian Utz
+ - Oliver Hader
- Adrien Gallou (agallou)
- Maks Rafalko (bornfree)
- Conrad Kleinespel (conradk)
@@ -2417,6 +2421,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Cédric Lahouste (rapotor)
- Samuel Vogel (samuelvogel)
- Berat Doğan
+ - Christian Kolb
- Guillaume LECERF
- Juanmi Rodriguez Cerón
- twifty
@@ -2437,6 +2442,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Eric Stern
- ShiraNai7
- Antal Áron (antalaron)
+ - Alexander Grimalovsky (flying)
+ - Ivan Pepelko (pepelko)
- Vašek Purchart (vasek-purchart)
- Janusz Jabłoński (yanoosh)
- Fleuv
@@ -2581,6 +2588,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Nicolas Schwartz (nicoschwartz)
- Tim Jabs (rubinum)
- Stéphane Seng (stephaneseng)
+ - Robert Korulczyk
- Jonathan Gough
- Benoit Leveque
- Benjamin Bender
@@ -2652,6 +2660,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Martin Schophaus (m_schophaus_adcada)
- Martynas Sudintas (martiis)
- Anton Sukhachev (mrsuh)
+ - Vitaliy Zhuk (zhukv)
- Marcel Siegert
- ryunosuke
- Roy de Vos Burchart
@@ -2707,6 +2716,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jovan Perovic (jperovic)
- Pablo Maria Martelletti (pmartelletti)
- Sander van der Vlugt (stranding)
+ - Maxim Tugaev (tugmaks)
- Florian Bogey
- Waqas Ahmed
- Bert Hekman
@@ -3141,6 +3151,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Antoine LA
- Vyacheslav Slinko
- Benjamin Laugueux
+ - Lane Shukhov
- Jakub Chábek
- William Pinaud (DocFX)
- Johannes
@@ -3244,6 +3255,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Ahmed Abdulrahman
- dinitrol
- Penny Leach
+ - Kevin Mian Kraiker
- Yurii K
- Richard Trebichavský
- g123456789l
@@ -3282,6 +3294,7 @@ The Symfony Connect username in parenthesis allows to get more information
- ADmad
- Nicolas Roudaire
- Abdouni Karim (abdounikarim)
+ - Adrian Günter (adrianguenter)
- Andreas Forsblom (aforsblo)
- Alex Olmos (alexolmos)
- Cedric BERTOLINI (alsciende)
diff --git a/composer.json b/composer.json
index d5e8a209d3be0..469c30715f775 100644
--- a/composer.json
+++ b/composer.json
@@ -136,13 +136,14 @@
"nyholm/psr7": "^1.0",
"pda/pheanstalk": "^4.0",
"php-http/httplug": "^1.0|^2.0",
+ "php-http/message-factory": "^1.0",
"phpstan/phpdoc-parser": "^1.0",
"predis/predis": "~1.1",
"psr/http-client": "^1.0",
"psr/simple-cache": "^1.0|^2.0",
"egulias/email-validator": "^2.1.10|^3.1|^4",
"symfony/mercure-bundle": "^0.3",
- "symfony/phpunit-bridge": "^5.2|^6.0",
+ "symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
"symfony/runtime": "self.version",
"symfony/security-acl": "~2.8|~3.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
@@ -160,10 +161,11 @@
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"ocramius/proxy-manager": "<2.1",
- "phpunit/phpunit": "<5.4.3"
+ "phpunit/phpunit": "<7.5|9.1.2"
},
"config": {
"allow-plugins": {
+ "php-http/discovery": false,
"symfony/runtime": true
}
},
diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php
index 71da329a996cd..eb835caa41b25 100644
--- a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php
+++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php
@@ -46,8 +46,8 @@ public function stop(): void
}
/**
- * @param string|int $param
- * @param string|int|float|bool|null $variable
+ * @param string|int $param
+ * @param mixed $variable
*/
public function setParam($param, &$variable, int $type): void
{
diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json
index a0dbbe9636adb..3aa2e352401ca 100644
--- a/src/Symfony/Bridge/Doctrine/composer.json
+++ b/src/Symfony/Bridge/Doctrine/composer.json
@@ -54,7 +54,6 @@
"doctrine/dbal": "<2.13.1",
"doctrine/lexer": "<1.1",
"doctrine/orm": "<2.7.4",
- "phpunit/phpunit": "<5.4.3",
"symfony/cache": "<5.4",
"symfony/dependency-injection": "<4.4",
"symfony/form": "<5.4.21|>=6,<6.2.7",
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php
index b0d5565398d2d..053810757a0d8 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php
@@ -137,14 +137,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($output->isVerbose()) {
$io->comment('Warming up optional cache...');
}
- $warmer = $kernel->getContainer()->get('cache_warmer');
- // non optional warmers already ran during container compilation
- $warmer->enableOnlyOptionalWarmers();
- $preload = (array) $warmer->warmUp($realCacheDir);
-
- if ($preload && file_exists($preloadFile = $realCacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) {
- Preloader::append($preloadFile, $preload);
- }
+ $this->warmupOptionals($realCacheDir);
}
} else {
$fs->mkdir($warmupDir);
@@ -153,7 +146,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($output->isVerbose()) {
$io->comment('Warming up cache...');
}
- $this->warmup($warmupDir, $realCacheDir, !$input->getOption('no-optional-warmers'));
+ $this->warmup($warmupDir, $realBuildDir);
+
+ if (!$input->getOption('no-optional-warmers')) {
+ if ($output->isVerbose()) {
+ $io->comment('Warming up optional cache...');
+ }
+ $this->warmupOptionals($realCacheDir);
+ }
}
if (!$fs->exists($warmupDir.'/'.$containerDir)) {
@@ -227,7 +227,7 @@ private function isNfs(string $dir): bool
return false;
}
- private function warmup(string $warmupDir, string $realBuildDir, bool $enableOptionalWarmers = true)
+ private function warmup(string $warmupDir, string $realBuildDir): void
{
// create a temporary kernel
$kernel = $this->getApplication()->getKernel();
@@ -236,18 +236,6 @@ private function warmup(string $warmupDir, string $realBuildDir, bool $enableOpt
}
$kernel->reboot($warmupDir);
- // warmup temporary dir
- if ($enableOptionalWarmers) {
- $warmer = $kernel->getContainer()->get('cache_warmer');
- // non optional warmers already ran during container compilation
- $warmer->enableOnlyOptionalWarmers();
- $preload = (array) $warmer->warmUp($warmupDir);
-
- if ($preload && file_exists($preloadFile = $warmupDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) {
- Preloader::append($preloadFile, $preload);
- }
- }
-
// fix references to cached files with the real cache directory name
$search = [$warmupDir, str_replace('\\', '\\\\', $warmupDir)];
$replace = str_replace('\\', '/', $realBuildDir);
@@ -258,4 +246,17 @@ private function warmup(string $warmupDir, string $realBuildDir, bool $enableOpt
}
}
}
+
+ private function warmupOptionals(string $realCacheDir): void
+ {
+ $kernel = $this->getApplication()->getKernel();
+ $warmer = $kernel->getContainer()->get('cache_warmer');
+ // non optional warmers already ran during container compilation
+ $warmer->enableOnlyOptionalWarmers();
+ $preload = (array) $warmer->warmUp($realCacheDir);
+
+ if ($preload && file_exists($preloadFile = $realCacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) {
+ Preloader::append($preloadFile, $preload);
+ }
+ }
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 10d479a11c699..82b798b18e698 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -920,6 +920,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
$workflows[$workflowId] = $definitionDefinition;
// Create MarkingStore
+ $markingStoreDefinition = null;
if (isset($workflow['marking_store']['type'])) {
$markingStoreDefinition = new ChildDefinition('workflow.marking_store.method');
$markingStoreDefinition->setArguments([
@@ -933,7 +934,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $
// Create Workflow
$workflowDefinition = new ChildDefinition(sprintf('%s.abstract', $type));
$workflowDefinition->replaceArgument(0, new Reference(sprintf('%s.definition', $workflowId)));
- $workflowDefinition->replaceArgument(1, $markingStoreDefinition ?? null);
+ $workflowDefinition->replaceArgument(1, $markingStoreDefinition);
$workflowDefinition->replaceArgument(3, $name);
$workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']);
$workflowDefinition->addTag('container.private', [
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php
index a26dfb5adc612..cc2810fc6dbb4 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php
@@ -110,7 +110,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : []
->set('url_helper', UrlHelper::class)
->args([
service('request_stack'),
- service('router.request_context')->ignoreOnInvalid(),
+ service('router')->ignoreOnInvalid(),
])
->alias(UrlHelper::class, 'url_helper')
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
index c763e2bd211b4..871b62e8811da 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
@@ -85,6 +85,62 @@ public function testWorkflowValidationStateMachine()
});
}
+ public function testWorkflowDefaultMarkingStoreDefinition()
+ {
+ $container = $this->createContainerFromClosure(function ($container) {
+ $container->loadFromExtension('framework', [
+ 'workflows' => [
+ 'workflow_a' => [
+ 'type' => 'state_machine',
+ 'marking_store' => [
+ 'type' => 'method',
+ 'property' => 'status',
+ ],
+ 'supports' => [
+ __CLASS__,
+ ],
+ 'places' => [
+ 'a',
+ 'b',
+ ],
+ 'transitions' => [
+ 'a_to_b' => [
+ 'from' => ['a'],
+ 'to' => ['b'],
+ ],
+ ],
+ ],
+ 'workflow_b' => [
+ 'type' => 'state_machine',
+ 'supports' => [
+ __CLASS__,
+ ],
+ 'places' => [
+ 'a',
+ 'b',
+ ],
+ 'transitions' => [
+ 'a_to_b' => [
+ 'from' => ['a'],
+ 'to' => ['b'],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ });
+
+ $workflowA = $container->getDefinition('state_machine.workflow_a');
+ $argumentsA = $workflowA->getArguments();
+ $this->assertArrayHasKey('index_1', $argumentsA, 'workflow_a has a marking_store argument');
+ $this->assertNotNull($argumentsA['index_1'], 'workflow_a marking_store argument is not null');
+
+ $workflowB = $container->getDefinition('state_machine.workflow_b');
+ $argumentsB = $workflowB->getArguments();
+ $this->assertArrayHasKey('index_1', $argumentsB, 'workflow_b has a marking_store argument');
+ $this->assertNull($argumentsB['index_1'], 'workflow_b marking_store argument is null');
+ }
+
public function testRateLimiterWithLockFactory()
{
try {
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 089e2e0827fea..320b993b1f8d1 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -24,7 +24,7 @@
"symfony/deprecation-contracts": "^2.1|^3",
"symfony/event-dispatcher": "^5.1|^6.0",
"symfony/error-handler": "^4.4.1|^5.0.1|^6.0",
- "symfony/http-foundation": "^5.3|^6.0",
+ "symfony/http-foundation": "^5.4.24|^6.2.11",
"symfony/http-kernel": "^5.4|^6.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "^1.16",
@@ -74,7 +74,6 @@
"doctrine/persistence": "<1.3",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
- "phpunit/phpunit": "<5.4.3",
"symfony/asset": "<5.3",
"symfony/console": "<5.2.5",
"symfony/dotenv": "<5.1",
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig
index a8a5c273656b5..f48c9fce7c4df 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig
@@ -155,22 +155,20 @@
-
-
{% endblock messages %}
{% endif %}
{% endblock %}
{% macro render_table(messages, is_fallback) %}
-
+
- Locale |
+ Locale |
{% if is_fallback %}
Fallback locale |
{% endif %}
- Domain |
+ Domain |
Times used |
Message ID |
Message Preview |
@@ -178,7 +176,7 @@
{% for message in messages %}
-
+
{{ message.locale }} |
{% if is_fallback %}
{{ message.fallbackLocale|default('-') }} |
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/
8000
profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig
index 24ec0863d6117..2c59150b5a345 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig
@@ -963,42 +963,6 @@ tr.status-warning td {
display: block;
}
-{# Filters
- ========================================================================= #}
-[data-filters] { position: relative; }
-[data-filtered] { cursor: pointer; }
-[data-filtered]:after { content: '\00a0\25BE'; }
-[data-filtered]:hover .filter-list li { display: inline-flex; }
-[class*="filter-hidden-"] { display: none; }
-.filter-list { position: absolute; border: var(--border); box-shadow: var(--shadow); margin: 0; padding: 0; display: flex; flex-direction: column; }
-.filter-list :after { content: ''; }
-.filter-list li {
- background: var(--tab-disabled-background);
- border-bottom: var(--border);
- color: var(--tab-disabled-color);
- display: none;
- list-style: none;
- margin: 0;
- padding: 5px 10px;
- text-align: left;
- font-weight: normal;
-}
-.filter-list li.active {
- background: var(--tab-background);
- color: var(--tab-color);
-}
-.filter-list li.last-active {
- background: var(--tab-active-background);
- color: var(--tab-active-color);
-}
-
-.filter-list-level li { cursor: s-resize; }
-.filter-list-level li.active { cursor: n-resize; }
-.filter-list-level li.last-active { cursor: default; }
-.filter-list-level li.last-active:before { content: '\2714\00a0'; }
-.filter-list-choice li:before { content: '\2714\00a0'; color: transparent; }
-.filter-list-choice li.active:before { color: unset; }
-
{# Twig panel
========================================================================= #}
#twig-dump pre {
diff --git a/src/Symfony/Component/Console/Command/CompleteCommand.php b/src/Symfony/Component/Console/Command/CompleteCommand.php
index 11ada4e4489b3..0e35143c3335d 100644
--- a/src/Symfony/Component/Console/Command/CompleteCommand.php
+++ b/src/Symfony/Component/Console/Command/CompleteCommand.php
@@ -155,10 +155,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
throw $e;
}
- return self::FAILURE;
+ return 2;
}
- return self::SUCCESS;
+ return 0;
}
private function createCompletionInput(InputInterface $input): CompletionInput
diff --git a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php
index 6f809e2f139a1..eaf22be1a9ad4 100644
--- a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php
+++ b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php
@@ -85,7 +85,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($input->getOption('debug')) {
$this->tailDebugLog($commandName, $output);
- return self::SUCCESS;
+ return 0;
}
$shell = $input->getArgument('shell') ?? self::guessShell();
@@ -102,12 +102,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$output->writeln(sprintf('Shell not detected, Symfony shell completion only supports "%s").>', implode('", "', $supportedShells)));
}
- return self::INVALID;
+ return 2;
}
$output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile)));
- return self::SUCCESS;
+ return 0;
}
private static function guessShell(): string
diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php
index 10602038c299f..e236be92a3913 100644
--- a/src/Symfony/Component/Console/Helper/QuestionHelper.php
+++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php
@@ -128,7 +128,18 @@ private function doAsk(OutputInterface $output, Question $question)
}
if (false === $ret) {
+ $isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true;
+
+ if (!$isBlocked) {
+ stream_set_blocking($inputStream, true);
+ }
+
$ret = $this->readInput($inputStream, $question);
+
+ if (!$isBlocked) {
+ stream_set_blocking($inputStream, false);
+ }
+
if (false === $ret) {
throw new MissingInputException('Aborted.');
}
@@ -500,13 +511,11 @@ private function isInteractiveInput($inputStream): bool
return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r'));
}
- if (!\function_exists('exec')) {
+ if (!\function_exists('shell_exec')) {
return self::$stdinIsInteractive = true;
}
- exec('stty 2> /dev/null', $output, $status);
-
- return self::$stdinIsInteractive = 1 !== $status;
+ return self::$stdinIsInteractive = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'));
}
/**
diff --git a/src/Symfony/Component/Console/Input/InputArgument.php b/src/Symfony/Component/Console/Input/InputArgument.php
index ecfcdad58b907..8a64f7ac8a8e9 100644
--- a/src/Symfony/Component/Console/Input/InputArgument.php
+++ b/src/Symfony/Component/Console/Input/InputArgument.php
@@ -32,7 +32,7 @@ class InputArgument
/**
* @param string $name The argument name
- * @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL
+ * @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY
* @param string $description A description text
* @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only)
*
diff --git a/src/Symfony/Component/Console/Terminal.php b/src/Symfony/Component/Console/Terminal.php
index 04653d3bd2bbc..b91e8afc5cac4 100644
--- a/src/Symfony/Component/Console/Terminal.php
+++ b/src/Symfony/Component/Console/Terminal.php
@@ -64,14 +64,12 @@ public static function hasSttyAvailable(): bool
return self::$stty;
}
- // skip check if exec function is disabled
- if (!\function_exists('exec')) {
+ // skip check if shell_exec function is disabled
+ if (!\function_exists('shell_exec')) {
return false;
}
- exec('stty 2>&1', $output, $exitcode);
-
- return self::$stty = 0 === $exitcode;
+ return self::$stty = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null'));
}
private static function initDimensions()
diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php
index 9761e8f979345..74315d8982638 100644
--- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php
+++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php
@@ -430,7 +430,7 @@ public function testAskHiddenResponse()
$this->assertEquals('8AM', $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("8AM\n")), $this->createOutputInterface(), $question));
}
- public function testAskHiddenResponseTrimmed()
+ public function testAskHiddenResponseNotTrimmed()
{
if ('\\' === \DIRECTORY_SEPARATOR) {
$this->markTestSkipped('This test is not supported on Windows');
@@ -442,7 +442,7 @@ public function testAskHiddenResponseTrimmed()
$question->setHidden(true);
$question->setTrimmable(false);
- $this->assertEquals(' 8AM', $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream(' 8AM')), $this->createOutputInterface(), $question));
+ $this->assertEquals(' 8AM'.\PHP_EOL, $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream(' 8AM'.\PHP_EOL)), $this->createOutputInterface(), $question));
}
public function testAskMultilineResponseWithEOF()
diff --git a/src/Symfony/Component/Console/Tests/TerminalTest.php b/src/Symfony/Component/Console/Tests/TerminalTest.php
index c2d36fe0ab899..34d32b08ebecb 100644
--- a/src/Symfony/Component/Console/Tests/TerminalTest.php
+++ b/src/Symfony/Component/Console/Tests/TerminalTest.php
@@ -77,8 +77,8 @@ public function testSttyOnWindows()
$this->markTestSkipped('Must be on windows');
}
- $sttyString = exec('(stty -a | grep columns) 2>&1', $output, $exitcode);
- if (0 !== $exitcode) {
+ $sttyString = shell_exec('(stty -a | grep columns) 2> NUL');
+ if (!$sttyString) {
$this->markTestSkipped('Must have stty support');
}
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php
index b7ec85cefb489..59e39067e777e 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php
@@ -210,7 +210,7 @@ private function checkType(Definition $checkedDefinition, $value, \ReflectionPar
$class = null;
if ($value instanceof Definition) {
- if ($value->getFactory()) {
+ if ($value->hasErrors() || $value->getFactory()) {
return;
}
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php
index 97e167c99cb10..e3c87ef13b74f 100644
--- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php
+++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php
@@ -23,6 +23,7 @@
use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar;
+use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarErroredDependency;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgument;
use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgumentNotNull;
@@ -1010,6 +1011,20 @@ public function testIgnoreDefinitionFactoryArgument()
$this->addToAssertionCount(1);
}
+
+ public function testErroredDefinitionsAreNotChecked()
+ {
+ $container = new ContainerBuilder();
+ $container->register('errored_dependency', BarErroredDependency::class)
+ ->setArguments([
+ (new Definition(Foo::class))
+ ->addError('error'),
+ ]);
+
+ (new CheckTypeDeclarationsPass(true))->process($container);
+
+ $this->addToAssertionCount(1);
+ }
}
class CallableClass
diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php
new file mode 100644
index 0000000000000..d1368c3f7ef44
--- /dev/null
+++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php
@@ -0,0 +1,10 @@
+checkClass($use);
}
if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class])
- && !(HttplugClient::class === $class && \in_array($use, [\Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], true))
+ && !(HttplugClient::class === $class && \in_array($use, [\Http\Client\HttpClient::class, \Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], true))
) {
$type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait');
$verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses');
diff --git a/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js b/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js
index a85409da3cc89..95b8ea17197c9 100644
--- a/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js
+++ b/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js
@@ -1,297 +1,285 @@
/* This file is based on WebProfilerBundle/Resources/views/Profiler/base_js.html.twig.
If you make any change in this file, verify the same change is needed in the other file. */
/* .tab');
+ var tabNavigation = document.createElement('ul');
+ tabNavigation.className = 'tab-navigation';
+
+ var selectedTabId = 'tab-' + i + '-0'; /* select the first tab by default */
+ for (var j = 0; j < tabs.length; j++) {
+ var tabId = 'tab-' + i + '-' + j;
+ var tabTitle = tabs[j].querySelector('.tab-title').innerHTML;
+
+ var tabNavigationItem = document.createElement('li');
+ tabNavigationItem.setAttribute('data-tab-id', tabId);
+ if (hasClass(tabs[j], 'active')) { selectedTabId = tabId; }
+ if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); }
+ tabNavigationItem.innerHTML = tabTitle;
+ tabNavigation.appendChild(tabNavigationItem);
+
+ var tabContent = tabs[j].querySelector('.tab-content');
+ tabContent.parentElement.setAttribute('id', tabId);
+ }
- if (navigator.clipboard) {
- document.querySelectorAll('[data-clipboard-text]').forEach(function(element) {
- removeClass(element, 'hidden');
- element.addEventListener('click', function() {
- navigator.clipboard.writeText(element.getAttribute('data-clipboard-text'));
- })
- });
+ tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild);
+ addClass(document.querySelector('[data-tab-id="' + selectedTabId + '"]'), 'active');
}
- return {
- addEventListener: addEventListener,
+ /* display the active tab and add the 'click' event listeners */
+ for (i = 0; i < tabGroups.length; i++) {
+ tabNavigation = tabGroups[i].querySelectorAll(':scope >.tab-navigation li');
- createTabs: function() {
- var tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])');
+ for (j = 0; j < tabNavigation.length; j++) {
+ tabId = tabNavigation[j].getAttribute('data-tab-id');
+ document.getElementById(tabId).querySelector('.tab-title').className = 'hidden';
- /* create the tab navigation for each group of tabs */
- for (var i = 0; i < tabGroups.length; i++) {
- var tabs = tabGroups[i].querySelectorAll(':scope > .tab');
- var tabNavigation = document.createElement('ul');
- tabNavigation.className = 'tab-navigation';
-
- var selectedTabId = 'tab-' + i + '-0'; /* select the first tab by default */
- for (var j = 0; j < tabs.length; j++) {
- var tabId = 'tab-' + i + '-' + j;
- var tabTitle = tabs[j].querySelector('.tab-title').innerHTML;
+ if (hasClass(tabNavigation[j], 'active')) {
+ document.getElementById(tabId).className = 'block';
+ } else {
+ document.getElementById(tabId).className = 'hidden';
+ }
- var tabNavigationItem = document.createElement('li');
- tabNavigationItem.setAttribute('data-tab-id', tabId);
- if (hasClass(tabs[j], 'active')) { selectedTabId = tabId; }
- if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); }
- tabNavigationItem.innerHTML = tabTitle;
- tabNavigation.appendChild(tabNavigationItem);
+ tabNavigation[j].addEventListener('click', function(e) {
+ var activeTab = e.target || e.srcElement;
- var tabContent = tabs[j].querySelector('.tab-content');
- tabContent.parentElement.setAttribute('id', tabId);
+ /* needed because when the tab contains HTML contents, user can click */
+ /* on any of those elements instead of their parent '' element */
+ while (activeTab.tagName.toLowerCase() !== 'li') {
+ activeTab = activeTab.parentNode;
}
- tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild);
- addClass(document.querySelector('[data-tab-id="' + selectedTabId + '"]'), 'active');
- }
+ /* get the full list of tabs through the parent of the active tab element */
+ var tabNavigation = activeTab.parentNode.children;
+ for (var k = 0; k < tabNavigation.length; k++) {
+ var tabId = tabNavigation[k].getAttribute('data-tab-id');
+ document.getElementById(tabId).className = 'hidden';
+ removeClass(tabNavigation[k], 'active');
+ }
- /* display the active tab and add the 'click' event listeners */
- for (i = 0; i < tabGroups.length; i++) {
- tabNavigation = tabGroups[i].querySelectorAll(':scope >.tab-navigation li');
+ addClass(activeTab, 'active');
+ var activeTabId = activeTab.getAttribute('data-tab-id');
+ document.getElementById(activeTabId).className = 'block';
+ });
+ }
- for (j = 0; j < tabNavigation.length; j++) {
- tabId = tabNavigation[j].getAttribute('data-tab-id');
- document.getElementById(tabId).querySelector('.tab-title').className = 'hidden';
+ tabGroups[i].setAttribute('data-processed', 'true');
+ }
+ })();
- if (hasClass(tabNavigation[j], 'active')) {
- document.getElementById(tabId).className = 'block';
- } else {
- document.getElementById(tabId).className = 'hidden';
- }
+ (function createToggles() {
+ var toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])');
- tabNavigation[j].addEventListener('click', function(e) {
- var activeTab = e.target || e.srcElement;
+ for (var i = 0; i < toggles.length; i++) {
+ var elementSelector = toggles[i].getAttribute('data-toggle-selector');
+ var element = document.querySelector(elementSelector);
- /* needed because when the tab contains HTML contents, user can click */
- /* on any of those elements instead of their parent '' element */
- while (activeTab.tagName.toLowerCase() !== 'li') {
- activeTab = activeTab.parentNode;
- }
+ addClass(element, 'sf-toggle-content');
- /* get the full list of tabs through the parent of the active tab element */
- var tabNavigation = activeTab.parentNode.children;
- for (var k = 0; k < tabNavigation.length; k++) {
- var tabId = tabNavigation[k].getAttribute('data-tab-id');
- document.getElementById(tabId).className = 'hidden';
- removeClass(tabNavigation[k], 'active');
- }
+ if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') {
+ addClass(toggles[i], 'sf-toggle-on');
+ addClass(element, 'sf-toggle-visible');
+ } else {
+ addClass(toggles[i], 'sf-toggle-off');
+ addClass(element, 'sf-toggle-hidden');
+ }
- addClass(activeTab, 'active');
- var activeTabId = activeTab.getAttribute('data-tab-id');
- document.getElementById(activeTabId).className = 'block';
- });
- }
+ addEventListener(toggles[i], 'click', function(e) {
+ e.preventDefault();
- tabGroups[i].setAttribute('data-processed', 'true');
+ if ('' !== window.getSelection().toString()) {
+ /* Don't do anything on text selection */
+ return;
}
- },
-
- createToggles: function() {
- var toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])');
-
- for (var i = 0; i < toggles.length; i++) {
- var elementSelector = toggles[i].getAttribute('data-toggle-selector');
- var element = document.querySelector(elementSelector);
-
- addClass(element, 'sf-toggle-content');
-
- if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') {
- addClass(toggles[i], 'sf-toggle-on');
- addClass(element, 'sf-toggle-visible');
- } else {
- addClass(toggles[i], 'sf-toggle-off');
- addClass(element, 'sf-toggle-hidden');
- }
-
- addEventListener(toggles[i], 'click', function(e) {
- e.preventDefault();
- if ('' !== window.getSelection().toString()) {
- /* Don't do anything on text selection */
- return;
- }
+ var toggle = e.target || e.srcElement;
- var toggle = e.target || e.srcElement;
+ /* needed because when the toggle contains HTML contents, user can click */
+ /* on any of those elements instead of their parent '.sf-toggle' element */
+ while (!hasClass(toggle, 'sf-toggle')) {
+ toggle = toggle.parentNode;
+ }
- /* needed because when the toggle contains HTML contents, user can click */
- /* on any of those elements instead of their parent '.sf-toggle' element */
- while (!hasClass(toggle, 'sf-toggle')) {
- toggle = toggle.parentNode;
- }
+ var element = document.querySelector(toggle.getAttribute('data-toggle-selector'));
- var element = document.querySelector(toggle.getAttribute('data-toggle-selector'));
+ toggleClass(toggle, 'sf-toggle-on');
+ toggleClass(toggle, 'sf-toggle-off');
+ toggleClass(element, 'sf-toggle-hidden');
+ toggleClass(element, 'sf-toggle-visible');
- toggleClass(toggle, 'sf-toggle-on');
- toggleClass(toggle, 'sf-toggle-off');
- toggleClass(element, 'sf-toggle-hidden');
- toggleClass(element, 'sf-toggle-visible');
+ /* the toggle doesn't change its contents when clicking on it */
+ if (!toggle.hasAttribute('data-toggle-alt-content')) {
+ return;
+ }
- /* the toggle doesn't change its contents when clicking on it */
- if (!toggle.hasAttribute('data-toggle-alt-content')) {
- return;
- }
+ if (!toggle.hasAttribute('data-toggle-original-content')) {
+ toggle.setAttribute('data-toggle-original-content', toggle.innerHTML);
+ }
- if (!toggle.hasAttribute('data-toggle-original-content')) {
- toggle.setAttribute('data-toggle-original-content', toggle.innerHTML);
- }
+ var currentContent = toggle.innerHTML;
+ var originalContent = toggle.getAttribute('data-toggle-original-content');
+ var altContent = toggle.getAttribute('data-toggle-alt-content');
+ toggle.innerHTML = currentContent !== altContent ? altContent : originalContent;
+ });
- var currentContent = toggle.innerHTML;
- var originalContent = toggle.getAttribute('data-toggle-original-content');
- var altContent = toggle.getAttribute('data-toggle-alt-content');
- toggle.innerHTML = currentContent !== altContent ? altContent : originalContent;
- });
+ /* Prevents from disallowing clicks on links inside toggles */
+ var toggleLinks = toggles[i].querySelectorAll('a');
+ for (var j = 0; j < toggleLinks.length; j++) {
+ addEventListener(toggleLinks[j], 'click', function(e) {
+ e.stopPropagation();
+ });
+ }
- /* Prevents from disallowing clicks on links inside toggles */
- var toggleLinks = toggles[i].querySelectorAll('a');
- for (var j = 0; j < toggleLinks.length; j++) {
- addEventListener(toggleLinks[j], 'click', function(e) {
- e.stopPropagation();
- });
- }
+ /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */
+ var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]');
+ for (var k = 0; k < copyToClipboardElements.length; k++) {
+ addEventListener(copyToClipboardElements[k], 'click', function(e) {
+ e.stopPropagation();
+ });
+ }
- /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */
- var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]');
- for (var k = 0; k < copyToClipboardElements.length; k++) {
- addEventListener(copyToClipboardElements[k], 'click', function(e) {
- e.stopPropagation();
- });
- }
+ toggles[i].setAttribute('data-processed', 'true');
+ }
+ })();
- toggles[i].setAttribute('data-processed', 'true');
+ (function createFilters() {
+ document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) {
+ var filters = filter.closest('[data-filters]'),
+ type = 'choice',
+ name = filter.dataset.filter,
+ ucName = name.charAt(0).toUpperCase()+name.slice(1),
+ list = document.createElement('ul'),
8000
+ values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'),
+ labels = {},
+ defaults = null,
+ indexed = {},
+ processed = {};
+ if (typeof values === 'string') {
+ type = 'level';
+ labels = values.split(',');
+ values = values.toLowerCase().split(',');
+ defaults = values.length - 1;
+ }
+ addClass(list, 'filter-list');
+ addClass(list, 'filter-list-'+type);
+ values.forEach(function (value, i) {
+ if (value instanceof HTMLElement) {
+ value = value.dataset['filter'+ucName];
}
- },
-
- createFilters: function() {
- document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) {
- var filters = filter.closest('[data-filters]'),
- type = 'choice',
- name = filter.dataset.filter,
- ucName = name.charAt(0).toUpperCase()+name.slice(1),
- list = document.createElement('ul'),
- values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'),
- labels = {},
- defaults = null,
- indexed = {},
- processed = {};
- if (typeof values === 'string') {
- type = 'level';
- labels = values.split(',');
- values = values.toLowerCase().split(',');
- defaults = values.length - 1;
- }
- addClass(list, 'filter-list');
- addClass(list, 'filter-list-'+type);
- values.forEach(function (value, i) {
- if (value instanceof HTMLElement) {
- value = value.dataset['filter'+ucName];
- }
- if (value in processed) {
+ if (value in processed) {
+ return;
+ }
+ var option = document.createElement('li'),
+ label = i in labels ? labels[i] : value,
+ active = false,
+ matches;
+ if ('' === label) {
+ option.innerHTML = '(none)';
+ } else {
+ option.innerText = label;
+ }
+ option.dataset.filter = value;
+ option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows');
+ indexed[value] = i;
+ list.appendChild(option);
+ addEventListener(option, 'click', function () {
+ if ('choice' === type) {
+ filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) {
+ if (option.dataset.filter === row.dataset['filter'+ucName]) {
+ toggleClass(row, 'filter-hidden-'+name);
+ }
+ });
+ toggleClass(option, 'active');
+ } else if ('level' === type) {
+ if (i === this.parentNode.querySelectorAll('.active').length - 1) {
return;
}
- var option = document.createElement('li'),
- label = i in labels ? labels[i] : value,
- active = false,
- matches;
- if ('' === label) {
- option.innerHTML = '(none)';
- } else {
- option.innerText = label;
- }
- option.dataset.filter = value;
- option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows');
- indexed[value] = i;
- list.appendChild(option);
- addEventListener(option, 'click', function () {
- if ('choice' === type) {
- filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) {
- if (option.dataset.filter === row.dataset['filter'+ucName]) {
- toggleClass(row, 'filter-hidden-'+name);
- }
- });
- toggleClass(option, 'active');
- } else if ('level' === type) {
- if (i === this.parentNode.querySelectorAll('.active').length - 1) {
- return;
+ this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) {
+ if (j <= i) {
+ addClass(currentOption, 'active');
+ if (i === j) {
+ addClass(currentOption, 'last-active');
+ } else {
+ removeClass(currentOption, 'last-active');
}
- this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) {
- if (j <= i) {
- addClass(currentOption, 'active');
- if (i === j) {
- addClass(currentOption, 'last-active');
- } else {
- removeClass(currentOption, 'last-active');
- }
- } else {
- removeClass(currentOption, 'active');
- removeClass(currentOption, 'last-active');
- }
- });
- filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) {
- if (i < indexed[row.dataset['filter'+ucName]]) {
- addClass(row, 'filter-hidden-'+name);
- } else {
- removeClass(row, 'filter-hidden-'+name);
- }
- });
+ } else {
+ removeClass(currentOption, 'active');
+ removeClass(currentOption, 'last-active');
}
});
- if ('choice' === type) {
- active = null === defaults || 0 <= defaults.indexOf(value);
- } else if ('level' === type) {
- active = i <= defaults;
- if (active && i === defaults) {
- addClass(option, 'last-active');
+ filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) {
+ if (i < indexed[row.dataset['filter'+ucName]]) {
+ addClass(row, 'filter-hidden-'+name);
+ } else {
+ removeClass(row, 'filter-hidden-'+name);
}
- }
- if (active) {
- addClass(option, 'active');
- } else {
- filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) {
- toggleClass(row, 'filter-hidden-'+name);
- });
- }
- processed[value] = true;
- });
-
- if (1 < list.childNodes.length) {
- filter.appendChild(list);
- filter.dataset.filtered = '';
+ });
}
});
+ if ('choice' === type) {
+ active = null === defaults || 0 <= defaults.indexOf(value);
+ } else if ('level' === type) {
+ active = i <= defaults;
+ if (active && i === defaults) {
+ addClass(option, 'last-active');
+ }
+ }
+ if (active) {
+ addClass(option, 'active');
+ } else {
+ filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) {
+ toggleClass(row, 'filter-hidden-'+name);
+ });
+ }
+ processed[value] = true;
+ });
+
+ if (1 < list.childNodes.length) {
+ filter.appendChild(list);
+ filter.dataset.filtered = '';
}
- };
+ });
})();
-
- Sfjs.addEventListener(document, 'DOMContentLoaded', function() {
- Sfjs.createTabs();
- Sfjs.createToggles();
- Sfjs.createFilters();
- });
-}
+})();
/*]]>*/
diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json
index 6b02d77e29dff..39babd350174e 100644
--- a/src/Symfony/Component/Form/composer.json
+++ b/src/Symfony/Component/Form/composer.json
@@ -44,7 +44,6 @@
"symfony/uid": "^5.1|^6.0"
},
"conflict": {
- "phpunit/phpunit": "<5.4.3",
"symfony/console": "<4.4",
"symfony/dependency-injection": "<4.4",
"symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7",
diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php
index 491ea9e4033b7..2d9eec30f1238 100644
--- a/src/Symfony/Component/HttpClient/HttplugClient.php
+++ b/src/Symfony/Component/HttpClient/HttplugClient.php
@@ -47,7 +47,7 @@
}
if (!interface_exists(RequestFactory::class)) {
- throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require nyholm/psr7".');
+ throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory".');
}
/**
diff --git a/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php b/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php
index 9f5658f560fbc..c61be22e34405 100644
--- a/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php
+++ b/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php
@@ -120,7 +120,11 @@ public function createPsr7Response(ResponseInterface $response, bool $buffer = f
foreach ($response->getHeaders(false) as $name => $values) {
foreach ($values as $value) {
- $psrResponse = $psrResponse->withAddedHeader($name, $value);
+ try {
+ $psrResponse = $psrResponse->withAddedHeader($name, $value);
+ } catch (\InvalidArgumentException $e) {
+ // ignore invalid header
+ }
}
}
diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponse.php b/src/Symfony/Component/HttpClient/Response/AmpResponse.php
index 6d0ce6e33e01f..900c70d6e0e3a 100644
--- a/src/Symfony/Component/HttpClient/Response/AmpResponse.php
+++ b/src/Symfony/Component/HttpClient/Response/AmpResponse.php
@@ -47,7 +47,6 @@ final class AmpResponse implements ResponseInterface, StreamableInterface
private $multi;
private $options;
- private $canceller;
private $onProgress;
private static $delay;
@@ -73,7 +72,7 @@ public function __construct(AmpClientState $multi, Request $request, array $opti
$info = &$this->info;
$headers = &$this->headers;
- $canceller = $this->canceller = new CancellationTokenSource();
+ $canceller = new CancellationTokenSource();
$handle = &$this->handle;
$info['url'] = (string) $request->getUri();
@@ -358,7 +357,7 @@ private static function followRedirects(Request $originRequest, AmpClientState $
}
foreach ($originRequest->getRawHeaders() as [$name, $value]) {
- $request->setHeader($name, $value);
+ $request->addHeader($name, $value);
}
if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) {
diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php
index 7cfad581af4c4..2418203060c82 100644
--- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php
+++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php
@@ -76,17 +76,7 @@ public function __construct(CurlClientState $multi, $ch, array $options = null,
}
curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int {
- if (0 !== substr_compare($data, "\r\n", -2)) {
- return 0;
- }
-
- $len = 0;
-
- foreach (explode("\r\n", substr($data, 0, -2)) as $data) {
- $len += 2 + self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
- }
-
- return $len;
+ return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
});
if (null === $options) {
@@ -381,19 +371,29 @@ private static function select(ClientState $multi, float $timeout): int
*/
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int
{
+ if (!str_ends_with($data, "\r\n")) {
+ return 0;
+ }
+
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if ('H' !== $waitFor[0]) {
return \strlen($data); // Ignore HTTP trailers
}
- if ('' !== $data) {
+ $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE);
+
+ if ($statusCode !== $info['http_code'] && !preg_match("#^HTTP/\d+(?:\.\d+)? {$statusCode}(?: |\r\n$)#", $data)) {
+ return \strlen($data); // Ignore headers from responses to CONNECT requests
+ }
+
+ if ("\r\n" !== $data) {
// Regular header line: add it to the list
- self::addResponseHeaders([$data], $info, $headers);
+ self::addResponseHeaders([substr($data, 0, -2)], $info, $headers);
if (!str_starts_with($data, 'HTTP/')) {
if (0 === stripos($data, 'Location:')) {
- $location = trim(substr($data, 9));
+ $location = trim(substr($data, 9, -2));
}
return \strlen($data);
@@ -416,7 +416,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array &
// End of headers: handle informational responses, redirects, etc.
- if (200 > $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE)) {
+ if (200 > $statusCode) {
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
$location = null;
diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php
index 1f48be5c574c2..ba8fcbe3d68eb 100644
--- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php
@@ -267,4 +267,22 @@ function (\Exception $exception) use ($errorMessage, &$failureCallableCalled, $c
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('OK', (string) $response->getBody());
}
+
+ public function testInvalidHeaderResponse()
+ {
+ $responseHeaders = [
+ // space in header name not allowed in RFC 7230
+ ' X-XSS-Protection' => '0',
+ 'Cache-Control' => 'no-cache',
+ ];
+ $response = new MockResponse('body', ['response_headers' => $responseHeaders]);
+ $this->assertArrayHasKey(' x-xss-protection', $response->getHeaders());
+
+ $client = new HttplugClient(new MockHttpClient($response));
+ $request = $client->createRequest('POST', 'http://localhost:8057/post')
+ ->withBody($client->createStream('foo=0123456789'));
+
+ $resultResponse = $client->sendRequest($request);
+ $this->assertCount(1, $resultResponse->getHeaders());
+ }
}
diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json
index 57d31c12dfd8c..7f546b3a23981 100644
--- a/src/Symfony/Component/HttpClient/composer.json
+++ b/src/Symfony/Component/HttpClient/composer.json
@@ -38,6 +38,7 @@
"guzzlehttp/promises": "^1.4",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
+ "php-http/message-factory": "^1.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^4.4|^5.0|^6.0",
"symfony/http-kernel": "^4.4.13|^5.1.5|^6.0",
diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
index 03cf1a23ee3d8..d3caa36aa08ff 100644
--- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
+++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
@@ -349,7 +349,7 @@ public function sendContent()
while ('' !== $data) {
$read = fwrite($out, $data);
if (false === $read || connection_aborted()) {
- break;
+ break 2;
}
if (0 < $length) {
$length -= $read;
diff --git a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php
index 2057dd7097cc8..080b5ffea3482 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php
@@ -16,6 +16,7 @@
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\UrlHelper;
use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\RequestContextAwareInterface;
class UrlHelperTest extends TestCase
{
@@ -64,11 +65,46 @@ public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host
}
$requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path);
+
$helper = new UrlHelper(new RequestStack(), $requestContext);
$this->assertEquals($expected, $helper->getAbsoluteUrl($path));
}
+ /**
+ * @dataProvider getGenerateAbsoluteUrlRequestContextData
+ */
+ public function testGenerateAbsoluteUrlWithRequestContextAwareInterface($path, $baseUrl, $host, $scheme, $httpPort, $httpsPort, $expected)
+ {
+ if (!class_exists(RequestContext::class)) {
+ $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.');
+ }
+
+ $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path);
+ $contextAware = new class($requestContext) implements RequestContextAwareInterface {
+ private $requestContext;
+
+ public function __construct($requestContext)
+ {
+ $this->requestContext = $requestContext;
+ }
+
+ public function setContext(RequestContext $context)
+ {
+ $this->requestContext = $context;
+ }
+
+ public function getContext()
+ {
+ return $this->requestContext;
+ }
+ };
+
+ $helper = new UrlHelper(new RequestStack(), $contextAware);
+
+ $this->assertEquals($expected, $helper->getAbsoluteUrl($path));
+ }
+
/**
* @dataProvider getGenerateAbsoluteUrlRequestContextData
*/
diff --git a/src/Symfony/Component/HttpFoundation/UrlHelper.php b/src/Symfony/Component/HttpFoundation/UrlHelper.php
index c15f101cdf80b..90659947dba6d 100644
--- a/src/Symfony/Component/HttpFoundation/UrlHelper.php
+++ b/src/Symfony/Component/HttpFoundation/UrlHelper.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation;
use Symfony\Component\Routing\RequestContext;
+use Symfony\Component\Routing\RequestContextAwareInterface;
/**
* A helper service for manipulating URLs within and outside the request scope.
@@ -23,8 +24,15 @@ final class UrlHelper
private $requestStack;
private $requestContext;
- public function __construct(RequestStack $requestStack, RequestContext $requestContext = null)
+ /**
+ * @param RequestContextAwareInterface|RequestContext|null $requestContext
+ */
+ public function __construct(RequestStack $requestStack, $requestContext = null)
{
+ if (null !== $requestContext && !$requestContext instanceof RequestContext && !$requestContext instanceof RequestContextAwareInterface) {
+ throw new \TypeError(__METHOD__.': Argument #2 ($requestContext) must of type Symfony\Component\Routing\RequestContextAwareInterface|Symfony\Component\Routing\RequestContext|null, '.get_debug_type($requestContext).' given.');
+ }
+
$this->requestStack = $requestStack;
$this->requestContext = $requestContext;
}
@@ -73,28 +81,36 @@ public function getRelativePath(string $path): string
private function getAbsoluteUrlFromContext(string $path): string
{
- if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) {
+ if (null === $context = $this->requestContext) {
+ return $path;
+ }
+
+ if ($context instanceof RequestContextAwareInterface) {
+ $context = $context->getContext();
+ }
+
+ if ('' === $host = $context->getHost()) {
return $path;
}
- $scheme = $this->requestContext->getScheme();
+ $scheme = $context->getScheme();
$port = '';
- if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) {
- $port = ':'.$this->requestContext->getHttpPort();
- } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) {
- $port = ':'.$this->requestContext->getHttpsPort();
+ if ('http' === $scheme && 80 !== $context->getHttpPort()) {
+ $port = ':'.$context->getHttpPort();
+ } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) {
+ $port = ':'.$context->getHttpsPort();
}
if ('#' === $path[0]) {
- $queryString = $this->requestContext->getQueryString();
- $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path;
+ $queryString = $context->getQueryString();
+ $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path;
} elseif ('?' === $path[0]) {
- $path = $this->requestContext->getPathInfo().$path;
+ $path = $context->getPathInfo().$path;
}
if ('/' !== $path[0]) {
- $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path;
+ $path = rtrim($context->getBaseUrl(), '/').'/'.$path;
}
return $scheme.'://'.$host.$port.$path;
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php
index f2d809e8de97d..e1d73dc74827d 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php
@@ -133,4 +133,15 @@ protected function removeFromControl(Response $response)
$response->headers->set('Surrogate-Control', preg_replace(sprintf('#content="%s/1.0",\s*#', $upperName), '', $value));
}
}
+
+ protected static function generateBodyEvalBoundary(): string
+ {
+ static $cookie;
+ $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true);
+ $boundary = base64_encode($cookie);
+
+ \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === \strlen($boundary));
+
+ return $boundary;
+ }
}
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
index cd6a00a10d61f..9f453249325b2 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php
@@ -80,8 +80,8 @@ public function process(Request $request, Response $response)
$content = preg_replace('#.*?#s', '', $content);
$content = preg_replace('#]+>#s', '', $content);
+ $boundary = self::generateBodyEvalBoundary();
$chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
- $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]);
$i = 1;
while (isset($chunks[$i])) {
@@ -95,16 +95,10 @@ public function process(Request $request, Response $response)
throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.');
}
- $chunks[$i] = sprintf('surrogate->handle($this, %s, %s, %s) ?>'."\n",
- var_export($options['src'], true),
- var_export($options['alt'] ?? '', true),
- isset($options['onerror']) && 'continue' === $options['onerror'] ? 'true' : 'false'
- );
- ++$i;
- $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]);
- ++$i;
+ $chunks[$i] = $boundary.$options['src']."\n".($options['alt'] ?? '')."\n".('continue' === ($options['onerror'] ?? ''))."\n";
+ $i += 2;
}
- $content = implode('', $chunks);
+ $content = $boundary.implode('', $chunks).$boundary;
$response->setContent($content);
$response->headers->set('X-Body-Eval', 'ESI');
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
index 5688fc0c13ccd..b01bd722607a9 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
@@ -29,6 +29,8 @@
*/
class HttpCache implements HttpKernelInterface, TerminableInterface
{
+ public const BODY_EVAL_BOUNDARY_LENGTH = 24;
+
private $kernel;
private $store;
private $request;
@@ -631,12 +633,22 @@ protected function store(Request $request, Response $response)
private function restoreResponseBody(Request $request, Response $response)
{
if ($response->headers->has('X-Body-Eval')) {
+ \assert(self::BODY_EVAL_BOUNDARY_LENGTH === 24);
+
ob_start();
- if ($response->headers->has('X-Body-File')) {
- include $response->headers->get('X-Body-File');
- } else {
- eval('; ?>'.$response->getContent().'getContent();
+ $boundary = substr($content, 0, 24);
+ $j = strpos($content, $boundary, 24);
+ echo substr($content, 24, $j - 24);
+ $i = $j + 24;
+
+ while (false !== $j = strpos($content, $boundary, $i)) {
+ [$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4);
+ $i = $j + 24;
+
+ echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors);
+ echo $part;
}
$response->setContent(ob_get_clean());
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php
index cf8682257ee8f..5f372c566dd44 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php
@@ -147,7 +147,7 @@ public function update(Response $response)
if (is_numeric($this->ageDirectives['expires'])) {
$date = clone $response->getDate();
- $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds');
+ $date = $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds');
$response->setExpires($date);
}
}
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php
index f114e05cfb2f6..61909100e6157 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php
@@ -64,9 +64,8 @@ public function process(Request $request, Response $response)
// we don't use a proper XML parser here as we can have SSI tags in a plain text response
$content = $response->getContent();
-
+ $boundary = self::generateBodyEvalBoundary();
$chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
- $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]);
$i = 1;
while (isset($chunks[$i])) {
@@ -80,14 +79,10 @@ public function process(Request $request, Response $response)
throw new \RuntimeException('Unable to process an SSI tag without a "virtual" attribute.');
}
- $chunks[$i] = sprintf('surrogate->handle($this, %s, \'\', false) ?>'."\n",
- var_export($options['virtual'], true)
- );
- ++$i;
- $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]);
- ++$i;
+ $chunks[$i] = $boundary.$options['virtual']."\n\n\n";
+ $i += 2;
}
- $content = implode('', $chunks);
+ $content = $boundary.implode('', $chunks).$boundary;
$response->setContent($content);
$response->headers->set('X-Body-Eval', 'SSI');
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php
index 5db94f73d68c2..9d7f3e4f6949d 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php
@@ -475,15 +475,25 @@ private function persistResponse(Response $response): array
/**
* Restores a Response from the HTTP headers and body.
*/
- private function restoreResponse(array $headers, string $path = null): Response
+ private function restoreResponse(array $headers, string $path = null): ?Response
{
$status = $headers['X-Status'][0];
unset($headers['X-Status']);
+ $content = null;
if (null !== $path) {
$headers['X-Body-File'] = [$path];
+ unset($headers['x-body-file']);
+
+ if ($headers['X-Body-Eval'] ?? $headers['x-body-eval'] ?? false) {
+ $content = file_get_contents($path);
+ \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === 24);
+ if (48 > \strlen($content) || substr($content, -24) !== substr($content, 0, 24)) {
+ return null;
+ }
+
8000
}
}
- return new Response($path, $status, $headers);
+ return new Response($content, $status, $headers);
}
}
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index 7006a45cf2f49..f44eac93104ea 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -78,11 +78,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static $freshCache = [];
- public const VERSION = '5.4.23';
- public const VERSION_ID = 50423;
+ public const VERSION = '5.4.24';
+ public const VERSION_ID = 50424;
public const MAJOR_VERSION = 5;
public const MINOR_VERSION = 4;
- public const RELEASE_VERSION = 23;
+ public const RELEASE_VERSION = 24;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '11/2024';
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php
index 290bd94bdcb97..e876f28189087 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php
@@ -102,7 +102,7 @@ public function testMultilineEsiRemoveTagsAreRemoved()
$response = new Response(' Keep this'." And this");
$this->assertSame($response, $esi->process($request, $response));
- $this->assertEquals(' Keep this And this', $response->getContent());
+ $this->assertEquals(' Keep this And this', substr($response->getContent(), 24, -24));
}
public function testCommentTagsAreRemoved()
@@ -113,7 +113,7 @@ public function testCommentTagsAreRemoved()
$response = new Response(' Keep this');
$this->assertSame($response, $esi->process($request, $response));
- $this->assertEquals(' Keep this', $response->getContent());
+ $this->assertEquals(' Keep this', substr($response->getContent(), 24, -24));
}
public function testProcess()
@@ -124,23 +124,27 @@ public function testProcess()
$response = new Response('foo ');
$this->assertSame($response, $esi->process($request, $response));
- $this->assertEquals('foo surrogate->handle($this, \'...\', \'alt\', true) ?>'."\n", $response->getContent());
+ $content = explode(substr($response->getContent(), 0, 24), $response->getContent());
+ $this->assertSame(['', 'foo ', "...\nalt\n1\n", ''], $content);
$this->assertEquals('ESI', $response->headers->get('x-body-eval'));
$response = new Response('foo ');
$this->assertSame($response, $esi->process($request, $response));
- $this->assertEquals('foo surrogate->handle($this, \'foo\\\'\', \'bar\\\'\', true) ?>'."\n", $response->getContent());
+ $content = explode(substr($response->getContent(), 0, 24), $response->getContent());
+ $this->assertSame(['', 'foo ', "foo'\nbar'\n1\n", ''], $content);
$response = new Response('foo ');
$this->assertSame($response, $esi->process($request, $response));
- $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent());
+ $content = explode(substr($response->getContent(), 0, 24), $response->getContent());
+ $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content);
$response = new Response('foo ');
$this->assertSame($response, $esi->process($request, $response));
- $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent());
+ $content = explode(substr($response->getContent(), 0, 24), $response->getContent());
+ $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content);
}
public function testProcessEscapesPhpTags()
@@ -151,7 +155,8 @@ public function testProcessEscapesPhpTags()
$response = new Response('');
$this->assertSame($response, $esi->process($request, $response));
- $this->assertEquals('php cript language=php>', $response->getContent());
+ $content = explode(substr($response->getContent(), 0, 24), $response->getContent());
+ $this->assertSame(['', '', ''], $content);
}
public function testProcessWhenNoSrcInAnEsi()
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
index e47631d1780ea..c8b48ff811c76 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php
@@ -18,7 +18,7 @@
use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpKernelInterface;
-class HttpCacheTestCase extends TestCase
+abstract class HttpCacheTestCase extends TestCase
{
protected $kernel;
protected $cache;
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php
index c5c510f85832e..ce9f5ba1a158a 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php
@@ -257,7 +257,7 @@ public function testCacheControlMerging(array $expects, array $master, array $su
case 'expires':
$expires = clone $response->getDate();
- $expires->modify('+'.$value.' seconds');
+ $expires = $expires->modify('+'.$value.' seconds');
$response->setExpires($expires);
break;
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php
index a1f1f1593d3f3..97cc8fccd03d0 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php
@@ -101,13 +101,15 @@ public function testProcess()
$response = new Response('foo ');
$ssi->process($request, $response);
- $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent());
+ $content = explode(substr($response->getContent(), 0, 24), $response->getContent());
+ $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content);
$this->assertEquals('SSI', $response->headers->get('x-body-eval'));
$response = new Response('foo ');
$ssi->process($request, $response);
- $this->assertEquals("foo surrogate->handle(\$this, 'foo\\'', '', false) ?>\n", $response->getContent());
+ $content = explode(substr($response->getContent(), 0, 24), $response->getContent());
+ $this->assertSame(['', 'foo ', "foo'\n\n\n", ''], $content);
}
public function testProcessEscapesPhpTags()
@@ -118,7 +120,8 @@ public function testProcessEscapesPhpTags()
$response = new Response('');
$ssi->process($request, $response);
- $this->assertEquals('php cript language=php>', $response->getContent());
+ $content = explode(substr($response->getContent(), 0, 24), $response->getContent());
+ $this->assertSame(['', '', ''], $content);
}
public function testProcessWhenNoSrcInAnSsi()
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
index 239361bc8c337..aff5329cc96f8 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
@@ -200,7 +200,7 @@ public function testRestoresResponseContentFromEntityStoreWithLookup()
{
$this->storeSimpleEntry();
$response = $this->store->lookup($this->request);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->headers->get('X-Body-File'));
}
public function testInvalidatesMetaAndEntityStoreEntriesWithInvalidate()
@@ -253,9 +253,9 @@ public function testStoresMultipleResponsesForEachVaryCombination()
$res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']);
$this->store->write($req3, $res3);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent());
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent());
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File'));
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File'));
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File'));
$this->assertCount(3, $this->getStoreMetadata($key));
}
@@ -265,17 +265,17 @@ public function testOverwritesNonVaryingResponseWithStore()
$req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']);
$res1 = new Response('test 1', 200, ['Vary' => 'Foo Bar']);
$this->store->write($req1, $res1);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File'));
$req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam']);
$res2 = new Response('test 2', 200, ['Vary' => 'Foo Bar']);
$this->store->write($req2, $res2);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File'));
$req3 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']);
$res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']);
$key = $this->store->write($req3, $res3);
- $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent());
+ $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File'));
$this->assertCount(2, $this->getStoreMetadata($key));
}
@@ -330,6 +330,33 @@ public function testDoesNotStorePrivateHeaders()
$this->assertNotEmpty($response->headers->getCookies());
}
+ public function testDiscardsInvalidBodyEval()
+ {
+ $request = Request::create('https://example.com/foo');
+ $response = new Response('foo', 200, ['X-Body-Eval' => 'SSI']);
+
+ $this->store->write($request, $response);
+ $this->assertNull($this->store->lookup($request));
+
+ $request = Request::create('https://example.com/foo');
+ $content = str_repeat('a', 24).'b'.str_repeat('a', 24).'b';
+ $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']);
+
+ $this->store->write($request, $response);
+ $this->assertNull($this->store->lookup($request));
+ }
+
+ public function testLoadsBodyEval()
+ {
+ $request = Request::create('https://example.com/foo');
+ $content = str_repeat('a', 24).'b'.str_repeat('a', 24);
+ $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']);
+
+ $this->store->write($request, $response);
+ $response = $this->store->lookup($request);
+ $this->assertSame($content, $response->getContent());
+ }
+
protected function storeSimpleEntry($path = null, $headers = [])
{
if (null === $path) {
diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php
index f423889713972..4a4e00c81dcbf 100644
--- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php
+++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php
@@ -143,7 +143,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds)
}
if ('__invoke' !== $method) {
- $wrapperDefinition = (new Definition('callable'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable');
+ $wrapperDefinition = (new Definition('Closure'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable');
$definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method)] = $wrapperDefinition;
} else {
diff --git a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php
index dab74b203f795..ddd7f1b1f61b0 100644
--- a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php
+++ b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php
@@ -123,7 +123,8 @@ public static function getSubscribedEvents()
private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInterface $retryStrategy): bool
{
- if ($e instanceof RecoverableExceptionInterface) {
+ $isRetryable = $retryStrategy->isRetryable($envelope, $e);
+ if ($isRetryable && $e instanceof RecoverableExceptionInterface) {
return true;
}
@@ -132,7 +133,7 @@ private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInt
if ($e instanceof HandlerFailedException) {
$shouldNotRetry = true;
foreach ($e->getNestedExceptions() as $nestedException) {
- if ($nestedException instanceof RecoverableExceptionInterface) {
+ if ($isRetryable && $nestedException instanceof RecoverableExceptionInterface) {
return true;
}
@@ -150,7 +151,7 @@ private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInt
return false;
}
- return $retryStrategy->isRetryable($envelope, $e);
+ return $isRetryable;
}
private function getRetryStrategyForTransport(string $alias): ?RetryStrategyInterface
diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php
index 3d29497d34c1a..3411b0bbef482 100644
--- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php
+++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php
@@ -255,7 +255,7 @@ public function testGetClassesAndMethodsAndPrioritiesFromTheSubscriber()
$dummyHandlerReference = $dummyHandlerDescriptorDefinition->getArgument(0);
$dummyHandlerDefinition = $container->getDefinition($dummyHandlerReference);
- $this->assertSame('callable', $dummyHandlerDefinition->getClass());
+ $this->assertSame('Closure', $dummyHandlerDefinition->getClass());
$this->assertEquals([new Reference(HandlerMappingMethods::class), 'dummyMethod'], $dummyHandlerDefinition->getArgument(0));
$this->assertSame(['Closure', 'fromCallable'], $dummyHandlerDefinition->getFactory());
diff --git a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php
index a5fe10e85578b..8d795d7b86c23 100644
--- a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php
+++ b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php
@@ -63,7 +63,7 @@ public function testRecoverableStrategyCausesRetry()
$senderLocator->expects($this->once())->method('has')->willReturn(true);
$senderLocator->expects($this->once())->method('get')->willReturn($sender);
$retryStategy = $this->createMock(RetryStrategyInterface::class);
- $retryStategy->expects($this->never())->method('isRetryable');
+ $retryStategy->expects($this->once())->method('isRetryable')->willReturn(true);
$retryStategy->expects($this->once())->method('getWaitingTime')->willReturn(1000);
$retryStrategyLocator = $this->createMock(ContainerInterface::class);
$retryStrategyLocator->expects($this->once())->method('has')->willReturn(true);
@@ -78,6 +78,27 @@ public function testRecoverableStrategyCausesRetry()
$listener->onMessageFailed($event);
}
+ public function testRetryIsOnlyAllowedWhenPermittedByRetryStrategy()
+ {
+ $senderLocator = $this->createMock(ContainerInterface::class);
+ $senderLocator->expects($this->never())->method('has');
+ $senderLocator->expects($this->never())->method('get');
+ $retryStrategy = $this->createMock(RetryStrategyInterface::class);
+ $retryStrategy->expects($this->once())->method('isRetryable')->willReturn(false);
+ $retryStrategy->expects($this->never())->method('getWaitingTime');
+ $retryStrategyLocator = $this->createMock(ContainerInterface::class);
+ $retryStrategyLocator->expects($this->once())->method('has')->willReturn(true);
+ $retryStrategyLocator->expects($this->once())->method('get')->willReturn($retryStrategy);
+
+ $listener = new SendFailedMessageForRetryListener($senderLocator, $retryStrategyLocator);
+
+ $exception = new RecoverableMessageHandlingException('retry');
+ $envelope = new Envelope(new \stdClass());
+ $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception);
+
+ $listener->onMessageFailed($event);
+ }
+
public function testEnvelopeIsSentToTransportOnRetry()
{
$exception = new \Exception('no!');
diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php b/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php
index fe82d23972325..9e482c156e78b 100644
--- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php
+++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php
@@ -52,7 +52,7 @@ public function recipient(string $topic): self
}
/**
- * @see PublishInput::$Subject
+ * @see PublishInput::$subject
*
* @return $this
*/
@@ -64,7 +64,7 @@ public function subject(string $subject): self
}
/**
- * @see PublishInput::$MessageStructure
+ * @see PublishInput::$messageStructure
*
* @return $this
*/
diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md
index 3dbcbb1546247..db4759327f502 100644
--- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md
+++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md
@@ -1,7 +1,7 @@
Amazon Notifier
===============
-Provides [Amazon SNS](https://aws.amazon.com/de/sns/) integration for Symfony Notifier.
+Provides [Amazon SNS](https://aws.amazon.com/en/sns/) integration for Symfony Notifier.
DSN example
-----------
@@ -10,6 +10,30 @@ DSN example
AMAZON_SNS_DSN=sns://ACCESS_ID:ACCESS_KEY@default?region=REGION
```
+Adding Options to a Chat Message
+--------------------------------
+
+With an Amazon SNS Chat Message, you can use the `AmazonSnsOptions` class to add
+message options.
+
+```php
+use Symfony\Component\Notifier\Message\ChatMessage;
+use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsOptions;
+
+$chatMessage = new ChatMessage('Contribute To Symfony');
+
+$options = (new AmazonSnsOptions('topic_arn'))
+ ->subject('subject')
+ ->messageStructure('json')
+ // ...
+ ;
+
+// Add the custom options to the chat message and send the message
+$chatMessage->options($options);
+
+$chatter->send($chatMessage);
+```
+
Resources
---------
diff --git a/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md b/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md
index 2d9b0b6d1488d..d9760759c9f90 100644
--- a/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md
+++ b/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md
@@ -16,6 +16,29 @@ where:
- `FROM` is the sender
- `TYPE_QUALITY` is the quality of your message: `N` for high, `L` for medium, `LL` for low (default: `L`)
+Adding Options to a Message
+---------------------------
+
+With a Mobyt Message, you can use the `MobytOptions` class to add
+[message options](https://gatewayapi.com/docs/apis/rest/).
+
+```php
+use Symfony\Component\Notifier\Message\SmsMessage;
+use Symfony\Component\Notifier\Bridge\Mobyt\MobytOptions;
+
+$sms = new SmsMessage('+1411111111', 'My message');
+
+$options = (new MobytOptions())
+ ->messageType(MobytOptions::MESSAGE_TYPE_QUALITY_HIGH)
+ // ...
+ ;
+
+// Add the custom options to the sms message and send the message
+$sms->options($options);
+
+$texter->send($sms);
+```
+
Resources
---------
diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php
index b47ecca17bfe5..9b19475ac5d78 100644
--- a/src/Symfony/Component/Process/Process.php
+++ b/src/Symfony/Component/Process/Process.php
@@ -428,7 +428,7 @@ public function wait(callable $callback = null)
do {
$this->checkTimeout();
- $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
+ $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen());
$this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
} while ($running);
diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php
index 790167fcade94..36acf02a7cd6b 100644
--- a/src/Symfony/Component/Process/Tests/ProcessTest.php
+++ b/src/Symfony/Component/Process/Tests/ProcessTest.php
@@ -1538,6 +1538,16 @@ public function testEnvCaseInsensitiveOnWindows()
}
}
+ public function testNotTerminableInputPipe()
+ {
+ $process = $this->getProcess('echo foo');
+ $process->setInput(\STDIN);
+ $process->start();
+ $process->setTimeout(2);
+ $process->wait();
+ $this->assertFalse($process->isRunning());
+ }
+
/**
* @param string|array $commandline
* @param mixed $input
diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php
index 52a43d16aee46..a964b5036893b 100644
--- a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php
+++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php
@@ -171,7 +171,10 @@ private function getDocBlockFromConstructor(string $class, string $property): ?P
return null;
}
- $rawDocNode = $reflectionConstructor->getDocComment();
+ if (!$rawDocNode = $reflectionConstructor->getDocComment()) {
+ return null;
+ }
+
$tokens = new TokenIterator($this->lexer->tokenize($rawDocNode));
$phpDocNode = $this->phpDocParser->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_END);
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php
index c6a02b5f2f3e4..b3489d9fb0c10 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php
@@ -169,7 +169,7 @@ public function testExtractCollection($property, array $type = null, $shortDescr
public static function provideCollectionTypes()
{
return [
- ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, null, new Type(Type::BUILTIN_TYPE_STRING))], null, null],
+ ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], new Type(Type::BUILTIN_TYPE_STRING))], null, null],
['iteratorCollectionWithKey', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], null, null],
[
'nestedIterators',
@@ -265,6 +265,8 @@ public static function typesWithCustomPrefixesProvider()
['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null],
['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTime')], null, null],
['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null],
+ ['nonNullableCollectionOfNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, true))], null, null],
+ ['nullableCollectionOfMultipleNonNullableElementTypes', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)])], null, null],
['donotexist', null, null, null],
['staticGetter', null, null, null],
['staticSetter', null, null, null],
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
index a517e7ac30469..c607f2abc3761 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
+use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy;
@@ -353,6 +354,14 @@ public function testExtractConstructorTypes($property, array $type = null)
$this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property));
}
+ /**
+ * @dataProvider constructorTypesProvider
+ */
+ public function testExtractConstructorTypesReturnNullOnEmptyDocBlock($property)
+ {
+ $this->assertNull($this->extractor->getTypesFromConstructor(ConstructorDummyWithoutDocBlock::class, $property));
+ }
+
public static function constructorTypesProvider()
{
return [
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php
index f33ad70c4bdf8..c8bb2758f2b50 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php
@@ -62,6 +62,8 @@ public function testGetProperties()
'i',
'j',
'nullableCollectionOfNonNullableElements',
+ 'nonNullableCollectionOfNullableElements',
+ 'nullableCollectionOfMultipleNonNullableElementTypes',
'emptyVar',
'iteratorCollection',
'iteratorCollectionWithKey',
@@ -124,6 +126,8 @@ public function testGetPropertiesWithCustomPrefixes()
'i',
'j',
'nullableCollectionOfNonNullableElements',
+ 'nonNullableCollectionOfNullableElements',
+ 'nullableCollectionOfMultipleNonNullableElementTypes',
'emptyVar',
'iteratorCollection',
'iteratorCollectionWithKey',
@@ -175,6 +179,8 @@ public function testGetPropertiesWithNoPrefixes()
'i',
'j',
'nullableCollectionOfNonNullableElements',
+ 'nonNullableCollectionOfNullableElements',
+ 'nullableCollectionOfMultipleNonNullableElementTypes',
'emptyVar',
'iteratorCollection',
'iteratorCollectionWithKey',
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php
new file mode 100644
index 0000000000000..7e2087d467f7d
--- /dev/null
+++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
+
+class ConstructorDummyWithoutDocBlock
+{
+ public function __construct(\DateTimeZone $timezone, $date, $dateObject, \DateTimeImmutable $dateTime, $mixed)
+ {
+ }
+}
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php
index 8d956a1103fc0..2fb3d2e0f807c 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php
@@ -98,6 +98,16 @@ class Dummy extends ParentDummy
*/
public $nullableCollectionOfNonNullableElements;
+ /**
+ * @var array
+ */
+ public $nonNullableCollectionOfNullableElements;
+
+ /**
+ * @var null|array
+ */
+ public $nullableCollectionOfMultipleNonNullableElementTypes;
+
/**
* @var array
*/
diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php
index 2c858c3bf9f8b..44a4614985563 100644
--- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php
+++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php
@@ -115,15 +115,10 @@ private function createType(DocType $type, bool $nullable, string $docType = nul
[$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen);
- $key = $this->getTypes($type->getKeyType());
- $value = $this->getTypes($type->getValueType());
+ $keys = $this->getTypes($type->getKeyType());
+ $values = $this->getTypes($type->getValueType());
- // More than 1 type returned means it is a Compound type, which is
- // not handled by Type, so better use a null value.
- $key = 1 === \count($key) ? $key[0] : null;
- $value = 1 === \count($value) ? $value[0] : null;
-
- return new Type($phpType, $nullable, $class, true, $key, $value);
+ return new Type($phpType, $nullable, $class, true, $keys, $values);
3972
}
// Cannot guess
@@ -131,27 +126,20 @@ private function createType(DocType $type, bool $nullable, string $docType = nul
return null;
}
- if (str_ends_with($docType, '[]')) {
- $collectionKeyType = new Type(Type::BUILTIN_TYPE_INT);
- $collectionValueType = $this->createType($type, false, substr($docType, 0, -2));
+ if (str_ends_with($docType, '[]') && $type instanceof Array_) {
+ $collectionKeyTypes = new Type(Type::BUILTIN_TYPE_INT);
+ $collectionValueTypes = $this->getTypes($type->getValueType());
- return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType);
+ return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes);
}
if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) {
// array is converted to x[] which is handled above
// so it's only necessary to handle array here
- $collectionKeyType = $this->getTypes($type->getKeyType())[0];
-
+ $collectionKeyTypes = $this->getTypes($type->getKeyType());
$collectionValueTypes = $this->getTypes($type->getValueType());
- if (1 != \count($collectionValueTypes)) {
- // the Type class does not support union types yet, so assume that no type was defined
- $collectionValueType = null;
- } else {
- $collectionValueType = $collectionValueTypes[0];
- }
- return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType);
+ return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes);
}
$docType = $this->normalizeType($docType);
diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
index efd8cbb567637..bd41b8da6fa72 100644
--- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php
@@ -342,6 +342,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex
$constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes);
if ($constructor) {
+ $context['has_constructor'] = true;
if (true !== $constructor->isPublic()) {
return $reflectionClass->newInstanceWithoutConstructor();
}
@@ -431,6 +432,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex
}
}
+ unset($context['has_constructor']);
+
return new $class();
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
index 21fac3248cd6e..e7efb0057c09f 100644
--- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php
@@ -64,7 +64,11 @@ public function denormalize($data, string $type, string $format = null, array $c
try {
return $type::from($data);
} catch (\ValueError $e) {
- throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type);
+ if (isset($context['has_constructor'])) {
+ throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type);
+ }
+
+ throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e);
}
}
diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php
index 71df2e6bdfb7c..9736d8e78e4a0 100644
--- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php
+++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php
@@ -91,7 +91,14 @@ public function denormalize($data, string $type, string $format = null, array $c
$dateTimeFormat = $context[self::FORMAT_KEY] ?? null;
$timezone = $this->getTimezone($context);
- if (null === $data || !\is_string($data) || '' === trim($data)) {
+ if (\is_int($data) || \is_float($data)) {
+ switch ($dateTimeFormat) {
+ case 'U': $data = sprintf('%d', $data); break;
+ case 'U.u': $data = sprintf('%.6F', $data); break;
+ }
+ }
+
+ if (!\is_string($data) || '' === trim($data)) {
throw NotNormalizableValueException::createForUnexpectedDataType('The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
}
diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php
new file mode 100644
index 0000000000000..f2677195f2820
--- /dev/null
+++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php
@@ -0,0 +1,10 @@
+expectException(InvalidArgumentException::class);
+ $this->expectException(NotNormalizableValueException::class);
$this->expectExceptionMessage('The data must belong to a backed enumeration of type '.StringBackedEnumDummy::class);
$this->normalizer->denormalize('POST', StringBackedEnumDummy::class);
diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php
index 674dfaab5382d..25b7c784fe0e2 100644
--- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php
@@ -178,6 +178,8 @@ public function testDenormalize()
$this->assertEquals(new \DateTimeImmutable('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeImmutable::class));
$this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTime::class));
$this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize(' 2016-01-01T00:00:00+00:00 ', \DateTime::class));
+ $this->assertEquals(new \DateTimeImmutable('2023-05-06T17:35:34.000000+0000', new \DateTimeZone('UTC')), $this->normalizer->denormalize(1683394534, \DateTimeImmutable::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U']));
+ $this->assertEquals(new \DateTimeImmutable('2023-05-06T17:35:34.123400+0000', new \DateTimeZone('UTC')), $this->normalizer->denormalize(1683394534.1234, \DateTimeImmutable::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U.u']));
}
public function testDenormalizeUsingTimezonePassedInConstructor()
diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
index fc0b6cc5af876..b4e84132a0858 100644
--- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php
+++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php
@@ -60,6 +60,7 @@
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne;
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo;
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
+use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty;
use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy;
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Php74Full;
@@ -1230,7 +1231,51 @@ public function testCollectDenormalizationErrorsWithEnumConstructor()
/**
* @requires PHP 8.1
*/
- public function testNoCollectDenormalizationErrorsWithWrongEnum()
+ public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruct()
+ {
+ $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader());
+ $reflectionExtractor = new ReflectionExtractor();
+ $propertyInfoExtractor = new PropertyInfoExtractor([], [$reflectionExtractor], [], [], []);
+
+ $serializer = new Serializer(
+ [
+ new BackedEnumNormalizer(),
+ new ObjectNormalizer($classMetadataFactory, null, null, $propertyInfoExtractor),
+ ],
+ ['json' => new JsonEncoder()]
+ );
+
+ try {
+ $serializer->deserialize('{"get": "POST"}', DummyObjectWithEnumProperty::class, 'json', [
+ DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
+ ]);
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(PartialDenormalizationException::class, $e);
+ }
+
+ $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array {
+ return [
+ 'currentType' => $e->getCurrentType(),
+ 'useMessageForUser' => $e->canUseMessageForUser(),
+ 'message' => $e->getMessage(),
+ ];
+ }, $e->getErrors());
+
+ $expected = [
+ [
+ 'currentType' => 'string',
+ 'useMessageForUser' => true,
+ 'message' => 'The data must belong to a backed enumeration of type Symfony\Component\Serializer\Tests\Fixtures\StringBackedEnumDummy',
+ ],
+ ];
+
+ $this->assertSame($expected, $exceptionsAsArray);
+ }
+
+ /**
+ * @requires PHP 8.1
+ */
+ public function testNoCollectDenormalizationErrorsWithWrongEnumOnConstructor()
{
$serializer = new Serializer(
[
@@ -1241,7 +1286,7 @@ public function testNoCollectDenormalizationErrorsWithWrongEnum()
);
try {
- $serializer->deserialize('{"get": "invalid"}', DummyObjectWithEnumConstructor::class, 'json', [
+ $serializer->deserialize('{"get": "POST"}', DummyObjectWithEnumConstructor::class, 'json', [
DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true,
]);
} catch (\Throwable $th) {
diff --git a/src/Symfony/Component/Translation/Loader/ArrayLoader.php b/src/Symfony/Component/Translation/Loader/ArrayLoader.php
index 0758da8b59a54..feed0de4b0025 100644
--- a/src/Symfony/Component/Translation/Loader/ArrayLoader.php
+++ b/src/Symfony/Component/Translation/Loader/ArrayLoader.php
@@ -46,9 +46,11 @@ private function flatten(array $messages): array
foreach ($messages as $key => $value) {
if (\is_array($value)) {
foreach ($this->flatten($value) as $k => $v) {
- $result[$key.'.'.$k] = $v;
+ if (null !== $v) {
+ $result[$key.'.'.$k] = $v;
+ }
}
- } else {
+ } elseif (null !== $value) {
$result[$key] = $value;
}
}
diff --git a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php
index 230c02e539e45..cac0b6f87823a 100644
--- a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php
+++ b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php
@@ -30,6 +30,15 @@ public function testLoad()
$this->assertEquals([new FileResource($resource)], $catalogue->getResources());
}
+ public function testLoadNonStringMessages()
+ {
+ $loader = new YamlFileLoader();
+ $resource = __DIR__.'/../fixtures/non-string.yml';
+ $catalogue = $loader->load($resource, 'en', 'domain1');
+
+ $this->assertSame(['root.foo2' => '', 'root.bar' => 'bar'], $catalogue->all('domain1'));
+ }
+
public function testLoadDoesNothingIfEmpty()
{
$loader = new YamlFileLoader();
diff --git a/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml b/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml
new file mode 100644
index 0000000000000..41e245e19a4a6
--- /dev/null
+++ b/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml
@@ -0,0 +1,4 @@
+root:
+ foo1:
+ foo2: ''
+ bar: 'bar'
diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json
index 019a46fc15282..3e860daa30e30 100644
--- a/src/Symfony/Component/Validator/composer.json
+++ b/src/Symfony/Component/Validator/composer.json
@@ -49,7 +49,6 @@
"doctrine/annotations": "<1.13",
"doctrine/cache": "<1.11",
"doctrine/lexer": "<1.1",
- "phpunit/phpunit": "<5.4.3",
"symfony/dependency-injection": "<4.4",
"symfony/expression-language": "<5.1",
"symfony/http-kernel": "<4.4",
diff --git a/src/Symfony/Component/VarDumper/composer.json b/src/Symfony/Component/VarDumper/composer.json
index dc46f58d99eca..fc127d721ab16 100644
--- a/src/Symfony/Component/VarDumper/composer.json
+++ b/src/Symfony/Component/VarDumper/composer.json
@@ -28,7 +28,6 @@
"twig/twig": "^2.13|^3.0.4"
},
"conflict": {
- "phpunit/phpunit": "<5.4.3",
"symfony/console": "<4.4"
},
"suggest": {
0