From f23a9969fc96803007f6641cae217996e23f2a68 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:23:29 +0200 Subject: [PATCH 01/73] Update CHANGELOG for 4.4.44 --- CHANGELOG-4.4.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md index 0231db585bc0b..9e6451685ab5c 100644 --- a/CHANGELOG-4.4.md +++ b/CHANGELOG-4.4.md @@ -7,6 +7,31 @@ in 4.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.4.0...v4.4.1 +* 4.4.44 (2022-07-29) + + * bug #47069 [Security] Allow redirect after login to absolute URLs (Tim Ward) + * bug #47073 [HttpKernel] Fix non-scalar check in surrogate fragment renderer (aschempp) + * bug #43329 [Serializer] Respect default context in DateTimeNormalizer::denormalize (hultberg) + * bug #47086 Workaround disabled "var_dump" (nicolas-grekas) + * bug #40828 [BrowserKit] Merge fields and files recursively if they are multidimensional array (januszmk) + * bug #47048 [Serializer] Fix XmlEncoder encoding attribute false (alamirault) + * bug #47000 [ErrorHandler] Fix return type patching for list and class-string pseudo types (derrabus) + * bug #43998 [HttpKernel] [HttpCache] Don't throw on 304 Not Modified (aleho) + * bug #46981 [Mime]  quote address names if they contain parentheses (xabbuh) + * bug #46960 [FrameworkBundle] Fail gracefully when forms use disabled CSRF (HeahDude) + * bug #46973 [DependencyInjection] Fail gracefully when attempting to autowire composite types (derrabus) + * bug #46963 [Mime] Fix inline parts when added via attachPart() (fabpot) + * bug #46968 [PropertyInfo] Make sure nested composite types do not crash ReflectionExtractor (derrabus) + * bug #46931 Flush backend output buffer after closing. (bradjones1) + * bug #46905 [BrowserKit] fix sending request to paths containing multiple slashes (xabbuh) + * bug #42033 [HttpFoundation] Fix deleteFileAfterSend on client abortion (nerg4l) + * bug #46941 [Messenger] Fix calls to deprecated DBAL methods (derrabus) + * bug #46863 [Mime] Fix invalid DKIM signature with multiple parts (BrokenSourceCode) + * bug #46808 [HttpFoundation] Fix TypeError on null `$_SESSION` in `NativeSessionStorage::save()` (chalasr) + * bug #46790 [HttpFoundation] Prevent PHP Warning: Session ID is too long or contains illegal characters (BrokenSourceCode) + * bug #46800 Spaces in system temp folder path cause deprecation errors in php 8 (demeritcowboy) + * bug #46797 [Messenger] Ceil waiting time when multiplier is a float on retry (WissameMekhilef) + * 4.4.43 (2022-06-26) * bug #46765 [Serializer] Fix denormalization union types with constructor (Gwemox) From db43e04b4df6433ca21a1b2da745f043ce02bd70 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:23:36 +0200 Subject: [PATCH 02/73] Update CONTRIBUTORS for 4.4.44 --- CONTRIBUTORS.md | 171 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 57 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ec4fddccc86be..99d8c121d9fce 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -21,8 +21,8 @@ The Symfony Connect username in parenthesis allows to get more information - Jordi Boggiano (seldaek) - Roland Franssen (ro0) - Victor Berchet (victor) - - Tobias Nyholm (tobias) - Yonel Ceruto (yonelceruto) + - Tobias Nyholm (tobias) - Oskar Stark (oskarstark) - Ryan Weaver (weaverryan) - Javier Eguiluz (javier.eguiluz) @@ -34,35 +34,35 @@ The Symfony Connect username in parenthesis allows to get more information - Samuel ROZE (sroze) - Pascal Borreli (pborreli) - Romain Neutron - - Joseph Bielawski (stloyd) - Jules Pietri (heah) + - Joseph Bielawski (stloyd) - Drak (drak) - Abdellatif Ait boudad (aitboudad) - Lukas Kahwe Smith (lsmith) - Jan Schädlich (jschaedl) - Martin Hasoň (hason) - - Jeremy Mikola (jmikola) - Jérôme Tamarelle (gromnan) + - Jeremy Mikola (jmikola) - Jean-François Simon (jfsimon) - Benjamin Eberlei (beberlei) - Kevin Bond (kbond) - Igor Wiedler - Valentin Udaltsov (vudaltsov) - Vasilij Duško (staff) + - HypeMC (hypemc) - Matthias Pigulla (mpdude) - Laurent VOULLEMIER (lvo) - Pierre du Plessis (pierredup) - - Grégoire Paris (greg0ire) - - Jonathan Wage (jwage) - Antoine Makdessi (amakdessi) - - HypeMC (hypemc) + - Grégoire Paris (greg0ire) - Gabriel Ostrolucký (gadelat) + - Jonathan Wage (jwage) - David Maicher (dmaicher) - Titouan Galopin (tgalopin) - Alexandre Salomé (alexandresalome) - William DURAND - - ornicar - Alexander Schranz (alexander-schranz) + - ornicar - Dany Maillard (maidmaid) - Eriksen Costa - Diego Saint Esteben (dosten) @@ -70,26 +70,26 @@ The Symfony Connect username in parenthesis allows to get more information - Alexander Mols (asm89) - Gábor Egyed (1ed) - Francis Besset (francisbesset) + - Alexandre Daubois (alexandre-daubois) - Vasilij Dusko | CREATION - - Bulat Shakirzyanov (avalanche123) - Mathieu Santostefano (welcomattic) + - Bulat Shakirzyanov (avalanche123) - Iltar van der Berg - - Alexandre Daubois (alexandre-daubois) - Miha Vrhovnik (mvrhov) - Saša Stamenković (umpirsky) - Mathieu Piot (mpiot) - - Guilhem N (guilhemn) - Alex Pott + - Guilhem N (guilhemn) - Vladimir Reznichenko (kalessil) - Sarah Khalil (saro0h) - Konstantin Kudryashov (everzet) + - Vincent Langlet (deviling) - Bilal Amarni (bamarni) + - Tomas Norkūnas (norkunas) - Eriksen Costa - Florin Patan (florinpatan) - Peter Rehm (rpet) - - Tomas Norkūnas (norkunas) - Henrik Bjørnskov (henrikbjorn) - - Vincent Langlet (deviling) - Konstantin Myakshin (koc) - Andrej Hudec (pulzarraider) - Julien Falque (julienfalque) @@ -104,8 +104,8 @@ The Symfony Connect username in parenthesis allows to get more information - Fran Moreno (franmomu) - Jáchym Toušek (enumag) - Malte Schlüter (maltemaltesich) - - Vasilij Dusko - Mathias Arlaud (mtarld) + - Vasilij Dusko - Denis (yethee) - Arnout Boks (aboks) - Charles Sarrazin (csarrazi) @@ -125,14 +125,15 @@ The Symfony Connect username in parenthesis allows to get more information - Toni Uebernickel (havvg) - Bart van den Burg (burgov) - Jordan Alliot (jalliot) + - Smaine Milianni (ismail1432) - John Wards (johnwards) - Dariusz Ruminski - Lars Strojny (lstrojny) - - Smaine Milianni (ismail1432) + - Yanick Witschi (toflar) - Antoine Hérault (herzult) - Konstantin.Myakshin + - Rokas Mikalkėnas (rokasm) - Arman Hosseini (arman) - - Yanick Witschi (toflar) - Arnaud Le Blanc (arnaud-lb) - Maxime STEINHAUSSER - Peter Kokot (maastermedia) @@ -145,22 +146,23 @@ The Symfony Connect username in parenthesis allows to get more information - YaFou - Gary PEGEOT (gary-p) - Chris Wilkinson (thewilkybarkid) - - Rokas Mikalkėnas (rokasm) - Brice BERNARD (brikou) - Roman Martinuk (a2a4) - Gregor Harlan (gharlan) + - Antoine Lamirault - Baptiste Clavié (talus) - Adrien Brault (adrienbrault) - Michal Piotrowski - marc.weistroff - lenar + - Jesse Rushlow (geeshoe) - Théo FIDRY - jeremyFreeAgent (jeremyfreeagent) - Włodzimierz Gajda (gajdaw) - Christian Scheb - Guillaume (guill) - Mathieu Lechat (mat_the_cat) - - Jesse Rushlow (geeshoe) + - Tugdual Saunier (tucksaun) - Jacob Dreesen (jdreesen) - Joel Wurtz (brouznouf) - Michael Babker (mbabker) @@ -169,7 +171,6 @@ The Symfony Connect username in parenthesis allows to get more information - zairig imad (zairigimad) - Colin Frei - Javier Spagnoletti (phansys) - - Tugdual Saunier (tucksaun) - excelwebzone - Jérôme Parmentier (lctrs) - HeahDude @@ -209,6 +210,7 @@ The Symfony Connect username in parenthesis allows to get more information - Timo Bakx (timobakx) - Juti Noppornpitak (shiroyuki) - Joe Bennett (kralos) + - Hugo Alliaume (kocal) - Anthony MARTIN - Colin O'Dell (colinodell) - Sebastian Hörl (blogsh) @@ -223,16 +225,16 @@ The Symfony Connect username in parenthesis allows to get more information - Chi-teck - Guilliam Xavier - Nate Wiebe (natewiebe13) - - Hugo Alliaume (kocal) - Michael Voříšek - SpacePossum - - Antoine Lamirault + - Andreas Schempp (aschempp) - Pablo Godel (pgodel) - Romaric Drigon (romaricdrigon) - Andréia Bohner (andreia) - Jannik Zschiesche - Rafael Dohms (rdohms) - George Mponos (gmponos) + - Fritz Michael Gschwantner (fritzmg) - Aleksandar Jakovljevic (ajakov) - jwdeitch - Jurica Vlahoviček (vjurica) @@ -243,7 +245,8 @@ The Symfony Connect username in parenthesis allows to get more information - Farhad Safarov (safarov) - Jérémy Derussé - Nicolas Philippe (nikophil) - - Andreas Schempp (aschempp) + - Hubert Lenoir (hubert_lenoir) + - Florent Mata (fmata) - mcfedr (mcfedr) - Denis Brumann (dbrumann) - Maciej Malarz (malarzm) @@ -260,6 +263,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dmitrii Poddubnyi (karser) - soyuka - Rouven Weßling (realityking) + - BoShurik - Zmey - Clemens Tolboom - Oleg Voronkovich @@ -269,6 +273,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ben Hakim - Sylvain Fabre (sylfabre) - Filippo Tessarotto (slamdunk) + - Tom Van Looy (tvlooy) - 77web - Bohan Yang (brentybh) - Bastien Jaillot (bastnic) @@ -279,7 +284,6 @@ The Symfony Connect username in parenthesis allows to get more information - Dawid Nowak - Amal Raghav (kertz) - Jonathan Ingram - - Fritz Michael Gschwantner (fritzmg) - Artur Kotyrba - Tyson Andre - Thomas Landauer (thomas-landauer) @@ -291,11 +295,9 @@ The Symfony Connect username in parenthesis allows to get more information - Sebastien Morel (plopix) - Sergey (upyx) - Yoann RENARD (yrenard) - - BoShurik + - Thomas Lallement (raziel057) - Timothée Barray (tyx) - James Halsall (jaitsu) - - Hubert Lenoir (hubert_lenoir) - - Florent Mata (fmata) - Mikael Pajunen - Warnar Boekkooi (boekkooi) - Marco Petersen (ocrampete16) @@ -303,13 +305,13 @@ The Symfony Connect username in parenthesis allows to get more information - Dmitrii Chekaliuk (lazyhammer) - Clément JOBEILI (dator) - Vilius Grigaliūnas - - Tom Van Looy (tvlooy) - Marek Štípek (maryo) - Patrick Landolt (scube) - François Pluchino (francoispluchino) - Daniel Espendiller - Arnaud PETITPAS (apetitpa) - Dorian Villet (gnutix) + - Wojciech Kania - Alexey Kopytko (sanmai) - Sergey Linnik (linniksa) - Richard Miller @@ -330,7 +332,6 @@ The Symfony Connect username in parenthesis allows to get more information - Julien Pauli - Islam Israfilov (islam93) - Oleg Andreyev (oleg.andreyev) - - Thomas Lallement (raziel057) - Daniel Gorgan - Hendrik Luup (hluup) - Martin Herndl (herndlm) @@ -366,6 +367,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dominique Bongiraud - Hidde Wieringa (hiddewie) - Christopher Davis (chrisguitarguy) + - Lukáš Holeczy (holicz) - Florian Lonqueu-Brochard (florianlb) - Leszek Prabucki (l3l0) - Emanuele Panzeri (thepanz) @@ -391,7 +393,6 @@ The Symfony Connect username in parenthesis allows to get more information - Pascal Montoya - Julien Brochet - Michaël Perrin (michael.perrin) - - Wojciech Kania - Tristan Darricau (tristandsensio) - Fabien S (bafs) - Victor Bocharsky (bocharsky_bw) @@ -425,6 +426,7 @@ The Symfony Connect username in parenthesis allows to get more information - Iker Ibarguren (ikerib) - Manuel Reinhard (sprain) - Johann Pardanaud + - Alexis Lefebvre - Indra Gunawan (indragunawan) - Tim Goudriaan (codedmonkey) - Harm van Tilborg (hvt) @@ -449,7 +451,6 @@ The Symfony Connect username in parenthesis allows to get more information - Xavier Perez - Arjen Brouwer (arjenjb) - Tavo Nieves J (tavoniievez) - - Lukáš Holeczy (holicz) - Arjen van der Meijden - Patrick McDougle (patrick-mcdougle) - Jerzy (jlekowski) @@ -467,14 +468,17 @@ The Symfony Connect username in parenthesis allows to get more information - David Badura (davidbadura) - Uwe Jäger (uwej711) - Eugene Leonovich (rybakit) + - Damien Alexandre (damienalexandre) - Joseph Rouff (rouffj) - Félix Labrecque (woodspire) - GordonsLondon - Roman Anasal - Jan Sorgalla (jsor) - Piotr Kugla (piku235) + - Quynh Xuan Nguyen (seriquynh) - Ray - Philipp Cordes (corphi) + - Simon Podlipsky (simpod) - Chekote - bhavin (bhavin4u) - Pavel Popov (metaer) @@ -494,6 +498,7 @@ The Symfony Connect username in parenthesis allows to get more information - Frank de Jonge - Chris Tanaskoski - julien57 + - Loïc Frémont (loic425) - Ben Ramsey (ramsey) - Matthieu Auger (matthieuauger) - Josip Kruslin (jkruslin) @@ -505,12 +510,12 @@ The Symfony Connect username in parenthesis allows to get more information - Beau Simensen (simensen) - Robert Kiss (kepten) - Zan Baldwin (zanbaldwin) - - Alexis Lefebvre - Antonio J. García Lagar (ajgarlag) - Alexandre Quercia (alquerci) - Marcos Sánchez - Jérôme Tanghe (deuchnord) - Kim Hemsø Rasmussen (kimhemsoe) + - Maximilian Reichel (phramz) - Dane Powell - jaugustin - Dmytro Borysovskyi (dmytr0) @@ -530,7 +535,6 @@ The Symfony Connect username in parenthesis allows to get more information - Martin Kirilov (wucdbm) - Chris Smith (cs278) - Florian Klein (docteurklein) - - Damien Alexandre (damienalexandre) - Bilge - Rhodri Pugh (rodnaph) - Manuel Kiessling (manuelkiessling) @@ -544,15 +548,14 @@ The Symfony Connect username in parenthesis allows to get more information - Andrew Moore (finewolf) - Bertrand Zuchuat (garfield-fr) - Marc Morera (mmoreram) - - Quynh Xuan Nguyen (seriquynh) - Gabor Toth (tgabi333) - realmfoo - Thomas Tourlourat (armetiz) - Andrey Esaulov (andremaha) - - Simon Podlipsky (simpod) - Grégoire Passault (gregwar) - Jerzy Zawadzki (jzawadzki) - Ismael Ambrosi (iambrosi) + - Yannick Ihmels (ihmels) - Saif Eddin G - Emmanuel BORGES (eborges78) - Aurelijus Valeiša (aurelijus) @@ -566,6 +569,7 @@ The Symfony Connect username in parenthesis allows to get more information - Adrian Rudnik (kreischweide) - Pavel Batanov (scaytrase) - Francesc Rosàs (frosas) + - Andrii Dembitskyi - Bongiraud Dominique - janschoenherr - Marko Kaznovac (kaznovac) @@ -577,7 +581,6 @@ The Symfony Connect username in parenthesis allows to get more information - James Hemery - Egor Taranov - Philippe Segatori - - Loïc Frémont (loic425) - Adrian Nguyen (vuphuong87) - benjaminmal - Thierry T (lepiaf) @@ -595,6 +598,7 @@ The Symfony Connect username in parenthesis allows to get more information - Erkhembayar Gantulga (erheme318) - Fractal Zombie - Gunnstein Lye (glye) + - Thomas Talbot (ioni) - Kévin THERAGE (kevin_therage) - Noémi Salaün (noemi-salaun) - Michel Hunziker @@ -638,6 +642,7 @@ The Symfony Connect username in parenthesis allows to get more information - rtek - Inal DJAFAR (inalgnu) - Christian Gärtner (dagardner) + - Artem Stepin (astepin) - Adrien Jourdier (eclairia) - Ivan Grigoriev (greedyivan) - Tomasz Kowalczyk (thunderer) @@ -652,6 +657,7 @@ The Symfony Connect username in parenthesis allows to get more information - Thomas P - Kristijan Kanalaš (kristijan_kanalas_infostud) - Felix Labrecque + - mondrake (mondrake) - Yaroslav Kiliba - “Filip - Simon Watiau (simonwatiau) @@ -679,6 +685,7 @@ The Symfony Connect username in parenthesis allows to get more information - Dalibor Karlović - Randy Geraads - Sanpi (sanpi) + - James Gilliland (neclimdul) - Eduardo Gulias (egulias) - Andreas Leathley (iquito) - Nathanael Noblet (gnat) @@ -724,7 +731,6 @@ The Symfony Connect username in parenthesis allows to get more information - Tamas Szijarto - stlrnz - Adrien Wilmet (adrienfr) - - Yannick Ihmels (ihmels) - Alex Bacart - hugovms - Michele Locati @@ -763,7 +769,6 @@ The Symfony Connect username in parenthesis allows to get more information - Shakhobiddin - Kai - Lee Rowlands - - Maximilian Reichel (phramz) - siganushka (siganushka) - Alain Hippolyte (aloneh) - Karoly Negyesi (chx) @@ -793,7 +798,6 @@ The Symfony Connect username in parenthesis allows to get more information - Vadim Kharitonov (vadim) - Oscar Cubo Medina (ocubom) - Karel Souffriau - - Andrii Dembitskyi - Christophe L. (christophelau) - Daniël Brekelmans (dbrekelmans) - Simon Heimberg (simon_heimberg) @@ -814,12 +818,14 @@ The Symfony Connect username in parenthesis allows to get more information - Thiago Cordeiro (thiagocordeiro) - Julien Maulny - Brian King + - Paul Oms - Steffen Roßkamp - Alexandru Furculita (afurculita) - Michel Salib (michelsalib) - Valentin Jonovs - geoffrey - Bastien DURAND (deamon) + - Benoit Galati (benoitgalati) - Jon Gotlin (jongotlin) - Jeanmonod David (jeanmonod) - Daniel González (daniel.gonzalez) @@ -833,6 +839,7 @@ The Symfony Connect username in parenthesis allows to get more information - Markus Bachmann (baachi) - Roger Guasch (rogerguasch) - Luis Tacón (lutacon) + - Alex Hofbauer (alexhofbauer) - Andrii Popov (andrii-popov) - lancergr - Ivan Nikolaev (destillat) @@ -842,6 +849,7 @@ The Symfony Connect username in parenthesis allows to get more information - ampaze - Arturs Vonda - Xavier Briand (xavierbriand) + - Daniel Badura - Asmir Mustafic (goetas) - vagrant - Asier Illarramendi (doup) @@ -851,7 +859,6 @@ The Symfony Connect username in parenthesis allows to get more information - Vlad Gregurco (vgregurco) - Boris Vujicic (boris.vujicic) - Chris Sedlmayr (catchamonkey) - - mondrake (mondrake) - Kamil Kokot (pamil) - Seb Koelen - Christoph Mewes (xrstf) @@ -893,13 +900,14 @@ The Symfony Connect username in parenthesis allows to get more information - Pablo Díez (pablodip) - Damien Fa - Kevin McBride + - BrokenSourceCode - Sergio Santoro - - James Gilliland (neclimdul) - Philipp Rieber (bicpi) - Dennis Væversted (srnzitcom) - Manuel de Ruiter (manuel) - nikos.sotiropoulos - Eduardo Oliveira (entering) + - Jonathan Johnson (jrjohnson) - Eugene Wissner - Ricardo Oliveira (ricardolotr) - Roy Van Ginneken (rvanginneken) @@ -907,6 +915,7 @@ The Symfony Connect username in parenthesis allows to get more information - Barry vd. Heuvel (barryvdh) - Jon Dufresne - Chad Sikorra (chadsikorra) + - Mathias Brodala (mbrodala) - Evan S Kaufman (evanskaufman) - Jonathan Sui Lioung Lee Slew (jlslew) - mcben @@ -914,6 +923,7 @@ The Symfony Connect username in parenthesis allows to get more information - Filip Procházka (fprochazka) - stoccc - Markus Lanthaler (lanthaler) + - Gigino Chianese (sajito) - Xav` (xavismeh) - Remi Collet - Mathieu Rochette (mathroc) @@ -957,6 +967,7 @@ The Symfony Connect username in parenthesis allows to get more information - Rodrigo Borrego Bernabé (rodrigobb) - John Bafford (jbafford) - Emanuele Iannone + - Gasan Guseynov (gassan) - Ondrej Machulda (ondram) - Denis Gorbachev (starfall) - Martin Morávek (keeo) @@ -990,12 +1001,12 @@ The Symfony Connect username in parenthesis allows to get more information - Markus S. (staabm) - Geoffrey Tran (geoff) - Elan Ruusamäe (glen) + - Brad Jones - Nicolas de Marqué (nicola) - a.dmitryuk - Jannik Zschiesche - Jan Ole Behrens (deegital) - Mantas Var (mvar) - - Paul Oms - Yann LUCAS (drixs6o9) - Sebastian Krebs - Htun Htun Htet (ryanhhh91) @@ -1036,7 +1047,9 @@ The Symfony Connect username in parenthesis allows to get more information - Iliya Miroslavov Iliev (i.miroslavov) - Safonov Nikita (ns3777k) - Simon DELICATA + - Thibault Buathier (gwemox) - vitaliytv + - Arnaud Frézet - Nicolas Martin (cocorambo) - luffy1727 - LHommet Nicolas (nicolaslh) @@ -1044,7 +1057,6 @@ The Symfony Connect username in parenthesis allows to get more information - Amirreza Shafaat (amirrezashafaat) - Laurent Clouet - Adoni Pavlakis (adoni) - - Alex Hofbauer (alexhofbauer) - Maarten Nusteling (nusje2000) - Ahmed EBEN HASSINE (famas23) - Eduard Bulava (nonanerz) @@ -1096,6 +1108,7 @@ 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 @@ -1105,6 +1118,7 @@ The Symfony Connect username in parenthesis allows to get more information - tamar peled - Reinier Kip - Geoffrey Brier (geoffrey-brier) + - Sofien Naas - Christophe Meneses (c77men) - Vladimir Tsykun - Andrei O @@ -1167,6 +1181,7 @@ The Symfony Connect username in parenthesis allows to get more information - Israel J. Carberry - Julius Kiekbusch - Miquel Rodríguez Telep (mrtorrent) + - Tamás Nagy (t-bond) - Sergey Kolodyazhnyy (skolodyazhnyy) - umpirski - Benjamin @@ -1174,6 +1189,7 @@ The Symfony Connect username in parenthesis allows to get more information - Chris Heng (gigablah) - Oleksii Svitiashchuk - Tristan Bessoussa (sf_tristanb) + - FORT Pierre-Louis (plfort) - Richard Bradley - Nathanaël Martel (nathanaelmartel) - Nicolas Jourdan (nicolasjc) @@ -1189,7 +1205,6 @@ The Symfony Connect username in parenthesis allows to get more information - Andreas Erhard (andaris) - Evgeny Efimov (edefimov) - John VanDeWeghe - - Daniel Badura - Oleg Mifle - gnito-org - Michael Devery (mickadoo) @@ -1198,6 +1213,7 @@ The Symfony Connect username in parenthesis allows to get more information - Markkus Millend - Clément - Jorrit Schippers (jorrit) + - Aurimas Niekis (aurimasniekis) - maxime.perrimond - rvoisin - cthulhu @@ -1286,6 +1302,7 @@ The Symfony Connect username in parenthesis allows to get more information - develop - flip111 - Artem Oliinyk (artemoliynyk) + - Marvin Feldmann (breyndotechse) - fruty - VJ - RJ Garcia @@ -1298,9 +1315,9 @@ The Symfony Connect username in parenthesis allows to get more information - Ondrej Exner - Mark Sonnabaum - Adiel Cristo (arcristo) - - Artem Stepin (astepin) - Fabian Kropfhamer (fabiank) - Junaid Farooq (junaidfarooq) + - Chris Jones (magikid) - Massimiliano Braglia (massimilianobraglia) - Swen van Zanten (swenvanzanten) - Frankie Wittevrongel @@ -1346,6 +1363,7 @@ The Symfony Connect username in parenthesis allows to get more information - Tayfun Aydin - Arne Groskurth - Ilya Chekalsky + - zenas1210 - Ostrzyciel - Julien DIDIER (juliendidier) - Ilia Sergunin (maranqz) @@ -1384,6 +1402,7 @@ The Symfony Connect username in parenthesis allows to get more information - BrokenSourceCode - Fabian Haase - Nikita Popov (nikic) + - Robert Fischer (sandoba) - Tarjei Huse (tarjei) - Besnik Br - Michael Olšavský @@ -1475,6 +1494,7 @@ The Symfony Connect username in parenthesis allows to get more information - Martin (meckhardt) - Radosław Kowalewski - JustDylan23 + - buffcode - Juraj Surman - Victor - Andreas Allacher @@ -1493,7 +1513,6 @@ The Symfony Connect username in parenthesis allows to get more information - John Stevenson - everyx - Stanislav Gamayunov (happyproff) - - Jonathan Johnson (jrjohnson) - Alexander McCullagh (mccullagh) - Paul L McNeely (mcneely) - Mike Meier (mykon) @@ -1504,6 +1523,7 @@ The Symfony Connect username in parenthesis allows to get more information - Francis Turmel (fturmel) - Nikita Nefedov (nikita2206) - Bernat Llibre + - Daniel Burger - cgonzalez - Ben - Joni Halme @@ -1534,6 +1554,7 @@ The Symfony Connect username in parenthesis allows to get more information - Zhuravlev Alexander (scif) - Stefano Degenkamp (steef) - James Michael DuPont + - kor3k kor3k (kor3k) - Eric Schildkamp - agaktr - Vincent CHALAMON @@ -1725,7 +1746,6 @@ The Symfony Connect username in parenthesis allows to get more information - robmro27 - Vallel Blanco - Bastien Clément - - Thomas Talbot - Benjamin Franzke - Pavinthan - Sylvain METAYER @@ -1784,7 +1804,6 @@ The Symfony Connect username in parenthesis allows to get more information - Evgeniy Koval - Claas Augner - Balazs Csaba - - Benoit Galati (benoitgalati) - Bill Hance (billhance) - Douglas Reith (douglas_reith) - Harry Walter (haswalt) @@ -1817,7 +1836,6 @@ The Symfony Connect username in parenthesis allows to get more information - Michel Bardelmeijer - Ikko Ashimine - Erwin Dirks - - Brad Jones - Markus Ramšak - den - George Dietrich @@ -1889,7 +1907,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jordane VASPARD (elementaire) - Erwan Nader (ernadoo) - Faizan Akram Dar (faizanakram) - - Gasan Guseynov (gassan) - Greg Szczotka (greg606) - Ian Littman (iansltx) - Nathan DIdier (icz) @@ -1901,7 +1918,6 @@ The Symfony Connect username in parenthesis allows to get more information - Florent Viel (luxifer) - Maks 3w (maks3w) - Mamikon Arakelyan (mamikon) - - Mathias Brodala (mbrodala) - Michiel Boeckaert (milio) - Mike Milano (mmilano) - Guillaume Lajarige (molkobain) @@ -1945,21 +1961,23 @@ The Symfony Connect username in parenthesis allows to get more information - Antoine Bluchet (soyuka) - Patrick Kaufmann - Anton Dyshkant + - Kirill Nesmeyanov (serafim) - Reece Fowell (reecefowell) - Guillaume Gammelin - Valérian Galliat - d-ph - Renan Taranto (renan-taranto) - - Thomas Talbot - Rikijs Murgs - Uladzimir Tsykun - Amaury Leroux de Lens (amo__) - Christian Jul Jensen + - Franck RANAIVO-HARISOA (franckranaivo) - Alexandre GESLIN - The Whole Life to Learn - Mikkel Paulson - ergiegonzaga - Liverbool (liverbool) + - Julien Boudry - Dalibor Karlović - Sam Malone - Ha Phan (haphan) @@ -1976,15 +1994,18 @@ The Symfony Connect username in parenthesis allows to get more information - Ganesh Chandrasekaran (gxc4795) - Sander Marechal - Franz Wilding (killerpoke) + - Ferenczi Krisztian (fchris82) - Oleg Golovakhin (doc_tr) - Icode4Food (icode4food) - Radosław Benkel + - Bert ter Heide (bertterheide) - Kevin Nadin (kevinjhappy) - jean pasqualini (darkilliant) - Ross Motley (rossmotley) - ttomor - Mei Gwilym (meigwilym) - Michael H. Arieli + - Jitendra Adhikari (adhocore) - Tom Panier (neemzy) - Fred Cox - Luciano Mammino (loige) @@ -1994,8 +2015,10 @@ The Symfony Connect username in parenthesis allows to get more information - Anne-Sophie Bachelard - Marvin Butkereit - Ben Oman + - Jack Worman (jworman) - Chris de Kok - Andreas Kleemann (andesk) + - Hubert Moreau (hmoreau) - Manuele Menozzi - Anton Babenko (antonbabenko) - Irmantas Šiupšinskas (irmantas) @@ -2033,6 +2056,7 @@ The Symfony Connect username in parenthesis allows to get more information - tamirvs - gauss - julien.galenski + - Florian Guimier - Christian Neff (secondtruth) - Chris Tiearney - Oliver Hoff @@ -2043,6 +2067,7 @@ The Symfony Connect username in parenthesis allows to get more information - Goran Juric - Laurent G. (laurentg) - Nicolas Macherey + - Bhujagendra Ishaya - Guido Donnari - Mert Simsek (mrtsmsk0) - Lin Clark @@ -2070,12 +2095,12 @@ The Symfony Connect username in parenthesis allows to get more information - Paul Mitchum (paul-m) - Angel Koilov (po_taka) - Dan Finnie - - Sofien Naas - Ken Marfilla (marfillaster) - Max Grigorian (maxakawizard) - benatespina (benatespina) - Denis Kop - Jean-Guilhem Rouel (jean-gui) + - Ivan Yivoff - EdgarPE - jfcixmedia - Dominic Tubach @@ -2086,11 +2111,11 @@ The Symfony Connect username in parenthesis allows to get more information - Serge (nfx) - Mikkel Paulson - Michał Strzelecki - - Aurimas Niekis (aurimasniekis) - Hugo Fonseca (fonsecas72) - Martynas Narbutas - Bailey Parker - Antanas Arvasevicius + - Kris Kelly - Eddie Abou-Jaoude (eddiejaoude) - Haritz Iturbe (hizai) - Nerijus Arlauskas (nercury) @@ -2113,7 +2138,6 @@ The Symfony Connect username in parenthesis allows to get more information - Jonathan Hedstrom - Peter Smeets (darkspartan) - Julien Bianchi (jubianchi) - - Tamás Nagy (t-bond) - Robert Meijers - Tijs Verkoyen - James Sansbury @@ -2177,6 +2201,7 @@ The Symfony Connect username in parenthesis allows to get more information - Oxan van Leeuwen - pkowalczyk - Soner Sayakci + - Andreas Hennings - Max Voloshin (maxvoloshin) - Nicolas Fabre (nfabre) - Raul Rodriguez (raul782) @@ -2184,11 +2209,11 @@ The Symfony Connect username in parenthesis allows to get more information - MightyBranch - Kacper Gunia (cakper) - Derek Lambert (dlambert) + - Mark Pedron (markpedron) - Peter Thompson (petert82) - error56 - Felicitus - alexpozzi - - Marvin Feldmann (breyndotechse) - Krzysztof Przybyszewski (kprzybyszewski) - Boullé William (williamboulle) - Frederic Godfrin @@ -2226,7 +2251,9 @@ The Symfony Connect username in parenthesis allows to get more information - Jelte Steijaert (jelte) - David Négrier (moufmouf) - Quique Porta (quiqueporta) + - Tobias Feijten (tobias93) - Andrea Quintino (dirk39) + - Andreas Heigl (heiglandreas) - Tomasz Szymczyk (karion) - Peter Dietrich (xosofox) - Alex Vasilchenko @@ -2249,7 +2276,9 @@ The Symfony Connect username in parenthesis allows to get more information - Ross Tuck - omniError - Zander Baldwin + - László GÖRÖG - Kévin Gomez (kevin) + - Kevin van Sonsbeek (kevin_van_sonsbeek) - Mihai Nica (redecs) - Andrei Igna - azine @@ -2299,8 +2328,8 @@ The Symfony Connect username in parenthesis allows to get more information - Steve Frécinaux - Constantine Shtompel - Jules Lamur - - zenas1210 - Renato Mendes Figueiredo + - Raphaël Droz - Eric Stern - ShiraNai7 - Antal Áron (antalaron) @@ -2318,6 +2347,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jason Desrosiers - m.chwedziak - Andreas Frömer + - Bikal Basnet - Philip Frank - Lance McNearney - Illia Antypenko (aivus) @@ -2339,10 +2369,10 @@ The Symfony Connect username in parenthesis allows to get more information - Martin Pärtel - Frédéric Bouchery (fbouchery) - Patrick Daley (padrig) + - Phillip Look (plook) - Max Summe - Ema Panz - Chihiro Adachi (chihiro-adachi) - - Benjamin Georgeault (wedgesama) - Raphaëll Roussel - Tadcka - Abudarham Yuval @@ -2400,6 +2430,7 @@ The Symfony Connect username in parenthesis allows to get more information - Michael Lively (mlivelyjr) - Abderrahim (phydev) - Attila Bukor (r1pp3rj4ck) + - Thomas Boileau (tboileau) - Thomas Chmielowiec (chmielot) - Jānis Lukss - rkerner @@ -2410,6 +2441,7 @@ The Symfony Connect username in parenthesis allows to get more information - AnrDaemon - Charly Terrier (charlypoppins) - Emre Akinci (emre) + - Rustam Bakeev (nommyde) - psampaz (psampaz) - Maxwell Vandervelde - kaywalker @@ -2441,7 +2473,6 @@ The Symfony Connect username in parenthesis allows to get more information - Ciaran McNulty (ciaranmcnulty) - Andrew (drew) - j4nr6n (j4nr6n) - - kor3k kor3k (kor3k) - Stelian Mocanita (stelian) - Gautier Deuette - Kirk Madera @@ -2469,6 +2500,7 @@ The Symfony Connect username in parenthesis allows to get more information - georaldc - wusuopu - Wouter de Wild + - Peter Potrowl - povilas - Gavin Staniforth - Alessandro Tagliapietra (alex88) @@ -2495,6 +2527,7 @@ The Symfony Connect username in parenthesis allows to get more information - Martin Schophaus (m_schophaus_adcada) - Martynas Sudintas (martiis) - Anton Sukhachev (mrsuh) + - Marcel Siegert - ryunosuke - Francisco Facioni (fran6co) - Iwan van Staveren (istaveren) @@ -2513,12 +2546,14 @@ The Symfony Connect username in parenthesis allows to get more information - Matt Farmer - catch - Alexandre Segura + - Asier Etxebeste - Josef Cech - Andrii Boiko - Harold Iedema - Ikhsan Agustian - Benoit Lévêque (benoit_leveque) - Simon Bouland (bouland) + - Jakub Janata (janatjak) - Jibé Barth (jibbarth) - Matthew Foster (mfoster) - Reyo Stallenberg (reyostallenberg) @@ -2552,6 +2587,7 @@ The Symfony Connect username in parenthesis allows to get more information - Houziaux mike - Phobetor - Markus + - Janusz Mocek - Thomas Chmielowiec - shdev - Andrey Ryaguzov @@ -2569,6 +2605,7 @@ The Symfony Connect username in parenthesis allows to get more information - František Bereň - Jeremiah VALERIE - Mike Francis + - Nil Borodulia - Almog Baku (almogbaku) - Gerd Christian Kunze (derdu) - Ionel Scutelnicu (ionelscutelnicu) @@ -2578,6 +2615,7 @@ The Symfony Connect username in parenthesis allows to get more information - Nick Stemerdink - David Stone - Grayson Koonce + - Wissame MEKHILEF - Romain Dorgueil - Christopher Parotat - Dennis Haarbrink @@ -2598,6 +2636,7 @@ The Symfony Connect username in parenthesis allows to get more information - Felix Marezki - Normunds - Thomas Rothe + - Troy Crawford - nietonfir - alefranz - David Barratt @@ -2623,6 +2662,7 @@ The Symfony Connect username in parenthesis allows to get more information - efeen - Nicolas Pion - Muhammed Akbulut + - Xesau - Aaron Somi - Michał Dąbrowski (defrag) - Simone Fumagalli (hpatoio) @@ -2644,10 +2684,12 @@ The Symfony Connect username in parenthesis allows to get more information - Gijs Kunze - Artyom Protaskin - Nathanael d. Noblet + - Yurun - helmer - ged15 - Simon Asika - Daan van Renterghem + - Boudry Julien - amcastror - Bram Van der Sype (brammm) - Guile (guile) @@ -2723,6 +2765,7 @@ The Symfony Connect username in parenthesis allows to get more information - Rémi Blaise - Nicolas Séverin - Joel Marcey + - zolikonta - David Christmann - root - pf @@ -2764,8 +2807,10 @@ The Symfony Connect username in parenthesis allows to get more information - Jelle Kapitein - Jochen Mandl - Marin Nicolae + - Albert Prat - Alessandro Loffredo - Ian Phillips + - Remi Collet - Haritz - Matthieu Prat - Brieuc Thomas @@ -2791,6 +2836,7 @@ The Symfony Connect username in parenthesis allows to get more information - Erik van Wingerden - Valouleloup - Alexis MARQUIS + - Matheus Gontijo - Gerrit Drost - Linnaea Von Lavia - Simon Mönch @@ -2810,6 +2856,8 @@ The Symfony Connect username in parenthesis allows to get more information - Rafał - Adria Lopez (adlpz) - Aaron Scherer (aequasi) + - Alexandre Jardin (alexandre.jardin) + - Bart Brouwer (bartbrouwer) - Rosio (ben-rosio) - Simon Paarlberg (blamh) - Masao Maeda (brtriver) @@ -2836,6 +2884,7 @@ The Symfony Connect username in parenthesis allows to get more information - Kevin Verschaeve (keversc) - Kevin Herrera (kherge) - Luis Ramón López López (lrlopez) + - Matheo Daninos (mathdns) - Mehdi Mabrouk (mehdidev) - Bart Reunes (metalarend) - Muriel (metalmumu) @@ -2847,6 +2896,7 @@ The Symfony Connect username in parenthesis allows to get more information - Olivier Laviale (olvlvl) - Pablo Monterde Perez (plebs) - Jimmy Leger (redpanda) + - Mokhtar Tlili (sf-djuba) - Marcin Szepczynski (szepczynski) - Simone Di Maulo (toretto460) - Cyrille Jouineau (tuxosaurus) @@ -2861,6 +2911,7 @@ The Symfony Connect username in parenthesis allows to get more information - Taylan Kasap - Michael Orlitzky - Nicolas A. Bérard-Nault + - Francois Martin - Saem Ghani - Stefan Oderbolz - Gabriel Moreira @@ -2901,6 +2952,7 @@ The Symfony Connect username in parenthesis allows to get more information - temperatur - Paul Andrieux - Cas + - Gwendolen Lynch - ghazy ben ahmed - Karolis - Myke79 @@ -2941,6 +2993,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jack Wright - MrNicodemuz - Anonymous User + - demeritcowboy - Paweł Tomulik - Eric J. Duran - Blackfelix @@ -3025,7 +3078,9 @@ The Symfony Connect username in parenthesis allows to get more information - Yurii K - Richard Trebichavský - g123456789l + - Mark Ogilvie - Jonathan Vollebregt + - Vladimir Vasilev - oscartv - DanSync - Peter Zwosta @@ -3053,6 +3108,7 @@ The Symfony Connect username in parenthesis allows to get more information - sualko - ADmad - Nicolas Roudaire + - Abdouni Karim (abdounikarim) - Andreas Forsblom (aforsblo) - Alex Olmos (alexolmos) - Cedric BERTOLINI (alsciende) @@ -3150,6 +3206,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jesper Søndergaard Pedersen (zerrvox) - Florent Cailhol - szymek + - Konrad - Kovacs Nicolas - craigmarvelley - Stano Turza From 9be4b4c2c9e43a70d488658d671a520f2c1a8159 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:23:38 +0200 Subject: [PATCH 03/73] Update VERSION for 4.4.44 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 9f132dcd9a22e..978324f29fb6a 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,12 +76,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - public const VERSION = '4.4.44-DEV'; + public const VERSION = '4.4.44'; public const VERSION_ID = 40444; public const MAJOR_VERSION = 4; public const MINOR_VERSION = 4; public const RELEASE_VERSION = 44; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2022'; public const END_OF_LIFE = '11/2023'; From 42938ef51ff3b4209ef6bf20c1b795eed25df913 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:27:20 +0200 Subject: [PATCH 04/73] Bump Symfony version to 4.4.45 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 978324f29fb6a..3a699112e1a5d 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,12 +76,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - public const VERSION = '4.4.44'; - public const VERSION_ID = 40444; + public const VERSION = '4.4.45-DEV'; + public const VERSION_ID = 40445; public const MAJOR_VERSION = 4; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 44; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 45; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '11/2022'; public const END_OF_LIFE = '11/2023'; From 65318af8a13998aef071a7e505279c1024c28f1b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:30:16 +0200 Subject: [PATCH 05/73] Update CHANGELOG for 5.4.11 --- CHANGELOG-5.4.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CHANGELOG-5.4.md b/CHANGELOG-5.4.md index f5067f0cdcbd2..16ce00ef58d71 100644 --- a/CHANGELOG-5.4.md +++ b/CHANGELOG-5.4.md @@ -7,6 +7,43 @@ in 5.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v5.4.0...v5.4.1 +* 5.4.11 (2022-07-29) + + * bug #47069 [Security] Allow redirect after login to absolute URLs (Tim Ward) + * bug #47073 [HttpKernel] Fix non-scalar check in surrogate fragment renderer (aschempp) + * bug #47003 [Cache] Ensured that redis adapter can use multiple redis sentinel hosts (warslett) + * bug #43329 [Serializer] Respect default context in DateTimeNormalizer::denormalize (hultberg) + * bug #47070 [Messenger] Fix function name in TriggerSql on postgresql bridge to support table name with schema (zimny9932) + * bug #47086 Workaround disabled "var_dump" (nicolas-grekas) + * bug #40828 [BrowserKit] Merge fields and files recursively if they are multidimensional array (januszmk) + * bug #47010 [String] Fix `width` method in `AbstractUnicodeString` (TBoileau) + * bug #47048 [Serializer] Fix XmlEncoder encoding attribute false (alamirault) + * bug #47022 [Console] get full command path for command in search path (remicollet) + * bug #47000 [ErrorHandler] Fix return type patching for list and class-string pseudo types (derrabus) + * bug #43998 [HttpKernel] [HttpCache] Don't throw on 304 Not Modified (aleho) + * bug #46792 [Bridge] Corrects bug in test listener trait (magikid) + * bug #46985 [DoctrineBridge] Avoid calling `AbstractPlatform::hasNativeGuidType()` (derrabus) + * bug #46958 [Serializer] Ignore getter with required parameters (Fix #46592) (astepin) + * bug #46981 [Mime]  quote address names if they contain parentheses (xabbuh) + * bug #46960 [FrameworkBundle] Fail gracefully when forms use disabled CSRF (HeahDude) + * bug #46973 [DependencyInjection] Fail gracefully when attempting to autowire composite types (derrabus) + * bug #45884 [Serializer] Fix inconsistent behaviour of nullable objects in key/value arrays (phramz) + * bug #46963 [Mime] Fix inline parts when added via attachPart() (fabpot) + * bug #46968 [PropertyInfo] Make sure nested composite types do not crash ReflectionExtractor (derrabus) + * bug #46931 Flush backend output buffer after closing. (bradjones1) + * bug #46947 [Serializer] Prevent that bad Ignore method annotations lead to incorrect results (astepin) + * bug #46948 [Validator] : Fix "PHP Warning: Undefined array key 1" in NotCompromisedPasswordValidator (KevinVanSonsbeek) + * bug #46905 [BrowserKit] fix sending request to paths containing multiple slashes (xabbuh) + * bug #46244 [Validator] Fix traverse option on Valid constraint when used as Attribute (tobias-93) + * bug #42033 [HttpFoundation] Fix deleteFileAfterSend on client abortion (nerg4l) + * bug #46941 [Messenger] Fix calls to deprecated DBAL methods (derrabus) + * bug #46863 [Mime] Fix invalid DKIM signature with multiple parts (BrokenSourceCode) + * bug #46808 [HttpFoundation] Fix TypeError on null `$_SESSION` in `NativeSessionStorage::save()` (chalasr) + * bug #46811 [DoctrineBridge] Fix comment for type on Query::setValue (middlewares) (l-vo) + * bug #46790 [HttpFoundation] Prevent PHP Warning: Session ID is too long or contains illegal characters (BrokenSourceCode) + * bug #46800 Spaces in system temp folder path cause deprecation errors in php 8 (demeritcowboy) + * bug #46797 [Messenger] Ceil waiting time when multiplier is a float on retry (WissameMekhilef) + * 5.4.10 (2022-06-26) * bug #46779 [String] Add an invariable word in french (lemonlab) From 48e20c0ecd1a834069e3cb57fe9543457e46a637 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:30:22 +0200 Subject: [PATCH 06/73] Update VERSION for 5.4.11 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index ddfb2ec6d887f..358b926460e26 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,12 +78,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static $freshCache = []; - public const VERSION = '5.4.11-DEV'; + public const VERSION = '5.4.11'; public const VERSION_ID = 50411; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 4; public const RELEASE_VERSION = 11; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2024'; public const END_OF_LIFE = '11/2025'; From f7a2c37a37d08ff40ffcf42f5b41a4348ebabf8c Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:36:18 +0200 Subject: [PATCH 07/73] Bump Symfony version to 5.4.12 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 358b926460e26..266492171bf64 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,12 +78,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static $freshCache = []; - public const VERSION = '5.4.11'; - public const VERSION_ID = 50411; + public const VERSION = '5.4.12-DEV'; + public const VERSION_ID = 50412; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 11; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 12; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '11/2024'; public const END_OF_LIFE = '11/2025'; From ec4e5ad03e77c93e4fb8219dd23b7edc7b16634b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:37:36 +0200 Subject: [PATCH 08/73] Update CHANGELOG for 6.0.11 --- CHANGELOG-6.0.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/CHANGELOG-6.0.md b/CHANGELOG-6.0.md index 4416525001ff2..54219602ca6bb 100644 --- a/CHANGELOG-6.0.md +++ b/CHANGELOG-6.0.md @@ -7,6 +7,44 @@ in 6.0 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.0.0...v6.0.1 +* 6.0.11 (2022-07-29) + + * bug #47069 [Security] Allow redirect after login to absolute URLs (Tim Ward) + * bug #47073 [HttpKernel] Fix non-scalar check in surrogate fragment renderer (aschempp) + * bug #47003 [Cache] Ensured that redis adapter can use multiple redis sentinel hosts (warslett) + * bug #43329 [Serializer] Respect default context in DateTimeNormalizer::denormalize (hultberg) + * bug #47070 [Messenger] Fix function name in TriggerSql on postgresql bridge to support table name with schema (zimny9932) + * bug #47086 Workaround disabled "var_dump" (nicolas-grekas) + * bug #40828 [BrowserKit] Merge fields and files recursively if they are multidimensional array (januszmk) + * bug #47010 [String] Fix `width` method in `AbstractUnicodeString` (TBoileau) + * bug #47048 [Serializer] Fix XmlEncoder encoding attribute false (alamirault) + * bug #46957 [HttpFoundation] Fix `\Stringable` support in `InputBag::get()` (chalasr) + * bug #47022 [Console] get full command path for command in search path (remicollet) + * bug #47000 [ErrorHandler] Fix return type patching for list and class-string pseudo types (derrabus) + * bug #43998 [HttpKernel] [HttpCache] Don't throw on 304 Not Modified (aleho) + * bug #46792 [Bridge] Corrects bug in test listener trait (magikid) + * bug #46985 [DoctrineBridge] Avoid calling `AbstractPlatform::hasNativeGuidType()` (derrabus) + * bug #46958 [Serializer] Ignore getter with required parameters (Fix #46592) (astepin) + * bug #46981 [Mime]  quote address names if they contain parentheses (xabbuh) + * bug #46960 [FrameworkBundle] Fail gracefully when forms use disabled CSRF (HeahDude) + * bug #46973 [DependencyInjection] Fail gracefully when attempting to autowire composite types (derrabus) + * bug #45884 [Serializer] Fix inconsistent behaviour of nullable objects in key/value arrays (phramz) + * bug #46963 [Mime] Fix inline parts when added via attachPart() (fabpot) + * bug #46968 [PropertyInfo] Make sure nested composite types do not crash ReflectionExtractor (derrabus) + * bug #46931 Flush backend output buffer after closing. (bradjones1) + * bug #46947 [Serializer] Prevent that bad Ignore method annotations lead to incorrect results (astepin) + * bug #46948 [Validator] : Fix "PHP Warning: Undefined array key 1" in NotCompromisedPasswordValidator (KevinVanSonsbeek) + * bug #46905 [BrowserKit] fix sending request to paths containing multiple slashes (xabbuh) + * bug #46244 [Validator] Fix traverse option on Valid constraint when used as Attribute (tobias-93) + * bug #42033 [HttpFoundation] Fix deleteFileAfterSend on client abortion (nerg4l) + * bug #46941 [Messenger] Fix calls to deprecated DBAL methods (derrabus) + * bug #46863 [Mime] Fix invalid DKIM signature with multiple parts (BrokenSourceCode) + * bug #46808 [HttpFoundation] Fix TypeError on null `$_SESSION` in `NativeSessionStorage::save()` (chalasr) + * bug #46811 [DoctrineBridge] Fix comment for type on Query::setValue (middlewares) (l-vo) + * bug #46790 [HttpFoundation] Prevent PHP Warning: Session ID is too long or contains illegal characters (BrokenSourceCode) + * bug #46800 Spaces in system temp folder path cause deprecation errors in php 8 (demeritcowboy) + * bug #46797 [Messenger] Ceil waiting time when multiplier is a float on retry (WissameMekhilef) + * 6.0.10 (2022-06-26) * bug #46779 [String] Add an invariable word in french (lemonlab) From daf41ad77d72cf9dd68679db96d3219eec0c23fe Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:37:40 +0200 Subject: [PATCH 09/73] Update VERSION for 6.0.11 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 231a8ab98b622..238b96c60b713 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,12 +78,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.0.11-DEV'; + public const VERSION = '6.0.11'; public const VERSION_ID = 60011; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 0; public const RELEASE_VERSION = 11; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '01/2023'; public const END_OF_LIFE = '01/2023'; From b72dbb12d0b95260701af6422e691c6804cc8623 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 14:58:40 +0200 Subject: [PATCH 10/73] Bump Symfony version to 6.0.12 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 238b96c60b713..8b3106f5b93da 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,12 +78,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.0.11'; - public const VERSION_ID = 60011; + public const VERSION = '6.0.12-DEV'; + public const VERSION_ID = 60012; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 0; - public const RELEASE_VERSION = 11; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 12; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '01/2023'; public const END_OF_LIFE = '01/2023'; From 6b0112c34e21db38ae5e7afc5b55015e5d8fd8d3 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 29 Jul 2022 15:07:33 +0200 Subject: [PATCH 11/73] Bump Symfony version to 6.1.4 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 29c7aac0811ae..b36b18c3a6b00 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,12 +78,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.1.3'; - public const VERSION_ID = 60103; + public const VERSION = '6.1.4-DEV'; + public const VERSION_ID = 60104; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 1; - public const RELEASE_VERSION = 3; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 4; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '01/2023'; public const END_OF_LIFE = '01/2023'; From 5c726c0e7c56addd640f921ca281368db9d0132b Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 30 Jul 2022 19:38:39 +0200 Subject: [PATCH 12/73] remove the ChatterInterface alias when the chatter service is removed --- .../DependencyInjection/FrameworkExtension.php | 4 ++++ .../Tests/DependencyInjection/FrameworkExtensionTest.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d72ef5f6da78e..77d8a9d27e9a7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -162,8 +162,10 @@ use Symfony\Component\Notifier\Bridge\Vonage\VonageTransportFactory; use Symfony\Component\Notifier\Bridge\Yunpian\YunpianTransportFactory; use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory; +use Symfony\Component\Notifier\ChatterInterface; use Symfony\Component\Notifier\Notifier; use Symfony\Component\Notifier\Recipient\Recipient; +use Symfony\Component\Notifier\TexterInterface; use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; @@ -2489,11 +2491,13 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ $container->getDefinition('chatter.transports')->setArgument(0, $config['chatter_transports']); } else { $container->removeDefinition('chatter'); + $container->removeAlias(ChatterInterface::class); } if ($config['texter_transports']) { $container->getDefinition('texter.transports')->setArgument(0, $config['texter_transports']); } else { $container->removeDefinition('texter'); + $container->removeAlias(TexterInterface::class); } if ($this->mailerConfigEnabled) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 36d0f441ccf8c..3fb337b47aaee 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -53,6 +53,8 @@ use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; use Symfony\Component\HttpKernel\Fragment\FragmentUriGeneratorInterface; use Symfony\Component\Messenger\Transport\TransportFactory; +use Symfony\Component\Notifier\ChatterInterface; +use Symfony\Component\Notifier\TexterInterface; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; @@ -2022,7 +2024,9 @@ public function testNotifierWithoutTransports() $this->assertTrue($container->hasDefinition('notifier')); $this->assertFalse($container->hasDefinition('chatter')); + $this->assertFalse($container->hasAlias(ChatterInterface::class)); $this->assertFalse($container->hasDefinition('texter')); + $this->assertFalse($container->hasAlias(TexterInterface::class)); } public function testIfNotifierTransportsAreKnownByFrameworkExtension() From 6410c3ab116b63d01284882c49d8343696d07703 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Sun, 31 Jul 2022 00:50:40 +0200 Subject: [PATCH 13/73] [Serializer] Fix error message --- .../Component/Serializer/Normalizer/AbstractNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index c09d471396763..24f751367e6a1 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -404,7 +404,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex } $exception = NotNormalizableValueException::createForUnexpectedDataType( - sprintf('Failed to create object because the object miss the "%s" property.', $constructorParameter->name), + sprintf('Failed to create object because it misses the "%s" property.', $constructorParameter->name), $data, ['unknown'], $context['deserialization_path'] ?? null, From 252666e54303ee84f8c89dd9efde382e32bed4dd Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Sun, 31 Jul 2022 00:52:52 +0200 Subject: [PATCH 14/73] minor: fix test --- .../Component/Serializer/Normalizer/AbstractNormalizer.php | 2 +- src/Symfony/Component/Serializer/Tests/SerializerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 24f751367e6a1..143ce4a36b07b 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -404,7 +404,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex } $exception = NotNormalizableValueException::createForUnexpectedDataType( - sprintf('Failed to create object because it misses the "%s" property.', $constructorParameter->name), + sprintf('Failed to create object because the class misses the "%s" property.', $constructorParameter->name), $data, ['unknown'], $context['deserialization_path'] ?? null, diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 3b6811cfe0710..5d5425f88fa2f 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -1000,7 +1000,7 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet ], 'path' => 'php74FullWithConstructor', 'useMessageForUser' => true, - 'message' => 'Failed to create object because the object miss the "constructorArgument" property.', + 'message' => 'Failed to create object because the class misses the "constructorArgument" property.', ], $classMetadataFactory ? [ From 16d01765351ee4a2c18ea26e8824bd9a9ba26b7e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 1 Aug 2022 16:37:53 +0200 Subject: [PATCH 15/73] [HttpClient] Fix memory leak when using StreamWrapper --- .../HttpClient/Response/StreamWrapper.php | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php index 644f2ee190f57..6bb31687b949c 100644 --- a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php +++ b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php @@ -53,20 +53,18 @@ public static function createResource(ResponseInterface $response, HttpClientInt throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__)); } - if (false === stream_wrapper_register('symfony', __CLASS__)) { + static $registered = false; + + if (!$registered = $registered || stream_wrapper_register(strtr(__CLASS__, '\\', '-'), __CLASS__)) { throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.'); } - try { - $context = [ - 'client' => $client ?? $response, - 'response' => $response, - ]; - - return fopen('symfony://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context])) ?: null; - } finally { - stream_wrapper_unregister('symfony'); - } + $context = [ + 'client' => $client ?? $response, + 'response' => $response, + ]; + + return fopen(strtr(__CLASS__, '\\', '-').'://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context])); } public function getResponse(): ResponseInterface From 4775c88681e7f780715cb2667c9aad1af1c0c021 Mon Sep 17 00:00:00 2001 From: BrokenSourceCode <59090546+BrokenSourceCode@users.noreply.github.com> Date: Sat, 30 Jul 2022 21:46:44 +0200 Subject: [PATCH 16/73] [HttpFoundation] Fix invalid ID not regenerated with native PHP file sessions --- .../Storage/Handler/StrictSessionHandler.php | 10 ++++++++++ .../Storage/Proxy/SessionHandlerProxy.php | 4 +++- .../Storage/Proxy/SessionHandlerProxyTest.php | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php index 627bcfa1dfa84..7e5b5c019cea3 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/StrictSessionHandler.php @@ -30,6 +30,16 @@ public function __construct(\SessionHandlerInterface $handler) $this->handler = $handler; } + /** + * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. + * + * @internal + */ + public function isWrapper(): bool + { + return $this->handler instanceof \SessionHandler; + } + /** * @return bool */ diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php index 6539acf989387..0defa4a7ab19a 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Proxy/SessionHandlerProxy.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; + /** * @author Drak */ @@ -22,7 +24,7 @@ public function __construct(\SessionHandlerInterface $handler) { $this->handler = $handler; $this->wrapper = $handler instanceof \SessionHandler; - $this->saveHandlerName = $this->wrapper ? \ini_get('session.save_handler') : 'user'; + $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user'; } /** diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php index 972a2745132e1..a4f45fec68708 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Proxy/SessionHandlerProxyTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Proxy; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; /** @@ -159,6 +161,23 @@ public function testUpdateTimestamp() $this->proxy->updateTimestamp('id', 'data'); } + + /** + * @dataProvider provideNativeSessionStorageHandler + */ + public function testNativeSessionStorageSaveHandlerName($handler) + { + $this->assertSame('files', (new NativeSessionStorage([], $handler))->getSaveHandler()->getSaveHandlerName()); + } + + public function provideNativeSessionStorageHandler() + { + return [ + [new \SessionHandler()], + [new StrictSessionHandler(new \SessionHandler())], + [new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler()))], + ]; + } } abstract class TestSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface From cef44afeaf2ddfc519fe484010442073dbbd633a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 1 Aug 2022 19:57:53 +0200 Subject: [PATCH 17/73] [HttpClient] Fix shared connections not being freed on PHP < 8 --- src/Symfony/Component/HttpClient/Internal/CurlClientState.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php index 5821f67a6f700..904cc47b90669 100644 --- a/src/Symfony/Component/HttpClient/Internal/CurlClientState.php +++ b/src/Symfony/Component/HttpClient/Internal/CurlClientState.php @@ -96,7 +96,7 @@ public function reset() curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS); curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION); - if (\defined('CURL_LOCK_DATA_CONNECT')) { + if (\defined('CURL_LOCK_DATA_CONNECT') && \PHP_VERSION_ID >= 80000) { curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT); } } From aee6d084b97180eed78da2f68b2ecd4fffbda2de Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 1 Aug 2022 14:50:42 +0200 Subject: [PATCH 18/73] [Mailer] Fix error message in case of an STMP error --- .../Mailer/Transport/Smtp/EsmtpTransport.php | 35 +++++++++---------- .../Mailer/Transport/Smtp/SmtpTransport.php | 10 +++--- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php index e3c30487ca6c4..b7948c12d2333 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php @@ -94,15 +94,7 @@ public function addAuthenticator(AuthenticatorInterface $authenticator): void protected function doHeloCommand(): void { - try { - $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); - } catch (TransportExceptionInterface $e) { - parent::doHeloCommand(); - - return; - } - - $capabilities = $this->getCapabilities($response); + $capabilities = $this->callHeloCommand(); /** @var SocketStream $stream */ $stream = $this->getStream(); @@ -116,14 +108,7 @@ protected function doHeloCommand(): void throw new TransportException('Unable to connect with STARTTLS.'); } - try { - $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); - $capabilities = $this->getCapabilities($response); - } catch (TransportExceptionInterface $e) { - parent::doHeloCommand(); - - return; - } + $capabilities = $this->callHeloCommand(); } if (\array_key_exists('AUTH', $capabilities)) { @@ -131,10 +116,22 @@ protected function doHeloCommand(): void } } - private function getCapabilities(string $ehloResponse): array + private function callHeloCommand(): array { + try { + $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); + } catch (TransportExceptionInterface $e) { + try { + parent::doHeloCommand(); + } catch (TransportExceptionInterface $ex) { + if (!$ex->getCode()) { + throw $e; + } + } + } + $capabilities = []; - $lines = explode("\r\n", trim($ehloResponse)); + $lines = explode("\r\n", trim($response)); array_shift($lines); foreach ($lines as $line) { if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php index e4c2ec215ed88..3c05e94e376d1 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php @@ -295,15 +295,13 @@ private function assertResponseCode(string $response, array $codes): void throw new LogicException('You must set the expected response code.'); } - if (!$response) { - throw new TransportException(sprintf('Expected response code "%s" but got an empty response.', implode('/', $codes))); - } - [$code] = sscanf($response, '%3d'); $valid = \in_array($code, $codes); - if (!$valid) { - throw new TransportException(sprintf('Expected response code "%s" but got code "%s", with message "%s".', implode('/', $codes), $code, trim($response)), $code); + if (!$valid || !$response) { + $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code'; + $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : ''; + throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes), $codeStr).$codeStr.$responseStr.'.', $code); } } From 2b46650b9cd3f3765b5751954957ae978355b2f7 Mon Sep 17 00:00:00 2001 From: Gwendolen Lynch Date: Mon, 7 Feb 2022 10:53:23 +0100 Subject: [PATCH 19/73] Extract dispatching console signal handling and include subscribers --- src/Symfony/Component/Console/Application.php | 35 ++-- .../Console/Tests/ApplicationTest.php | 161 +++++++++++++++--- 2 files changed, 158 insertions(+), 38 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index a81cfdcbbc4d3..bb6b29edd2808 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -974,22 +974,31 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } } - if ($command instanceof SignalableCommandInterface && ($this->signalsToDispatchEvent || $command->getSubscribedSignals())) { - if (!$this->signalRegistry) { - throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); - } + if ($this->signalsToDispatchEvent) { + $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []; + $dispatchSignals = $this->dispatcher && $this->dispatcher->hasListeners(ConsoleEvents::SIGNAL); - if (Terminal::hasSttyAvailable()) { - $sttyMode = shell_exec('stty -g'); + if ($commandSignals || $dispatchSignals) { + if (!$this->signalRegistry) { + throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + } - foreach ([\SIGINT, \SIGTERM] as $signal) { - $this->signalRegistry->register($signal, static function () use ($sttyMode) { - shell_exec('stty '.$sttyMode); - }); + if (Terminal::hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + foreach ([\SIGINT, \SIGTERM] as $signal) { + $this->signalRegistry->register($signal, static function () use ($sttyMode) { + shell_exec('stty '.$sttyMode); + }); + } + } + + foreach ($commandSignals as $signal) { + $this->signalRegistry->register($signal, [$command, 'handleSignal']); } } - if ($this->dispatcher) { + if ($dispatchSignals) { foreach ($this->signalsToDispatchEvent as $signal) { $event = new ConsoleSignalEvent($command, $input, $output, $signal); @@ -1005,10 +1014,6 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI }); } } - - foreach ($command->getSubscribedSignals() as $signal) { - $this->signalRegistry->register($signal, [$command, 'handleSignal']); - } } if (null === $this->dispatcher) { diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index a5918aa3fc81b..08e795b5e0bb8 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\NamespaceNotFoundException; @@ -42,6 +43,8 @@ use Symfony\Component\Console\Tester\ApplicationTester; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Process\Process; class ApplicationTest extends TestCase @@ -1843,9 +1846,9 @@ public function testCommandNameMismatchWithCommandLoaderKeyThrows() /** * @requires extension pcntl */ - public function testSignal() + public function testSignalListenerNotCalledByDefault() { - $command = new SignableCommand(); + $command = new SignableCommand(false); $dispatcherCalled = false; $dispatcher = new EventDispatcher(); @@ -1853,29 +1856,97 @@ public function testSignal() $dispatcherCalled = true; }); - $application = new Application(); - $application->setAutoExit(false); - $application->setDispatcher($dispatcher); - $application->setSignalsToDispatchEvent(\SIGALRM); - $application->add(new LazyCommand('signal', [], '', false, function () use ($command) { return $command; }, true)); - - $this->assertFalse($command->signaled); - $this->assertFalse($dispatcherCalled); + $application = $this->createSignalableApplication($command, $dispatcher); $this->assertSame(0, $application->run(new ArrayInput(['signal']))); $this->assertFalse($command->signaled); $this->assertFalse($dispatcherCalled); + } + + /** + * @requires extension pcntl + */ + public function testSignalListener() + { + $command = new SignableCommand(); + + $dispatcherCalled = false; + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('console.signal', function () use (&$dispatcherCalled) { + $dispatcherCalled = true; + }); + + $application = $this->createSignalableApplication($command, $dispatcher); - $command->loop = 100000; - pcntl_alarm(1); $this->assertSame(1, $application->run(new ArrayInput(['signal']))); - $this->assertTrue($command->signaled); $this->assertTrue($dispatcherCalled); + $this->assertTrue($command->signaled); + } + + /** + * @requires extension pcntl + */ + public function testSignalSubscriberNotCalledByDefault() + { + $command = new BaseSignableCommand(false); + + $subscriber = new SignalEventSubscriber(); + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(0, $application->run(new ArrayInput(['signal']))); + $this->assertFalse($subscriber->signaled); + } + + /** + * @requires extension pcntl + */ + public function testSignalSubscriber() + { + $command = new BaseSignableCommand(); + + $subscriber1 = new SignalEventSubscriber(); + $subscriber2 = new SignalEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber1); + $dispatcher->addSubscriber($subscriber2); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(1, $application->run(new ArrayInput(['signal']))); + $this->assertTrue($subscriber1->signaled); + $this->assertTrue($subscriber2->signaled); + } + + /** + * @requires extension pcntl + */ + public function testSetSignalsToDispatchEvent() + { + $command = new BaseSignableCommand(); + + $subscriber = new SignalEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber); + + $application = $this->createSignalableApplication($command, $dispatcher); + $application->setSignalsToDispatchEvent(\SIGUSR2); + $this->assertSame(0, $application->run(new ArrayInput(['signal']))); + $this->assertFalse($subscriber->signaled); + + $application = $this->createSignalableApplication($command, $dispatcher); + $application->setSignalsToDispatchEvent(\SIGUSR1); + $this->assertSame(1, $application->run(new ArrayInput(['signal']))); + $this->assertTrue($subscriber->signaled); } public function testSignalableCommandInterfaceWithoutSignals() { - $command = new SignableCommand(); + $command = new SignableCommand(false); $dispatcher = new EventDispatcher(); $application = new Application(); @@ -1917,6 +1988,18 @@ public function testSignalableRestoresStty() $this->assertSame($previousSttyMode, $sttyMode); } + + private function createSignalableApplication(Command $command, ?EventDispatcherInterface $dispatcher): Application + { + $application = new Application(); + $application->setAutoExit(false); + if ($dispatcher) { + $application->setDispatcher($dispatcher); + } + $application->add(new LazyCommand('signal', [], '', false, function () use ($command) { return $command; }, true)); + + return $application; + } } class CustomApplication extends Application @@ -1971,25 +2054,26 @@ public function isEnabled(): bool } } -class SignableCommand extends Command implements SignalableCommandInterface +class BaseSignableCommand extends Command { public $signaled = false; - public $loop = 100; + public $loop = 1000; + private $emitsSignal; protected static $defaultName = 'signal'; - public function getSubscribedSignals(): array + public function __construct(bool $emitsSignal = true) { - return SignalRegistry::isSupported() ? [\SIGALRM] : []; - } - - public function handleSignal(int $signal): void - { - $this->signaled = true; + parent::__construct(); + $this->emitsSignal = $emitsSignal; } protected function execute(InputInterface $input, OutputInterface $output): int { + if ($this->emitsSignal) { + posix_kill(posix_getpid(), SIGUSR1); + } + for ($i = 0; $i < $this->loop; ++$i) { usleep(100); if ($this->signaled) { @@ -2000,3 +2084,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } } + +class SignableCommand extends BaseSignableCommand implements SignalableCommandInterface +{ + protected static $defaultName = 'signal'; + + public function getSubscribedSignals(): array + { + return SignalRegistry::isSupported() ? [\SIGUSR1] : []; + } + + public function handleSignal(int $signal): void + { + $this->signaled = true; + } +} + +class SignalEventSubscriber implements EventSubscriberInterface +{ + public $signaled = false; + + public function onSignal(ConsoleSignalEvent $event): void + { + $this->signaled = true; + $event->getCommand()->signaled = true; + } + + public static function getSubscribedEvents(): array + { + return ['console.signal' => 'onSignal']; + } +} From 9ac7fb1f0700a322bc4228168116a61dcae85899 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 2 Aug 2022 11:31:21 +0200 Subject: [PATCH 20/73] [Serializer] Revert deprecation of `ContextAwareEncoderInterface` and `ContextAwareDecoderInterface` --- .github/expected-missing-return-types.diff | 6 +++--- src/Symfony/Component/Serializer/CHANGELOG.md | 2 -- src/Symfony/Component/Serializer/Encoder/ChainDecoder.php | 6 +++++- src/Symfony/Component/Serializer/Encoder/ChainEncoder.php | 6 +++++- .../Serializer/Encoder/ContextAwareDecoderInterface.php | 2 -- .../Serializer/Encoder/ContextAwareEncoderInterface.php | 2 -- src/Symfony/Component/Serializer/Encoder/CsvEncoder.php | 8 ++------ .../Component/Serializer/Encoder/DecoderInterface.php | 5 ++--- .../Component/Serializer/Encoder/EncoderInterface.php | 5 ++--- src/Symfony/Component/Serializer/Encoder/JsonDecode.php | 4 +--- src/Symfony/Component/Serializer/Encoder/JsonEncode.php | 4 +--- src/Symfony/Component/Serializer/Encoder/JsonEncoder.php | 8 ++------ src/Symfony/Component/Serializer/Encoder/XmlEncoder.php | 8 ++------ src/Symfony/Component/Serializer/Encoder/YamlEncoder.php | 8 ++------ .../Serializer/Tests/Encoder/ChainDecoderTest.php | 3 ++- .../Serializer/Tests/Encoder/ChainEncoderTest.php | 5 +++-- 16 files changed, 32 insertions(+), 50 deletions(-) diff --git a/.github/expected-missing-return-types.diff b/.github/expected-missing-return-types.diff index fe0381a0b0a7e..cf4c237e3070c 100644 --- a/.github/expected-missing-return-types.diff +++ b/.github/expected-missing-return-types.diff @@ -899,11 +899,11 @@ index f38069e471..0966eb3e89 100644 + public function decode(string $data, string $format, array $context = []): mixed; /** -@@ -45,4 +45,4 @@ interface DecoderInterface +@@ -44,4 +44,4 @@ interface DecoderInterface * @return bool */ -- public function supportsDecoding(string $format /* , array $context = [] */); -+ public function supportsDecoding(string $format /* , array $context = [] */): bool; +- public function supportsDecoding(string $format); ++ public function supportsDecoding(string $format): bool; } diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index 44ba45f581..3398115497 100644 diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index c2b3ebfb3c6c2..0a475a0eafb9c 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -9,8 +9,6 @@ CHANGELOG * Set `Context` annotation as not final * Deprecate `ContextAwareNormalizerInterface`, use `NormalizerInterface` instead * Deprecate `ContextAwareDenormalizerInterface`, use `DenormalizerInterface` instead - * Deprecate `ContextAwareEncoderInterface`, use `EncoderInterface` instead - * Deprecate `ContextAwareDecoderInterface`, use `DecoderInterface` instead * Deprecate supporting denormalization for `AbstractUid` in `UidNormalizer`, use one of `AbstractUid` child class instead * Deprecate denormalizing to an abstract class in `UidNormalizer` * Add support for `can*()` methods to `ObjectNormalizer` diff --git a/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php b/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php index 0f7f94fad8842..7a01938ab4d52 100644 --- a/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php +++ b/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php @@ -67,9 +67,13 @@ private function getDecoder(string $format, array $context): DecoderInterface return $this->decoders[$this->decoderByFormat[$format]]; } + $cache = true; foreach ($this->decoders as $i => $decoder) { + $cache = $cache && !$decoder instanceof ContextAwareDecoderInterface; if ($decoder->supportsDecoding($format, $context)) { - $this->decoderByFormat[$format] = $i; + if ($cache) { + $this->decoderByFormat[$format] = $i; + } return $decoder; } diff --git a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php index 70d7c9fd10645..061a9cf748bd8 100644 --- a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php @@ -90,9 +90,13 @@ private function getEncoder(string $format, array $context): EncoderInterface return $this->encoders[$this->encoderByFormat[$format]]; } + $cache = true; foreach ($this->encoders as $i => $encoder) { + $cache = $cache && !$encoder instanceof ContextAwareEncoderInterface; if ($encoder->supportsEncoding($format, $context)) { - $this->encoderByFormat[$format] = $i; + if ($cache) { + $this->encoderByFormat[$format] = $i; + } return $encoder; } diff --git a/src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php b/src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php index 910b26bac1fc8..6ac2e38cc4657 100644 --- a/src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php +++ b/src/Symfony/Component/Serializer/Encoder/ContextAwareDecoderInterface.php @@ -15,8 +15,6 @@ * Adds the support of an extra $context parameter for the supportsDecoding method. * * @author Kévin Dunglas - * - * @deprecated since symfony/serializer 6.1, use DecoderInterface instead */ interface ContextAwareDecoderInterface extends DecoderInterface { diff --git a/src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php b/src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php index f828f87a4f82f..832b600eeca57 100644 --- a/src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php +++ b/src/Symfony/Component/Serializer/Encoder/ContextAwareEncoderInterface.php @@ -15,8 +15,6 @@ * Adds the support of an extra $context parameter for the supportsEncoding method. * * @author Kévin Dunglas - * - * @deprecated since symfony/serializer 6.1, use EncoderInterface instead */ interface ContextAwareEncoderInterface extends EncoderInterface { diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php index b2c6fcd81d4ae..a3733a53dee24 100644 --- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php @@ -124,10 +124,8 @@ public function encode(mixed $data, string $format, array $context = []): string /** * {@inheritdoc} - * - * @param array $context */ - public function supportsEncoding(string $format /* , array $context = [] */): bool + public function supportsEncoding(string $format): bool { return self::FORMAT === $format; } @@ -212,10 +210,8 @@ public function decode(string $data, string $format, array $context = []): mixed /** * {@inheritdoc} - * - * @param array $context */ - public function supportsDecoding(string $format /* , array $context = [] */): bool + public function supportsDecoding(string $format): bool { return self::FORMAT === $format; } diff --git a/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php b/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php index 5014b9bd514ab..84a84ad1f3e69 100644 --- a/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php +++ b/src/Symfony/Component/Serializer/Encoder/DecoderInterface.php @@ -39,10 +39,9 @@ public function decode(string $data, string $format, array $context = []); /** * Checks whether the deserializer can decode from given format. * - * @param string $format Format name - * @param array $context Options that decoders have access to + * @param string $format Format name * * @return bool */ - public function supportsDecoding(string $format /* , array $context = [] */); + public function supportsDecoding(string $format); } diff --git a/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php b/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php index c913ac3fb14ad..e0f303b1e3dcd 100644 --- a/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php +++ b/src/Symfony/Component/Serializer/Encoder/EncoderInterface.php @@ -32,8 +32,7 @@ public function encode(mixed $data, string $format, array $context = []): string /** * Checks whether the serializer can encode to given format. * - * @param string $format Format name - * @param array $context Options that normalizers/encoders have access to + * @param string $format Format name */ - public function supportsEncoding(string $format /* , array $context = [] */): bool; + public function supportsEncoding(string $format): bool; } diff --git a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php index 50d2d2e3f266f..ad094afaca161 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonDecode.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonDecode.php @@ -95,10 +95,8 @@ public function decode(string $data, string $format, array $context = []): mixed /** * {@inheritdoc} - * - * @param array $context */ - public function supportsDecoding(string $format /* , array $context = [] */): bool + public function supportsDecoding(string $format): bool { return JsonEncoder::FORMAT === $format; } diff --git a/src/Symfony/Component/Serializer/Encoder/JsonEncode.php b/src/Symfony/Component/Serializer/Encoder/JsonEncode.php index 86baf99994eb6..23d0fdd960e3e 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonEncode.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonEncode.php @@ -57,10 +57,8 @@ public function encode(mixed $data, string $format, array $context = []): string /** * {@inheritdoc} - * - * @param array $context */ - public function supportsEncoding(string $format /* , array $context = [] */): bool + public function supportsEncoding(string $format): bool { return JsonEncoder::FORMAT === $format; } diff --git a/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php b/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php index e6ccbfba50b2b..d17ef049285ef 100644 --- a/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/JsonEncoder.php @@ -47,20 +47,16 @@ public function decode(string $data, string $format, array $context = []): mixed /** * {@inheritdoc} - * - * @param array $context */ - public function supportsEncoding(string $format /* , array $context = [] */): bool + public function supportsEncoding(string $format): bool { return self::FORMAT === $format; } /** * {@inheritdoc} - * - * @param array $context */ - public function supportsDecoding(string $format /* , array $context = [] */): bool + public function supportsDecoding(string $format): bool { return self::FORMAT === $format; } diff --git a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php index a47e6d69583f3..2474f4439a443 100644 --- a/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/XmlEncoder.php @@ -166,20 +166,16 @@ public function decode(string $data, string $format, array $context = []): mixed /** * {@inheritdoc} - * - * @param array $context */ - public function supportsEncoding(string $format /* , array $context = [] */): bool + public function supportsEncoding(string $format): bool { return self::FORMAT === $format; } /** * {@inheritdoc} - * - * @param array $context */ - public function supportsDecoding(string $format /* , array $context = [] */): bool + public function supportsDecoding(string $format): bool { return self::FORMAT === $format; } diff --git a/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php b/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php index ecb9815eee553..51f600786aa3b 100644 --- a/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/YamlEncoder.php @@ -67,10 +67,8 @@ public function encode(mixed $data, string $format, array $context = []): string /** * {@inheritdoc} - * - * @param array $context */ - public function supportsEncoding(string $format /* , array $context = [] */): bool + public function supportsEncoding(string $format): bool { return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format; } @@ -87,10 +85,8 @@ public function decode(string $data, string $format, array $context = []): mixed /** * {@inheritdoc} - * - * @param array $context */ - public function supportsDecoding(string $format /* , array $context = [] */): bool + public function supportsDecoding(string $format): bool { return self::FORMAT === $format || self::ALTERNATIVE_FORMAT === $format; } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php index 5cac8d99a5270..a181bb145c571 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Encoder\ChainDecoder; +use Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -28,7 +29,7 @@ class ChainDecoderTest extends TestCase protected function setUp(): void { - $this->decoder1 = $this->createMock(DecoderInterface::class); + $this->decoder1 = $this->createMock(ContextAwareDecoderInterface::class); $this->decoder1 ->method('supportsDecoding') ->willReturnMap([ diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php index 848087145bafe..227e251c1dc6f 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Debug\TraceableEncoder; use Symfony\Component\Serializer\Encoder\ChainEncoder; +use Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -30,7 +31,7 @@ class ChainEncoderTest extends TestCase protected function setUp(): void { - $this->encoder1 = $this->createMock(EncoderInterface::class); + $this->encoder1 = $this->createMock(ContextAwareEncoderInterface::class); $this->encoder1 ->method('supportsEncoding') ->willReturnMap([ @@ -106,7 +107,7 @@ public function testNeedsNormalizationTraceableEncoder() class NormalizationAwareEncoder implements EncoderInterface, NormalizationAwareInterface { - public function supportsEncoding(string $format, array $context = []): bool + public function supportsEncoding(string $format): bool { return true; } From 21515b32da436750a22fca8ed00818304affa459 Mon Sep 17 00:00:00 2001 From: Andreas Hennings Date: Tue, 21 Jun 2022 21:37:16 +0200 Subject: [PATCH 21/73] [Yaml] Improve test coverage in DumperTest and ParserTest --- .../Component/Yaml/Tests/DumperTest.php | 244 +++++++++++++++--- .../multiple_lines_as_literal_block.yml | 14 - ...nes_as_literal_block_for_tagged_values.yml | 2 - .../Component/Yaml/Tests/ParserTest.php | 134 ++++++++-- 4 files changed, 326 insertions(+), 68 deletions(-) delete mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block.yml delete mode 100644 src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml diff --git a/src/Symfony/Component/Yaml/Tests/DumperTest.php b/src/Symfony/Component/Yaml/Tests/DumperTest.php index 2ebbbd047313c..59a47a8130f31 100644 --- a/src/Symfony/Component/Yaml/Tests/DumperTest.php +++ b/src/Symfony/Component/Yaml/Tests/DumperTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Yaml\Dumper; use Symfony\Component\Yaml\Exception\DumpException; +use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Tag\TaggedValue; use Symfony\Component\Yaml\Yaml; @@ -78,7 +79,8 @@ public function testIndentationInConstructor() - foo EOF; - $this->assertEquals($expected, $dumper->dump($this->array, 4, 0)); + $this->assertSame($expected, $dumper->dump($this->array, 4, 0)); + $this->assertSameData($this->array, $this->parser->parse($expected)); } public function testSpecifications() @@ -94,14 +96,17 @@ public function testSpecifications() } $test = $this->parser->parse($yaml); - if (isset($test['dump_skip']) && $test['dump_skip']) { + if ($test['dump_skip'] ?? false) { continue; - } elseif (isset($test['todo']) && $test['todo']) { + } + + if ($test['todo'] ?? false) { // TODO - } else { - eval('$expected = '.trim($test['php']).';'); - $this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10)), $test['test']); + continue; } + + $expected = eval('return '.trim($test['php']).';'); + $this->assertSame($expected, $this->parser->parse($this->dumper->dump($expected, 10)), $test['test']); } } } @@ -111,8 +116,9 @@ public function testInlineLevel() $expected = <<<'EOF' { '': bar, foo: '#bar', 'foo''bar': { }, bar: [1, foo, { a: A }], foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } } } EOF; - $this->assertEquals($expected, $this->dumper->dump($this->array, -10), '->dump() takes an inline level argument'); - $this->assertEquals($expected, $this->dumper->dump($this->array, 0), '->dump() takes an inline level argument'); + $this->assertSame($expected, $this->dumper->dump($this->array, -10), '->dump() takes an inline level argument'); + $this->assertSame($expected, $this->dumper->dump($this->array, 0), '->dump() takes an inline level argument'); + $this->assertSameData($this->array, $this->parser->parse($expected)); $expected = <<<'EOF' '': bar @@ -122,7 +128,8 @@ public function testInlineLevel() foobar: { foo: bar, bar: [1, foo], foobar: { foo: bar, bar: [1, foo] } } EOF; - $this->assertEquals($expected, $this->dumper->dump($this->array, 1), '->dump() takes an inline level argument'); + $this->assertSame($expected, $this->dumper->dump($this->array, 1), '->dump() takes an inline level argument'); + $this->assertSameData($this->array, $this->parser->parse($expected)); $expected = <<<'EOF' '': bar @@ -138,7 +145,8 @@ public function testInlineLevel() foobar: { foo: bar, bar: [1, foo] } EOF; - $this->assertEquals($expected, $this->dumper->dump($this->array, 2), '->dump() takes an inline level argument'); + $this->assertSame($expected, $this->dumper->dump($this->array, 2), '->dump() takes an inline level argument'); + $this->assertSameData($this->array, $this->parser->parse($expected)); $expected = <<<'EOF' '': bar @@ -159,7 +167,8 @@ public function testInlineLevel() bar: [1, foo] EOF; - $this->assertEquals($expected, $this->dumper->dump($this->array, 3), '->dump() takes an inline level argument'); + $this->assertSame($expected, $this->dumper->dump($this->array, 3), '->dump() takes an inline level argument'); + $this->assertSameData($this->array, $this->parser->parse($expected)); $expected = <<<'EOF' '': bar @@ -182,22 +191,23 @@ public function testInlineLevel() - foo EOF; - $this->assertEquals($expected, $this->dumper->dump($this->array, 4), '->dump() takes an inline level argument'); - $this->assertEquals($expected, $this->dumper->dump($this->array, 10), '->dump() takes an inline level argument'); + $this->assertSame($expected, $this->dumper->dump($this->array, 4), '->dump() takes an inline level argument'); + $this->assertSame($expected, $this->dumper->dump($this->array, 10), '->dump() takes an inline level argument'); + $this->assertSameData($this->array, $this->parser->parse($expected)); } public function testObjectSupportEnabled() { $dump = $this->dumper->dump(['foo' => new A(), 'bar' => 1], 0, 0, Yaml::DUMP_OBJECT); - $this->assertEquals('{ foo: !php/object \'O:30:"Symfony\Component\Yaml\Tests\A":1:{s:1:"a";s:3:"foo";}\', bar: 1 }', $dump, '->dump() is able to dump objects'); + $this->assertSame('{ foo: !php/object \'O:30:"Symfony\Component\Yaml\Tests\A":1:{s:1:"a";s:3:"foo";}\', bar: 1 }', $dump, '->dump() is able to dump objects'); } public function testObjectSupportDisabledButNoExceptions() { $dump = $this->dumper->dump(['foo' => new A(), 'bar' => 1]); - $this->assertEquals('{ foo: null, bar: 1 }', $dump, '->dump() does not dump objects when disabled'); + $this->assertSame('{ foo: null, bar: 1 }', $dump, '->dump() does not dump objects when disabled'); } public function testObjectSupportDisabledWithExceptions() @@ -211,7 +221,8 @@ public function testObjectSupportDisabledWithExceptions() */ public function testEscapedEscapeSequencesInQuotedScalar($input, $expected) { - $this->assertEquals($expected, $this->dumper->dump($input)); + $this->assertSame($expected, $this->dumper->dump($input)); + $this->assertSameData($input, $this->parser->parse($expected)); } public function getEscapeSequences() @@ -261,7 +272,7 @@ public function testDumpObjectAsMap($object, $expected) { $yaml = $this->dumper->dump($object, 0, 0, Yaml::DUMP_OBJECT_AS_MAP); - $this->assertEquals($expected, Yaml::parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP)); + $this->assertSameData($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP)); } public function objectAsMapProvider() @@ -339,7 +350,7 @@ public function testDumpingArrayObjectInstancesWithNumericKeysRespectsInlineLeve 2: { 0: d, 1: e } YAML; - $this->assertEquals($expected, $yaml); + $this->assertSame($expected, $yaml); } public function testDumpEmptyArrayObjectInstanceAsMap() @@ -378,6 +389,7 @@ public function testDumpingStdClassInstancesRespectsInlineLevel() YAML; $this->assertSame($expected, $yaml); + $this->assertSameData($outer, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP)); } public function testDumpingTaggedValueSequenceRespectsInlineLevel() @@ -403,6 +415,59 @@ public function testDumpingTaggedValueSequenceRespectsInlineLevel() YAML; $this->assertSame($expected, $yaml); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); + } + + public function testDumpingTaggedValueTopLevelScalar() + { + $data = new TaggedValue('user', 'jane'); + + $yaml = $this->dumper->dump($data); + + $expected = '!user jane'; + $this->assertSame($expected, $yaml); + $this->assertSameData($data, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS)); + } + + public function testDumpingTaggedValueTopLevelAssocInline() + { + $data = new TaggedValue('user', ['name' => 'jane']); + + $yaml = $this->dumper->dump($data); + + $expected = '!user { name: jane }'; + $this->assertSame($expected, $yaml); + $this->assertSameData($data, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS)); + } + + public function testDumpingTaggedValueTopLevelAssoc() + { + $data = new TaggedValue('user', ['name' => 'jane']); + + // @todo Fix the dumper, the output should not be ''. + $expected = ''; + $yaml = $this->dumper->dump($data, 2); + $this->assertSame($expected, $yaml); + } + + public function testDumpingTaggedValueTopLevelMultiLine() + { + $data = new TaggedValue('text', "a\nb\n"); + + // @todo Fix the dumper, the output should not be ''. + $expected = ''; + $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + } + + public function testDumpingTaggedValueSpecialCharsInTag() + { + // @todo Validate the tag name in the TaggedValue constructor. + $data = new TaggedValue('a b @ c', 5); + $expected = '!a b @ c 5'; + $this->assertSame($expected, $this->dumper->dump($data)); + // The data changes after a round trip, due to the illegal tag name. + $data = new TaggedValue('a', 'b @ c 5'); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); } public function testDumpingTaggedValueSequenceWithInlinedTagValues() @@ -415,6 +480,7 @@ public function testDumpingTaggedValueSequenceWithInlinedTagValues() 'john', 'claire', ]), + new TaggedValue('number', 5), ]; $yaml = $this->dumper->dump($data, 1); @@ -422,9 +488,13 @@ public function testDumpingTaggedValueSequenceWithInlinedTagValues() $expected = <<assertSame($expected, $yaml); + // @todo Fix the parser, preserve numbers. + $data[2] = new TaggedValue('number', '5'); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); } public function testDumpingTaggedValueMapRespectsInlineLevel() @@ -437,6 +507,7 @@ public function testDumpingTaggedValueMapRespectsInlineLevel() 'john', 'claire', ]), + 'count' => new TaggedValue('number', 5), ]; $yaml = $this->dumper->dump($data, 2); @@ -447,9 +518,13 @@ public function testDumpingTaggedValueMapRespectsInlineLevel() names1: !names - john - claire +count: !number 5 YAML; $this->assertSame($expected, $yaml); + // @todo Fix the parser, preserve numbers. + $data['count'] = new TaggedValue('number', '5'); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); } public function testDumpingTaggedValueMapWithInlinedTagValues() @@ -472,6 +547,7 @@ public function testDumpingTaggedValueMapWithInlinedTagValues() YAML; $this->assertSame($expected, $yaml); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); } public function testDumpingNotInlinedScalarTaggedValue() @@ -487,6 +563,7 @@ public function testDumpingNotInlinedScalarTaggedValue() YAML; $this->assertSame($expected, $this->dumper->dump($data, 2)); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); } public function testDumpingNotInlinedNullTaggedValue() @@ -500,6 +577,10 @@ public function testDumpingNotInlinedNullTaggedValue() YAML; $this->assertSame($expected, $this->dumper->dump($data, 2)); + + // @todo Fix the parser, don't stringify null. + $data['foo'] = new TaggedValue('bar', 'null'); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS | Yaml::PARSE_CONSTANT)); } public function testDumpingMultiLineStringAsScalarBlockTaggedValue() @@ -519,6 +600,53 @@ public function testDumpingMultiLineStringAsScalarBlockTaggedValue() ' baz'; $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); + } + + public function testDumpingTaggedMultiLineInList() + { + $data = [ + new TaggedValue('bar', "a\nb"), + ]; + $expected = "- !bar |\n a\n b"; + $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + + // @todo Fix the parser, eliminate these exceptions. + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Unable to parse at line 3 (near "!bar |").'); + + $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS); + } + + public function testDumpingTaggedMultiLineTrailingNewlinesInMap() + { + $data = [ + 'foo' => new TaggedValue('bar', "a\nb\n\n\n"), + ]; + $expected = "foo: !bar |\n a\n b\n \n \n "; + $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + + // @todo Fix the parser, the result should be identical to $data. + $this->assertSameData( + [ + 'foo' => new TaggedValue('bar', "a\nb\n"), + ], + $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); + } + + public function testDumpingTaggedMultiLineTrailingNewlinesInList() + { + $data = [ + new TaggedValue('bar', "a\nb\n\n\n"), + ]; + $expected = "- !bar |\n a\n b\n \n \n "; + $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + + // @todo Fix the parser, eliminate these exceptions. + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Unable to parse at line 6 (near "!bar |").'); + + $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS); } public function testDumpingInlinedMultiLineIfRnBreakLineInTaggedValue() @@ -528,8 +656,14 @@ public function testDumpingInlinedMultiLineIfRnBreakLineInTaggedValue() 'foo' => new TaggedValue('bar', "foo\r\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz"), ], ]; + $expected = <<<'YAML' +data: + foo: !bar "foo\r\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz" - $this->assertSame(file_get_contents(__DIR__.'/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml'), $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); +YAML; + $yml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + $this->assertSame($expected, $yml); + $this->assertSameData($data, $this->parser->parse($expected, Yaml::PARSE_CUSTOM_TAGS)); } public function testDumpMultiLineStringAsScalarBlock() @@ -544,8 +678,27 @@ public function testDumpMultiLineStringAsScalarBlock() ], ], ]; - - $this->assertSame(file_get_contents(__DIR__.'/Fixtures/multiple_lines_as_literal_block.yml'), $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $yml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + $expected = str_replace("@\n", "\n", <<<'YAML' +data: + single_line: 'foo bar baz' + multi_line: |- + foo + line with trailing spaces: + @ + bar + integer like line: + 123456789 + empty line: + + baz + multi_line_with_carriage_return: "foo\nbar\r\nbaz" + nested_inlined_multi_line_string: { inlined_multi_line: "foo\nbar\r\nempty line:\n\nbaz" } + +YAML +); + $this->assertSame($expected, $yml); + $this->assertSame($data, $this->parser->parse($yml)); } public function testDumpMultiLineStringAsScalarBlockWhenFirstLineHasLeadingSpace() @@ -558,27 +711,33 @@ public function testDumpMultiLineStringAsScalarBlockWhenFirstLineHasLeadingSpace $expected = "data:\n multi_line: |4-\n the first line has leading spaces\n The second line does not."; - $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $yml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + $this->assertSame($expected, $yml); + $this->assertSame($data, $this->parser->parse($yml)); } public function testCarriageReturnFollowedByNewlineIsMaintainedWhenDumpingAsMultiLineLiteralBlock() { - $this->assertSame("- \"a\\r\\nb\\nc\"\n", $this->dumper->dump(["a\r\nb\nc"], 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $data = ["a\r\nb\nc"]; + $expected = "- \"a\\r\\nb\\nc\"\n"; + $this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $this->assertSame($data, $this->parser->parse($expected)); } public function testCarriageReturnNotFollowedByNewlineIsPreservedWhenDumpingAsMultiLineLiteralBlock() { + $data = [ + 'parent' => [ + 'foo' => "bar\n\rbaz: qux", + ], + ]; $expected = <<<'YAML' parent: foo: "bar\n\rbaz: qux" YAML; - - $this->assertSame($expected, $this->dumper->dump([ - 'parent' => [ - 'foo' => "bar\n\rbaz: qux", - ], - ], 4, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $this->assertSame($expected, $this->dumper->dump($data, 4, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK)); + $this->assertSame($data, $this->parser->parse($expected)); } public function testNoExtraTrailingNewlineWhenDumpingAsMultiLineLiteralBlock() @@ -590,7 +749,15 @@ public function testNoExtraTrailingNewlineWhenDumpingAsMultiLineLiteralBlock() $yaml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $this->assertSame("- |-\n a\n b\n- |-\n c\n d", $yaml); - $this->assertSame($data, Yaml::parse($yaml)); + $this->assertSame($data, $this->parser->parse($yaml)); + } + + public function testTopLevelMultiLineStringLiteral() + { + $data = "a\nb\n"; + $yaml = $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + $this->assertSame('"a\nb\n"', $yaml); + $this->assertSame($data, $this->parser->parse($yaml)); } public function testDumpTrailingNewlineInMultiLineLiteralBlocks() @@ -600,6 +767,7 @@ public function testDumpTrailingNewlineInMultiLineLiteralBlocks() 'clip 2' => "one\ntwo\n", 'keep 1' => "one\ntwo\n", 'keep 2' => "one\ntwo\n\n", + 'keep 3' => "one\ntwo\n\n\n", 'strip 1' => "one\ntwo", 'strip 2' => "one\ntwo", ]; @@ -619,6 +787,11 @@ public function testDumpTrailingNewlineInMultiLineLiteralBlocks() one two +'keep 3': |+ + one + two + + 'strip 1': |- one two @@ -628,7 +801,7 @@ public function testDumpTrailingNewlineInMultiLineLiteralBlocks() YAML; $this->assertSame($expected, $yaml); - $this->assertSame($data, Yaml::parse($yaml)); + $this->assertSame($data, $this->parser->parse($yaml)); } public function testZeroIndentationThrowsException() @@ -664,6 +837,15 @@ public function testDumpIdeographicSpaces() 'regular_space' => 'a b', ], 2)); } + + private function assertSameData($expected, $actual) + { + $this->assertEquals($expected, $actual); + $this->assertSame( + var_export($expected, true), + var_export($actual, true) + ); + } } class A diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block.yml deleted file mode 100644 index 1f61eb1216a52..0000000000000 --- a/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block.yml +++ /dev/null @@ -1,14 +0,0 @@ -data: - single_line: 'foo bar baz' - multi_line: |- - foo - line with trailing spaces: - - bar - integer like line: - 123456789 - empty line: - - baz - multi_line_with_carriage_return: "foo\nbar\r\nbaz" - nested_inlined_multi_line_string: { inlined_multi_line: "foo\nbar\r\nempty line:\n\nbaz" } diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml b/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml deleted file mode 100644 index f8c9112fd52a5..0000000000000 --- a/src/Symfony/Component/Yaml/Tests/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml +++ /dev/null @@ -1,2 +0,0 @@ -data: - foo: !bar "foo\r\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz" diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 9a14422e434fe..08751d5fe8d91 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -34,6 +34,90 @@ protected function tearDown(): void chmod(__DIR__.'/Fixtures/not_readable.yml', 0644); } + public function testTopLevelNumber() + { + $yml = '5'; + $data = $this->parser->parse($yml); + $expected = 5; + $this->assertSameData($expected, $data); + } + + public function testTopLevelNull() + { + $yml = 'null'; + $data = $this->parser->parse($yml); + $expected = null; + $this->assertSameData($expected, $data); + } + + public function testTaggedValueTopLevelNumber() + { + $yml = '!number 5'; + $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS); + // @todo Preserve the number, don't turn into string. + $expected = new TaggedValue('number', '5'); + $this->assertSameData($expected, $data); + } + + public function testTaggedValueTopLevelNull() + { + $yml = '!tag null'; + $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS); + // @todo Preserve literal null, don't turn into string. + $expected = new TaggedValue('tag', 'null'); + $this->assertSameData($expected, $data); + } + + public function testTaggedValueTopLevelString() + { + $yml = '!user barbara'; + $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS); + $expected = new TaggedValue('user', 'barbara'); + $this->assertSameData($expected, $data); + } + + public function testTaggedValueTopLevelAssocInline() + { + $yml = '!user { name: barbara }'; + $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS); + $expected = new TaggedValue('user', ['name' => 'barbara']); + $this->assertSameData($expected, $data); + } + + public function testTaggedValueTopLevelAssoc() + { + $yml = <<<'YAML' +!user +name: barbara +YAML; + $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS); + $expected = new TaggedValue('user', ['name' => 'barbara']); + $this->assertSameData($expected, $data); + } + + public function testTaggedValueTopLevelList() + { + $yml = <<<'YAML' +!users +- barbara +YAML; + $data = $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS); + $expected = new TaggedValue('users', ['barbara']); + $this->assertSameData($expected, $data); + } + + public function testTaggedTextAsListItem() + { + $yml = <<<'YAML' +- !text | + first line +YAML; + // @todo Fix the parser, eliminate this exception. + $this->expectException(ParseException::class); + $this->expectExceptionMessage('Unable to parse at line 2 (near "!text |").'); + $this->parser->parse($yml, Yaml::PARSE_CUSTOM_TAGS); + } + /** * @dataProvider getDataFormSpecifications */ @@ -104,7 +188,7 @@ public function testParserIsStateless() public function testValidTokenSeparation(string $given, array $expected) { $actual = $this->parser->parse($given); - $this->assertEquals($expected, $actual); + $this->assertSameData($expected, $actual); } public function validTokenSeparators(): array @@ -482,7 +566,7 @@ public function testObjectSupportEnabled() foo: !php/object O:30:"Symfony\Component\Yaml\Tests\B":1:{s:1:"b";s:3:"foo";} bar: 1 EOF; - $this->assertEquals(['foo' => new B(), 'bar' => 1], $this->parser->parse($input, Yaml::PARSE_OBJECT), '->parse() is able to parse objects'); + $this->assertSameData(['foo' => new B(), 'bar' => 1], $this->parser->parse($input, Yaml::PARSE_OBJECT), '->parse() is able to parse objects'); } public function testObjectSupportDisabledButNoExceptions() @@ -491,7 +575,7 @@ public function testObjectSupportDisabledButNoExceptions() foo: !php/object O:30:"Symfony\Tests\Component\Yaml\B":1:{s:1:"b";s:3:"foo";} bar: 1 EOF; - $this->assertEquals(['foo' => null, 'bar' => 1], $this->parser->parse($input), '->parse() does not parse objects'); + $this->assertSameData(['foo' => null, 'bar' => 1], $this->parser->parse($input), '->parse() does not parse objects'); } /** @@ -501,7 +585,7 @@ public function testObjectForMap($yaml, $expected) { $flags = Yaml::PARSE_OBJECT_FOR_MAP; - $this->assertEquals($expected, $this->parser->parse($yaml, $flags)); + $this->assertSameData($expected, $this->parser->parse($yaml, $flags)); } public function getObjectForMapTests() @@ -957,12 +1041,12 @@ public function testEmptyValue() hash: EOF; - $this->assertEquals(['hash' => null], Yaml::parse($input)); + $this->assertSame(['hash' => null], Yaml::parse($input)); } public function testCommentAtTheRootIndent() { - $this->assertEquals([ + $this->assertSame([ 'services' => [ 'app.foo_service' => [ 'class' => 'Foo', @@ -988,7 +1072,7 @@ class: Bar public function testStringBlockWithComments() { - $this->assertEquals(['content' => <<<'EOT' + $this->assertSame(['content' => <<<'EOT' # comment 1 header @@ -1016,7 +1100,7 @@ public function testStringBlockWithComments() public function testFoldedStringBlockWithComments() { - $this->assertEquals([['content' => <<<'EOT' + $this->assertSame([['content' => <<<'EOT' # comment 1 header @@ -1045,7 +1129,7 @@ public function testFoldedStringBlockWithComments() public function testNestedFoldedStringBlockWithComments() { - $this->assertEquals([[ + $this->assertSame([[ 'title' => 'some title', 'content' => <<<'EOT' # comment 1 @@ -1077,7 +1161,7 @@ public function testNestedFoldedStringBlockWithComments() public function testReferenceResolvingInInlineStrings() { - $this->assertEquals([ + $this->assertSame([ 'var' => 'var-value', 'scalar' => 'var-value', 'list' => ['var-value'], @@ -1117,7 +1201,7 @@ public function testYamlDirective() foo: 1 bar: 2 EOF; - $this->assertEquals(['foo' => 1, 'bar' => 2], $this->parser->parse($yaml)); + $this->assertSame(['foo' => 1, 'bar' => 2], $this->parser->parse($yaml)); } public function testFloatKeys() @@ -1167,7 +1251,7 @@ public function testExplicitStringCasting() '~' => 'null', ]; - $this->assertEquals($expected, $this->parser->parse($yaml)); + $this->assertSame($expected, $this->parser->parse($yaml)); } public function testColonInMappingValueException() @@ -1468,7 +1552,7 @@ public function testParseDateAsMappingValue() $expectedDate->setDate(2002, 12, 14); $expectedDate->setTime(0, 0, 0); - $this->assertEquals(['date' => $expectedDate], $this->parser->parse($yaml, Yaml::PARSE_DATETIME)); + $this->assertSameData(['date' => $expectedDate], $this->parser->parse($yaml, Yaml::PARSE_DATETIME)); } /** @@ -1688,7 +1772,7 @@ public function testBackslashInSingleQuotedString() public function testParseMultiLineString() { - $this->assertEquals("foo bar\nbaz", $this->parser->parse("foo\nbar\n\nbaz")); + $this->assertSame("foo bar\nbaz", $this->parser->parse("foo\nbar\n\nbaz")); } /** @@ -1696,7 +1780,7 @@ public function testParseMultiLineString() */ public function testParseMultiLineMappingValue($yaml, $expected, $parseError) { - $this->assertEquals($expected, $this->parser->parse($yaml)); + $this->assertSame($expected, $this->parser->parse($yaml)); } public function multiLineDataProvider() @@ -1763,7 +1847,7 @@ public function multiLineDataProvider() */ public function testInlineNotationSpanningMultipleLines($expected, string $yaml) { - $this->assertEquals($expected, $this->parser->parse($yaml)); + $this->assertSame($expected, $this->parser->parse($yaml)); } public function inlineNotationSpanningMultipleLinesProvider(): array @@ -2137,7 +2221,7 @@ public function testRootLevelInlineMappingFollowedByMoreContentIsInvalid() public function testTaggedInlineMapping() { - $this->assertEquals(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS)); + $this->assertSameData(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS)); } public function testInvalidInlineSequenceContainingStringWithEscapedQuotationCharacter() @@ -2152,7 +2236,7 @@ public function testInvalidInlineSequenceContainingStringWithEscapedQuotationCha */ public function testCustomTagSupport($expected, $yaml) { - $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS)); + $this->assertSameData($expected, $this->parser->parse($yaml, Yaml::PARSE_CUSTOM_TAGS)); } public function taggedValuesProvider() @@ -2348,7 +2432,7 @@ public function testCanParseVeryLongValue() $yamlString = Yaml::dump($trickyVal); $arrayFromYaml = $this->parser->parse($yamlString); - $this->assertEquals($trickyVal, $arrayFromYaml); + $this->assertSame($trickyVal, $arrayFromYaml); } public function testParserCleansUpReferencesBetweenRuns() @@ -2463,7 +2547,7 @@ public function testMergeKeysWhenMappingsAreParsedAsObjects() ], ]; - $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP)); + $this->assertSameData($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP)); } public function testFilenamesAreParsedAsStringsWithoutFlag() @@ -2556,7 +2640,7 @@ public function testParseReferencesOnMergeKeysWithMappingsParsedAsObjects() ], ]; - $this->assertEquals($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP)); + $this->assertSameData($expected, $this->parser->parse($yaml, Yaml::PARSE_OBJECT_FOR_MAP)); } public function testEvalRefException() @@ -2831,6 +2915,14 @@ public function testParseIdeographicSpaces() 'regular_space' => 'a b', ], $this->parser->parse($expected)); } + + private function assertSameData($expected, $actual) + { + $this->assertEquals($expected, $actual); + $this->assertSame( + var_export($expected, true), + var_export($actual, true)); + } } class B From 99ca5f9fa7d0edae384b6dd626619bf352b73c21 Mon Sep 17 00:00:00 2001 From: Xavier RENAUDIN Date: Wed, 27 Apr 2022 14:54:44 +0200 Subject: [PATCH 22/73] [Translator] Fix translator overlapse --- .../Translation/MessageCatalogue.php | 19 ++++++--------- .../Translation/Tests/TranslatorTest.php | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Translation/MessageCatalogue.php b/src/Symfony/Component/Translation/MessageCatalogue.php index 6e6b9fe0eaa95..b43b22d6370f3 100644 --- a/src/Symfony/Component/Translation/MessageCatalogue.php +++ b/src/Symfony/Component/Translation/MessageCatalogue.php @@ -159,19 +159,14 @@ public function replace($messages, $domain = 'messages') */ public function add($messages, $domain = 'messages') { - if (!isset($this->messages[$domain])) { - $this->messages[$domain] = []; - } - $intlDomain = $domain; - if (!str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { - $intlDomain .= self::INTL_DOMAIN_SUFFIX; - } + $altDomain = str_ends_with($domain, self::INTL_DOMAIN_SUFFIX) ? substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)) : $domain.self::INTL_DOMAIN_SUFFIX; foreach ($messages as $id => $message) { - if (isset($this->messages[$intlDomain]) && \array_key_exists($id, $this->messages[$intlDomain])) { - $this->messages[$intlDomain][$id] = $message; - } else { - $this->messages[$domain][$id] = $message; - } + unset($this->messages[$altDomain][$id]); + $this->messages[$domain][$id] = $message; + } + + if ([] === ($this->messages[$altDomain] ?? null)) { + unset($this->messages[$altDomain]); } } diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index 073f2255fe7f4..6c9bc7a176664 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -15,6 +15,10 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; +use Symfony\Component\Translation\Formatter\IntlFormatter; +use Symfony\Component\Translation\Formatter\IntlFormatterInterface; +use Symfony\Component\Translation\Formatter\MessageFormatter; +use Symfony\Component\Translation\Formatter\MessageFormatterInterface; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Translator; @@ -683,6 +687,26 @@ public function testIntlFormattedDomain() $this->assertSame('Hi Bob', $translator->trans('some_message', ['%name%' => 'Bob'])); } + public function testIntlDomainOverlapseWithIntlResourceBefore() + { + $intlFormatterMock = $this->createMock(IntlFormatterInterface::class); + $intlFormatterMock->expects($this->once())->method('formatIntl')->with('hello intl', 'en', [])->willReturn('hello intl'); + + $messageFormatter = new MessageFormatter(null, $intlFormatterMock); + + $translator = new Translator('en', $messageFormatter); + $translator->addLoader('array', new ArrayLoader()); + + $translator->addResource('array', ['some_message' => 'hello intl'], 'en', 'messages+intl-icu'); + $translator->addResource('array', ['some_message' => 'hello'], 'en', 'messages'); + + $this->assertSame('hello', $translator->trans('some_message', [], 'messages')); + + $translator->addResource('array', ['some_message' => 'hello intl'], 'en', 'messages+intl-icu'); + + $this->assertSame('hello intl', $translator->trans('some_message', [], 'messages')); + } + /** * @group legacy */ From 07242ccfb50ec433365428f9477b44ba4dcf3058 Mon Sep 17 00:00:00 2001 From: HellFirePvP Date: Tue, 2 Aug 2022 14:54:16 +0200 Subject: [PATCH 23/73] [Filesystem] Remove needless `mb_*` calls --- src/Symfony/Component/Filesystem/Path.php | 68 +++++++++---------- .../Component/Filesystem/Tests/PathTest.php | 2 + 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/Symfony/Component/Filesystem/Path.php b/src/Symfony/Component/Filesystem/Path.php index 0bbd5b4772aff..6d3755e0a6ac3 100644 --- a/src/Symfony/Component/Filesystem/Path.php +++ b/src/Symfony/Component/Filesystem/Path.php @@ -81,7 +81,7 @@ public static function canonicalize(string $path): string // Replace "~" with user's home directory. if ('~' === $path[0]) { - $path = self::getHomeDirectory().mb_substr($path, 1); + $path = self::getHomeDirectory().substr($path, 1); } $path = self::normalize($path); @@ -151,14 +151,14 @@ public static function getDirectory(string $path): string $path = self::canonicalize($path); // Maintain scheme - if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) { - $scheme = mb_substr($path, 0, $schemeSeparatorPosition + 3); - $path = mb_substr($path, $schemeSeparatorPosition + 3); + if (false !== $schemeSeparatorPosition = strpos($path, '://')) { + $scheme = substr($path, 0, $schemeSeparatorPosition + 3); + $path = substr($path, $schemeSeparatorPosition + 3); } else { $scheme = ''; } - if (false === ($dirSeparatorPosition = strrpos($path, '/'))) { + if (false === $dirSeparatorPosition = strrpos($path, '/')) { return ''; } @@ -169,10 +169,10 @@ public static function getDirectory(string $path): string // Directory equals Windows root "C:/" if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) { - return $scheme.mb_substr($path, 0, 3); + return $scheme.substr($path, 0, 3); } - return $scheme.mb_substr($path, 0, $dirSeparatorPosition); + return $scheme.substr($path, 0, $dirSeparatorPosition); } /** @@ -219,7 +219,7 @@ public static function getRoot(string $path): string } // Maintain scheme - if (false !== ($schemeSeparatorPosition = strpos($path, '://'))) { + if (false !== $schemeSeparatorPosition = strpos($path, '://')) { $scheme = substr($path, 0, $schemeSeparatorPosition + 3); $path = substr($path, $schemeSeparatorPosition + 3); } else { @@ -233,7 +233,7 @@ public static function getRoot(string $path): string return $scheme.'/'; } - $length = mb_strlen($path); + $length = \strlen($path); // Windows root if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) { @@ -349,16 +349,16 @@ public static function changeExtension(string $path, string $extension): string $extension = ltrim($extension, '.'); // No extension for paths - if ('/' === mb_substr($path, -1)) { + if ('/' === substr($path, -1)) { return $path; } // No actual extension in path if (empty($actualExtension)) { - return $path.('.' === mb_substr($path, -1) ? '' : '.').$extension; + return $path.('.' === substr($path, -1) ? '' : '.').$extension; } - return mb_substr($path, 0, -mb_strlen($actualExtension)).$extension; + return substr($path, 0, -\strlen($actualExtension)).$extension; } public static function isAbsolute(string $path): bool @@ -368,8 +368,8 @@ public static function isAbsolute(string $path): bool } // Strip scheme - if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) { - $path = mb_substr($path, $schemeSeparatorPosition + 3); + if (false !== $schemeSeparatorPosition = strpos($path, '://')) { + $path = substr($path, $schemeSeparatorPosition + 3); } $firstCharacter = $path[0]; @@ -380,9 +380,9 @@ public static function isAbsolute(string $path): bool } // Windows root - if (mb_strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) { + if (\strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) { // Special case: "C:" - if (2 === mb_strlen($path)) { + if (2 === \strlen($path)) { return true; } @@ -451,9 +451,9 @@ public static function makeAbsolute(string $path, string $basePath): string return self::canonicalize($path); } - if (false !== ($schemeSeparatorPosition = mb_strpos($basePath, '://'))) { - $scheme = mb_substr($basePath, 0, $schemeSeparatorPosition + 3); - $basePath = mb_substr($basePath, $schemeSeparatorPosition + 3); + if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) { + $scheme = substr($basePath, 0, $schemeSeparatorPosition + 3); + $basePath = substr($basePath, $schemeSeparatorPosition + 3); } else { $scheme = ''; } @@ -574,7 +574,7 @@ public static function makeRelative(string $path, string $basePath): string */ public static function isLocal(string $path): bool { - return '' !== $path && false === mb_strpos($path, '://'); + return '' !== $path && false === strpos($path, '://'); } /** @@ -638,7 +638,7 @@ public static function getLongestCommonBasePath(string ...$paths): ?string // Prevent false positives for common prefixes // see isBasePath() - if (0 === mb_strpos($path.'/', $basePath.'/')) { + if (0 === strpos($path.'/', $basePath.'/')) { // next path continue 2; } @@ -666,12 +666,12 @@ public static function join(string ...$paths): string if (null === $finalPath) { // For first part we keep slashes, like '/top', 'C:\' or 'phar://' $finalPath = $path; - $wasScheme = (false !== mb_strpos($path, '://')); + $wasScheme = (false !== strpos($path, '://')); continue; } // Only add slash if previous part didn't end with '/' or '\' - if (!\in_array(mb_substr($finalPath, -1), ['/', '\\'])) { + if (!\in_array(substr($finalPath, -1), ['/', '\\'])) { $finalPath .= '/'; } @@ -717,7 +717,7 @@ public static function isBasePath(string $basePath, string $ofPath): bool // Don't append a slash for the root "/", because then that root // won't be discovered as common prefix ("//" is not a prefix of // "/foobar/"). - return 0 === mb_strpos($ofPath.'/', rtrim($basePath, '/').'/'); + return 0 === strpos($ofPath.'/', rtrim($basePath, '/').'/'); } /** @@ -776,19 +776,19 @@ private static function split(string $path): array } // Remember scheme as part of the root, if any - if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) { - $root = mb_substr($path, 0, $schemeSeparatorPosition + 3); - $path = mb_substr($path, $schemeSeparatorPosition + 3); + if (false !== $schemeSeparatorPosition = strpos($path, '://')) { + $root = substr($path, 0, $schemeSeparatorPosition + 3); + $path = substr($path, $schemeSeparatorPosition + 3); } else { $root = ''; } - $length = mb_strlen($path); + $length = \strlen($path); // Remove and remember root directory - if (0 === mb_strpos($path, '/')) { + if (0 === strpos($path, '/')) { $root .= '/'; - $path = $length > 1 ? mb_substr($path, 1) : ''; + $path = $length > 1 ? substr($path, 1) : ''; } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { if (2 === $length) { // Windows special case: "C:" @@ -796,8 +796,8 @@ private static function split(string $path): array $path = ''; } elseif ('/' === $path[2]) { // Windows normal case: "C:/".. - $root .= mb_substr($path, 0, 3); - $path = $length > 3 ? mb_substr($path, 3) : ''; + $root .= substr($path, 0, 3); + $path = $length > 3 ? substr($path, 3) : ''; } } @@ -806,11 +806,11 @@ private static function split(string $path): array private static function toLower(string $string): string { - if (false !== $encoding = mb_detect_encoding($string)) { + if (false !== $encoding = mb_detect_encoding($string, null, true)) { return mb_strtolower($string, $encoding); } - return strtolower($string, $encoding); + return strtolower($string); } private function __construct() diff --git a/src/Symfony/Component/Filesystem/Tests/PathTest.php b/src/Symfony/Component/Filesystem/Tests/PathTest.php index 4fb2c013066f9..2f04c790c396a 100644 --- a/src/Symfony/Component/Filesystem/Tests/PathTest.php +++ b/src/Symfony/Component/Filesystem/Tests/PathTest.php @@ -223,6 +223,8 @@ public function provideGetDirectoryTests(): \Generator yield ['/..', '/']; yield ['C:webmozart', '']; + + yield ['D:/Folder/Aééé/Subfolder', 'D:/Folder/Aééé']; } /** From 9da305a733074d70a2db2922c01edb0f503a5449 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 2 Aug 2022 17:26:52 +0200 Subject: [PATCH 24/73] [Messenger] Fix Doctrine transport on MySQL --- .../Component/Messenger/Transport/Doctrine/Connection.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php b/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php index 83434752d91ec..57bf5346ef55d 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/Connection.php @@ -162,7 +162,7 @@ public function get(): ?array { if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { try { - $this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31']); + $this->driverConnection->delete($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59']); } catch (DriverException $e) { // Ignore the exception } @@ -252,7 +252,7 @@ public function ack(string $id): bool { try { if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { - return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31'], ['id' => $id]) > 0; + return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0; } return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0; @@ -265,7 +265,7 @@ public function reject(string $id): bool { try { if ($this->driverConnection->getDatabasePlatform() instanceof MySQLPlatform) { - return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31'], ['id' => $id]) > 0; + return $this->driverConnection->update($this->configuration['table_name'], ['delivered_at' => '9999-12-31 23:59:59'], ['id' => $id]) > 0; } return $this->driverConnection->delete($this->configuration['table_name'], ['id' => $id]) > 0; From e62c7b6de82fcc8e146186bb85e090bb4d6343c0 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 2 Aug 2022 17:47:23 +0200 Subject: [PATCH 25/73] cs fix --- src/Symfony/Component/Yaml/Tests/ParserTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 08751d5fe8d91..6bcc937d6f49e 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -2921,7 +2921,8 @@ private function assertSameData($expected, $actual) $this->assertEquals($expected, $actual); $this->assertSame( var_export($expected, true), - var_export($actual, true)); + var_export($actual, true) + ); } } From daeac3d670fcb7f70153bc74458318e40febe2d8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 2 Aug 2022 19:05:58 +0200 Subject: [PATCH 26/73] [Mailer] Fix logic --- src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php index b7948c12d2333..25006fbb7d326 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php @@ -127,6 +127,8 @@ private function callHeloCommand(): array if (!$ex->getCode()) { throw $e; } + + throw $ex; } } From 6a886636f955f3d643de9bd5e4fe9abf005881be Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 2 Aug 2022 19:04:19 +0200 Subject: [PATCH 27/73] [Mailer] Fix error message in case of an SMTP error --- .../Mailer/Transport/Smtp/EsmtpTransport.php | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php index 9a5b214590047..61b3a0e157b17 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/EsmtpTransport.php @@ -114,8 +114,16 @@ private function doEhloCommand(): string { try { $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); - } catch (TransportExceptionInterface) { - return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]); + } catch (TransportExceptionInterface $e) { + try { + return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]); + } catch (TransportExceptionInterface $ex) { + if (!$ex->getCode()) { + throw $e; + } + + throw $ex; + } } $this->capabilities = $this->parseCapabilities($response); @@ -132,12 +140,8 @@ private function doEhloCommand(): string throw new TransportException('Unable to connect with STARTTLS.'); } - try { - $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); - $this->capabilities = $this->parseCapabilities($response); - } catch (TransportExceptionInterface) { - return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]); - } + $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); + $this->capabilities = $this->parseCapabilities($response); } if (\array_key_exists('AUTH', $this->capabilities)) { From 6337bfd0437d35dac854a5cef19c9ba77192253c Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 3 Aug 2022 08:15:11 +0300 Subject: [PATCH 28/73] [Serializer] Fix throwing right exception in ArrayDenormalizer with invalid type --- .../Serializer/Normalizer/ArrayDenormalizer.php | 3 ++- .../Component/Serializer/Tests/Fixtures/Php74Full.php | 2 ++ .../Component/Serializer/Tests/SerializerTest.php | 10 +++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php index 3c64eead1c8b9..cd90ac68191ae 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Serializer\Normalizer; +use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\BadMethodCallException; use Symfony\Component\Serializer\Exception\InvalidArgumentException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; @@ -40,7 +41,7 @@ public function denormalize($data, string $type, string $format = null, array $c throw new BadMethodCallException('Please set a denormalizer before calling denormalize()!'); } if (!\is_array($data)) { - throw new InvalidArgumentException('Data expected to be an array, '.get_debug_type($data).' given.'); + throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('Data expected to be "%s", "%s" given.', $type, get_debug_type($data)), $data, [Type::BUILTIN_TYPE_ARRAY], $context['deserialization_path'] ?? null); } if (!str_ends_with($type, '[]')) { throw new InvalidArgumentException('Unsupported class: '.$type); diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php index 8b53906c405dc..5aea0fa4af76f 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Php74Full.php @@ -31,6 +31,8 @@ final class Php74Full public DummyMessageInterface $dummyMessage; /** @var TestFoo[] $nestedArray */ public TestFoo $nestedObject; + /** @var Php74Full[] */ + public $anotherCollection; } diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 5d5425f88fa2f..761ea066b5962 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -854,7 +854,8 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet }, "nestedObject": { "int": "string" - } + }, + "anotherCollection": null }'; $extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]); @@ -1030,6 +1031,13 @@ public function testCollectDenormalizationErrors(?ClassMetadataFactory $classMet 'useMessageForUser' => true, 'message' => 'The type of the key "int" must be "int" ("string" given).', ], + [ + 'currentType' => 'null', + 'expectedTypes' => ['array'], + 'path' => 'anotherCollection', + 'useMessageForUser' => false, + 'message' => 'Data expected to be "Symfony\Component\Serializer\Tests\Fixtures\Php74Full[]", "null" given.', + ], ]; $this->assertSame($expected, $exceptionsAsArray); From 9e4b0cfb342d204dc46c15cadaee80912a94c76e Mon Sep 17 00:00:00 2001 From: Antoine Makdessi Date: Tue, 2 Aug 2022 21:35:19 +0200 Subject: [PATCH 29/73] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 58caff2209f37..00a686580d01f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,8 +14,9 @@ This will help reviewers and should be a good start for the documentation. Additionally (see https://symfony.com/releases): - Always add tests and ensure they pass. - Bug fixes must be submitted against the lowest maintained branch where they apply - (lowest branches are regularly merged to upper ones so they get the fixes too.) + (lowest branches are regularly merged to upper ones so they get the fixes too). - Features and deprecations must be submitted against the latest branch. + - For new features, provide some code snippets to help understand usage. - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry - Never break backward compatibility (see https://symfony.com/bc). --> From c203ef2a23c7038908ab5303cc2516dfa48f8add Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 3 Aug 2022 11:28:57 +0200 Subject: [PATCH 30/73] suggest to install the Twig bundle when the required component is already installed --- src/Symfony/Bridge/Twig/UndefinedCallableHandler.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php index 16381fddac6c8..43aff010e3c48 100644 --- a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php +++ b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Twig; +use Composer\InstalledVersions; use Symfony\Bundle\FullStack; use Twig\Error\SyntaxError; @@ -93,6 +94,12 @@ private static function onUndefined(string $name, string $type, string $componen throw new SyntaxError(sprintf('Did you forget to %s? Unknown %s "%s".', self::FULL_STACK_ENABLE[$component], $type, $name)); } - throw new SyntaxError(sprintf('Did you forget to run "composer require symfony/%s"? Unknown %s "%s".', $component, $type, $name)); + $missingPackage = 'symfony/'.$component; + + if (class_exists(InstalledVersions::class) && InstalledVersions::isInstalled($missingPackage)) { + $missingPackage = 'symfony/twig-bundle'; + } + + throw new SyntaxError(sprintf('Did you forget to run "composer require %s"? Unknown %s "%s".', $missingPackage, $type, $name)); } } From f68805dbcfa2991253b1b3080601aeb93d352b89 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 3 Aug 2022 11:57:59 +0200 Subject: [PATCH 31/73] [Translation] Fix reading intl-icu domains with LocoProvider --- .../Translation/Bridge/Loco/LocoProvider.php | 6 +++++- .../Bridge/Loco/Tests/LocoProviderTest.php | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php index ca2dad34168f1..3169c5252a542 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php @@ -123,8 +123,12 @@ public function read(array $domains, array $locales): TranslatorBag $this->logger->info(sprintf('No modifications found for locale "%s" and domain "%s" in Loco.', $locale, $domain)); $catalogue = new MessageCatalogue($locale); + $previousMessages = $previousCatalogue->all($domain); - foreach ($previousCatalogue->all($domain) as $key => $message) { + if (!str_ends_with($domain, $catalogue::INTL_DOMAIN_SUFFIX)) { + $previousMessages = array_diff_key($previousMessages, $previousCatalogue->all($domain.$catalogue::INTL_DOMAIN_SUFFIX)); + } + foreach ($previousMessages as $key => $message) { $catalogue->set($this->retrieveKeyFromId($key, $domain), $message, $domain); } diff --git a/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php b/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php index af6342d339d8a..d8678808d714c 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php @@ -458,9 +458,9 @@ public function getResponsesForOneLocaleAndOneDomain(): \Generator $expectedTranslatorBagEn->addCatalogue($arrayLoader->load([ 'index.hello' => 'Hello', 'index.greetings' => 'Welcome, {firstname}!', - ], 'en')); + ], 'en', 'messages+intl-icu')); - yield ['en', 'messages', <<<'XLIFF' + yield ['en', 'messages+intl-icu', <<<'XLIFF' @@ -468,7 +468,7 @@ public function getResponsesForOneLocaleAndOneDomain(): \Generator - + index.hello Hello @@ -488,9 +488,9 @@ public function getResponsesForOneLocaleAndOneDomain(): \Generator $expectedTranslatorBagFr->addCatalogue($arrayLoader->load([ 'index.hello' => 'Bonjour', 'index.greetings' => 'Bienvenue, {firstname} !', - ], 'fr')); + ], 'fr', 'messages+intl-icu')); - yield ['fr', 'messages', <<<'XLIFF' + yield ['fr', 'messages+intl-icu', <<<'XLIFF' @@ -498,7 +498,7 @@ public function getResponsesForOneLocaleAndOneDomain(): \Generator - + index.hello Bonjour From a632fe27eb2e7fba0ee7e3c9eff74fdd90746773 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 3 Aug 2022 14:46:39 +0200 Subject: [PATCH 32/73] [DowCrawler] Fix locale-sensitivity of whitespace normalization --- src/Symfony/Component/DomCrawler/Crawler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/DomCrawler/Crawler.php b/src/Symfony/Component/DomCrawler/Crawler.php index 4f89eec75a74b..ec8e023ec1d05 100644 --- a/src/Symfony/Component/DomCrawler/Crawler.php +++ b/src/Symfony/Component/DomCrawler/Crawler.php @@ -620,7 +620,7 @@ public function text(/* string $default = null, bool $normalizeWhitespace = true $text = $this->getNode(0)->nodeValue; if (\func_num_args() <= 1) { - if (trim(preg_replace('/(?:\s{2,}+|[^\S ])/', ' ', $text)) !== $text) { + if (trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $text), " \n\r\t\x0C") !== $text) { @trigger_error(sprintf('"%s()" will normalize whitespaces by default in Symfony 5.0, set the second "$normalizeWhitespace" argument to false to retrieve the non-normalized version of the text.', __METHOD__), \E_USER_DEPRECATED); } @@ -628,7 +628,7 @@ public function text(/* string $default = null, bool $normalizeWhitespace = true } if (\func_num_args() > 1 && func_get_arg(1)) { - return trim(preg_replace('/(?:\s{2,}+|[^\S ])/', ' ', $text)); + return trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $text), " \n\r\t\x0C"); } return $text; From 1416dbcc7b064a7cfb35cad6d28aef1eb6d2cb8f Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 4 Aug 2022 16:12:07 +0200 Subject: [PATCH 33/73] [String] Fix snake conversion --- src/Symfony/Component/String/AbstractUnicodeString.php | 2 +- src/Symfony/Component/String/ByteString.php | 2 +- src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/String/AbstractUnicodeString.php b/src/Symfony/Component/String/AbstractUnicodeString.php index a482300d28682..6fd418e65afe9 100644 --- a/src/Symfony/Component/String/AbstractUnicodeString.php +++ b/src/Symfony/Component/String/AbstractUnicodeString.php @@ -366,7 +366,7 @@ public function reverse(): parent public function snake(): parent { - $str = $this->camel()->title(); + $str = $this->camel(); $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8'); return $str; diff --git a/src/Symfony/Component/String/ByteString.php b/src/Symfony/Component/String/ByteString.php index bbf8614cf7be6..d9ee3edb52cb2 100644 --- a/src/Symfony/Component/String/ByteString.php +++ b/src/Symfony/Component/String/ByteString.php @@ -363,7 +363,7 @@ public function slice(int $start = 0, int $length = null): parent public function snake(): parent { - $str = $this->camel()->title(); + $str = $this->camel(); $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); return $str; diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php index d28cfb6f53d78..b3c3d9086e1e6 100644 --- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php @@ -1041,6 +1041,7 @@ public static function provideCamel() { return [ ['', ''], + ['xY', 'x_y'], ['symfonyIsGreat', 'symfony_is_great'], ['symfony5IsGreat', 'symfony_5_is_great'], ['symfonyIsGreat', 'Symfony is great'], @@ -1063,6 +1064,8 @@ public static function provideSnake() { return [ ['', ''], + ['x_y', 'x_y'], + ['x_y', 'X_Y'], ['symfony_is_great', 'symfonyIsGreat'], ['symfony5_is_great', 'symfony5IsGreat'], ['symfony5is_great', 'symfony5isGreat'], From 3924498acefcd2f8cdd7185654a5e9be66a2441d Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 4 Aug 2022 18:19:35 +0200 Subject: [PATCH 34/73] [Validator] Hint that `egulias/email-validator` needs to be installed for strict mode as default config --- .../Component/Validator/Constraints/EmailValidator.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Symfony/Component/Validator/Constraints/EmailValidator.php b/src/Symfony/Component/Validator/Constraints/EmailValidator.php index c5a4b21ef21b3..8ea5cee68a2c8 100644 --- a/src/Symfony/Component/Validator/Constraints/EmailValidator.php +++ b/src/Symfony/Component/Validator/Constraints/EmailValidator.php @@ -100,6 +100,10 @@ public function validate($value, Constraint $constraint) } if (null === $constraint->mode) { + if (Email::VALIDATION_MODE_STRICT === $this->defaultMode && !class_exists(EguliasEmailValidator::class)) { + throw new LogicException(sprintf('The "egulias/email-validator" component is required to make the "%s" constraint default to strict mode.', EguliasEmailValidator::class)); + } + $constraint->mode = $this->defaultMode; } From a03c1e5fca63a96f33c775fbf26e20eb812f09be Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 Aug 2022 08:34:37 +0200 Subject: [PATCH 35/73] [Notifier] Fix test logic --- .../Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php | 4 ++-- .../Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Notifier/Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php b/src/Symfony/Component/Notifier/Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php index 46e4289c65c47..35fbd57fad8e8 100644 --- a/src/Symfony/Component/Notifier/Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/FakeChat/Tests/FakeChatEmailTransportTest.php @@ -120,7 +120,7 @@ public function testSendWithCustomTransportAndWithRecipient() $this->assertSame(sprintf('New Chat message for recipient: %s', $recipient), $sentEmail->getSubject()); $this->assertSame($subject, $sentEmail->getTextBody()); $this->assertTrue($sentEmail->getHeaders()->has('X-Transport')); - $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBodyAsString()); + $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBody()); } public function testSendWithCustomTransportAndWithoutRecipient() @@ -144,6 +144,6 @@ public function testSendWithCustomTransportAndWithoutRecipient() $this->assertSame('New Chat message without specified recipient!', $sentEmail->getSubject()); $this->assertSame($subject, $sentEmail->getTextBody()); $this->assertTrue($sentEmail->getHeaders()->has('X-Transport')); - $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBodyAsString()); + $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBody()); } } diff --git a/src/Symfony/Component/Notifier/Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php b/src/Symfony/Component/Notifier/Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php index 2d4c8d9a73d6b..28506b9352458 100644 --- a/src/Symfony/Component/Notifier/Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php +++ b/src/Symfony/Component/Notifier/Bridge/FakeSms/Tests/FakeSmsEmailTransportTest.php @@ -97,6 +97,6 @@ public function testSendWithCustomTransport() $this->assertSame(sprintf('New SMS on phone number: %s', $phone), $sentEmail->getSubject()); $this->assertSame($subject, $sentEmail->getTextBody()); $this->assertTrue($sentEmail->getHeaders()->has('X-Transport')); - $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBodyAsString()); + $this->assertSame($transportName, $sentEmail->getHeaders()->get('X-Transport')->getBody()); } } From 458e0aaaa8959d03e2fd525740e28d4d951798d2 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 5 Aug 2022 10:08:33 +0200 Subject: [PATCH 36/73] fix writes to static $kernel property --- src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php index 1ec6548be2738..5df1cc87df7af 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -77,7 +77,7 @@ protected static function bootKernel(array $options = []) $kernel = static::createKernel($options); $kernel->boot(); - self::$kernel = $kernel; + static::$kernel = $kernel; static::$booted = true; $container = static::$kernel->getContainer(); From 6d79f68649d647a4e9b07aa599660ce4d4f3d82a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 5 Aug 2022 08:58:55 +0200 Subject: [PATCH 37/73] ignore missing keys when mapping DateTime objects to uninitialized arrays --- .../Core/DataMapper/PropertyPathMapper.php | 5 ++++ .../Component/Form/Tests/CompoundFormTest.php | 26 +++++++++++++++++++ .../DataMapper/PropertyPathMapperTest.php | 26 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php index 2a78e03f89715..edd58b83096ec 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php +++ b/src/Symfony/Component/Form/Extension/Core/DataMapper/PropertyPathMapper.php @@ -14,6 +14,7 @@ use Symfony\Component\Form\DataMapperInterface; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\PropertyAccess\Exception\AccessException; +use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -96,6 +97,10 @@ private function getPropertyValue($data, $propertyPath) try { return $this->propertyAccessor->getValue($data, $propertyPath); } catch (AccessException $e) { + if (\is_array($data) && $e instanceof NoSuchIndexException) { + return null; + } + if (!$e instanceof UninitializedPropertyException // For versions without UninitializedPropertyException check the exception message && (class_exists(UninitializedPropertyException::class) || !str_contains($e->getMessage(), 'You should initialize it')) diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index fcfd8fe9e520f..5f6cc60654d62 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -15,6 +15,7 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\Exception\AlreadySubmittedException; use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationRequestHandler; @@ -36,6 +37,7 @@ use Symfony\Component\Form\Tests\Fixtures\Map; use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\PropertyAccess\PropertyAccess; class CompoundFormTest extends TestCase { @@ -1076,6 +1078,30 @@ public function testFileUpload() $this->assertNull($this->form->get('bar')->getData()); } + public function testMapDateTimeObjectsWithEmptyArrayData() + { + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + $form = $this->factory->createBuilder() + ->setDataMapper(new PropertyPathMapper($propertyAccessor)) + ->add('date', DateType::class, [ + 'auto_initialize' => false, + 'format' => 'dd/MM/yyyy', + 'html5' => false, + 'model_timezone' => 'UTC', + 'view_timezone' => 'UTC', + 'widget' => 'single_text', + ]) + ->getForm(); + + $form->submit([ + 'date' => '04/08/2022', + ]); + + $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData()); + } + private function createForm(string $name = 'name', bool $compound = true): FormInterface { $builder = $this->getBuilder($name); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php index 76d936d9789a6..5d729eff971ec 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/PropertyPathMapperTest.php @@ -15,8 +15,10 @@ use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormConfigBuilder; +use Symfony\Component\Form\FormFactoryBuilder; use Symfony\Component\Form\Tests\Fixtures\TypehintedPropertiesCar; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -362,6 +364,30 @@ public function provideDate() [new \DateTimeImmutable()], ]; } + + public function testMapFormsToDataMapsDateTimeInstanceToArrayIfNotSetBefore() + { + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + $form = (new FormFactoryBuilder())->getFormFactory()->createBuilder() + ->setDataMapper(new PropertyPathMapper($propertyAccessor)) + ->add('date', DateType::class, [ + 'auto_initialize' => false, + 'format' => 'dd/MM/yyyy', + 'html5' => false, + 'model_timezone' => 'UTC', + 'view_timezone' => 'UTC', + 'widget' => 'single_text', + ]) + ->getForm(); + + $form->submit([ + 'date' => '04/08/2022', + ]); + + $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData()); + } } class SubmittedForm extends Form From 6ce5d524e3ee67a9e241c5e6fb36fe275655c0dc Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 5 Aug 2022 15:13:10 +0200 Subject: [PATCH 38/73] ignore missing keys when mapping DateTime objects to uninitialized arrays --- .../DataAccessor/PropertyPathAccessor.php | 5 +++ .../Component/Form/Tests/CompoundFormTest.php | 30 +++++++++++++++++- .../Core/DataMapper/DataMapperTest.php | 31 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php b/src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php index 06b0d3602d9f3..3c97075e5fcb4 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php +++ b/src/Symfony/Component/Form/Extension/Core/DataAccessor/PropertyPathAccessor.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\Exception\AccessException; use Symfony\Component\Form\FormInterface; use Symfony\Component\PropertyAccess\Exception\AccessException as PropertyAccessException; +use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException; use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -90,6 +91,10 @@ private function getPropertyValue($data, PropertyPathInterface $propertyPath) try { return $this->propertyAccessor->getValue($data, $propertyPath); } catch (PropertyAccessException $e) { + if (\is_array($data) && $e instanceof NoSuchIndexException) { + return null; + } + if (!$e instanceof UninitializedPropertyException // For versions without UninitializedPropertyException check the exception message && (class_exists(UninitializedPropertyException::class) || false === strpos($e->getMessage(), 'You should initialize it')) diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index deaf1c3515f11..f45c2617389b1 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Form\Exception\AlreadySubmittedException; +use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Extension\Core\Type\DateType; @@ -1079,7 +1080,10 @@ public function testFileUpload() $this->assertNull($this->form->get('bar')->getData()); } - public function testMapDateTimeObjectsWithEmptyArrayData() + /** + * @group legacy + */ + public function testMapDateTimeObjectsWithEmptyArrayDataUsingPropertyPathMapper() { $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() ->enableExceptionOnInvalidIndex() @@ -1103,6 +1107,30 @@ public function testMapDateTimeObjectsWithEmptyArrayData() $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData()); } + public function testMapDateTimeObjectsWithEmptyArrayDataUsingDataMapper() + { + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + $form = $this->factory->createBuilder() + ->setDataMapper(new DataMapper(new PropertyPathAccessor($propertyAccessor))) + ->add('date', DateType::class, [ + 'auto_initialize' => false, + 'format' => 'dd/MM/yyyy', + 'html5' => false, + 'model_timezone' => 'UTC', + 'view_timezone' => 'UTC', + 'widget' => 'single_text', + ]) + ->getForm(); + + $form->submit([ + 'date' => '04/08/2022', + ]); + + $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData()); + } + private function createForm(string $name = 'name', bool $compound = true): FormInterface { $builder = $this->getBuilder($name); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/DataMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/DataMapperTest.php index bc6efb6d3bdc5..0a9a73ca5ba81 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/DataMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataMapper/DataMapperTest.php @@ -14,10 +14,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; +use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormConfigBuilder; +use Symfony\Component\Form\FormFactoryBuilder; use Symfony\Component\Form\Tests\Fixtures\TypehintedPropertiesCar; +use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyPath; class DataMapperTest extends TestCase @@ -388,6 +392,33 @@ public function testMapFormsToDataUsingSetCallbackOption() self::assertSame('Jane Doe', $person->myName()); } + + public function testMapFormsToDataMapsDateTimeInstanceToArrayIfNotSetBefore() + { + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() + ->enableExceptionOnInvalidIndex() + ->getPropertyAccessor(); + $form = (new FormFactoryBuilder())->getFormFactory()->createBuilder() + ->setDataMapper(new DataMapper(new PropertyPathAccessor($propertyAccessor))) + ->add('date', DateType::class, [ + 'auto_initialize' => false, + 'format' => 'dd/MM/yyyy', + 'html5' => false, + 'model_timezone' => 'UTC', + 'view_timezone' => 'UTC', + 'widget' => 'single_text', + ]) + ->getForm(); + + $form->submit([ + 'date' => '04/08/2022', + ]); + + $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData()); + } } class SubmittedForm extends Form From fe4d66737e05f37ed4f5786213248678ea45e6e8 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Sun, 7 Aug 2022 19:49:10 +0200 Subject: [PATCH 39/73] [Translation] Fix Crowdin documentation urls --- .../Bridge/Crowdin/CrowdinProvider.php | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php index a865de1202076..ecfee45369566 100644 --- a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php @@ -228,8 +228,8 @@ private function addFile(string $domain, string $content): ?array $storageId = $this->addStorage($domain, $content); /** - * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.getMany (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.files.getMany (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.projects.files.getMany (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.getMany (Crowdin Enterprise API) */ $response = $this->client->request('POST', 'files', [ 'json' => [ @@ -252,8 +252,8 @@ private function updateFile(int $fileId, string $domain, string $content): ?arra $storageId = $this->addStorage($domain, $content); /** - * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.put (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.files.put (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.projects.files.put (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.put (Crowdin Enterprise API) */ $response = $this->client->request('PUT', 'files/'.$fileId, [ 'json' => [ @@ -275,8 +275,8 @@ private function uploadTranslations(int $fileId, string $domain, string $content $storageId = $this->addStorage($domain, $content); /* - * @see https://support.crowdin.com/api/v2/#operation/api.projects.translations.postOnLanguage (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.translations.postOnLanguage (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.projects.translations.postOnLanguage (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.postOnLanguage (Crowdin Enterprise API) */ return $this->client->request('POST', 'translations/'.str_replace('_', '-', $locale), [ 'json' => [ @@ -289,8 +289,8 @@ private function uploadTranslations(int $fileId, string $domain, string $content private function exportProjectTranslations(string $languageId, int $fileId): ResponseInterface { /* - * @see https://support.crowdin.com/api/v2/#operation/api.projects.translations.exports.post (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.translations.exports.post (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.projects.translations.exports.post (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.translations.exports.post (Crowdin Enterprise API) */ return $this->client->request('POST', 'translations/exports', [ 'json' => [ @@ -303,8 +303,8 @@ private function exportProjectTranslations(string $languageId, int $fileId): Res private function downloadSourceFile(int $fileId): ResponseInterface { /* - * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.download.get (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.files.download.get (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.projects.files.download.get (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.download.get (Crowdin Enterprise API) */ return $this->client->request('GET', sprintf('files/%d/download', $fileId)); } @@ -312,8 +312,8 @@ private function downloadSourceFile(int $fileId): ResponseInterface private function listStrings(int $fileId, int $limit, int $offset): array { /** - * @see https://support.crowdin.com/api/v2/#operation/api.projects.strings.getMany (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.strings.getMany (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.projects.strings.getMany (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.strings.getMany (Crowdin Enterprise API) */ $response = $this->client->request('GET', 'strings', [ 'query' => [ @@ -335,8 +335,8 @@ private function listStrings(int $fileId, int $limit, int $offset): array private function deleteString(int $stringId): ResponseInterface { /* - * @see https://support.crowdin.com/api/v2/#operation/api.projects.strings.delete (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.strings.delete (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.projects.strings.delete (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2#operation/api.projects.strings.delete (Crowdin Enterprise API) */ return $this->client->request('DELETE', 'strings/'.$stringId); } @@ -344,8 +344,8 @@ private function deleteString(int $stringId): ResponseInterface private function addStorage(string $domain, string $content): int { /** - * @see https://support.crowdin.com/api/v2/#operation/api.storages.post (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.storages.post (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.storages.post (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.storages.post (Crowdin Enterprise API) */ $response = $this->client->request('POST', '../../storages', [ 'headers' => [ @@ -367,8 +367,8 @@ private function getFileList(): array $result = []; /** - * @see https://support.crowdin.com/api/v2/#operation/api.projects.files.getMany (Crowdin API) - * @see https://support.crowdin.com/enterprise/api/#operation/api.projects.files.getMany (Crowdin Enterprise API) + * @see https://developer.crowdin.com/api/v2/#operation/api.projects.files.getMany (Crowdin API) + * @see https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.files.getMany (Crowdin Enterprise API) */ $response = $this->client->request('GET', 'files'); From 6b295915fd42a51bdb8e65410faf589e6851c29c Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 8 Aug 2022 09:18:14 +0200 Subject: [PATCH 40/73] fix dispatch signal event check for compatibility with the contract interface --- src/Symfony/Component/Console/Application.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 82f75d4fc6a60..991c94d95ff2d 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -985,9 +985,8 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI if ($this->signalsToDispatchEvent) { $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []; - $dispatchSignals = $this->dispatcher && $this->dispatcher->hasListeners(ConsoleEvents::SIGNAL); - if ($commandSignals || $dispatchSignals) { + if ($commandSignals || null !== $this->dispatcher) { if (!$this->signalRegistry) { throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); } @@ -1007,7 +1006,7 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } } - if ($dispatchSignals) { + if (null !== $this->dispatcher) { foreach ($this->signalsToDispatchEvent as $signal) { $event = new ConsoleSignalEvent($command, $input, $output, $signal); From e5530939b33cd672e21a3eaf1357597c10edf7ef Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 6 Aug 2022 13:08:17 +0200 Subject: [PATCH 41/73] validate nested constraints only if they are in the same group --- .../Constraints/AtLeastOneOfValidator.php | 4 ++++ .../Constraints/AtLeastOneOfValidatorTest.php | 23 ++++++++++++++++++ .../Constraints/SequentiallyValidatorTest.php | 24 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php b/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php index 95558519d8510..888f583eb92b6 100644 --- a/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php +++ b/src/Symfony/Component/Validator/Constraints/AtLeastOneOfValidator.php @@ -34,6 +34,10 @@ public function validate($value, Constraint $constraint) $messages = [$constraint->message]; foreach ($constraint->constraints as $key => $item) { + if (!\in_array($this->context->getGroup(), $item->groups, true)) { + continue; + } + $executionContext = clone $this->context; $executionContext->setNode($value, $this->context->getObject(), $this->context->getMetadata(), $this->context->getPropertyPath()); $violations = $validator->inContext($executionContext)->validate($value, $item, $this->context->getGroup())->getViolations(); diff --git a/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php index 6be6a5d6f702c..0fb735a84cdb2 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/AtLeastOneOfValidatorTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Validator\Constraints\DivisibleBy; use Symfony\Component\Validator\Constraints\EqualTo; use Symfony\Component\Validator\Constraints\Expression; +use Symfony\Component\Validator\Constraints\GreaterThan; use Symfony\Component\Validator\Constraints\GreaterThanOrEqual; use Symfony\Component\Validator\Constraints\IdenticalTo; use Symfony\Component\Validator\Constraints\Language; @@ -235,6 +236,28 @@ public function hasMetadataFor($classOrObject): bool $this->assertSame('custom message foo', $violations->get(0)->getMessage()); $this->assertSame('This value should satisfy at least one of the following constraints: [1] custom message bar', $violations->get(1)->getMessage()); } + + public function testNestedConstraintsAreNotExecutedWhenGroupDoesNotMatch() + { + $validator = Validation::createValidator(); + + $violations = $validator->validate(50, new AtLeastOneOf([ + 'constraints' => [ + new Range([ + 'groups' => 'adult', + 'min' => 18, + 'max' => 55, + ]), + new GreaterThan([ + 'groups' => 'senior', + 'value' => 55, + ]), + ], + 'groups' => ['adult', 'senior'], + ]), 'senior'); + + $this->assertCount(1, $violations); + } } class ExpressionConstraintNested diff --git a/src/Symfony/Component/Validator/Tests/Constraints/SequentiallyValidatorTest.php b/src/Symfony/Component/Validator/Tests/Constraints/SequentiallyValidatorTest.php index be11448ad28e4..1dca3ccd1c186 100644 --- a/src/Symfony/Component/Validator/Tests/Constraints/SequentiallyValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Constraints/SequentiallyValidatorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Tests\Constraints; +use Symfony\Component\Validator\Constraints\GreaterThan; use Symfony\Component\Validator\Constraints\NotEqualTo; use Symfony\Component\Validator\Constraints\Range; use Symfony\Component\Validator\Constraints\Regex; @@ -19,6 +20,7 @@ use Symfony\Component\Validator\Constraints\Type; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; +use Symfony\Component\Validator\Validation; class SequentiallyValidatorTest extends ConstraintValidatorTestCase { @@ -61,4 +63,26 @@ public function testStopsAtFirstConstraintWithViolations() $this->assertCount(1, $this->context->getViolations()); } + + public function testNestedConstraintsAreNotExecutedWhenGroupDoesNotMatch() + { + $validator = Validation::createValidator(); + + $violations = $validator->validate(50, new Sequentially([ + 'constraints' => [ + new GreaterThan([ + 'groups' => 'senior', + 'value' => 55, + ]), + new Range([ + 'groups' => 'adult', + 'min' => 18, + 'max' => 55, + ]), + ], + 'groups' => ['adult', 'senior'], + ]), 'adult'); + + $this->assertCount(0, $violations); + } } From 17774e1fc1a53abbf49af1c975c472cfba14e4dd Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 9 Aug 2022 11:43:52 +0200 Subject: [PATCH 42/73] clean up legacy test --- .../Component/Form/Tests/CompoundFormTest.php | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/Symfony/Component/Form/Tests/CompoundFormTest.php b/src/Symfony/Component/Form/Tests/CompoundFormTest.php index f45c2617389b1..e5a4aeec332aa 100644 --- a/src/Symfony/Component/Form/Tests/CompoundFormTest.php +++ b/src/Symfony/Component/Form/Tests/CompoundFormTest.php @@ -16,7 +16,6 @@ use Symfony\Component\Form\Exception\AlreadySubmittedException; use Symfony\Component\Form\Extension\Core\DataAccessor\PropertyPathAccessor; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; -use Symfony\Component\Form\Extension\Core\DataMapper\PropertyPathMapper; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -1080,33 +1079,6 @@ public function testFileUpload() $this->assertNull($this->form->get('bar')->getData()); } - /** - * @group legacy - */ - public function testMapDateTimeObjectsWithEmptyArrayDataUsingPropertyPathMapper() - { - $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() - ->enableExceptionOnInvalidIndex() - ->getPropertyAccessor(); - $form = $this->factory->createBuilder() - ->setDataMapper(new PropertyPathMapper($propertyAccessor)) - ->add('date', DateType::class, [ - 'auto_initialize' => false, - 'format' => 'dd/MM/yyyy', - 'html5' => false, - 'model_timezone' => 'UTC', - 'view_timezone' => 'UTC', - 'widget' => 'single_text', - ]) - ->getForm(); - - $form->submit([ - 'date' => '04/08/2022', - ]); - - $this->assertEquals(['date' => new \DateTime('2022-08-04', new \DateTimeZone('UTC'))], $form->getData()); - } - public function testMapDateTimeObjectsWithEmptyArrayDataUsingDataMapper() { $propertyAccessor = PropertyAccess::createPropertyAccessorBuilder() From 3327e74c1088044a226fa4dde6f6250c886f36dc Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 9 Aug 2022 13:54:29 +0200 Subject: [PATCH 43/73] add missing changelog entry for the AtLeastOneOf constraint --- src/Symfony/Component/Validator/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 35f7a3e0e1ab6..f15d18b6501b5 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -53,6 +53,7 @@ CHANGELOG 5.1.0 ----- + * Add `AtLeastOneOf` constraint that is considered to be valid if at least one of the nested constraints is valid * added the `Hostname` constraint and validator * added the `alpha3` option to the `Country` and `Language` constraints * allow to define a reusable set of constraints by extending the `Compound` constraint From e912f85a07fa787f9b1f926db6f58928d3f47893 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Tue, 9 Aug 2022 14:01:15 +0200 Subject: [PATCH 44/73] execute tests with the full range of supported Messenger component releases The conflict rule does not exclude the 5.4 and 6.0 releases of the Messenger component. So we should make sure that we also run tests against these versions. --- .../FrameworkExtension.php | 35 +++++++++++++------ .../Bundle/FrameworkBundle/composer.json | 2 +- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 3f92439a0e766..1335e0a80f7c2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -113,6 +113,7 @@ use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Middleware\RouterContextMiddleware; +use Symfony\Component\Messenger\Stamp\SerializedMessageStamp; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Messenger\Transport\TransportFactoryInterface; use Symfony\Component\Messenger\Transport\TransportInterface; @@ -651,18 +652,30 @@ public function load(array $configs, ContainerBuilder $container) $container->registerAttributeForAutoconfiguration(AsController::class, static function (ChildDefinition $definition, AsController $attribute): void { $definition->addTag('controller.service_arguments'); }); - $container->registerAttributeForAutoconfiguration(AsMessageHandler::class, static function (ChildDefinition $definition, AsMessageHandler $attribute, \ReflectionClass|\ReflectionMethod $reflector): void { - $tagAttributes = get_object_vars($attribute); - $tagAttributes['from_transport'] = $tagAttributes['fromTransport']; - unset($tagAttributes['fromTransport']); - if ($reflector instanceof \ReflectionMethod) { - if (isset($tagAttributes['method'])) { - throw new LogicException(sprintf('AsMessageHandler attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name)); + + if (class_exists(SerializedMessageStamp::class)) { + // symfony/messenger >= 6.1 + $container->registerAttributeForAutoconfiguration(AsMessageHandler::class, static function (ChildDefinition $definition, AsMessageHandler $attribute, \ReflectionClass|\ReflectionMethod $reflector): void { + $tagAttributes = get_object_vars($attribute); + $tagAttributes['from_transport'] = $tagAttributes['fromTransport']; + unset($tagAttributes['fromTransport']); + if ($reflector instanceof \ReflectionMethod) { + if (isset($tagAttributes['method'])) { + throw new LogicException(sprintf('AsMessageHandler attribute cannot declare a method on "%s::%s()".', $reflector->class, $reflector->name)); + } + $tagAttributes['method'] = $reflector->getName(); } - $tagAttributes['method'] = $reflector->getName(); - } - $definition->addTag('messenger.message_handler', $tagAttributes); - }); + $definition->addTag('messenger.message_handler', $tagAttributes); + }); + } else { + // symfony/messenger < 6.1 + $container->registerAttributeForAutoconfiguration(AsMessageHandler::class, static function (ChildDefinition $definition, AsMessageHandler $attribute): void { + $tagAttributes = get_object_vars($attribute); + $tagAttributes['from_transport'] = $tagAttributes['fromTransport']; + unset($tagAttributes['fromTransport']); + $definition->addTag('messenger.message_handler', $tagAttributes); + }); + } if (!$container->getParameter('kernel.debug')) { // remove tagged iterator argument for resource checkers diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 1a301287d043d..dc1e717e28595 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -48,7 +48,7 @@ "symfony/http-client": "^5.4|^6.0", "symfony/lock": "^5.4|^6.0", "symfony/mailer": "^5.4|^6.0", - "symfony/messenger": "^6.1", + "symfony/messenger": "^5.4|^6.0", "symfony/mime": "^5.4|^6.0", "symfony/notifier": "^5.4|^6.0", "symfony/process": "^5.4|^6.0", From 72ac5bfbe5d1b2e3be270dfb161c112456a32bed Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Sat, 6 Aug 2022 08:06:01 +0200 Subject: [PATCH 45/73] Always attempt to listen for notifications --- .../Transport/PostgreSqlConnectionTest.php | 67 +++++++++++++++++++ .../Transport/PostgreSqlConnection.php | 16 +---- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/PostgreSqlConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/PostgreSqlConnectionTest.php index f1ffffbb5687a..e8e00d97b3876 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/PostgreSqlConnectionTest.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/PostgreSqlConnectionTest.php @@ -11,6 +11,11 @@ namespace Symfony\Component\Messenger\Bridge\Doctrine\Tests\Transport; +use Doctrine\DBAL\Cache\ArrayResult; +use Doctrine\DBAL\Cache\ArrayStatement; +use Doctrine\DBAL\Platforms\PostgreSQLPlatform; +use Doctrine\DBAL\Query\QueryBuilder; +use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\Table; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Bridge\Doctrine\Transport\PostgreSqlConnection; @@ -42,6 +47,68 @@ public function testUnserialize() $connection->__wakeup(); } + public function testListenOnConnection() + { + $driverConnection = $this->createMock(\Doctrine\DBAL\Connection::class); + + $driverConnection + ->expects(self::any()) + ->method('getDatabasePlatform') + ->willReturn(new PostgreSQLPlatform()); + + $driverConnection + ->expects(self::any()) + ->method('createQueryBuilder') + ->willReturn(new QueryBuilder($driverConnection)); + + $wrappedConnection = new class() { + private $notifyCalls = 0; + + public function pgsqlGetNotify() + { + ++$this->notifyCalls; + + return false; + } + + public function countNotifyCalls() + { + return $this->notifyCalls; + } + }; + + // dbal 2.x + if (interface_exists(Result::class)) { + $driverConnection + ->expects(self::exactly(2)) + ->method('getWrappedConnection') + ->willReturn($wrappedConnection); + + $driverConnection + ->expects(self::any()) + ->method('executeQuery') + ->willReturn(new ArrayStatement([])); + } else { + // dbal 3.x + $driverConnection + ->expects(self::exactly(2)) + ->method('getNativeConnection') + ->willReturn($wrappedConnection); + + $driverConnection + ->expects(self::any()) + ->method('executeQuery') + ->willReturn(new Result(new ArrayResult([]), $driverConnection)); + } + $connection = new PostgreSqlConnection(['table_name' => 'queue_table'], $driverConnection); + + $connection->get(); // first time we have queueEmptiedAt === null, fallback on the parent implementation + $connection->get(); + $connection->get(); + + $this->assertSame(2, $wrappedConnection->countNotifyCalls()); + } + public function testGetExtraSetupSql() { $driverConnection = $this->createMock(\Doctrine\DBAL\Connection::class); diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php index 39615f7344be1..3691a9383f293 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/PostgreSqlConnection.php @@ -33,8 +33,6 @@ final class PostgreSqlConnection extends Connection 'get_notify_timeout' => 0, ]; - private $listening = false; - public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); @@ -62,12 +60,9 @@ public function get(): ?array return parent::get(); } - if (!$this->listening) { - // This is secure because the table name must be a valid identifier: - // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - $this->executeStatement(sprintf('LISTEN "%s"', $this->configuration['table_name'])); - $this->listening = true; - } + // This is secure because the table name must be a valid identifier: + // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS + $this->executeStatement(sprintf('LISTEN "%s"', $this->configuration['table_name'])); if (method_exists($this->driverConnection, 'getNativeConnection')) { $wrappedConnection = $this->driverConnection->getNativeConnection(); @@ -150,11 +145,6 @@ private function createTriggerFunctionName(): string private function unlisten() { - if (!$this->listening) { - return; - } - $this->executeStatement(sprintf('UNLISTEN "%s"', $this->configuration['table_name'])); - $this->listening = false; } } From 3b7aed281be5dbe57098a3ec5b56a9dc973a7776 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Sun, 7 Aug 2022 21:19:23 +0200 Subject: [PATCH 46/73] [Translation] Crowdin provider throw Exception when status is 50x --- .../Bridge/Crowdin/CrowdinProvider.php | 40 +- .../Crowdin/Tests/CrowdinProviderTest.php | 428 ++++++++++++++++ .../Translation/Bridge/Loco/LocoProvider.php | 64 ++- .../Bridge/Loco/Tests/LocoProviderTest.php | 481 ++++++++++++++++++ .../Bridge/Lokalise/LokaliseProvider.php | 30 +- .../Lokalise/Tests/LokaliseProviderTest.php | 302 +++++++++++ 6 files changed, 1316 insertions(+), 29 deletions(-) diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php index a865de1202076..73fa7c158f440 100644 --- a/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Crowdin/CrowdinProvider.php @@ -95,8 +95,12 @@ public function write(TranslatorBagInterface $translatorBag): void } foreach ($responses as $response) { - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to upload translations to Crowdin: "%s".', $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException('Unable to upload translations to Crowdin.', $response); + } } } } @@ -135,9 +139,13 @@ public function read(array $domains, array $locales): TranslatorBag continue; } - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to export file: "%s".', $response->getContent(false))); + if (500 <= $statusCode) { + throw new ProviderException('Unable to export file.', $response); + } + continue; } @@ -146,9 +154,13 @@ public function read(array $domains, array $locales): TranslatorBag } foreach ($downloads as [$response, $locale, $domain]) { - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to download file content: "%s".', $response->getContent(false))); + if (500 <= $statusCode) { + throw new ProviderException('Unable to download file content.', $response); + } + continue; } @@ -192,8 +204,12 @@ public function delete(TranslatorBagInterface $translatorBag): void continue; } - if (204 !== $response->getStatusCode()) { + if (204 !== $statusCode = $response->getStatusCode()) { $this->logger->warning(sprintf('Unable to delete string: "%s".', $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException('Unable to delete string.', $response); + } } } } @@ -238,9 +254,13 @@ private function addFile(string $domain, string $content): ?array ], ]); - if (201 !== $response->getStatusCode()) { + if (201 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to create a File in Crowdin for domain "%s": "%s".', $domain, $response->getContent(false))); + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to create a File in Crowdin for domain "%s".', $domain), $response); + } + return null; } @@ -261,9 +281,13 @@ private function updateFile(int $fileId, string $domain, string $content): ?arra ], ]); - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to update file in Crowdin for file ID "%d" and domain "%s": "%s".', $fileId, $domain, $response->getContent(false))); + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to update file in Crowdin for file ID "%d" and domain "%s".', $fileId, $domain), $response); + } + return null; } @@ -324,9 +348,7 @@ private function listStrings(int $fileId, int $limit, int $offset): array ]); if (200 !== $response->getStatusCode()) { - $this->logger->error(sprintf('Unable to list strings for file %d: "%s".', $fileId, $response->getContent())); - - return []; + throw new ProviderException(sprintf('Unable to list strings for file "%d".', $fileId), $response); } return $response->toArray()['data']; diff --git a/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php b/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php index c21820829834e..8ecee4d1bfe95 100644 --- a/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Crowdin/Tests/CrowdinProviderTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Translation\Bridge\Crowdin\CrowdinProvider; use Symfony\Component\Translation\Dumper\XliffFileDumper; +use Symfony\Component\Translation\Exception\ProviderException; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\MessageCatalogue; @@ -155,6 +156,250 @@ public function testCompleteWriteProcessAddFiles() $provider->write($translatorBag); } + public function testWriteAddFileServerError() + { + $this->xliffFileDumper = new XliffFileDumper(); + + $expectedMessagesFileContent = <<<'XLIFF' + + + +
+ +
+ + + a + trans_en_a + + +
+
+ +XLIFF; + + $responses = [ + 'listFiles' => function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url); + $this->assertSame('Authorization: Bearer API_TOKEN', $options['normalized_headers']['authorization'][0]); + + return new MockResponse(json_encode(['data' => []])); + }, + 'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.crowdin.com/api/v2/storages', $url); + $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]); + $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]); + $this->assertSame($expectedMessagesFileContent, $options['body']); + + return new MockResponse(json_encode(['data' => ['id' => 19]]), ['http_code' => 201]); + }, + 'addFile' => function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url); + $this->assertSame('{"storageId":19,"name":"messages.xlf"}', $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + 'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/', + 'auth_bearer' => 'API_TOKEN', + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to create a File in Crowdin for domain "messages".'); + + $provider->write($translatorBag); + } + + public function testWriteUpdateFileServerError() + { + $this->xliffFileDumper = new XliffFileDumper(); + + $expectedMessagesFileContent = <<<'XLIFF' + + + +
+ +
+ + + a + trans_en_a + + +
+
+ +XLIFF; + + $responses = [ + 'listFiles' => function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url); + $this->assertSame('Authorization: Bearer API_TOKEN', $options['normalized_headers']['authorization'][0]); + + return new MockResponse(json_encode([ + 'data' => [ + ['data' => [ + 'id' => 12, + 'name' => 'messages.xlf', + ]], + ], + ])); + }, + 'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.crowdin.com/api/v2/storages', $url); + $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]); + $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]); + $this->assertSame($expectedMessagesFileContent, $options['body']); + + return new MockResponse(json_encode(['data' => ['id' => 19]]), ['http_code' => 201]); + }, + 'UpdateFile' => function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('PUT', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files/12', $url); + $this->assertSame('{"storageId":19}', $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + 'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/', + 'auth_bearer' => 'API_TOKEN', + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to update file in Crowdin for file ID "12" and domain "messages".'); + + $provider->write($translatorBag); + } + + public function testWriteUploadTranslationsServerError() + { + $this->xliffFileDumper = new XliffFileDumper(); + + $expectedMessagesTranslationsContent = <<<'XLIFF' + + + +
+ +
+ + + a + trans_fr_a + + +
+
+ +XLIFF; + + $expectedMessagesFileContent = <<<'XLIFF' + + + +
+ +
+ + + a + trans_en_a + + +
+
+ +XLIFF; + + $responses = [ + 'listFiles' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url); + + return new MockResponse(json_encode([ + 'data' => [ + ['data' => [ + 'id' => 12, + 'name' => 'messages.xlf', + ]], + ], + ])); + }, + 'addStorage' => function (string $method, string $url, array $options = []) use ($expectedMessagesFileContent): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.crowdin.com/api/v2/storages', $url); + $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]); + $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]); + $this->assertSame($expectedMessagesFileContent, $options['body']); + + return new MockResponse(json_encode(['data' => ['id' => 19]]), ['http_code' => 201]); + }, + 'updateFile' => function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('PUT', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files/12', $url); + $this->assertSame('{"storageId":19}', $options['body']); + + return new MockResponse(json_encode(['data' => ['id' => 12, 'name' => 'messages.xlf']])); + }, + 'addStorage2' => function (string $method, string $url, array $options = []) use ($expectedMessagesTranslationsContent): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.crowdin.com/api/v2/storages', $url); + $this->assertSame('Content-Type: application/octet-stream', $options['normalized_headers']['content-type'][0]); + $this->assertSame('Crowdin-API-FileName: messages.xlf', $options['normalized_headers']['crowdin-api-filename'][0]); + $this->assertSame($expectedMessagesTranslationsContent, $options['body']); + + return new MockResponse(json_encode(['data' => ['id' => 19]]), ['http_code' => 201]); + }, + 'UploadTranslations' => function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame(sprintf('https://api.crowdin.com/api/v2/projects/1/translations/%s', 'fr'), $url); + $this->assertSame('{"storageId":19,"fileId":12}', $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + $translatorBag->addCatalogue(new MessageCatalogue('fr', [ + 'messages' => ['a' => 'trans_fr_a'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/', + 'auth_bearer' => 'API_TOKEN', + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to upload translations to Crowdin.'); + + $provider->write($translatorBag); + } + public function testCompleteWriteProcessUpdateFiles() { $this->xliffFileDumper = new XliffFileDumper(); @@ -563,6 +808,82 @@ public function getResponsesForDefaultLocaleAndOneDomain(): \Generator ]; } + public function testReadServerException() + { + $responses = [ + 'listFiles' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url); + + return new MockResponse(json_encode([ + 'data' => [ + ['data' => [ + 'id' => 12, + 'name' => 'messages.xlf', + ]], + ], + ])); + }, + 'exportProjectTranslations' => function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/translations/exports', $url); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $crowdinProvider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/', + 'auth_bearer' => 'API_TOKEN', + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to export file.'); + + $crowdinProvider->read(['messages'], ['fr']); + } + + public function testReadDownloadServerException() + { + $responses = [ + 'listFiles' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url); + + return new MockResponse(json_encode([ + 'data' => [ + ['data' => [ + 'id' => 12, + 'name' => 'messages.xlf', + ]], + ], + ])); + }, + 'exportProjectTranslations' => function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/translations/exports', $url); + + return new MockResponse(json_encode(['data' => ['url' => 'https://file.url']])); + }, + 'downloadFile' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://file.url/', $url); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $crowdinProvider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/', + 'auth_bearer' => 'API_TOKEN', + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to download file content.'); + + $crowdinProvider->read(['messages'], ['fr']); + } + public function testDelete() { $responses = [ @@ -631,4 +952,111 @@ public function testDelete() $provider->delete($translatorBag); } + + public function testDeleteListStringServerException() + { + $responses = [ + 'listFiles' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url); + + return new MockResponse(json_encode([ + 'data' => [ + ['data' => [ + 'id' => 12, + 'name' => 'messages.xlf', + ]], + ], + ])); + }, + 'listStrings' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/strings?fileId=12&limit=500&offset=0', $url); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => [ + 'en a' => 'en a', + ], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/', + 'auth_bearer' => 'API_TOKEN', + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to list strings for file "12".'); + + $provider->delete($translatorBag); + } + + public function testDeleteDeleteStringServerException() + { + $responses = [ + 'listFiles' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/files', $url); + + return new MockResponse(json_encode([ + 'data' => [ + ['data' => [ + 'id' => 12, + 'name' => 'messages.xlf', + ]], + ], + ])); + }, + 'listStrings' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/strings?fileId=12&limit=500&offset=0', $url); + + return new MockResponse(json_encode([ + 'data' => [ + ['data' => ['id' => 1, 'text' => 'en a']], + ['data' => ['id' => 2, 'text' => 'en b']], + ], + ])); + }, + 'listStrings2' => function (string $method, string $url): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/strings?fileId=12&limit=500&offset=500', $url); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->any()) + ->method('getContent') + ->with(false) + ->willReturn(json_encode(['data' => []])); + + return $response; + }, + 'deleteString1' => function (string $method, string $url): ResponseInterface { + $this->assertSame('DELETE', $method); + $this->assertSame('https://api.crowdin.com/api/v2/projects/1/strings/1', $url); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => [ + 'en a' => 'en a', + ], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://api.crowdin.com/api/v2/projects/1/', + 'auth_bearer' => 'API_TOKEN', + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.crowdin.com/api/v2/projects/1/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to delete string.'); + + $provider->delete($translatorBag); + } } diff --git a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php index ce1eee839366a..54ad8d0cc8443 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/LocoProvider.php @@ -148,12 +148,16 @@ public function delete(TranslatorBagInterface $translatorBag): void } foreach ($responses as $key => $response) { - if (403 === $response->getStatusCode()) { + if (403 === $statusCode = $response->getStatusCode()) { $this->logger->error('The API key used does not have sufficient permissions to delete assets.'); } - if (200 !== $response->getStatusCode() && 404 !== $response->getStatusCode()) { + if (200 !== $statusCode && 404 !== $statusCode) { $this->logger->error(sprintf('Unable to delete translation key "%s" to Loco: "%s".', $key, $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to delete translation key "%s" to Loco.', $key), $response); + } } } } @@ -165,8 +169,12 @@ private function getAssetsIds(string $domain): array { $response = $this->client->request('GET', 'assets', ['query' => ['filter' => $domain]]); - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to get assets from Loco: "%s".', $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException('Unable to get assets from Loco.', $response); + } } return array_map(function ($asset) { @@ -190,8 +198,12 @@ private function createAssets(array $keys, string $domain): array } foreach ($responses as $key => $response) { - if (201 !== $response->getStatusCode()) { - $this->logger->error(sprintf('Unable to add new translation key "%s" to Loco: (status code: "%s") "%s".', $key, $response->getStatusCode(), $response->getContent(false))); + if (201 !== $statusCode = $response->getStatusCode()) { + $this->logger->error(sprintf('Unable to add new translation key "%s" to Loco: (status code: "%s") "%s".', $key, $statusCode, $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to add new translation key "%s" to Loco: (status code: "%s").', $key, $statusCode), $response); + } } else { $createdIds[] = $response->toArray(false)['id']; } @@ -212,8 +224,12 @@ private function translateAssets(array $translations, string $locale): void } foreach ($responses as $id => $response) { - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to add translation for key "%s" in locale "%s" to Loco: "%s".', $id, $locale, $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to add translation for key "%s" in locale "%s" to Loco.', $id, $locale), $response); + } } } } @@ -234,13 +250,19 @@ private function tagsAssets(array $ids, string $tag): void } } - // Set tags for all ids without comma. - $response = $this->client->request('POST', sprintf('tags/%s.json', rawurlencode($tag)), [ - 'body' => implode(',', $idsWithoutComma), - ]); + if ([] !== $idsWithoutComma) { + // Set tags for all ids without comma. + $response = $this->client->request('POST', sprintf('tags/%s.json', rawurlencode($tag)), [ + 'body' => implode(',', $idsWithoutComma), + ]); - if (200 !== $response->getStatusCode()) { - $this->logger->error(sprintf('Unable to tag assets with "%s" on Loco: "%s".', $tag, $response->getContent(false))); + if (200 !== $statusCode = $response->getStatusCode()) { + $this->logger->error(sprintf('Unable to tag assets with "%s" on Loco: "%s".', $tag, $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to tag assets with "%s" on Loco.', $tag), $response); + } + } } // Set tags for each id with comma one by one. @@ -249,8 +271,12 @@ private function tagsAssets(array $ids, string $tag): void 'body' => ['name' => $tag], ]); - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to tag asset "%s" with "%s" on Loco: "%s".', $id, $tag, $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to tag asset "%s" with "%s" on Loco.', $id, $tag), $response); + } } } } @@ -263,8 +289,12 @@ private function createTag(string $tag): void ], ]); - if (201 !== $response->getStatusCode()) { + if (201 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to create tag "%s" on Loco: "%s".', $tag, $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to create tag "%s" on Loco.', $tag), $response); + } } } @@ -288,8 +318,12 @@ private function createLocale(string $locale): void ], ]); - if (201 !== $response->getStatusCode()) { + if (201 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to create locale "%s" on Loco: "%s".', $locale, $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException(sprintf('Unable to create locale "%s" on Loco.', $locale), $response); + } } } diff --git a/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php b/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php index b5c49e0fa6e22..e38f9bf37f0a6 100644 --- a/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Loco/Tests/LocoProviderTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Translation\Bridge\Loco\LocoProvider; +use Symfony\Component\Translation\Exception\ProviderException; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Loader\XliffFileLoader; @@ -250,6 +251,451 @@ public function testCompleteWriteProcess() $provider->write($translatorBag); } + public function testWriteCreateAssetServerError() + { + $expectedAuthHeader = 'Authorization: Loco API_KEY'; + + $responses = [ + 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $expectedBody = http_build_query([ + 'id' => 'messages__a', + 'text' => 'a', + 'type' => 'text', + 'default' => 'untranslated', + ]); + + $this->assertSame('POST', $method); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame($expectedBody, $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://localise.biz/api/', + 'headers' => ['Authorization' => 'Loco API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to add new translation key "a" to Loco: (status code: "500").'); + + $provider->write($translatorBag); + } + + public function testWriteCreateTagServerError() + { + $expectedAuthHeader = 'Authorization: Loco API_KEY'; + + $responses = [ + 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $expectedBody = http_build_query([ + 'id' => 'messages__a', + 'text' => 'a', + 'type' => 'text', + 'default' => 'untranslated', + ]); + + $this->assertSame('POST', $method); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame($expectedBody, $options['body']); + + return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]); + }, + 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[]'); + }, + 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame(http_build_query(['name' => 'messages']), $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://localise.biz/api/', + 'headers' => ['Authorization' => 'Loco API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to create tag "messages" on Loco.'); + + $provider->write($translatorBag); + } + + public function testWriteTagAssetsServerError() + { + $expectedAuthHeader = 'Authorization: Loco API_KEY'; + + $responses = [ + 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $expectedBody = http_build_query([ + 'id' => 'messages__a', + 'text' => 'a', + 'type' => 'text', + 'default' => 'untranslated', + ]); + + $this->assertSame('POST', $method); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame($expectedBody, $options['body']); + + return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]); + }, + 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[]'); + }, + 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame(http_build_query(['name' => 'messages']), $options['body']); + + return new MockResponse('', ['http_code' => 201]); + }, + 'tagAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags/messages.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame('messages__a', $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://localise.biz/api/', + 'headers' => ['Authorization' => 'Loco API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to tag assets with "messages" on Loco.'); + + $provider->write($translatorBag); + } + + public function testWriteTagAssetsServerErrorWithComma() + { + $expectedAuthHeader = 'Authorization: Loco API_KEY'; + + $responses = [ + 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $expectedBody = http_build_query([ + 'id' => 'messages__a', + 'text' => 'a', + 'type' => 'text', + 'default' => 'untranslated', + ]); + + $this->assertSame('POST', $method); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame($expectedBody, $options['body']); + + return new MockResponse('{"id": "messages__a,messages__b"}', ['http_code' => 201]); + }, + 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[]'); + }, + 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame(http_build_query(['name' => 'messages']), $options['body']); + + return new MockResponse('', ['http_code' => 201]); + }, + 'tagAssetWithComma' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/assets/messages__a%2Cmessages__b/tags', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame('name=messages', $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://localise.biz/api/', + 'headers' => ['Authorization' => 'Loco API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to tag asset "messages__a,messages__b" with "messages" on Loco.'); + + $provider->write($translatorBag); + } + + public function testWriteCreateLocaleServerError() + { + $expectedAuthHeader = 'Authorization: Loco API_KEY'; + + $responses = [ + 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $expectedBody = http_build_query([ + 'id' => 'messages__a', + 'text' => 'a', + 'type' => 'text', + 'default' => 'untranslated', + ]); + + $this->assertSame('POST', $method); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame($expectedBody, $options['body']); + + return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]); + }, + 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[]'); + }, + 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame(http_build_query(['name' => 'messages']), $options['body']); + + return new MockResponse('', ['http_code' => 201]); + }, + 'tagAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags/messages.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame('messages__a', $options['body']); + + return new MockResponse(); + }, + 'getLocales' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/locales', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[{"code":"fr"}]'); + }, + 'createLocale' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/locales', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://localise.biz/api/', + 'headers' => ['Authorization' => 'Loco API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to create locale "en" on Loco.'); + + $provider->write($translatorBag); + } + + public function testWriteGetAssetsIdsServerError() + { + $expectedAuthHeader = 'Authorization: Loco API_KEY'; + + $responses = [ + 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $expectedBody = http_build_query([ + 'id' => 'messages__a', + 'text' => 'a', + 'type' => 'text', + 'default' => 'untranslated', + ]); + + $this->assertSame('POST', $method); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame($expectedBody, $options['body']); + + return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]); + }, + 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[]'); + }, + 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame(http_build_query(['name' => 'messages']), $options['body']); + + return new MockResponse('', ['http_code' => 201]); + }, + 'tagAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags/messages.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame('messages__a', $options['body']); + + return new MockResponse(); + }, + 'getLocales' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/locales', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[{"code":"en"}]'); + }, + 'getAssetsIds' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/assets?filter=messages', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + $translatorBag->addCatalogue(new MessageCatalogue('fr', [ + 'messages' => ['a' => 'trans_fr_a'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://localise.biz/api/', + 'headers' => ['Authorization' => 'Loco API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to get assets from Loco.'); + + $provider->write($translatorBag); + } + + public function testWriteTranslateAssetsServerError() + { + $expectedAuthHeader = 'Authorization: Loco API_KEY'; + + $responses = [ + 'createAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $expectedBody = http_build_query([ + 'id' => 'messages__a', + 'text' => 'a', + 'type' => 'text', + 'default' => 'untranslated', + ]); + + $this->assertSame('POST', $method); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame($expectedBody, $options['body']); + + return new MockResponse('{"id": "messages__a"}', ['http_code' => 201]); + }, + 'getTags' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[]'); + }, + 'createTag' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame(http_build_query(['name' => 'messages']), $options['body']); + + return new MockResponse('', ['http_code' => 201]); + }, + 'tagAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/tags/messages.json', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame('messages__a', $options['body']); + + return new MockResponse(); + }, + 'getLocales' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/locales', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[{"code":"en"}]'); + }, + 'getAssetsIds' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/assets?filter=messages', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + + return new MockResponse('[{"id":"messages__foo.existing_key"},{"id":"messages__a"}]'); + }, + 'translateAsset' => function (string $method, string $url, array $options = []) use ($expectedAuthHeader): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame('https://localise.biz/api/translations/messages__a/en', $url); + $this->assertSame($expectedAuthHeader, $options['normalized_headers']['authorization'][0]); + $this->assertSame('trans_en_a', $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }, + ]; + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + $translatorBag->addCatalogue(new MessageCatalogue('fr', [ + 'messages' => ['a' => 'trans_fr_a'], + ])); + + $provider = $this->createProvider((new MockHttpClient($responses))->withOptions([ + 'base_uri' => 'https://localise.biz/api/', + 'headers' => ['Authorization' => 'Loco API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'localise.biz/api/'); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to add translation for key "messages__a" in locale "en" to Loco.'); + + $provider->write($translatorBag); + } + /** * @dataProvider getResponsesForOneLocaleAndOneDomain */ @@ -363,6 +809,41 @@ function (string $method, string $url): MockResponse { $provider->delete($translatorBag); } + public function testDeleteServerError() + { + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['a' => 'trans_en_a'], + ])); + + $provider = $this->createProvider( + new MockHttpClient([ + function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://localise.biz/api/assets?filter=messages', $url); + $this->assertSame(['filter' => 'messages'], $options['query']); + + return new MockResponse('[{"id":"messages__a"}]'); + }, + function (string $method, string $url): MockResponse { + $this->assertSame('DELETE', $method); + $this->assertSame('https://localise.biz/api/assets/messages__a.json', $url); + + return new MockResponse('', ['http_code' => 500]); + }, + ], 'https://localise.biz/api/'), + $this->getLoader(), + $this->getLogger(), + $this->getDefaultLocale(), + 'localise.biz/api/' + ); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to delete translation key "messages__a" to Loco.'); + + $provider->delete($translatorBag); + } + public function getResponsesForOneLocaleAndOneDomain(): \Generator { $arrayLoader = new ArrayLoader(); diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php index b86d3924ffec8..ab9594c8ee860 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php @@ -198,9 +198,13 @@ private function createKeys(array $keys, string $domain): array $createdKeys = []; foreach ($responses as $response) { - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to create keys to Lokalise: "%s".', $response->getContent(false))); + if (500 <= $statusCode) { + throw new ProviderException('Unable to create keys to Lokalise.', $response); + } + continue; } @@ -254,8 +258,12 @@ private function updateTranslations(array $keysByDomain, TranslatorBagInterface 'json' => ['keys' => $keysToUpdate], ]); - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to create/update translations to Lokalise: "%s".', $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException('Unable to create/update translations to Lokalise.', $response); + } } } @@ -270,8 +278,12 @@ private function getKeysIds(array $keys, string $domain, int $page = 1): array ], ]); - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to get keys ids from Lokalise: "%s".', $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException('Unable to get keys ids from Lokalise.', $response); + } } $result = []; @@ -320,9 +332,13 @@ private function getLanguages(): array { $response = $this->client->request('GET', 'languages'); - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to get languages from Lokalise: "%s".', $response->getContent(false))); + if (500 <= $statusCode) { + throw new ProviderException('Unable to get languages from Lokalise.', $response); + } + return []; } @@ -345,8 +361,12 @@ private function createLanguages(array $languages): void ], ]); - if (200 !== $response->getStatusCode()) { + if (200 !== $statusCode = $response->getStatusCode()) { $this->logger->error(sprintf('Unable to create languages on Lokalise: "%s".', $response->getContent(false))); + + if (500 <= $statusCode) { + throw new ProviderException('Unable to create languages on Lokalise.', $response); + } } } diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php index 7ce9b8e067ada..06e3df223482c 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\Translation\Bridge\Lokalise\LokaliseProvider; +use Symfony\Component\Translation\Exception\ProviderException; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Loader\XliffFileLoader; @@ -247,6 +248,307 @@ public function testCompleteWriteProcess() $this->assertTrue($updateProcessed, 'Translations update was not called.'); } + public function testWriteGetLanguageServerError() + { + $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + + return new MockResponse('', ['http_code' => 500]); + }; + + $provider = $this->createProvider((new MockHttpClient([ + $getLanguagesResponse, + ]))->withOptions([ + 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/', + 'headers' => ['X-Api-Token' => 'API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com'); + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['young_dog' => 'puppy'], + ])); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to get languages from Lokalise.'); + + $provider->write($translatorBag); + } + + public function testWriteCreateLanguageServerError() + { + $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + + return new MockResponse(json_encode(['languages' => []])); + }; + + $createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedBody = json_encode([ + 'languages' => [ + ['lang_iso' => 'en'], + ], + ]); + + $this->assertSame('POST', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }; + + $provider = $this->createProvider((new MockHttpClient([ + $getLanguagesResponse, + $createLanguagesResponse, + ]))->withOptions([ + 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/', + 'headers' => ['X-Api-Token' => 'API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com'); + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['young_dog' => 'puppy'], + ])); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to create languages on Lokalise.'); + + $provider->write($translatorBag); + } + + public function testWriteGetKeysIdsServerError() + { + $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + + return new MockResponse(json_encode(['languages' => []])); + }; + + $createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedBody = json_encode([ + 'languages' => [ + ['lang_iso' => 'en'], + ], + ]); + + $this->assertSame('POST', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse(json_encode(['keys' => []])); + }; + + $getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedQuery = [ + 'filter_keys' => '', + 'filter_filenames' => 'messages.xliff', + 'limit' => 5000, + 'page' => 1, + ]; + + $this->assertSame('GET', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url); + $this->assertSame($expectedQuery, $options['query']); + + return new MockResponse('', ['http_code' => 500]); + }; + + $provider = $this->createProvider((new MockHttpClient([ + $getLanguagesResponse, + $createLanguagesResponse, + $getKeysIdsForMessagesDomainResponse, + ]))->withOptions([ + 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/', + 'headers' => ['X-Api-Token' => 'API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com'); + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['young_dog' => 'puppy'], + ])); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to get keys ids from Lokalise.'); + + $provider->write($translatorBag); + } + + public function testWriteCreateKeysServerError() + { + $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + + return new MockResponse(json_encode(['languages' => []])); + }; + + $createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedBody = json_encode([ + 'languages' => [ + ['lang_iso' => 'en'], + ], + ]); + + $this->assertSame('POST', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse(json_encode(['keys' => []])); + }; + + $getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedQuery = [ + 'filter_keys' => '', + 'filter_filenames' => 'messages.xliff', + 'limit' => 5000, + 'page' => 1, + ]; + + $this->assertSame('GET', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url); + $this->assertSame($expectedQuery, $options['query']); + + return new MockResponse(json_encode(['keys' => []])); + }; + + $createKeysForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedBody = json_encode([ + 'keys' => [ + [ + 'key_name' => 'young_dog', + 'platforms' => ['web'], + 'filenames' => [ + 'web' => 'messages.xliff', + 'ios' => null, + 'android' => null, + 'other' => null, + ], + ], + ], + ]); + + $this->assertSame('POST', $method); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse('', ['http_code' => 500]); + }; + + $provider = $this->createProvider((new MockHttpClient([ + $getLanguagesResponse, + $createLanguagesResponse, + $getKeysIdsForMessagesDomainResponse, + $createKeysForMessagesDomainResponse, + ]))->withOptions([ + 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/', + 'headers' => ['X-Api-Token' => 'API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com'); + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['young_dog' => 'puppy'], + ])); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to create keys to Lokalise.'); + + $provider->write($translatorBag); + } + + public function testWriteUploadTranslationsServerError() + { + $getLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + + return new MockResponse(json_encode(['languages' => []])); + }; + + $createLanguagesResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedBody = json_encode([ + 'languages' => [ + ['lang_iso' => 'en'], + ], + ]); + + $this->assertSame('POST', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/languages', $url); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse(json_encode(['keys' => []])); + }; + + $getKeysIdsForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedQuery = [ + 'filter_keys' => '', + 'filter_filenames' => 'messages.xliff', + 'limit' => 5000, + 'page' => 1, + ]; + + $this->assertSame('GET', $method); + $this->assertSame('https://api.lokalise.com/api2/projects/PROJECT_ID/keys?'.http_build_query($expectedQuery), $url); + $this->assertSame($expectedQuery, $options['query']); + + return new MockResponse(json_encode(['keys' => []])); + }; + + $createKeysForMessagesDomainResponse = function (string $method, string $url, array $options = []): ResponseInterface { + $expectedBody = json_encode([ + 'keys' => [ + [ + 'key_name' => 'young_dog', + 'platforms' => ['web'], + 'filenames' => [ + 'web' => 'messages.xliff', + 'ios' => null, + 'android' => null, + 'other' => null, + ], + ], + ], + ]); + + $this->assertSame('POST', $method); + $this->assertJsonStringEqualsJsonString($expectedBody, $options['body']); + + return new MockResponse(json_encode(['keys' => [ + [ + 'key_name' => ['web' => 'young_dog'], + 'key_id' => 29, + ], + ]])); + }; + + $updateTranslationsResponse = function (string $method, string $url, array $options = []) use (&$updateProcessed): ResponseInterface { + $this->assertSame('PUT', $method); + + return new MockResponse('', ['http_code' => 500]); + }; + + $provider = $this->createProvider((new MockHttpClient([ + $getLanguagesResponse, + $createLanguagesResponse, + $getKeysIdsForMessagesDomainResponse, + $createKeysForMessagesDomainResponse, + $updateTranslationsResponse, + ]))->withOptions([ + 'base_uri' => 'https://api.lokalise.com/api2/projects/PROJECT_ID/', + 'headers' => ['X-Api-Token' => 'API_KEY'], + ]), $this->getLoader(), $this->getLogger(), $this->getDefaultLocale(), 'api.lokalise.com'); + + $translatorBag = new TranslatorBag(); + $translatorBag->addCatalogue(new MessageCatalogue('en', [ + 'messages' => ['young_dog' => 'puppy'], + ])); + + $this->expectException(ProviderException::class); + $this->expectExceptionMessage('Unable to create/update translations to Lokalise.'); + + $provider->write($translatorBag); + } + /** * @dataProvider getResponsesForOneLocaleAndOneDomain */ From abd3b13cc688ebdc87567004ed3d4d7bcf62b47d Mon Sep 17 00:00:00 2001 From: Laurent VOULLEMIER Date: Wed, 10 Aug 2022 11:32:19 +0200 Subject: [PATCH 47/73] Remove wrong PHPDoc isGranted doesn't throw anymore an exception when there is no token in the storage --- .../Security/Core/Authorization/AuthorizationChecker.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php index 5946060fbfdf1..9bf138bc9ec7f 100644 --- a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php +++ b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php @@ -40,8 +40,6 @@ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionM /** * {@inheritdoc} - * - * @throws AuthenticationCredentialsNotFoundException when the token storage has no authentication token and $exceptionOnNoToken is set to true */ final public function isGranted(mixed $attribute, mixed $subject = null): bool { From b2fea0341cd86ae4a1e696ea09db7f8078ce6ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sama=C3=ABl=20Villette?= Date: Wed, 10 Aug 2022 09:21:11 +0200 Subject: [PATCH 48/73] [HttpKernel] Fix passing `null` to `\trim()` method in LoggerDataCollector --- .../Component/HttpKernel/DataCollector/LoggerDataCollector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index 15094fdbed5b5..2bbd2a039eab9 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -144,7 +144,7 @@ public function getFilters() $allChannels = []; foreach ($this->getProcessedLogs() as $log) { - if ('' === trim($log['channel'])) { + if ('' === trim($log['channel'] ?? '')) { continue; } From 11321713d932d0da1479796e1527152b825cf322 Mon Sep 17 00:00:00 2001 From: Jan Sorgalla Date: Thu, 11 Aug 2022 16:57:24 +0200 Subject: [PATCH 49/73] [Serializer] Fix get accessor regex in AnnotationLoader --- .../Component/Serializer/Mapping/Loader/AnnotationLoader.php | 3 +-- .../Fixtures/Annotations/IgnoreDummyAdditionalGetter.php | 4 ++++ .../IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php | 4 ++++ .../Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php | 4 ++++ .../IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php | 4 ++++ .../Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php | 2 ++ 6 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php index c96f2946a6f9f..d6bef3c421a4a 100644 --- a/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php +++ b/src/Symfony/Component/Serializer/Mapping/Loader/AnnotationLoader.php @@ -100,8 +100,7 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata) continue; } - $getAccessor = preg_match('/^(get|)(.+)$/i', $method->name); - if ($getAccessor && 0 !== $method->getNumberOfRequiredParameters()) { + if (0 === stripos($method->name, 'get') && $method->getNumberOfRequiredParameters()) { continue; /* matches the BC behavior in `Symfony\Component\Serializer\Normalizer\ObjectNormalizer::extractAttributes` */ } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetter.php index a2fe769e36b8c..326a9cd07589e 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetter.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetter.php @@ -20,4 +20,8 @@ public function getExtraValue(string $parameter) { return $parameter; } + + public function setExtraValue2(string $parameter) + { + } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php index 11094dad012e6..2b717c93a9752 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Annotations/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php @@ -15,4 +15,8 @@ public function getExtraValue(string $parameter) { return $parameter; } + + public function setExtraValue2(string $parameter) + { + } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php index cec21db4be663..274479e63b5b3 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetter.php @@ -18,4 +18,8 @@ public function getExtraValue(string $parameter) { return $parameter; } + + public function setExtraValue2(string $parameter) + { + } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php index 6f0f6da1bb883..21abb870be477 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/IgnoreDummyAdditionalGetterWithoutIgnoreAnnotations.php @@ -15,4 +15,8 @@ public function getExtraValue(string $parameter) { return $parameter; } + + public function setExtraValue2(string $parameter) + { + } } diff --git a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php index f2f5325c0e264..735568110cd74 100644 --- a/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Mapping/Loader/AnnotationLoaderTest.php @@ -157,6 +157,7 @@ public function testIgnoreGetterWirhRequiredParameterIfIgnoreAnnotationIsUsed() $attributes = $classMetadata->getAttributesMetadata(); self::assertArrayNotHasKey('extraValue', $attributes); + self::assertArrayHasKey('extraValue2', $attributes); } public function testIgnoreGetterWirhRequiredParameterIfIgnoreAnnotationIsNotUsed() @@ -166,6 +167,7 @@ public function testIgnoreGetterWirhRequiredParameterIfIgnoreAnnotationIsNotUsed $attributes = $classMetadata->getAttributesMetadata(); self::assertArrayNotHasKey('extraValue', $attributes); + self::assertArrayHasKey('extraValue2', $attributes); } abstract protected function createLoader(): AnnotationLoader; From cce696ec4cfb58833db296a40e22c129d01f8d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 12 Aug 2022 14:26:43 +0200 Subject: [PATCH 50/73] [String] Add tests for AsciiSlugger --- .../String/Tests/Slugger/AsciiSluggerTest.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php diff --git a/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php b/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php new file mode 100644 index 0000000000000..5ce86f25dcb97 --- /dev/null +++ b/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\String\Slugger\AsciiSlugger; + +class AsciiSluggerTest extends TestCase +{ + public function provideSlugTests(): iterable + { + yield ['', '']; + yield ['foo', ' foo ']; + yield ['foo-bar', 'foo bar']; + + yield ['foo-bar', 'foo@bar', '-']; + yield ['foo-at-bar', 'foo@bar', '-', 'en']; + + yield ['e-a', 'é$!à']; + yield ['e_a', 'é$!à', '_']; + + yield ['a', 'ä']; + yield ['a', 'ä', '-', 'fr']; + yield ['ae', 'ä', '-', 'de']; + yield ['ae', 'ä', '-', 'de_fr']; // Ensure we get the parent locale + yield ['g', 'ғ', '-']; + yield ['gh', 'ғ', '-', 'uz']; + yield ['gh', 'ғ', '-', 'uz_fr']; // Ensure we get the parent locale + } + + /** @dataProvider provideSlugTests */ + public function testSlug(string $expected, string $string, string $separator = '-', string $locale = null) + { + $slugger = new AsciiSlugger(); + + $this->assertSame($expected, (string) $slugger->slug($string, $separator, $locale)); + } +} From 7020aded35197f67840ca104a10002c7d70fec46 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Fri, 12 Aug 2022 18:36:05 +0200 Subject: [PATCH 51/73] fix AsciiSlugger tests if transliterator_transliterate() isn't present --- .../Component/String/Tests/Slugger/AsciiSluggerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php b/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php index 5ce86f25dcb97..d58c002c40d99 100644 --- a/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php +++ b/src/Symfony/Component/String/Tests/Slugger/AsciiSluggerTest.php @@ -32,9 +32,9 @@ public function provideSlugTests(): iterable yield ['a', 'ä', '-', 'fr']; yield ['ae', 'ä', '-', 'de']; yield ['ae', 'ä', '-', 'de_fr']; // Ensure we get the parent locale - yield ['g', 'ғ', '-']; - yield ['gh', 'ғ', '-', 'uz']; - yield ['gh', 'ғ', '-', 'uz_fr']; // Ensure we get the parent locale + yield [\function_exists('transliterator_transliterate') ? 'g' : '', 'ғ', '-']; + yield [\function_exists('transliterator_transliterate') ? 'gh' : '', 'ғ', '-', 'uz']; + yield [\function_exists('transliterator_transliterate') ? 'gh' : '', 'ғ', '-', 'uz_fr']; // Ensure we get the parent locale } /** @dataProvider provideSlugTests */ From b08025d2e042044587de923bfb22d03c6691c080 Mon Sep 17 00:00:00 2001 From: Antonio Pauletich Date: Sun, 14 Aug 2022 02:40:10 +0200 Subject: [PATCH 52/73] Do not send deleted session cookie twice in the response --- .../deleted_cookie.expected | 11 ++++ .../response-functional/deleted_cookie.php | 60 +++++++++++++++++++ .../Component/HttpFoundation/composer.json | 2 + .../EventListener/AbstractSessionListener.php | 5 ++ 4 files changed, 78 insertions(+) create mode 100644 src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected create mode 100644 src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected new file mode 100644 index 0000000000000..e8b845e674a84 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected @@ -0,0 +1,11 @@ + +Array +( + [0] => Content-Type: text/plain; charset=utf-8 + [1] => Cache-Control: max-age=0, private, must-revalidate + [2] => Cache-Control: max-age=0, must-revalidate, private + [3] => Date: Sat, 12 Nov 1955 20:04:00 GMT + [4] => Expires: %s, %d %s %d %d:%d:%d GMT + [5] => Set-Cookie: PHPSESSID=deleted; expires=%s, %d-%s-%d %d:%d:%d GMT; Max-Age=%d; %s +) +shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php new file mode 100644 index 0000000000000..003b0c121f888 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.php @@ -0,0 +1,60 @@ +cookies->set($sessionName, $sessionId); + +$requestStack = new RequestStack(); +$requestStack->push($request); + +$sessionFactory = new SessionFactory($requestStack, new NativeSessionStorageFactory()); + +$container = new Container(); +$container->set('request_stack', $requestStack); +$container->set('session_factory', $sessionFactory); + +$listener = new SessionListener($container); + +$kernel = new class($r) implements HttpKernelInterface { + /** + * @var Response + */ + private $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function handle(Request $request, int $type = self::MAIN_REQUEST, bool $catch = true): Response + { + return $this->response; + } +}; + +$listener->onKernelRequest(new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST)); +$session = $request->getSession(); +$session->set('foo', 'bar'); +$session->invalidate(); + +$listener->onKernelResponse(new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $r)); + +$r->sendHeaders(); diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index d54bbfd16006b..358e14d166757 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -24,6 +24,8 @@ "require-dev": { "predis/predis": "~1.0", "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/http-kernel": "^5.4.12|^6.1.4", "symfony/mime": "^4.4|^5.0|^6.0", "symfony/expression-language": "^4.4|^5.0|^6.0" }, diff --git a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php index d0e8f45d038b6..4603052e933df 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php @@ -158,6 +158,11 @@ public function onKernelResponse(ResponseEvent $event) $isSessionEmpty = $session->isEmpty() && empty($_SESSION); // checking $_SESSION to keep compatibility with native sessions if ($requestSessionCookieId && $isSessionEmpty) { + // PHP internally sets the session cookie value to "deleted" when setcookie() is called with empty string $value argument + // which happens in \Symfony\Component\HttpFoundation\Session\Storage\Handler\AbstractSessionHandler::destroy + // when the session gets invalidated (for example on logout) so we must handle this case here too + // otherwise we would send two Set-Cookie headers back with the response + SessionUtils::popSessionCookie($sessionName, 'deleted'); $response->headers->clearCookie( $sessionName, $sessionCookiePath, From 60d6325bca845305d08edd9f0f12499422c00592 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Aug 2022 14:51:53 +0200 Subject: [PATCH 53/73] [Serializer] Add missing types to BackedEnumNormalizer --- .../Serializer/Normalizer/BackedEnumNormalizer.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php index dbb9c89ab5e74..8d5566bcc86f5 100644 --- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php @@ -29,7 +29,7 @@ final class BackedEnumNormalizer implements NormalizerInterface, DenormalizerInt * * @return int|string */ - public function normalize($object, $format = null, array $context = []) + public function normalize($object, string $format = null, array $context = []) { if (!$object instanceof \BackedEnum) { throw new InvalidArgumentException('The data must belong to a backed enumeration.'); @@ -41,7 +41,7 @@ public function normalize($object, $format = null, array $context = []) /** * {@inheritdoc} */ - public function supportsNormalization($data, $format = null): bool + public function supportsNormalization($data, string $format = null): bool { return $data instanceof \BackedEnum; } @@ -51,7 +51,7 @@ public function supportsNormalization($data, $format = null): bool * * @throws NotNormalizableValueException */ - public function denormalize($data, $type, $format = null, array $context = []) + public function denormalize($data, string $type, string $format = null, array $context = []) { if (!is_subclass_of($type, \BackedEnum::class)) { throw new InvalidArgumentException('The data must belong to a backed enumeration.'); @@ -71,7 +71,7 @@ public function denormalize($data, $type, $format = null, array $context = []) /** * {@inheritdoc} */ - public function supportsDenormalization($data, $type, $format = null): bool + public function supportsDenormalization($data, string $type, string $format = null): bool { return is_subclass_of($type, \BackedEnum::class); } From 5cd0eae5afcf385462f9366b18c1ad40d6261d71 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 17 Aug 2022 15:14:42 +0200 Subject: [PATCH 54/73] fix expected command name order with mixed integer and string namespaces --- .../Component/Console/Descriptor/ApplicationDescription.php | 2 +- .../Console/Tests/Descriptor/ApplicationDescriptionTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php index fac01ad37c89b..2a3acc99b7be4 100644 --- a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php +++ b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php @@ -131,7 +131,7 @@ private function sortCommands(array $commands): array } if ($namespacedCommands) { - ksort($namespacedCommands); + ksort($namespacedCommands, \SORT_STRING); foreach ($namespacedCommands as $key => $commandsSet) { ksort($commandsSet); $sortedCommands[$key] = $commandsSet; diff --git a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php index b3ba9d8482b0f..da64dca00b949 100644 --- a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php +++ b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php @@ -36,7 +36,7 @@ public function getNamespacesProvider() return [ [['_global'], ['foobar']], [['a', 'b'], ['b:foo', 'a:foo', 'b:bar']], - [['_global', 'b', 'z', 22, 33], ['z:foo', '1', '33:foo', 'b:foo', '22:foo:bar']], + [['_global', 22, 33, 'b', 'z'], ['z:foo', '1', '33:foo', 'b:foo', '22:foo:bar']], ]; } } From 59023805f3fe3af28e3f6a137f94b02e231ccd72 Mon Sep 17 00:00:00 2001 From: Axel Guckelsberger Date: Tue, 28 Sep 2021 12:41:47 +0200 Subject: [PATCH 55/73] [Serializer] Fix caching context-aware encoders/decoders in ChainEncoder/ChainDecoder --- .../Serializer/Encoder/ChainDecoder.php | 6 ++++- .../Serializer/Encoder/ChainEncoder.php | 6 ++++- .../Tests/Encoder/ChainDecoderTest.php | 22 ++++++++++++++++++- .../Tests/Encoder/ChainEncoderTest.php | 22 ++++++++++++++++++- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php b/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php index 2a55d93a6066f..29c656397f1f0 100644 --- a/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php +++ b/src/Symfony/Component/Serializer/Encoder/ChainDecoder.php @@ -67,9 +67,13 @@ private function getDecoder(string $format, array $context): DecoderInterface return $this->decoders[$this->decoderByFormat[$format]]; } + $cache = true; foreach ($this->decoders as $i => $decoder) { + $cache = $cache && !$decoder instanceof ContextAwareDecoderInterface; if ($decoder->supportsDecoding($format, $context)) { - $this->decoderByFormat[$format] = $i; + if ($cache) { + $this->decoderByFormat[$format] = $i; + } return $decoder; } diff --git a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php index b13333e88aabb..c2c9e3c8ad2e2 100644 --- a/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/ChainEncoder.php @@ -85,9 +85,13 @@ private function getEncoder(string $format, array $context): EncoderInterface return $this->encoders[$this->encoderByFormat[$format]]; } + $cache = true; foreach ($this->encoders as $i => $encoder) { + $cache = $cache && !$encoder instanceof ContextAwareEncoderInterface; if ($encoder->supportsEncoding($format, $context)) { - $this->encoderByFormat[$format] = $i; + if ($cache) { + $this->encoderByFormat[$format] = $i; + } return $encoder; } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php index 5cac8d99a5270..8f433ce0fa15a 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/ChainDecoderTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Encoder\ChainDecoder; +use Symfony\Component\Serializer\Encoder\ContextAwareDecoderInterface; use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -28,7 +29,7 @@ class ChainDecoderTest extends TestCase protected function setUp(): void { - $this->decoder1 = $this->createMock(DecoderInterface::class); + $this->decoder1 = $this->createMock(ContextAwareDecoderInterface::class); $this->decoder1 ->method('supportsDecoding') ->willReturnMap([ @@ -36,6 +37,7 @@ protected function setUp(): void [self::FORMAT_2, [], false], [self::FORMAT_3, [], false], [self::FORMAT_3, ['foo' => 'bar'], true], + [self::FORMAT_3, ['foo' => 'bar2'], false], ]); $this->decoder2 = $this->createMock(DecoderInterface::class); @@ -45,6 +47,8 @@ protected function setUp(): void [self::FORMAT_1, [], false], [self::FORMAT_2, [], true], [self::FORMAT_3, [], false], + [self::FORMAT_3, ['foo' => 'bar'], false], + [self::FORMAT_3, ['foo' => 'bar2'], true], ]); $this->chainDecoder = new ChainDecoder([$this->decoder1, $this->decoder2]); @@ -52,10 +56,26 @@ protected function setUp(): void public function testSupportsDecoding() { + $this->decoder1 + ->method('decode') + ->willReturn('result1'); + $this->decoder2 + ->method('decode') + ->willReturn('result2'); + $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_1)); + $this->assertEquals('result1', $this->chainDecoder->decode('', self::FORMAT_1, [])); + $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_2)); + $this->assertEquals('result2', $this->chainDecoder->decode('', self::FORMAT_2, [])); + $this->assertFalse($this->chainDecoder->supportsDecoding(self::FORMAT_3)); + $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar'])); + $this->assertEquals('result1', $this->chainDecoder->decode('', self::FORMAT_3, ['foo' => 'bar'])); + + $this->assertTrue($this->chainDecoder->supportsDecoding(self::FORMAT_3, ['foo' => 'bar2'])); + $this->assertEquals('result2', $this->chainDecoder->decode('', self::FORMAT_3, ['foo' => 'bar2'])); } public function testDecode() diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php index e80dc4f1843a6..8ae9e38c1337a 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/ChainEncoderTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Encoder\ChainEncoder; +use Symfony\Component\Serializer\Encoder\ContextAwareEncoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Encoder\NormalizationAwareInterface; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -29,7 +30,7 @@ class ChainEncoderTest extends TestCase protected function setUp(): void { - $this->encoder1 = $this->createMock(EncoderInterface::class); + $this->encoder1 = $this->createMock(ContextAwareEncoderInterface::class); $this->encoder1 ->method('supportsEncoding') ->willReturnMap([ @@ -37,6 +38,7 @@ protected function setUp(): void [self::FORMAT_2, [], false], [self::FORMAT_3, [], false], [self::FORMAT_3, ['foo' => 'bar'], true], + [self::FORMAT_3, ['foo' => 'bar2'], false], ]); $this->encoder2 = $this->createMock(EncoderInterface::class); @@ -46,6 +48,8 @@ protected function setUp(): void [self::FORMAT_1, [], false], [self::FORMAT_2, [], true], [self::FORMAT_3, [], false], + [self::FORMAT_3, ['foo' => 'bar'], false], + [self::FORMAT_3, ['foo' => 'bar2'], true], ]); $this->chainEncoder = new ChainEncoder([$this->encoder1, $this->encoder2]); @@ -53,10 +57,26 @@ protected function setUp(): void public function testSupportsEncoding() { + $this->encoder1 + ->method('encode') + ->willReturn('result1'); + $this->encoder2 + ->method('encode') + ->willReturn('result2'); + $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_1)); + $this->assertEquals('result1', $this->chainEncoder->encode('', self::FORMAT_1, [])); + $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_2)); + $this->assertEquals('result2', $this->chainEncoder->encode('', self::FORMAT_2, [])); + $this->assertFalse($this->chainEncoder->supportsEncoding(self::FORMAT_3)); + $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar'])); + $this->assertEquals('result1', $this->chainEncoder->encode('', self::FORMAT_3, ['foo' => 'bar'])); + + $this->assertTrue($this->chainEncoder->supportsEncoding(self::FORMAT_3, ['foo' => 'bar2'])); + $this->assertEquals('result2', $this->chainEncoder->encode('', self::FORMAT_3, ['foo' => 'bar2'])); } public function testEncode() From 1c574bba7cdb9b13e1756becbf968e6b44739166 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Aug 2022 16:43:25 +0200 Subject: [PATCH 56/73] [HttpFoundation] Fix tests on PHP 8.2 --- .../Session/Storage/Handler/AbstractSessionHandlerTest.php | 1 + .../Session/Storage/Handler/Fixtures/empty_destroys.expected | 2 +- .../Tests/Session/Storage/Handler/Fixtures/storage.expected | 2 +- .../Storage/Handler/Fixtures/with_cookie_and_session.expected | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php index f6417720d27aa..aca2bfd882b20 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php @@ -46,6 +46,7 @@ public function testSession($fixture) $context = ['http' => ['header' => "Cookie: sid=123abc\r\n"]]; $context = stream_context_create($context); $result = file_get_contents(sprintf('http://localhost:8053/%s.php', $fixture), false, $context); + $result = preg_replace_callback('/expires=[^;]++/', function ($m) { return str_replace('-', ' ', $m[0]); }, $result); $this->assertStringEqualsFile(__DIR__.sprintf('/Fixtures/%s.expected', $fixture), $result); } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected index 8203714740752..06a118888aba9 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/empty_destroys.expected @@ -12,6 +12,6 @@ Array ( [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: max-age=10800, private, must-revalidate - [2] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly + [2] => Set-Cookie: sid=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly ) shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.expected b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.expected index 05a5d5d0b090f..549c6847f11da 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.expected +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/storage.expected @@ -16,6 +16,6 @@ Array ( [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: max-age=0, private, must-revalidate - [2] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly + [2] => Set-Cookie: sid=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly ) shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected index 63078228df139..ac8ec061f0310 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/Fixtures/with_cookie_and_session.expected @@ -20,6 +20,6 @@ Array [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: max-age=10800, private, must-revalidate [2] => Set-Cookie: abc=def - [3] => Set-Cookie: sid=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly + [3] => Set-Cookie: sid=deleted; expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0; path=/; secure; HttpOnly ) shutdown From f454c02bcc7b119a6daa0e056c8cf62b6133a0c1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Aug 2022 16:34:04 +0200 Subject: [PATCH 57/73] minor #47299 [Console] fix expected command name order with mixed integer and string namespaces (xabbuh) This PR was merged into the 5.4 branch. Discussion ---------- [Console] fix expected command name order with mixed integer and string namespaces | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | no | New feature? | no | Deprecations? | no | Tickets | | License | MIT | Doc PR | Commits ------- 5cd0eae5af fix expected command name order with mixed integer and string namespaces --- .../Component/Console/Descriptor/ApplicationDescription.php | 2 +- .../Console/Tests/Descriptor/ApplicationDescriptionTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php index 3970b90007369..91b18460582fb 100644 --- a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php +++ b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php @@ -131,7 +131,7 @@ private function sortCommands(array $commands): array } if ($namespacedCommands) { - ksort($namespacedCommands); + ksort($namespacedCommands, \SORT_STRING); foreach ($namespacedCommands as $key => $commandsSet) { ksort($commandsSet); $sortedCommands[$key] = $commandsSet; diff --git a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php index 33d5c3840f3e3..f1408d087d5e7 100644 --- a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php +++ b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php @@ -36,7 +36,7 @@ public function getNamespacesProvider() return [ [['_global'], ['foobar']], [['a', 'b'], ['b:foo', 'a:foo', 'b:bar']], - [['_global', 'b', 'z', 22, 33], ['z:foo', '1', '33:foo', 'b:foo', '22:foo:bar']], + [['_global', 22, 33, 'b', 'z'], ['z:foo', '1', '33:foo', 'b:foo', '22:foo:bar']], ]; } } From 464cfd4c38f406ae0f38b806f7f7c8cb965393a5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Aug 2022 17:03:36 +0200 Subject: [PATCH 58/73] [HttpFoundation] Fix deps --- src/Symfony/Component/HttpFoundation/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index 358e14d166757..452281794b615 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -25,7 +25,7 @@ "predis/predis": "~1.0", "symfony/cache": "^4.4|^5.0|^6.0", "symfony/dependency-injection": "^5.4|^6.0", - "symfony/http-kernel": "^5.4.12|^6.1.4", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", "symfony/mime": "^4.4|^5.0|^6.0", "symfony/expression-language": "^4.4|^5.0|^6.0" }, From c571101c44e7c76c4d2ce2f9d9398239a2043684 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Aug 2022 17:11:44 +0200 Subject: [PATCH 59/73] [Serializer] Add missing types to FormErrorNormalizer --- .../Component/Serializer/Normalizer/FormErrorNormalizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php index 48399f4e6c068..c23507207e125 100644 --- a/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/FormErrorNormalizer.php @@ -25,7 +25,7 @@ final class FormErrorNormalizer implements NormalizerInterface, CacheableSupport /** * {@inheritdoc} */ - public function normalize($object, $format = null, array $context = []): array + public function normalize($object, string $format = null, array $context = []): array { $data = [ 'title' => $context[self::TITLE] ?? 'Validation Failed', @@ -44,7 +44,7 @@ public function normalize($object, $format = null, array $context = []): array /** * {@inheritdoc} */ - public function supportsNormalization($data, $format = null): bool + public function supportsNormalization($data, string $format = null): bool { return $data instanceof FormInterface && $data->isSubmitted() && !$data->isValid(); } From a709fd411d3cbeab3a8bc96aa35b9db4f405c745 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Aug 2022 17:14:22 +0200 Subject: [PATCH 60/73] Fix merge --- .../Component/Serializer/Normalizer/BackedEnumNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php index cb79a0294e705..1fdcf5b319746 100644 --- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php @@ -37,7 +37,7 @@ public function normalize(mixed $object, string $format = null, array $context = /** * {@inheritdoc} */ - public function supportsNormalization($data, string $format = null): bool + public function supportsNormalization(mixed $data, string $format = null): bool { return $data instanceof \BackedEnum; } From 54f4a02d585f7e667b505da3774e7e48ebaeca63 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Aug 2022 17:20:04 +0200 Subject: [PATCH 61/73] Fix merge --- .../Tests/Fixtures/response-functional/deleted_cookie.expected | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected index e8b845e674a84..0afe8a6333d00 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/deleted_cookie.expected @@ -6,6 +6,6 @@ Array [2] => Cache-Control: max-age=0, must-revalidate, private [3] => Date: Sat, 12 Nov 1955 20:04:00 GMT [4] => Expires: %s, %d %s %d %d:%d:%d GMT - [5] => Set-Cookie: PHPSESSID=deleted; expires=%s, %d-%s-%d %d:%d:%d GMT; Max-Age=%d; %s + [5] => Set-Cookie: PHPSESSID=deleted; expires=%s, %d %s %d %d:%d:%d GMT; Max-Age=%d; %s ) shutdown From d6517703c8500c4aa7f15228b6d629d590abf021 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 17 Aug 2022 17:29:03 +0200 Subject: [PATCH 62/73] [HttpFoundation] Fix tests on PHP 8.2 (bis) --- .../Tests/Fixtures/response-functional/cookie_max_age.expected | 2 +- .../Component/HttpFoundation/Tests/ResponseFunctionalTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_max_age.expected b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_max_age.expected index 6870a27728bbe..c4b31d4f3e7af 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_max_age.expected +++ b/src/Symfony/Component/HttpFoundation/Tests/Fixtures/response-functional/cookie_max_age.expected @@ -6,6 +6,6 @@ Array [0] => Content-Type: text/plain; charset=utf-8 [1] => Cache-Control: no-cache, private [2] => Date: Sat, 12 Nov 1955 20:04:00 GMT - [3] => Set-Cookie: foo=bar; expires=Sat, 01-Jan-10000 02:46:40 GMT; Max-Age=%d; path=/ + [3] => Set-Cookie: foo=bar; expires=Sat, 01 Jan 10000 02:46:40 GMT; Max-Age=%d; path=/ ) shutdown diff --git a/src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php b/src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php index 471455d708753..aa24291eda5dc 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/ResponseFunctionalTest.php @@ -48,6 +48,7 @@ public function testCookie($fixture) } $result = file_get_contents(sprintf('http://localhost:8054/%s.php', $fixture)); + $result = preg_replace_callback('/expires=[^;]++/', function ($m) { return str_replace('-', ' ', $m[0]); }, $result); $this->assertStringMatchesFormatFile(__DIR__.sprintf('/Fixtures/response-functional/%s.expected', $fixture), $result); } From c30f057db29a8fcc819b71adf390532085b10c9f Mon Sep 17 00:00:00 2001 From: allison guilhem Date: Sat, 30 Jul 2022 17:36:59 +0200 Subject: [PATCH 63/73] [Serializer] Throw InvalidArgumentException if the data needed in the constructor doesn't belong to a backedEnum --- .../Normalizer/BackedEnumNormalizer.php | 2 +- .../DummyObjectWithEnumConstructor.php | 12 ++++ .../Normalizer/BackedEnumNormalizerTest.php | 5 +- .../Serializer/Tests/SerializerTest.php | 65 +++++++++++++++++++ 4 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumConstructor.php diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php index 8d5566bcc86f5..ad9fb807aed19 100644 --- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php @@ -64,7 +64,7 @@ public function denormalize($data, string $type, string $format = null, array $c try { return $type::from($data); } catch (\ValueError $e) { - throw NotNormalizableValueException::createForUnexpectedDataType($e->getMessage(), $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true, $e->getCode(), $e); + throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type); } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumConstructor.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumConstructor.php new file mode 100644 index 0000000000000..be5ea3cff0ece --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumConstructor.php @@ -0,0 +1,12 @@ +expectException(NotNormalizableValueException::class); - $this->expectExceptionMessage('"POST" is not a valid backing value for enum "'.StringBackedEnumDummy::class.'"'); + $this->expectException(InvalidArgumentException::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/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 761ea066b5962..b2a33cbc0e5db 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -36,6 +36,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; use Symfony\Component\Serializer\Normalizer\CustomNormalizer; use Symfony\Component\Serializer\Normalizer\DataUriNormalizer; use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; @@ -58,6 +59,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface; 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\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Full; @@ -1173,6 +1175,69 @@ public function testCollectDenormalizationErrorsWithConstructor(?ClassMetadataFa $this->assertSame($expected, $exceptionsAsArray); } + /** + * @requires PHP 8.1 + */ + public function testCollectDenormalizationErrorsWithEnumConstructor() + { + $serializer = new Serializer( + [ + new BackedEnumNormalizer(), + new ObjectNormalizer(), + ], + ['json' => new JsonEncoder()] + ); + + try { + $serializer->deserialize('{"invalid": "GET"}', DummyObjectWithEnumConstructor::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + } catch (\Throwable $th) { + $this->assertInstanceOf(PartialDenormalizationException::class, $th); + } + + $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array { + return [ + 'currentType' => $e->getCurrentType(), + 'useMessageForUser' => $e->canUseMessageForUser(), + 'message' => $e->getMessage(), + ]; + }, $th->getErrors()); + + $expected = [ + [ + 'currentType' => 'array', + 'useMessageForUser' => true, + 'message' => 'Failed to create object because the class misses the "get" property.', + ], + ]; + + $this->assertSame($expected, $exceptionsAsArray); + } + + /** + * @requires PHP 8.1 + */ + public function testNoCollectDenormalizationErrorsWithWrongEnum() + { + $serializer = new Serializer( + [ + new BackedEnumNormalizer(), + new ObjectNormalizer(), + ], + ['json' => new JsonEncoder()] + ); + + try { + $serializer->deserialize('{"get": "invalid"}', DummyObjectWithEnumConstructor::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + } catch (\Throwable $th) { + $this->assertNotInstanceOf(PartialDenormalizationException::class, $th); + $this->assertInstanceOf(InvalidArgumentException::class, $th); + } + } + public function provideCollectDenormalizationErrors() { return [ From 57c49b4e1305c17c4f0d1d50b31784836c22bf87 Mon Sep 17 00:00:00 2001 From: Ayke Halder Date: Thu, 18 Aug 2022 23:14:46 +0200 Subject: [PATCH 64/73] Email image parts: regex for single closing quote The regex for image src matches for single and double opening quotes: `([\'"])` The corresponding matching for non-closing characters is implemented for double quotes only: ([^"]+) This change adds a non-greedy regex `.+?` which matches for as few characters as possbile before the "correspondingly matched opening quote" `\\1` appears. --- src/Symfony/Component/Mime/Email.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Mime/Email.php b/src/Symfony/Component/Mime/Email.php index 29da5ef2fdcb9..9cdde13e533c6 100644 --- a/src/Symfony/Component/Mime/Email.php +++ b/src/Symfony/Component/Mime/Email.php @@ -503,7 +503,7 @@ private function prepareParts(): ?array $html = stream_get_contents($html); } $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); - preg_match_all('(]*src\s*=\s*(?:([\'"])cid:([^"]+)\\1|cid:([^>\s]+)))i', $html, $names); + preg_match_all('(]*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+)))i', $html, $names); $names = array_filter(array_unique(array_merge($names[2], $names[3]))); } From 8188f1cf153830f0ac4aefb45209c1eb49767ae8 Mon Sep 17 00:00:00 2001 From: MatTheCat Date: Mon, 15 Aug 2022 19:09:15 +0200 Subject: [PATCH 65/73] [HttpFoundation] Prevent accepted rate limits with no remaining token to be preferred over denied ones --- .../AbstractRequestRateLimiter.php | 20 +++++- .../AbstractRequestRateLimiterTest.php | 64 +++++++++++++++++++ .../MockAbstractRequestRateLimiter.php | 34 ++++++++++ .../Component/HttpFoundation/composer.json | 3 +- 4 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/HttpFoundation/Tests/RateLimiter/AbstractRequestRateLimiterTest.php create mode 100644 src/Symfony/Component/HttpFoundation/Tests/RateLimiter/MockAbstractRequestRateLimiter.php diff --git a/src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php b/src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php index c91d614fe30bf..a6dd993b7315b 100644 --- a/src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php +++ b/src/Symfony/Component/HttpFoundation/RateLimiter/AbstractRequestRateLimiter.php @@ -35,9 +35,7 @@ public function consume(Request $request): RateLimit foreach ($limiters as $limiter) { $rateLimit = $limiter->consume(1); - if (null === $minimalRateLimit || $rateLimit->getRemainingTokens() < $minimalRateLimit->getRemainingTokens()) { - $minimalRateLimit = $rateLimit; - } + $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit; } return $minimalRateLimit; @@ -54,4 +52,20 @@ public function reset(Request $request): void * @return LimiterInterface[] a set of limiters using keys extracted from the request */ abstract protected function getLimiters(Request $request): array; + + private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit + { + if ($first->isAccepted() !== $second->isAccepted()) { + return $first->isAccepted() ? $second : $first; + } + + $firstRemainingTokens = $first->getRemainingTokens(); + $secondRemainingTokens = $second->getRemainingTokens(); + + if ($firstRemainingTokens === $secondRemainingTokens) { + return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first; + } + + return $firstRemainingTokens > $secondRemainingTokens ? $second : $first; + } } diff --git a/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/AbstractRequestRateLimiterTest.php b/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/AbstractRequestRateLimiterTest.php new file mode 100644 index 0000000000000..4790eae183802 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/AbstractRequestRateLimiterTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\RateLimiter; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\LimiterInterface; +use Symfony\Component\RateLimiter\RateLimit; + +class AbstractRequestRateLimiterTest extends TestCase +{ + /** + * @dataProvider provideRateLimits + */ + public function testConsume(array $rateLimits, ?RateLimit $expected) + { + $rateLimiter = new MockAbstractRequestRateLimiter(array_map(function (RateLimit $rateLimit) { + $limiter = $this->createStub(LimiterInterface::class); + $limiter->method('consume')->willReturn($rateLimit); + + return $limiter; + }, $rateLimits)); + + $this->assertSame($expected, $rateLimiter->consume(new Request())); + } + + public function provideRateLimits() + { + $now = new \DateTimeImmutable(); + + yield 'Both accepted with different count of remaining tokens' => [ + [ + $expected = new RateLimit(0, $now, true, 1), // less remaining tokens + new RateLimit(1, $now, true, 1), + ], + $expected, + ]; + + yield 'Both accepted with same count of remaining tokens' => [ + [ + $expected = new RateLimit(0, $now->add(new \DateInterval('P1D')), true, 1), // longest wait time + new RateLimit(0, $now, true, 1), + ], + $expected, + ]; + + yield 'Accepted and denied' => [ + [ + new RateLimit(0, $now, true, 1), + $expected = new RateLimit(0, $now, false, 1), // denied + ], + $expected, + ]; + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/MockAbstractRequestRateLimiter.php b/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/MockAbstractRequestRateLimiter.php new file mode 100644 index 0000000000000..0acc918bf4d5c --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/RateLimiter/MockAbstractRequestRateLimiter.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\RateLimiter; + +use Symfony\Component\HttpFoundation\RateLimiter\AbstractRequestRateLimiter; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\LimiterInterface; + +class MockAbstractRequestRateLimiter extends AbstractRequestRateLimiter +{ + /** + * @var LimiterInterface[] + */ + private $limiters; + + public function __construct(array $limiters) + { + $this->limiters = $limiters; + } + + protected function getLimiters(Request $request): array + { + return $this->limiters; + } +} diff --git a/src/Symfony/Component/HttpFoundation/composer.json b/src/Symfony/Component/HttpFoundation/composer.json index 452281794b615..cb8d59ffed0d5 100644 --- a/src/Symfony/Component/HttpFoundation/composer.json +++ b/src/Symfony/Component/HttpFoundation/composer.json @@ -27,7 +27,8 @@ "symfony/dependency-injection": "^5.4|^6.0", "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0" + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" }, "suggest" : { "symfony/mime": "To use the file extension guesser" From 2987d58708d2990386603387bf7ee62535bee9ce Mon Sep 17 00:00:00 2001 From: Mathieu Piot Date: Fri, 19 Aug 2022 14:56:02 +0200 Subject: [PATCH 66/73] [Security][AbstractToken] getUserIdentifier() must return a string --- .../Security/Core/Authentication/Token/AbstractToken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php index 1385a8ba409d2..d08d382e42c83 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php @@ -48,7 +48,7 @@ public function getRoleNames(): array public function getUserIdentifier(): string { - return $this->user->getUserIdentifier(); + return $this->user ? $this->user->getUserIdentifier() : ''; } /** From e35d6bab3e1a634ce0bd73a2c678d5510a8a76cd Mon Sep 17 00:00:00 2001 From: Warxcell Date: Mon, 22 Aug 2022 18:00:58 +0300 Subject: [PATCH 67/73] Fix RequestStack state if throwable is thrown --- .../Component/HttpKernel/HttpKernel.php | 6 +-- .../HttpKernel/Tests/HttpKernelTest.php | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 0ed82d777b1c4..d53b80665b467 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -76,6 +76,7 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ { $request->headers->set('X-Php-Ob-Level', (string) ob_get_level()); + $this->requestStack->push($request); try { return $this->handleRaw($request, $type); } catch (\Exception $e) { @@ -89,6 +90,8 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ } return $this->handleThrowable($e, $request, $type); + } finally { + $this->requestStack->pop(); } } @@ -127,8 +130,6 @@ public function terminateWithException(\Throwable $exception, Request $request = */ private function handleRaw(Request $request, int $type = self::MASTER_REQUEST): Response { - $this->requestStack->push($request); - // request $event = new RequestEvent($this, $request, $type); $this->dispatcher->dispatch($event, KernelEvents::REQUEST); @@ -205,7 +206,6 @@ private function filterResponse(Response $response, Request $request, int $type) private function finishRequest(Request $request, int $type) { $this->dispatcher->dispatch(new FinishRequestEvent($this, $request, $type), KernelEvents::FINISH_REQUEST); - $this->requestStack->pop(); } /** diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php index 014dc752c32ae..53e5f547d249f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -39,6 +39,45 @@ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrue() $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); } + public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCatchIsTrue() + { + $requestStack = new RequestStack(); + $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }, $requestStack); + + try { + $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + } catch (\Throwable $exception) { + } + + self::assertNull($requestStack->getCurrentRequest()); + } + + public function testRequestStackIsNotBrokenWhenControllerThrowsAnExceptionAndCatchIsFalse() + { + $requestStack = new RequestStack(); + $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }, $requestStack); + + try { + $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, false); + } catch (\Throwable $exception) { + } + + self::assertNull($requestStack->getCurrentRequest()); + } + + public function testRequestStackIsNotBrokenWhenControllerThrowsAnThrowable() + { + $requestStack = new RequestStack(); + $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \Error(); }, $requestStack); + + try { + $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); + } catch (\Throwable $exception) { + } + + self::assertNull($requestStack->getCurrentRequest()); + } + public function testHandleWhenControllerThrowsAnExceptionAndCatchIsFalseAndNoListenerIsRegistered() { $this->expectException(\RuntimeException::class); From 90562e466cc95f536ef495f9c18cccdbf58738be Mon Sep 17 00:00:00 2001 From: MatTheCat Date: Tue, 23 Aug 2022 12:55:18 +0200 Subject: [PATCH 68/73] Count cookie parts before accessing the second --- .../Security/Http/RememberMe/RememberMeDetails.php | 6 +++--- .../Tests/Authenticator/RememberMeAuthenticatorTest.php | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php b/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php index ba9b118a34af7..3126ca5d5e259 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php +++ b/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php @@ -37,12 +37,12 @@ public function __construct(string $userFqcn, string $userIdentifier, int $expir public static function fromRawCookie(string $rawCookie): self { $cookieParts = explode(self::COOKIE_DELIMITER, base64_decode($rawCookie), 4); - if (false === $cookieParts[1] = base64_decode($cookieParts[1], true)) { - throw new AuthenticationException('The user identifier contains a character from outside the base64 alphabet.'); - } if (4 !== \count($cookieParts)) { throw new AuthenticationException('The cookie contains invalid data.'); } + if (false === $cookieParts[1] = base64_decode($cookieParts[1], true)) { + throw new AuthenticationException('The user identifier contains a character from outside the base64 alphabet.'); + } return new static(...$cookieParts); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php index 406d48c164add..c7492a95a464f 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php @@ -89,4 +89,12 @@ public function testAuthenticateWithoutOldToken() $request = Request::create('/', 'GET', [], ['_remember_me_cookie' => base64_encode('foo:bar')]); $this->authenticator->authenticate($request); } + + public function testAuthenticateWithTokenWithoutDelimiter() + { + $this->expectException(AuthenticationException::class); + + $request = Request::create('/', 'GET', [], ['_remember_me_cookie' => 'invalid']); + $this->authenticator->authenticate($request); + } } From 7787c1558c26c935d0b56cb907e4f8f857ed16fe Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Tue, 23 Aug 2022 22:52:30 +0200 Subject: [PATCH 69/73] [Console] Fix OutputFormatterStyleStack::getCurrent return type --- .../Component/Console/Formatter/OutputFormatterStyleStack.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php b/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php index b425449ef389f..66f86a5f75ea1 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php @@ -77,7 +77,7 @@ public function pop(OutputFormatterStyleInterface $style = null): OutputFormatte /** * Computes current style with stacks top codes. */ - public function getCurrent(): OutputFormatterStyle + public function getCurrent(): OutputFormatterStyleInterface { if (empty($this->styles)) { return $this->emptyStyle; From 72afc77ed6e33729a269dbee76342ccee556056c Mon Sep 17 00:00:00 2001 From: Houssem Date: Wed, 24 Aug 2022 12:19:08 +0200 Subject: [PATCH 70/73] fix bad help message in cache warmup command --- .../Bundle/FrameworkBundle/Command/CacheWarmupCommand.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php index 33a214ea01aa5..50b51f90734c1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php @@ -53,11 +53,6 @@ protected function configure() Before running this command, the cache must be empty. -This command does not generate the classes cache (as when executing this -command, too many classes that should be part of the cache are already loaded -in memory). Use curl or any other similar tool to warm up -the classes cache if you want. - EOF ) ; From c4f651e9fc2188e52d68b75491691146338b77de Mon Sep 17 00:00:00 2001 From: Radek Wionczek Date: Fri, 26 Aug 2022 00:15:48 +0200 Subject: [PATCH 71/73] [LokaliseBridge] Fix push command --delete-missing options when there are no missing messages --- .../Translation/Bridge/Lokalise/LokaliseProvider.php | 8 +++++--- .../Bridge/Lokalise/Tests/LokaliseProviderTest.php | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php index ab9594c8ee860..aeada30847cea 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/LokaliseProvider.php @@ -120,10 +120,12 @@ public function delete(TranslatorBagInterface $translatorBag): void $keysIds = []; foreach ($catalogue->getDomains() as $domain) { - $keysToDelete = []; - foreach (array_keys($catalogue->all($domain)) as $key) { - $keysToDelete[] = $key; + $keysToDelete = array_keys($catalogue->all($domain)); + + if (!$keysToDelete) { + continue; } + $keysIds += $this->getKeysIds($keysToDelete, $domain); } diff --git a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php index 06e3df223482c..5df996e94327b 100644 --- a/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php +++ b/src/Symfony/Component/Translation/Bridge/Lokalise/Tests/LokaliseProviderTest.php @@ -699,10 +699,12 @@ public function testDeleteProcess() $translatorBag->addCatalogue(new MessageCatalogue('en', [ 'messages' => ['a' => 'trans_en_a'], 'validators' => ['post.num_comments' => '{count, plural, one {# comment} other {# comments}}'], + 'domain_without_missing_messages' => [], ])); $translatorBag->addCatalogue(new MessageCatalogue('fr', [ 'messages' => ['a' => 'trans_fr_a'], 'validators' => ['post.num_comments' => '{count, plural, one {# commentaire} other {# commentaires}}'], + 'domain_without_missing_messages' => [], ])); $provider = $this->createProvider( From 14b9f177e5ece16520692164533ab5cb395f60fa Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 26 Aug 2022 16:50:25 +0200 Subject: [PATCH 72/73] Update CHANGELOG for 6.1.4 --- CHANGELOG-6.1.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG-6.1.md b/CHANGELOG-6.1.md index c8755b2eab6fb..0b83806b53047 100644 --- a/CHANGELOG-6.1.md +++ b/CHANGELOG-6.1.md @@ -7,6 +7,45 @@ in 6.1 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.1.0...v6.1.1 +* 6.1.4 (2022-08-26) + + * bug #47372 [Console] Fix OutputFormatterStyleStack::getCurrent return type (alamirault) + * bug #47391 [LokaliseBridge] Fix push command --delete-missing options when there are no missing messages (rwionczek) + * bug #47368 [Security] Count remember me cookie parts before accessing the second (MatTheCat) + * bug #47358 Fix broken request stack state if throwable is thrown. (Warxcell) + * bug #47304 [Serializer] Fix caching context-aware encoders/decoders in ChainEncoder/ChainDecoder (Guite) + * bug #47150 [Serializer] Revert deprecation of `ContextAwareEncoderInterface` and `ContextAwareDecoderInterface` (nicolas-grekas) + * bug #47329 Email image parts: regex for single closing quote (rr-it) + * bug #47335 [Security] [AbstractToken] getUserIdentifier() must return a string (mpiot) + * bug #47283 [HttpFoundation] Prevent accepted rate limits with no remaining token to be preferred over denied ones (MatTheCat) + * bug #47128 [Serializer] Throw InvalidArgumentException if the data needed in the constructor doesn't belong to a backedEnum (allison guilhem) + * bug #47273 [HttpFoundation] Do not send Set-Cookie header twice for deleted session cookie (X-Coder264) + * bug #47255 [Serializer] Fix get accessor regex in AnnotationLoader (jsor) + * bug #47238 [HttpKernel] Fix passing `null` to `\trim()` method in LoggerDataCollector (SVillette) + * bug #47216 [Translation] Crowdin provider throw Exception when status is 50x (alamirault) + * bug #47209 Always attempt to listen for notifications (goetas) + * bug #47211 [Validator] validate nested constraints only if they are in the same group (xabbuh) + * bug #47218 [Console] fix dispatch signal event check for compatibility with the contract interface (xabbuh) + * bug #47200 [Form] ignore missing keys when mapping DateTime objects to uninitialized arrays (xabbuh) + * bug #47189 [Validator] Add additional hint when `egulias/email-validator` needs to be installed (mpdude) + * bug #47195 [FrameworkBundle] fix writes to static $kernel property (xabbuh) + * bug #47185 [String] Fix snake conversion (simPod) + * bug #47175 [DowCrawler] Fix locale-sensitivity of whitespace normalization (nicolas-grekas) + * bug #47172 [Translation] Fix reading intl-icu domains with LocoProvider (nicolas-grekas) + * bug #47171 [TwigBridge] suggest to install the Twig bundle when the required component is already installed (xabbuh) + * bug #47169 [Serializer] Fix throwing right exception in ArrayDenormalizer with invalid type (norkunas) + * bug #47162 [Mailer] Fix error message in case of an SMTP error (fabpot) + * bug #47161 [Mailer] Fix logic (fabpot) + * bug #47157 [Messenger] Fix Doctrine transport on MySQL (nicolas-grekas) + * bug #47155 [Filesystem] Remove needless `mb_*` calls (HellFirePvP) + * bug #46190 [Translation] Fix translator overlapse (Xavier RENAUDIN) + * bug #47142 [Mailer] Fix error message in case of an STMP error (fabpot) + * bug #45333 [Console] Fix ConsoleEvents::SIGNAL subscriber dispatch (GwendolenLynch) + * bug #47145 [HttpClient] Fix shared connections not being freed on PHP < 8 (nicolas-grekas) + * bug #47143 [HttpClient] Fix memory leak when using StreamWrapper (nicolas-grekas) + * bug #47130 [HttpFoundation] Fix invalid ID not regenerated with native PHP file sessions (BrokenSourceCode) + * bug #47129 [FrameworkBundle] remove the ChatterInterface alias when the chatter service is removed (xabbuh) + * 6.1.3 (2022-07-29) * bug #47069 [Security] Allow redirect after login to absolute URLs (Tim Ward) From a8165ab09526e964ccb683d8c6c90a55285876e7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 26 Aug 2022 16:50:30 +0200 Subject: [PATCH 73/73] Update VERSION for 6.1.4 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index b36b18c3a6b00..ec49d1a018616 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,12 +78,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static array $freshCache = []; - public const VERSION = '6.1.4-DEV'; + public const VERSION = '6.1.4'; public const VERSION_ID = 60104; public const MAJOR_VERSION = 6; public const MINOR_VERSION = 1; public const RELEASE_VERSION = 4; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '01/2023'; public const END_OF_LIFE = '01/2023';