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/scorecards.yml b/.github/workflows/scorecards.yml index 87cd21cf0d442..b66e1b53b60f4 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -6,7 +6,7 @@ on: schedule: - cron: '34 4 * * 6' push: - branches: [ "6.3" ] + branches: [ "6.4" ] # Declare default permissions as read only. permissions: read-all diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 358008506bff3..86206dae8d6ad 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -64,7 +64,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-6.2.md b/CHANGELOG-6.2.md index d685302315bcb..5a003a6343fe2 100644 --- a/CHANGELOG-6.2.md +++ b/CHANGELOG-6.2.md @@ -7,6 +7,42 @@ in 6.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v6.2.0...v6.2.1 +* 6.2.11 (2023-05-27) + + * bug #50442 [SecurityBundle] Update security-1.0.xsd to include missing access-token definition (aegypius) + * bug #50429 [Console] block input stream if needed (joelwurtz) + * bug #50312 [Security] Skip clearing CSRF Token on stateless logout (chalasr) + * bug #50315 [Translation] Fix handling of null messages in `ArrayLoader` (rob006) + * bug #50338 [Console] Remove ``exec`` and replace it by ``shell_exec`` (maxbeckers) + * bug #50193 [Serializer] Fix `SerializedPath` not working with constructor arguments (HypeMC) + * bug #50280 [PropertyAccess] Fix nullsafe operator on array index (HypeMC) + * 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 #50352 [Notifier][TurboSMS] Fix get sender name (ZhukV) + * 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 #50277 [Messenger] Add `IS_REPEATABLE` flag to `AsMessageHandler` attribute (adrianguenter) + * bug #50269 Fix param type annotation (l-vo) + * bug #50268 Allow resources in Query::setParam (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 #50241 [HttpKernel] Prevent initialising lazy services during services reset (tucksaun) + * 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) + * 6.2.10 (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 4f274c985c33f..24afd64907140 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -16,8 +16,8 @@ The Symfony Connect username in parenthesis allows to get more information - Grégoire Pineau (lyrixx) - Wouter de Jong (wouterj) - Maxime Steinhausser (ogizanagi) - - Kévin Dunglas (dunglas) - Christophe Coevoet (stof) + - Kévin Dunglas (dunglas) - Jordi Boggiano (seldaek) - Roland Franssen (ro0) - Victor Berchet (victor) @@ -41,8 +41,8 @@ The Symfony Connect username in parenthesis allows to get more information - Jan Schädlich (jschaedl) - Lukas Kahwe Smith (lsmith) - Jérôme Tamarelle (gromnan) - - Martin Hasoň (hason) - Kevin Bond (kbond) + - Martin Hasoň (hason) - Jeremy Mikola (jmikola) - Jean-François Simon (jfsimon) - Benjamin Eberlei (beberlei) @@ -56,21 +56,21 @@ The Symfony Connect username in parenthesis allows to get more information - Antoine Makdessi (amakdessi) - Laurent VOULLEMIER (lvo) - Pierre du Plessis (pierredup) + - Antoine Lamirault (alamirault) - Grégoire Paris (greg0ire) - Jonathan Wage (jwage) - - Antoine Lamirault (alamirault) - Titouan Galopin (tgalopin) - David Maicher (dmaicher) - - Alexander Schranz (alexander-schranz) - Gábor Egyed (1ed) - - Alexandre Salomé (alexandresalome) - Mathieu Santostefano (welcomattic) + - Alexander Schranz (alexander-schranz) + - Alexandre Salomé (alexandresalome) - William DURAND + - Mathieu Lechat (mat_the_cat) - ornicar - Dany Maillard (maidmaid) - Eriksen Costa - Diego Saint Esteben (dosten) - - Mathieu Lechat (mat_the_cat) - stealth35 ‏ (stealth35) - Alexander Mols (asm89) - Francis Besset (francisbesset) @@ -79,8 +79,8 @@ The Symfony Connect username in parenthesis allows to get more information - Iltar van der Berg - Miha Vrhovnik (mvrhov) - Mathieu Piot (mpiot) - - Saša Stamenković (umpirsky) - Vincent Langlet (deviling) + - Saša Stamenković (umpirsky) - Alex Pott - Guilhem N (guilhemn) - Vladimir Reznichenko (kalessil) @@ -90,26 +90,26 @@ The Symfony Connect username in parenthesis allows to get more information - Bilal Amarni (bamarni) - Eriksen Costa - Florin Patan (florinpatan) + - Konstantin Myakshin (koc) - Peter Rehm (rpet) + - Ruud Kamphuis (ruudk) - Henrik Bjørnskov (henrikbjorn) - David Buchmann (dbu) - - Ruud Kamphuis (ruudk) - - Konstantin Myakshin (koc) - Andrej Hudec (pulzarraider) - Julien Falque (julienfalque) - Massimiliano Arione (garak) + - Jáchym Toušek (enumag) - Douglas Greenshields (shieldo) - Christian Raue - Fran Moreno (franmomu) - - Jáchym Toušek (enumag) - Mathias Arlaud (mtarld) - Graham Campbell (graham) - Michel Weimerskirch (mweimerskirch) - Eric Clemmons (ericclemmons) - Issei Murasawa (issei_m) - Malte Schlüter (maltemaltesich) - - Vasilij Dusko - Denis (yethee) + - Vasilij Dusko - Arnout Boks (aboks) - Charles Sarrazin (csarrazi) - Przemysław Bogusz (przemyslaw-bogusz) @@ -118,8 +118,10 @@ The Symfony Connect username in parenthesis allows to get more information - Maxime Helias (maxhelias) - Ener-Getick - Sebastiaan Stok (sstok) + - Tugdual Saunier (tucksaun) - Jérôme Vasseur (jvasseur) - Ion Bazan (ionbazan) + - Rokas Mikalkėnas (rokasm) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) @@ -131,7 +133,6 @@ The Symfony Connect username in parenthesis allows to get more information - Smaine Milianni (ismail1432) - John Wards (johnwards) - Dariusz Ruminski - - Rokas Mikalkėnas (rokasm) - Lars Strojny (lstrojny) - Antoine Hérault (herzult) - Konstantin.Myakshin @@ -147,7 +148,6 @@ The Symfony Connect username in parenthesis allows to get more information - Andreas Braun - Teoh Han Hui (teohhanhui) - YaFou - - Tugdual Saunier (tucksaun) - Gary PEGEOT (gary-p) - Chris Wilkinson (thewilkybarkid) - Brice BERNARD (brikou) @@ -163,6 +163,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jeroen Spee (jeroens) - Michael Babker (mbabker) - Włodzimierz Gajda (gajdaw) + - Hugo Alliaume (kocal) - Christian Scheb - Guillaume (guill) - Christopher Hertel (chertel) @@ -171,7 +172,6 @@ The Symfony Connect username in parenthesis allows to get more information - Olivier Dolbeau (odolbeau) - Florian Voutzinos (florianv) - zairig imad (zairigimad) - - Hugo Alliaume (kocal) - Colin Frei - Javier Spagnoletti (phansys) - excelwebzone @@ -260,6 +260,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sokolov Evgeniy (ewgraf) - Stadly - Justin Hileman (bobthecow) + - Bastien Jaillot (bastnic) - Tom Van Looy (tvlooy) - Niels Keurentjes (curry684) - Vyacheslav Pavlov @@ -281,7 +282,6 @@ The Symfony Connect username in parenthesis allows to get more information - Filippo Tessarotto (slamdunk) - 77web - Bohan Yang (brentybh) - - Bastien Jaillot (bastnic) - W0rma - Matthieu Ouellette-Vachon (maoueh) - Lynn van der Berg (kjarli) @@ -293,6 +293,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tyson Andre - GDIBass - Samuel NELA (snela) + - Romain Monteil (ker0x) - dFayet - gnito-org - Karoly Gossler (connorhu) @@ -327,7 +328,6 @@ The Symfony Connect username in parenthesis allows to get more information - D (denderello) - Jonathan Scheiber (jmsche) - DQNEO - - Romain Monteil (ker0x) - Andrii Bodnar - Artem (artemgenvald) - ivan @@ -377,6 +377,7 @@ The Symfony Connect username in parenthesis allows to get more information - Hidde Wieringa (hiddewie) - Christopher Davis (chrisguitarguy) - Lukáš Holeczy (holicz) + - Michael Lee (zerustech) - Florian Lonqueu-Brochard (florianlb) - Leszek Prabucki (l3l0) - Emanuele Panzeri (thepanz) @@ -418,15 +419,18 @@ The Symfony Connect username in parenthesis allows to get more information - 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) - - Michael Lee (zerustech) - Dariusz - Francois Zaninotto - Laurent Masforné (heisenberg) - Claude Khedhiri (ck-developer) + - Giorgio Premi - Daniel Tschinder - Christian Schmidt - Alexander Kotynia (olden) @@ -507,20 +511,18 @@ The Symfony Connect username in parenthesis allows to get more information - Frank de Jonge - Chris Tanaskoski - julien57 - - Loïc Frémont (loic425) + - Renan (renanbr) - Ippei Sumida (ippey_s) - Ben Ramsey (ramsey) - - Allison Guilhem (a_guilhem) - Matthieu Auger (matthieuauger) - - Kévin THERAGE (kevin_therage) - Josip Kruslin (jkruslin) - - Giorgio Premi - renanbr - Maxim Dovydenok (shiftby) - Sébastien Lavoie (lavoiesl) - Alex Rock (pierstoval) - Wodor Wodorski - Beau Simensen (simensen) + - Magnus Nordlander (magnusnordlander) - Robert Kiss (kepten) - Zan Baldwin (zanbaldwin) - Antonio J. García Lagar (ajgarlag) @@ -535,10 +537,12 @@ The Symfony Connect username in parenthesis allows to get more information - Pascal Luna (skalpa) - Wouter Van Hecke - Michael Holm (hollo) + - Yassine Guedidi (yguedidi) - Giso Stallenberg (gisostallenberg) - Blanchon Vincent (blanchonvincent) - William Arslett (warslett) - Jérémy REYNAUD (babeuloula) + - Daniel Burger - Christian Schmidt - Gonzalo Vilaseca (gonzalovilaseca) - Vadim Borodavko (javer) @@ -596,6 +600,7 @@ The Symfony Connect username in parenthesis allows to get more information - Emanuele Gaspari (inmarelibero) - Dariusz Rumiński - Terje Bråten + - Florent Morselli (spomky_) - Gennadi Janzen - James Hemery - Egor Taranov @@ -609,6 +614,7 @@ The Symfony Connect username in parenthesis allows to get more information - Khoo Yong Jun - Christin Gruber (christingruber) - Jeremy Livingston (jeremylivingston) + - Tobias Bönner - Julien Turby - scyzoryck - Greg Anderson @@ -631,7 +637,6 @@ The Symfony Connect username in parenthesis allows to get more information - Angelov Dejan (angelov) - DT Inier (gam6itko) - Matthew Lewinski (lewinski) - - Magnus Nordlander (magnusnordlander) - Ricard Clau (ricardclau) - Dmitrii Tarasov (dtarasov) - Philipp Kolesnikov @@ -703,6 +708,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jelle Raaijmakers (gmta) - Roberto Nygaard - Joshua Nye + - Jordane VASPARD (elementaire) - Dalibor Karlović - Randy Geraads - Sanpi (sanpi) @@ -746,6 +752,7 @@ 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) @@ -830,7 +837,6 @@ The Symfony Connect username in parenthesis allows to get more information - Sebastian Paczkowski (sebpacz) - Dragos Protung (dragosprotung) - Thiago Cordeiro (thiagocordeiro) - - Florent Morselli (spomky_) - Julien Maulny - Brian King - Paul Oms @@ -845,9 +851,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jon Gotlin (jongotlin) - Jeanmonod David (jeanmonod) - Daniel González (daniel.gonzalez) - - Renan (renanbr) - Webnet team (webnet) - - Tobias Bönner - Berny Cantos (xphere81) - Mátyás Somfai (smatyas) - Simon Leblanc (leblanc_simon) @@ -856,6 +860,7 @@ The Symfony Connect username in parenthesis allows to get more information - Niklas Fiekas - Mark Challoner (markchalloner) - Markus Bachmann (baachi) + - Matthieu Lempereur (mryamous) - Roger Guasch (rogerguasch) - Luis Tacón (lutacon) - Alex Hofbauer (alexhofbauer) @@ -863,7 +868,9 @@ The Symfony Connect username in parenthesis allows to get more information - lancergr - Ivan Nikolaev (destillat) - Xavier Leune (xleune) + - Matthieu Calie (matth--) - Ben Roberts (benr77) + - Benjamin Georgeault (wedgesama) - Joost van Driel (j92) - ampaze - Arturs Vonda @@ -900,7 +907,6 @@ The Symfony Connect username in parenthesis allows to get more information - Adam Harvey - ilyes kooli (skafandri) - Anton Bakai - - Daniel Burger - Sam Fleming (sam_fleming) - Alex Bakhturin - Brayden Williams (redstar504) @@ -944,6 +950,7 @@ The Symfony Connect username in parenthesis allows to get more information - mcben - Jérôme Vieilledent (lolautruche) - Filip Procházka (fprochazka) + - Alex Kalineskou - stoccc - Markus Lanthaler (lanthaler) - Gigino Chianese (sajito) @@ -1015,11 +1022,9 @@ The Symfony Connect username in parenthesis allows to get more information - Matthias Schmidt - Lenar Lõhmus - Ilija Tovilo (ilijatovilo) - - Samaël Villette (samadu61) - Zach Badgett (zachbadgett) - Loïc Faugeron - Aurélien Fredouelle - - Jordane VASPARD (elementaire) - Pavel Campr (pcampr) - Forfarle (forfarle) - Johnny Robeson (johnny) @@ -1101,7 +1106,6 @@ The Symfony Connect username in parenthesis allows to get more information - Giuseppe Campanelli - Valentin - pizzaminded - - Matthieu Calie (matth--) - Stéphane Escandell (sescandell) - ivan - linh @@ -1140,7 +1144,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jacek Wilczyński (jacekwilczynski) - Hany el-Kerdany - Wang Jingyu - - Benjamin Georgeault (wedgesama) - Åsmund Garfors - Maxime Douailin - Jean Pasdeloup @@ -1163,6 +1166,7 @@ The Symfony Connect username in parenthesis allows to get more information - Łukasz Chruściel (lchrusciel) - Jan Vernieuwe (vernija) - zenmate + - Cédric Anne - j.schmitt - Georgi Georgiev - David Fuhr @@ -1171,6 +1175,7 @@ The Symfony Connect username in parenthesis allows to get more information - mwos - Aurimas Niekis (gcds) - Volker Killesreiter (ol0lll) + - Benjamin Zaslavsky (tiriel) - Vedran Mihočinec (v-m-i) - creiner - RevZer0 (rav) @@ -1212,6 +1217,7 @@ The Symfony Connect username in parenthesis allows to get more information - Atthaphon Urairat - Jon Green (jontjs) - Mickaël Isaert (misaert) + - alexandre.lassauge - Israel J. Carberry - Julius Kiekbusch - Miquel Rodríguez Telep (mrtorrent) @@ -1261,6 +1267,7 @@ 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 @@ -1283,6 +1290,7 @@ The Symfony Connect username in parenthesis allows to get more information - dbrekelmans - Piet Steinhart - mousezheng + - Nicolas Dousson - Rémy LESCALLIER - Simon Schick (simonsimcity) - Victor Macko (victor_m) @@ -1466,6 +1474,7 @@ The Symfony Connect username in parenthesis allows to get more information - Barney Hanlon - Bart Wach - Jos Elstgeest + - Thorry84 - Kirill Lazarev - Serhii Smirnov - Martins Eglitis @@ -1671,7 +1680,6 @@ The Symfony Connect username in parenthesis allows to get more information - andrey1s - Abhoryo - Fabian Vogler (fabian) - - Yassine Guedidi (yguedidi) - Korvin Szanto - Simon Ackermann - Stéphan Kochen @@ -1734,6 +1742,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jörn Lang - David Marín Carreño (davefx) - Fabien LUCAS (flucas2) + - Alex (garrett) - Hidde Boomsma (hboomsma) - Johan Wilfer (johanwilfer) - Toby Griffiths (tog) @@ -1774,7 +1783,6 @@ The Symfony Connect username in parenthesis allows to get more information - florian-michael-mast - Henry Snoek - Vlad Dumitrache - - Alex Kalineskou - Derek ROTH - Jeremy Benoist - Ben Johnson @@ -1833,6 +1841,7 @@ The Symfony Connect username in parenthesis allows to get more information - vladyslavstartsev - Kévin - Marc Abramowitz + - Markus Staab - michal - Martijn Evers - Sjoerd Adema @@ -1991,6 +2000,7 @@ The Symfony Connect username in parenthesis allows to get more information - Daniel Alejandro Castro Arellano (lexcast) - Aleksandar Dimitrov (netbull) - Gary Houbre (thegarious) + - Vincent Chalamon - Thomas Jarrand - Baptiste Leduc (bleduc) - Antoine Bluchet (soyuka) @@ -2032,6 +2042,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sander Marechal - Franz Wilding (killerpoke) - Ferenczi Krisztian (fchris82) + - Artyum Petrov - Oleg Golovakhin (doc_tr) - Icode4Food (icode4food) - Radosław Benkel @@ -2050,6 +2061,7 @@ The Symfony Connect username in parenthesis allows to get more information - Sander Coolen (scoolen) - Nicolas Le Goff (nlegoff) - Anne-Sophie Bachelard + - Gordienko Vladislav - Marvin Butkereit - Ben Oman - Chris de Kok @@ -2065,6 +2077,7 @@ The Symfony Connect username in parenthesis allows to get more information - Zachary Tong (polyfractal) - Ashura - Hryhorii Hrebiniuk + - Nsbx - Alex Plekhanov - johnstevenson - hamza @@ -2076,6 +2089,7 @@ The Symfony Connect username in parenthesis allows to get more information - Artem (digi) - boite - Silvio Ginter + - Peter Culka - MGDSoft - joris - Vadim Tyukov (vatson) @@ -2117,6 +2131,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jordi Rejas - Troy McCabe - Ville Mattila + - gstapinato - gr1ev0us - Léo VINCENT - mlazovla @@ -2141,7 +2156,6 @@ The Symfony Connect username in parenthesis allows to get more information - MARYNICH Mikhail (mmarynich-ext) - Viktor Novikov (nowiko) - Paul Mitchum (paul-m) - - Phil E. Taylor (philetaylor) - Angel Koilov (po_taka) - Dan Finnie - Ken Marfilla (marfillaster) @@ -2211,6 +2225,7 @@ The Symfony Connect username in parenthesis allows to get more information - Abderrahman DAIF (death_maker) - Yann Rabiller (einenlum) - Jochen Bayer (jocl) + - VAN DER PUTTE Guillaume (guillaume_vdp) - Patrick Carlo-Hickman - Bruno MATEU - Jeremy Bush @@ -2235,6 +2250,7 @@ The Symfony Connect username in parenthesis allows to get more information - BRAMILLE Sébastien (oktapodia) - Artem Kolesnikov (tyomo4ka) - Gustavo Adrian + - Matthias Neid - Yannick - Kuzia - Vladimir Luchaninov (luchaninov) @@ -2242,6 +2258,7 @@ The Symfony Connect username in parenthesis allows to get more information - rchoquet - v.shevelev - gitlost + - radar3301 - Taras Girnyk - Sergio - Mehrdad @@ -2311,6 +2328,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dmitri Petmanson - heccjj - Alexandre Melard + - Rafał Toboła - AlbinoDrought - Jay Klehr - Sergey Yuferev @@ -2330,11 +2348,11 @@ The Symfony Connect username in parenthesis allows to get more information - Jelte Steijaert (jelte) - David Négrier (moufmouf) - Quique Porta (quiqueporta) - - Benjamin Zaslavsky (tiriel) - Tobias Feijten (tobias93) - Andrea Quintino (dirk39) - Andreas Heigl (heiglandreas) - Tomasz Szymczyk (karion) + - Nadim AL ABDOU (nadim) - Peter Dietrich (xosofox) - Alex Vasilchenko - sez-open @@ -2365,6 +2383,7 @@ The Symfony Connect username in parenthesis allows to get more information - Andrei Igna - azine - Wojciech Zimoń + - Vladimir Melnik - Pierre Tachoire - Dawid Sajdak - Ludek Stepan @@ -2378,6 +2397,7 @@ The Symfony Connect username in parenthesis allows to get more information - karolsojko - Marco Jantke - Saem Ghani + - Claudiu Cristea - Zacharias Luiten - Sebastian Utz - Adrien Gallou (agallou) @@ -2462,6 +2482,8 @@ The Symfony Connect username in parenthesis allows to get more information - Max Summe - Ema Panz - Chihiro Adachi (chihiro-adachi) + - Thomas Trautner (thomastr) + - mfettig - Raphaëll Roussel - Tadcka - Abudarham Yuval @@ -2490,6 +2512,7 @@ The Symfony Connect username in parenthesis allows to get more information - Nicolas Eeckeloo (neeckeloo) - Andriy Prokopenko (sleepyboy) - Dariusz Ruminski + - Starfox64 - Thomas Hanke - Daniel Tschinder - Arnaud CHASSEUX @@ -2547,6 +2570,7 @@ The Symfony Connect username in parenthesis allows to get more information - Simon Neidhold - Valentin VALCIU - Jeremiah VALERIE + - Cas van Dongen - Patrik Patie Gmitter - Yannick Snobbert - Kevin Dew @@ -2574,8 +2598,8 @@ The Symfony Connect username in parenthesis allows to get more information - Kirk Madera - Keith Maika - Mephistofeles + - Oleh Korneliuk - Hoffmann András - - Cédric Anne - LubenZA - Flavian Sierk - Rik van der Heijden @@ -2595,6 +2619,7 @@ The Symfony Connect username in parenthesis allows to get more information - Olivier Scherler (oscherler) - Shane Preece (shane) - Johannes Goslar + - Mike Gladysch - Geoff - georaldc - wusuopu @@ -2667,6 +2692,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dan Blows - Matt Wells - Nicolas Appriou + - Javier Alfonso Bellota de Frutos - stloyd - Andreas - Chris Tickner @@ -2826,11 +2852,13 @@ The Symfony Connect username in parenthesis allows to get more information - Michael van Tricht - ReScO - Tim Strehle + - cay89 - Sam Ward - Hans N. Hjort - Walther Lalk - Adam - Ivo + - Markus Staab - Sören Bernstein - michael.kubovic - devel @@ -2918,6 +2946,7 @@ The Symfony Connect username in parenthesis allows to get more information - Götz Gottwald - Adrien Peyre - Christoph Krapp + - andreyserdjuk - Nick Chiu - Robert Campbell - Matt Lehner @@ -2939,7 +2968,6 @@ The Symfony Connect username in parenthesis allows to get more information - Alessandro Loffredo - Ian Phillips - Remi Collet - - Nicolas Dousson - Haritz - Matthieu Prat - Brieuc Thomas @@ -2964,6 +2992,7 @@ The Symfony Connect username in parenthesis allows to get more information - tourze - Erik van Wingerden - Valouleloup + - Roland Franssen :) - Alexis MARQUIS - Matheus Gontijo - Gerrit Drost @@ -2998,7 +3027,6 @@ The Symfony Connect username in parenthesis allows to get more information - Tomáš Polívka (draczris) - Dennis Smink (dsmink) - Franz Liedke (franzliedke) - - Alex (garrett) - Gaylord Poillon (gaylord_p) - gondo (gondo) - Joris Garonian (grifx) @@ -3034,6 +3062,7 @@ The Symfony Connect username in parenthesis allows to get more information - Yorkie Chadwick (yorkie76) - Pavel Barton - GuillaumeVerdon + - Marien Fressinaud - ureimers - akimsko - Youpie @@ -3175,8 +3204,8 @@ The Symfony Connect username in parenthesis allows to get more information - Arrilot - andrey-tech - Shaun Simmons - - Markus Staab - Pierre-Louis LAUNAY + - A. Pauly - djama - Michael Gwynne - Eduardo Conceição @@ -3242,6 +3271,7 @@ The Symfony Connect username in parenthesis allows to get more information - Alfonso Fernández García - phc - Дмитрий Пацура + - db306 - Michaël VEROUX - Julia - Lin Lu diff --git a/composer.json b/composer.json index 5c338d388cfd0..18539cf8b7540 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", "phpdocumentor/reflection-docblock": "^5.2", "phpstan/phpdoc-parser": "^1.0", "predis/predis": "~1.1", "psr/http-client": "^1.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "symfony/mercure-bundle": "^0.3", - "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", "symfony/runtime": "self.version", "symfony/security-acl": "~2.8|~3.0", "twig/cssinliner-extra": "^2.12|^3", @@ -158,10 +159,11 @@ "masterminds/html5": "<2.6", "phpdocumentor/reflection-docblock": "<5.2", "phpdocumentor/type-resolver": "<1.5.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 9813d712132f4..695a0a1535aa2 100644 --- a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php @@ -43,7 +43,7 @@ public function stop(): void } } - public function setParam(string|int $param, null|string|int|float|bool &$variable, int $type): void + public function setParam(string|int $param, mixed &$variable, int $type): void { // Numeric indexes start at 0 in profiler $idx = \is_int($param) ? $param - 1 : $param; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php index e605afec5a630..2bb628442459c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Middleware/Debug/MiddlewareTest.php @@ -145,23 +145,31 @@ public function testWithParamBound(callable $executeMethod) { $this->init(); - $product = 'product1'; - $price = 12.5; - $stock = 5; + $sql = <<getResourceFromString('mydata'); - $stmt = $this->conn->prepare('INSERT INTO products(name, price, stock) VALUES (?, ?, ?)'); + $stmt = $this->conn->prepare($sql); $stmt->bindParam(1, $product); $stmt->bindParam(2, $price); $stmt->bindParam(3, $stock, ParameterType::INTEGER); + $stmt->bindParam(4, $res, ParameterType::BINARY); + + $product = 'product1'; + $price = 12.5; + $stock = 5; $executeMethod($stmt); // Debug data should not be affected by these changes $debug = $this->debugDataHolder->getData()['default'] ?? []; $this->assertCount(2, $debug); - $this->assertSame('INSERT INTO products(name, price, stock) VALUES (?, ?, ?)', $debug[1]['sql']); - $this->assertSame(['product1', '12.5', 5], $debug[1]['params']); - $this->assertSame([ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER], $debug[1]['types']); + $this->assertSame($sql, $debug[1]['sql']); + $this->assertSame(['product1', 12.5, 5, $expectedRes], $debug[1]['params']); + $this->assertSame([ParameterType::STRING, ParameterType::STRING, ParameterType::INTEGER, ParameterType::BINARY], $debug[1]['types']); $this->assertGreaterThan(0, $debug[1]['executionMS']); } diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 7430758321d3e..abfa858cf037f 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -53,7 +53,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": "<5.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 ca46a577154a3..6a4ae1aefb5e9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -129,14 +129,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); @@ -145,7 +138,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)) { @@ -219,7 +219,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(); @@ -228,18 +228,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); @@ -250,4 +238,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/Command/CachePoolInvalidateTagsCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php index a69624c8372c4..9e6ef9330e24a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolInvalidateTagsCommand.php @@ -92,12 +92,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($errors) { $io->error('Done but with errors.'); - return self::FAILURE; + return 1; } $io->success('Successfully invalidated cache tags.'); - return self::SUCCESS; + return 0; } public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 7bf72ee6c801f..4fea9249a2f14 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -970,6 +970,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $definitionDefinition->addArgument(new Reference(sprintf('%s.metadata_store', $workflowId))); // Create MarkingStore + $markingStoreDefinition = null; if (isset($workflow['marking_store']['type'])) { $markingStoreDefinition = new ChildDefinition('workflow.marking_store.method'); $markingStoreDefinition->setArguments([ @@ -983,7 +984,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']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index 9facc09c783f8..a648e8fdb1e49 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -120,7 +120,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 5daeaa14ec28e..506162a42cdf7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -88,6 +88,63 @@ public function testWorkflowValidationStateMachine() }); } + public function testWorkflowDefaultMarkingStoreDefinition() + { + $container = $this->createContainerFromClosure(function ($container) { + $container->loadFromExtension('framework', [ + 'http_method_override' => false, + '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 4fc22ab7122bd..be8be43d1ca81 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -25,7 +25,7 @@ "symfony/deprecation-contracts": "^2.1|^3", "symfony/error-handler": "^6.1", "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/http-foundation": "^6.2", + "symfony/http-foundation": "^6.2.11", "symfony/http-kernel": "^6.2.1", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "^5.4|^6.0", @@ -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.4", "symfony/console": "<5.4", "symfony/dotenv": "<5.4", diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd index 33140fdae8d11..af058365cc716 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd @@ -140,6 +140,7 @@ + @@ -304,6 +305,17 @@ + + + + + + + + + + + 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 13503feeb4c05..40ef496fac0ae 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) %} - +
- + {% if is_fallback %} {% endif %} - + @@ -178,7 +176,7 @@ {% for message in messages %} - + {% if is_fallback %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 953ee53f8d2f9..1c59dd0cb5684 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -1637,42 +1637,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 e65b334ce89db..dbf5d7ddc3309 100644 --- a/src/Symfony/Component/Console/Command/CompleteCommand.php +++ b/src/Symfony/Component/Console/Command/CompleteCommand.php @@ -173,10 +173,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 10280f73abd83..cac944ec9375b 100644 --- a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php +++ b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php @@ -96,7 +96,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(); @@ -113,12 +113,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, CompleteCommand::COMPLETION_API_VERSION], 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 c345b4af7747f..f26ca577a22c9 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -123,7 +123,18 @@ private function doAsk(OutputInterface $output, Question $question): mixed } 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.'); } @@ -496,13 +507,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 a130c41226bf2..0e86e91674397 100644 --- a/src/Symfony/Component/Console/Input/InputArgument.php +++ b/src/Symfony/Component/Console/Input/InputArgument.php @@ -37,7 +37,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) * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion diff --git a/src/Symfony/Component/Console/Terminal.php b/src/Symfony/Component/Console/Terminal.php index 7d10b13ad5991..855f4114d751c 100644 --- a/src/Symfony/Component/Console/Terminal.php +++ b/src/Symfony/Component/Console/Terminal.php @@ -123,14 +123,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 c634f7afb80d6..6039acf571193 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 4a9b7a74abe93..d8af7dc1ff5e6 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 26e89a9b9a26a..6da7f0852a85c 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -207,7 +207,7 @@ private function checkType(Definition $checkedDefinition, mixed $value, \Reflect $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 2305d210ff781..c169d6c8ca7fc 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; @@ -980,6 +981,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'), + 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 ccf25ad5631ed..22a88f144f7c1 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -44,7 +44,6 @@ "symfony/uid": "^5.4|^6.0" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<5.4", "symfony/dependency-injection": "<5.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 4d539219174be..661f9fec0b897 100644 --- a/src/Symfony/Component/HttpClient/HttplugClient.php +++ b/src/Symfony/Component/HttpClient/HttplugClient.php @@ -46,7 +46,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".'); } if (!interface_exists(RequestFactoryInterface::class)) { diff --git a/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php b/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php index 27745e46d881f..85d7e01d6c96f 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 d46e4036d801c..9aad826417360 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 AmpClientState $multi; private ?array $options; - private CancellationTokenSource $canceller; private \Closure $onProgress; private static ?string $delay = null; @@ -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(); @@ -349,7 +348,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 bfea82b6ffa5c..9c151546a4472 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -79,17 +79,7 @@ public function __construct(CurlClientState $multi, \CurlHandle|string $ch, arra } 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) { @@ -366,19 +356,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); @@ -401,7 +401,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 23dc7b6d9f0b0..8bec22a1ebbad 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 1e9adde053093..34a1714bf3dd1 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -36,6 +36,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": "^5.4|^6.0", "symfony/http-kernel": "^5.4|^6.0", diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index 72cef7c0ea114..749be5b6c1525 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -319,7 +319,7 @@ public function sendContent(): static 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/HeaderBag.php b/src/Symfony/Component/HttpFoundation/HeaderBag.php index 0883024b3b50b..dc5a9a60a2e48 100644 --- a/src/Symfony/Component/HttpFoundation/HeaderBag.php +++ b/src/Symfony/Component/HttpFoundation/HeaderBag.php @@ -63,7 +63,7 @@ public function __toString(): string * * @param string|null $key The name of the headers to return or null to get them all * - * @return array>|array + * @return array>|list */ public function all(string $key = null): array { diff --git a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php index 168f69efe848f..02f6c64cfce38 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,44 @@ 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 { + public function __construct( + private RequestContext $requestContext, + ) { + } + + public function setContext(RequestContext $context): void + { + $this->requestContext = $context; + } + + public function getContext(): RequestContext + { + 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 42fcf0459ab8a..d5641eff86d58 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. @@ -20,13 +21,11 @@ */ final class UrlHelper { - private RequestStack $requestStack; - private ?RequestContext $requestContext; - public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) - { - $this->requestStack = $requestStack; - $this->requestContext = $requestContext; + public function __construct( + private RequestStack $requestStack, + private RequestContextAwareInterface|RequestContext|null $requestContext = null, + ) { } public function getAbsoluteUrl(string $path): string @@ -73,28 +72,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/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index 52ac242141af6..049902585a734 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -78,7 +78,7 @@ public function getArguments(Request $request, callable $controller, \Reflection $representative = get_debug_type($representative); } - throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.', $representative, $metadata->getName())); + throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or there is a non-optional argument after this one.', $representative, $metadata->getName())); } return $arguments; diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php index e7be3b88e7a34..799679effc0fc 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\DependencyInjection; +use ProxyManager\Proxy\LazyLoadingInterface; +use Symfony\Component\VarExporter\LazyObjectInterface; use Symfony\Contracts\Service\ResetInterface; /** @@ -39,6 +41,14 @@ public function __construct(\Traversable $resettableServices, array $resetMethod public function reset() { foreach ($this->resettableServices as $id => $service) { + if ($service instanceof LazyObjectInterface && !$service->isLazyObjectInitialized(true)) { + continue; + } + + if ($service instanceof LazyLoadingInterface && !$service->isProxyInitialized()) { + continue; + } + foreach ((array) $this->resetMethods[$id] as $resetMethod) { if ('?' === $resetMethod[0] && !method_exists($service, $resetMethod = substr($resetMethod, 1))) { continue; diff --git a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php index ca01a9240b373..a7882f531d606 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php @@ -119,4 +119,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('xxh128', $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 9e7c3f898ab83..08e0e899a1ba3 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php @@ -71,8 +71,8 @@ public function process(Request $request, Response $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])) { @@ -86,16 +86,10 @@ public function process(Request $request, Response $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 f77a99e0ffa83..fb84d96428f42 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 HttpKernelInterface $kernel; private StoreInterface $store; private Request $request; @@ -614,12 +616,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 0c9f10823a5c4..05b5f5d7d60d0 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php @@ -148,7 +148,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 4bd1cef0a7885..4ca5cc6bfacd1 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php @@ -52,9 +52,8 @@ public function process(Request $request, Response $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])) { @@ -68,14 +67,10 @@ public function process(Request $request, Response $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 b21cb45874ea7..f4a855e177864 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php @@ -467,15 +467,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; + } + } } - 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 e9694e978e3c5..1ad714e6d98a9 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -75,11 +75,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.2.10'; - public const VERSION_ID = 60210; + public const VERSION = '6.2.11'; + public const VERSION_ID = 60211; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 10; + public const RELEASE_VERSION = 11; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2023'; diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php index 604d2b0d13b82..3390dcc1e4d64 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php @@ -14,8 +14,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService; +use Symfony\Component\HttpKernel\Tests\Fixtures\LazyResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService; +use Symfony\Component\VarExporter\ProxyHelper; class ServicesResetterTest extends TestCase { @@ -46,4 +48,29 @@ public function testResetServices() $this->assertSame(1, MultiResettableService::$resetFirstCounter); $this->assertSame(1, MultiResettableService::$resetSecondCounter); } + + public function testResetLazyServices() + { + $proxyCode = ProxyHelper::generateLazyProxy(new \ReflectionClass(LazyResettableService::class)); + eval('class LazyResettableServiceProxy'.$proxyCode); + + $lazyService = \LazyResettableServiceProxy::createLazyProxy(fn (): LazyResettableService => new LazyResettableService()); + + $resetter = new ServicesResetter(new \ArrayIterator([ + 'lazy' => $lazyService, + ]), [ + 'lazy' => ['reset'], + ]); + + $resetter->reset(); + $this->assertSame(0, LazyResettableService::$counter); + + $resetter->reset(); + $this->assertSame(0, LazyResettableService::$counter); + + $this->assertTrue($lazyService->foo()); + + $resetter->reset(); + $this->assertSame(1, LazyResettableService::$counter); + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php index 78c8333af6a3d..90d9aa6d33544 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/CacheAttributeListenerTest.php @@ -149,7 +149,7 @@ public function testAttributeConfigurationsAreSetOnResponse() $this->assertSame('5', $this->response->headers->getCacheControlDirective('max-stale')); $this->assertSame('6', $this->response->headers->getCacheControlDirective('stale-while-revalidate')); $this->assertSame('7', $this->response->headers->getCacheControlDirective('stale-if-error')); - $this->assertInstanceOf(\DateTime::class, $this->response->getExpires()); + $this->assertInstanceOf(\DateTimeInterface::class, $this->response->getExpires()); } public function testCacheMaxAgeSupportsStrtotimeFormat() diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/LazyResettableService.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/LazyResettableService.php new file mode 100644 index 0000000000000..543cf0d9538d3 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/LazyResettableService.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Fixtures; + +class LazyResettableService +{ + public static $counter = 0; + + public function foo(): bool + { + return true; + } + + public function reset(): void + { + ++self::$counter; + } +} 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 bce749e85cc91..0cf32f6020f3c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php @@ -19,7 +19,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 72c12c75f2e77..fefa64572a60c 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -275,7 +275,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 014009e185bdd..dccadc01985d4 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('xxh128', 'test')), $response->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', '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('xxh128', 'test 3')), $this->store->lookup($req3)->getContent()); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 2')), $this->store->lookup($req2)->getContent()); - $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 1')), $this->store->lookup($req1)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File')); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File')); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', '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('xxh128', 'test 1')), $this->store->lookup($req1)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', '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('xxh128', 'test 2')), $this->store->lookup($req2)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', '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('xxh128', 'test 3')), $this->store->lookup($req3)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('xxh128', '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 = []) { $path ??= '/test'; diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index b6fb8c2d1fb5a..9ee322035cd32 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -40,6 +40,7 @@ "symfony/translation": "^5.4|^6.0", "symfony/translation-contracts": "^1.1|^2|^3", "symfony/uid": "^5.4|^6.0", + "symfony/var-exporter": "^6.2", "psr/cache": "^1.0|^2.0|^3.0", "twig/twig": "^2.13|^3.0.4" }, diff --git a/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php b/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php index c25c8b2c974dc..c0acf6f6c3d3b 100644 --- a/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php +++ b/src/Symfony/Component/Messenger/Attribute/AsMessageHandler.php @@ -16,7 +16,7 @@ * * @author Alireza Mirsepassi */ -#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] class AsMessageHandler { public function __construct( diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index 1763fe245a0e8..a25930bbd33f9 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -131,7 +131,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 0f071e82e702d..0af2673d1e13d 100644 --- a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php +++ b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php @@ -117,7 +117,8 @@ public static function getSubscribedEvents(): array 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; } @@ -126,7 +127,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; } @@ -144,7 +145,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 a72efcc15e29e..cffeac40ad22d 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -360,7 +360,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 fccdae9bfa215..cb79f539dc325 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 ce2f2e3d211d6..c06855c3146dc 100644 --- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php +++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php @@ -51,7 +51,7 @@ public function recipient(string $topic): static } /** - * @see PublishInput::$Subject + * @see PublishInput::$subject * * @return $this */ @@ -63,7 +63,7 @@ public function subject(string $subject): static } /** - * @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/Notifier/Bridge/TurboSms/Tests/TurboSmsTransportTest.php b/src/Symfony/Component/Notifier/Bridge/TurboSms/Tests/TurboSmsTransportTest.php index 386a69bdaeaa8..136ee2feb433c 100644 --- a/src/Symfony/Component/Notifier/Bridge/TurboSms/Tests/TurboSmsTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/TurboSms/Tests/TurboSmsTransportTest.php @@ -71,7 +71,16 @@ public function testSuccessfulSend() ])) ; - $client = new MockHttpClient(static function () use ($response): ResponseInterface { + $client = new MockHttpClient(static function (string $method, string $url, array $options) use ($response): ResponseInterface { + $body = json_decode($options['body'], true); + self::assertSame([ + 'sms' => [ + 'sender' => 'sender', + 'recipients' => ['380931234567'], + 'text' => 'Тест/Test', + ], + ], $body); + return $response; }); diff --git a/src/Symfony/Component/Notifier/Bridge/TurboSms/TurboSmsTransport.php b/src/Symfony/Component/Notifier/Bridge/TurboSms/TurboSmsTransport.php index 1d885abd2d07d..089e84a71b70e 100644 --- a/src/Symfony/Component/Notifier/Bridge/TurboSms/TurboSmsTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/TurboSms/TurboSmsTransport.php @@ -66,7 +66,8 @@ protected function doSend(MessageInterface $message): SentMessage $this->assertValidSubject($message->getSubject()); $fromMessage = $message->getFrom(); - if (null !== $fromMessage) { + + if ($fromMessage) { $this->assertValidFrom($fromMessage); $from = $fromMessage; } else { diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 63a402a55fc75..4436c6a16e996 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -421,7 +421,7 @@ public function wait(callable $callback = null): int 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 ad44373c1b650..4c4ebdd25e21e 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -1536,6 +1536,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/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 990d77b7a5ed0..83f9a225a1569 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -301,7 +301,7 @@ private function readPropertiesUntil(array $zval, PropertyPathInterface $propert if (($zval[self::VALUE] instanceof \ArrayAccess && !$zval[self::VALUE]->offsetExists($property)) || (\is_array($zval[self::VALUE]) && !isset($zval[self::VALUE][$property]) && !\array_key_exists($property, $zval[self::VALUE])) ) { - if (!$ignoreInvalidIndices) { + if (!$ignoreInvalidIndices && !$isNullSafe) { if (!\is_array($zval[self::VALUE])) { if (!$zval[self::VALUE] instanceof \Traversable) { throw new NoSuchIndexException(sprintf('Cannot read index "%s" while trying to traverse path "%s".', $property, (string) $propertyPath)); diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index c5ecd79be1bbf..ae773e91d87ec 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -573,12 +573,28 @@ public static function getValidReadPropertyPaths(): iterable yield [(object) ['foo' => null], 'foo?.bar.baz', null]; yield [(object) ['foo' => (object) ['bar' => null]], 'foo?.bar?.baz', null]; yield [(object) ['foo' => (object) ['bar' => null]], 'foo.bar?.baz', null]; + + yield from self::getNullSafeIndexPaths(); + } + + public static function getNullSafeIndexPaths(): iterable + { yield [(object) ['foo' => ['bar' => null]], 'foo[bar?].baz', null]; yield [[], '[foo?]', null]; yield [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?]', null]; yield [['foo' => ['firstName' => 'Bernhard']], '[foo][bar?][baz?]', null]; } + /** + * @dataProvider getNullSafeIndexPaths + */ + public function testNullSafeIndexWithThrowOnInvalidIndex($objectOrArray, $path, $value) + { + $this->propertyAccessor = new PropertyAccessor(PropertyAccessor::DISALLOW_MAGIC_METHODS, PropertyAccessor::THROW_ON_INVALID_INDEX | PropertyAccessor::THROW_ON_INVALID_PROPERTY_PATH); + + $this->assertSame($value, $this->propertyAccessor->getValue($objectOrArray, $path)); + } + public function testTicket5755() { $object = new Ticket5775Object(); diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php index 47b3e9497287b..e8fa2077a8fe8 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php @@ -179,7 +179,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 6985e2d55e939..1ef1562f9d8e5 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -142,7 +142,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', @@ -238,6 +238,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, 'DateTimeImmutable')], 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 99a5e3d0a4dc4..f3405d0409ae3 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; @@ -355,6 +356,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 2bcb1963ff4ec..1f3ba505ddcfb 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -59,6 +59,8 @@ public function testGetProperties() 'i', 'j', 'nullableCollectionOfNonNullableElements', + 'nonNullableCollectionOfNullableElements', + 'nullableCollectionOfMultipleNonNullableElementTypes', 'emptyVar', 'iteratorCollection', 'iteratorCollectionWithKey', @@ -121,6 +123,8 @@ public function testGetPropertiesWithCustomPrefixes() 'i', 'j', 'nullableCollectionOfNonNullableElements', + 'nonNullableCollectionOfNullableElements', + 'nullableCollectionOfMultipleNonNullableElementTypes', 'emptyVar', 'iteratorCollection', 'iteratorCollectionWithKey', @@ -172,6 +176,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 6682c8440f0d7..5b38fc1bcc722 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 6cf083bb4ee86..754b6ae5fc4ec 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -118,15 +118,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); } // Cannot guess @@ -134,27 +129,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); } if ($type instanceof PseudoType) { diff --git a/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php b/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php index 032821a18b69e..ec00bc1d9be6e 100644 --- a/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/CsrfTokenClearingLogoutListener.php @@ -13,6 +13,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Csrf\TokenStorage\ClearableTokenStorageInterface; +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; use Symfony\Component\Security\Http\Event\LogoutEvent; /** @@ -31,6 +32,10 @@ public function __construct(ClearableTokenStorageInterface $csrfTokenStorage) public function onLogout(LogoutEvent $event): void { + if ($this->csrfTokenStorage instanceof SessionTokenStorage && !$event->getRequest()->hasPreviousSession()) { + return; + } + $this->csrfTokenStorage->clear(); } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfTokenClearingLogoutListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfTokenClearingLogoutListenerTest.php new file mode 100644 index 0000000000000..405c7ae085510 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CsrfTokenClearingLogoutListenerTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\EventListener; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage; +use Symfony\Component\Security\Http\Event\LogoutEvent; +use Symfony\Component\Security\Http\EventListener\CsrfTokenClearingLogoutListener; + +class CsrfTokenClearingLogoutListenerTest extends TestCase +{ + public function testSkipsClearingSessionTokenStorageOnStatelessRequest() + { + try { + (new CsrfTokenClearingLogoutListener( + new SessionTokenStorage(new RequestStack()) + ))->onLogout(new LogoutEvent(new Request(), null)); + } catch (SessionNotFoundException) { + $this->fail('clear() must not be called if the request is not associated with a session instance'); + } + + $this->addToAssertionCount(1); + } +} diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 6a0f290796b1d..e2b6c1bc8d511 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -322,6 +322,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(); } @@ -412,6 +413,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/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index afd3acabc46a2..69dc534e417fb 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -128,8 +128,8 @@ public function __construct(ClassMetadataFactoryInterface $classMetadataFactory $this->propertyTypeExtractor = $propertyTypeExtractor; - if (null === $classDiscriminatorResolver && null !== $classMetadataFactory) { - $classDiscriminatorResolver = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + if ($classMetadataFactory) { + $classDiscriminatorResolver ??= new ClassDiscriminatorFromClassMetadata($classMetadataFactory); } $this->classDiscriminatorResolver = $classDiscriminatorResolver; $this->objectClassResolver = $objectClassResolver; @@ -217,7 +217,7 @@ public function normalize(mixed $object, string $format = null, array $context = } $preserveEmptyObjects = $context[self::PRESERVE_EMPTY_OBJECTS] ?? $this->defaultContext[self::PRESERVE_EMPTY_OBJECTS] ?? false; - if ($preserveEmptyObjects && !\count($data)) { + if ($preserveEmptyObjects && !$data) { return new \ArrayObject(); } @@ -226,19 +226,8 @@ public function normalize(mixed $object, string $format = null, array $context = protected function instantiateObject(array &$data, string $class, array &$context, \ReflectionClass $reflectionClass, array|bool $allowedAttributes, string $format = null) { - if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { - if (!isset($data[$mapping->getTypeProperty()])) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); - } - - $type = $data[$mapping->getTypeProperty()]; - if (null === ($mappedClass = $mapping->getClassForType($type))) { - throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true); - } - - if ($mappedClass !== $class) { - return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format); - } + if ($class !== $mappedClass = $this->getMappedClass($data, $class, $context)) { + return $this->instantiateObject($data, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format); } return parent::instantiateObject($data, $class, $context, $reflectionClass, $allowedAttributes, $format); @@ -270,7 +259,7 @@ protected function getAttributes(object $object, ?string $format, array $context $attributes = $this->extractAttributes($object, $format, $context); - if ($this->classDiscriminatorResolver && $mapping = $this->classDiscriminatorResolver->getMappingForMappedObject($object)) { + if ($mapping = $this->classDiscriminatorResolver?->getMappingForMappedObject($object)) { array_unshift($attributes, $mapping->getTypeProperty()); } @@ -319,11 +308,9 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $normalizedData = $this->prepareForDenormalization($data); $extraAttributes = []; - $reflectionClass = new \ReflectionClass($type); - $object = $this->instantiateObject($normalizedData, $type, $context, $reflectionClass, $allowedAttributes, $format); - $resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : $object::class; + $mappedClass = $this->getMappedClass($normalizedData, $type, $context); - $nestedAttributes = $this->getNestedAttributes($resolvedClass); + $nestedAttributes = $this->getNestedAttributes($mappedClass); $nestedData = []; $propertyAccessor = PropertyAccess::createPropertyAccessor(); foreach ($nestedAttributes as $property => $serializedPath) { @@ -336,6 +323,9 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $normalizedData = array_merge($normalizedData, $nestedData); + $object = $this->instantiateObject($normalizedData, $mappedClass, $context, new \ReflectionClass($mappedClass), $allowedAttributes, $format); + $resolvedClass = $this->objectClassResolver ? ($this->objectClassResolver)($object) : $object::class; + foreach ($normalizedData as $attribute => $value) { if ($this->nameConverter) { $notConverted = $attribute; @@ -675,11 +665,8 @@ private function updateData(array $data, string $attribute, mixed $attributeValu */ private function isMaxDepthReached(array $attributesMetadata, string $class, string $attribute, array &$context): bool { - $enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false; - if ( - !$enableMaxDepth || - !isset($attributesMetadata[$attribute]) || - null === $maxDepth = $attributesMetadata[$attribute]->getMaxDepth() + if (!($enableMaxDepth = $context[self::ENABLE_MAX_DEPTH] ?? $this->defaultContext[self::ENABLE_MAX_DEPTH] ?? false) + || null === $maxDepth = $attributesMetadata[$attribute]?->getMaxDepth() ) { return false; } @@ -755,7 +742,7 @@ private function isUninitializedValueError(\Error $e): bool */ private function getNestedAttributes(string $class): array { - if (!$this->classMetadataFactory || !$this->classMetadataFactory->hasMetadataFor($class)) { + if (!$this->classMetadataFactory?->hasMetadataFor($class)) { return []; } @@ -781,15 +768,30 @@ private function getNestedAttributes(string $class): array private function removeNestedValue(array $path, array $data): array { $element = array_shift($path); - if ([] === $path) { + if (!$path || !$data[$element] = $this->removeNestedValue($path, $data[$element])) { unset($data[$element]); - } else { - $data[$element] = $this->removeNestedValue($path, $data[$element]); - if ([] === $data[$element]) { - unset($data[$element]); - } } return $data; } + + /** + * @return class-string + */ + private function getMappedClass(array $data, string $class, array $context): string + { + if (!$mapping = $this->classDiscriminatorResolver?->getMappingForClass($class)) { + return $class; + } + + if (null === $type = $data[$mapping->getTypeProperty()] ?? null) { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); + } + + if (null === $mappedClass = $mapping->getClassForType($type)) { + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true); + } + + return $mappedClass; + } } diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php index 26943377b654a..18da1ae67d5ba 100644 --- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php @@ -52,7 +52,11 @@ public function denormalize(mixed $data, string $type, string $format = null, ar 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 096f766fadb77..b6e505b7ac518 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -83,7 +83,14 @@ public function denormalize(mixed $data, string $type, string $format = null, ar $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/Annotations/SerializedPathInConstructorDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/SerializedPathInConstructorDummy.php new file mode 100644 index 0000000000000..a6d5109086899 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/SerializedPathInConstructorDummy.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures\Annotations; + +use Symfony\Component\Serializer\Annotation\SerializedPath; + +class SerializedPathInConstructorDummy +{ + public function __construct( + /** + * @SerializedPath("[one][two]") + */ + public $three, + ) { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathInConstructorDummy.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathInConstructorDummy.php new file mode 100644 index 0000000000000..90aee115417d4 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/SerializedPathInConstructorDummy.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures\Attributes; + +use Symfony\Component\Serializer\Annotation\SerializedPath; + +class SerializedPathInConstructorDummy +{ + public function __construct( + #[SerializedPath('[one][two]')] public $three, + ) { + } +} 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 @@ + + + + + diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml index 7519b979efa96..e052d65a88779 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/serialization.yml @@ -22,6 +22,10 @@ serialized_path: '[one][two]' seven: serialized_path: '[three][four]' +'Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy': + attributes: + three: + serialized_path: '[one][two]' 'Symfony\Component\Serializer\Tests\Fixtures\Annotations\AbstractDummy': discriminator_map: type_property: type diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php index 5ce1931ba0cab..903612c684c0d 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Factory/ClassMetadataFactoryCompilerTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Annotations\MaxDepthDummy; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathDummy; +use Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy; use Symfony\Component\Serializer\Tests\Fixtures\Dummy; final class ClassMetadataFactoryCompilerTest extends TestCase @@ -46,18 +47,20 @@ public function testItDumpMetadata() $maxDepthDummyMetadata = $classMetatadataFactory->getMetadataFor(MaxDepthDummy::class); $serializedNameDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedNameDummy::class); $serializedPathDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathDummy::class); + $serializedPathInConstructorDummyMetadata = $classMetatadataFactory->getMetadataFor(SerializedPathInConstructorDummy::class); $code = (new ClassMetadataFactoryCompiler())->compile([ $dummyMetadata, $maxDepthDummyMetadata, $serializedNameDummyMetadata, $serializedPathDummyMetadata, + $serializedPathInConstructorDummyMetadata, ]); file_put_contents($this->dumpPath, $code); $compiledMetadata = require $this->dumpPath; - $this->assertCount(4, $compiledMetadata); + $this->assertCount(5, $compiledMetadata); $this->assertArrayHasKey(Dummy::class, $compiledMetadata); $this->assertEquals([ @@ -99,5 +102,13 @@ public function testItDumpMetadata() ], null, ], $compiledMetadata[SerializedPathDummy::class]); + + $this->assertArrayHasKey(SerializedPathInConstructorDummy::class, $compiledMetadata); + $this->assertEquals([ + [ + 'three' => [[], null, null, '[one][two]'], + ], + null, + ], $compiledMetadata[SerializedPathInConstructorDummy::class]); } } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php index de7accd844b32..2dbd03703a2ce 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTestCase.php @@ -103,6 +103,15 @@ public function testLoadSerializedPath() $this->assertEquals(new PropertyPath('[three][four]'), $attributesMetadata['seven']->getSerializedPath()); } + public function testLoadSerializedPathInConstructor() + { + $classMetadata = new ClassMetadata($this->getNamespace().'\SerializedPathInConstructorDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(new PropertyPath('[one][two]'), $attributesMetadata['three']->getSerializedPath()); + } + public function testLoadClassMetadataAndMerge() { $classMetadata = new ClassMetadata($this->getNamespace().'\GroupDummy'); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php index b1e9ed7222636..202534f56fca5 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/XmlFileLoaderTest.php @@ -94,6 +94,15 @@ public function testSerializedPath() $this->assertEquals('[three][four]', $attributesMetadata['seven']->getSerializedPath()); } + public function testSerializedPathInConstructor() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals('[one][two]', $attributesMetadata['three']->getSerializedPath()); + } + public function testLoadDiscriminatorMap() { $classMetadata = new ClassMetadata(AbstractDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php index bbe0a99aeab89..dcfd2b4afac51 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/YamlFileLoaderTest.php @@ -108,6 +108,15 @@ public function testSerializedPath() $this->assertEquals(new PropertyPath('[three][four]'), $attributesMetadata['seven']->getSerializedPath()); } + public function testSerializedPathInConstructor() + { + $classMetadata = new ClassMetadata('Symfony\Component\Serializer\Tests\Fixtures\Annotations\SerializedPathInConstructorDummy'); + $this->loader->loadClassMetadata($classMetadata); + + $attributesMetadata = $classMetadata->getAttributesMetadata(); + $this->assertEquals(new PropertyPath('[one][two]'), $attributesMetadata['three']->getSerializedPath()); + } + public function testLoadDiscriminatorMap() { $classMetadata = new ClassMetadata(AbstractDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 0c342b64d8000..d876193c8a741 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Annotation\Context; +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\SerializedName; use Symfony\Component\Serializer\Annotation\SerializedPath; use Symfony\Component\Serializer\Exception\ExtraAttributesException; @@ -171,6 +172,53 @@ public function testDenormalizeWithNestedAttributesDuplicateKeys() $normalizer->denormalize($data, DuplicateKeyNestedDummy::class, 'any'); } + public function testDenormalizeWithNestedAttributesInConstructor() + { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $data = [ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ]; + $test = $normalizer->denormalize($data, NestedDummyWithConstructor::class, 'any'); + $this->assertSame('foo', $test->foo); + $this->assertSame('quux', $test->quux); + $this->assertSame('notfoo', $test->notfoo); + $this->assertSame('baz', $test->baz); + } + + public function testDenormalizeWithNestedAttributesInConstructorAndDiscriminatorMap() + { + $normalizer = new AbstractObjectNormalizerWithMetadata(); + $data = [ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ]; + + $test1 = $normalizer->denormalize($data + ['type' => 'first'], AbstractNestedDummyWithConstructorAndDiscriminator::class, 'any'); + $this->assertInstanceOf(FirstNestedDummyWithConstructorAndDiscriminator::class, $test1); + $this->assertSame('foo', $test1->foo); + $this->assertSame('notfoo', $test1->notfoo); + $this->assertSame('baz', $test1->baz); + + $test2 = $normalizer->denormalize($data + ['type' => 'second'], AbstractNestedDummyWithConstructorAndDiscriminator::class, 'any'); + $this->assertInstanceOf(SecondNestedDummyWithConstructorAndDiscriminator::class, $test2); + $this->assertSame('quux', $test2->quux); + $this->assertSame('notfoo', $test2->notfoo); + $this->assertSame('baz', $test2->baz); + } + public function testNormalizeWithNestedAttributesMixingArrayTypes() { $this->expectException(LogicException::class); @@ -236,6 +284,52 @@ public function testNormalizeWithNestedAttributesWithoutMetadata() $this->assertSame($data, $test); } + public function testNormalizeWithNestedAttributesInConstructor() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $test = $normalizer->normalize(new NestedDummyWithConstructor('foo', 'quux', 'notfoo', 'baz'), 'any'); + $this->assertSame([ + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ], $test); + } + + public function testNormalizeWithNestedAttributesInConstructorAndDiscriminatorMap() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $normalizer = new ObjectNormalizer($classMetadataFactory, new MetadataAwareNameConverter($classMetadataFactory)); + + $test1 = $normalizer->normalize(new FirstNestedDummyWithConstructorAndDiscriminator('foo', 'notfoo', 'baz'), 'any'); + $this->assertSame([ + 'type' => 'first', + 'one' => [ + 'two' => [ + 'three' => 'foo', + ], + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ], $test1); + + $test2 = $normalizer->normalize(new SecondNestedDummyWithConstructorAndDiscriminator('quux', 'notfoo', 'baz'), 'any'); + $this->assertSame([ + 'type' => 'second', + 'one' => [ + 'four' => 'quux', + ], + 'foo' => 'notfoo', + 'baz' => 'baz', + ], $test2); + } + public function testDenormalizeCollectionDecodedFromXmlWithOneChild() { $denormalizer = $this->getDenormalizerForDummyCollection(); @@ -661,6 +755,78 @@ class NestedDummy public $baz; } +class NestedDummyWithConstructor +{ + public function __construct( + /** + * @SerializedPath("[one][two][three]") + */ + public $foo, + + /** + * @SerializedPath("[one][four]") + */ + public $quux, + + /** + * @SerializedPath("[foo]") + */ + public $notfoo, + + public $baz, + ) { + } +} + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "first" = FirstNestedDummyWithConstructorAndDiscriminator::class, + * "second" = SecondNestedDummyWithConstructorAndDiscriminator::class, + * }) + */ +abstract class AbstractNestedDummyWithConstructorAndDiscriminator +{ + public function __construct( + /** + * @SerializedPath("[foo]") + */ + public $notfoo, + + public $baz, + ) { + } +} + +class FirstNestedDummyWithConstructorAndDiscriminator extends AbstractNestedDummyWithConstructorAndDiscriminator +{ + public function __construct( + /** + * @SerializedPath("[one][two][three]") + */ + public $foo, + + $notfoo, + $baz, + ) { + parent::__construct($notfoo, $baz); + } +} + +class SecondNestedDummyWithConstructorAndDiscriminator extends AbstractNestedDummyWithConstructorAndDiscriminator +{ + public function __construct( + /** + * @SerializedPath("[one][four]") + */ + public $quux, + + $notfoo, + $baz, + ) { + parent::__construct($notfoo, $baz); + } +} + class DuplicateKeyNestedDummy { /** @@ -711,7 +877,9 @@ protected function getAttributeValue(object $object, string $attribute, string $ protected function setAttributeValue(object $object, string $attribute, $value, string $format = null, array $context = []) { - $object->$attribute = $value; + if (property_exists($object, $attribute)) { + $object->$attribute = $value; + } } } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php index b0063da5fe4e7..ef58a7f25b8fe 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/BackedEnumNormalizerTest.php @@ -88,7 +88,7 @@ public function testDenormalizeObjectThrowsException() public function testDenormalizeBadBackingValueThrowsException() { - $this->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 908f4b352a567..ee2693e54e42a 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; @@ -1232,7 +1233,48 @@ public function testCollectDenormalizationErrorsWithEnumConstructor() $this->assertSame($expected, $exceptionsAsArray); } - 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); + } + + public function testNoCollectDenormalizationErrorsWithWrongEnumOnConstructor() { $serializer = new Serializer( [ @@ -1243,7 +1285,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 e240493953731..e63a7d05bec94 100644 --- a/src/Symfony/Component/Translation/Loader/ArrayLoader.php +++ b/src/Symfony/Component/Translation/Loader/ArrayLoader.php @@ -43,9 +43,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 d6e690c02e8b5..f1a00aa72ca39 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -44,7 +44,6 @@ "conflict": { "doctrine/annotations": "<1.13", "doctrine/lexer": "<1.1", - "phpunit/phpunit": "<5.4.3", "symfony/dependency-injection": "<5.4", "symfony/expression-language": "<5.4", "symfony/http-kernel": "<5.4", diff --git a/src/Symfony/Component/VarDumper/composer.json b/src/Symfony/Component/VarDumper/composer.json index 71ec64c0de917..787f7c0b34b49 100644 --- a/src/Symfony/Component/VarDumper/composer.json +++ b/src/Symfony/Component/VarDumper/composer.json @@ -27,7 +27,6 @@ "twig/twig": "^2.13|^3.0.4" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<5.4" }, "suggest": { pFad - Phonifier reborn

    Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

    Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


    Alternative Proxies:

    Alternative Proxy

    pFad Proxy

    pFad v3 Proxy

    pFad v4 Proxy

  • LocaleLocaleFallback localeDomainDomain Times used Message ID Message Preview
    {{ message.locale }}{{ message.fallbackLocale|default('-') }}