diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f24caa613bcca..a5f1eb8c84e3e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -99,17 +99,12 @@ jobs: - name: Install system dependencies run: | - echo "::group::add apt sources" - sudo wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | sudo apt-key add - - echo "deb http://packages.couchbase.com/ubuntu bionic bionic/main" | sudo tee /etc/apt/sources.list.d/couchbase.list - echo "::endgroup::" - echo "::group::apt-get update" sudo apt-get update echo "::endgroup::" echo "::group::install tools & libraries" - sudo apt-get install libcouchbase-dev librdkafka-dev + sudo apt-get install librdkafka-dev echo "::endgroup::" - name: Configure Couchbase @@ -128,6 +123,11 @@ jobs: php-version: "${{ matrix.php }}" tools: pecl + - name: Display versions + run: | + php -r 'foreach (get_loaded_extensions() as $extension) echo $extension . " " . phpversion($extension) . PHP_EOL;' + php -i + - name: Load fixtures uses: docker://bitnami/openldap with: diff --git a/CHANGELOG-5.1.md b/CHANGELOG-5.1.md index 0f8e3a5c324dd..3d530f59a41d1 100644 --- a/CHANGELOG-5.1.md +++ b/CHANGELOG-5.1.md @@ -7,6 +7,54 @@ in 5.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/v5.1.0...v5.1.1 +* 5.1.9 (2020-11-29) + + * bug #39166 [Messenger] Fix mssql compatibility for doctrine transport. (bill moll) + * bug #39211 [HttpClient] fix binding to network interfaces (nicolas-grekas) + * bug #39129 [DependencyInjection] Fix circular in DI with lazy + byContruct loop (jderusse) + * bug #39068 [DependencyInjection][Translator] Silent deprecation triggered by libxml_disable_entity_loader (jderusse) + * bug #39119 [Form] prevent duplicated error message for file upload limits (xabbuh) + * bug #39099 [Form] ignore the pattern attribute for textareas (xabbuh) + * bug #39154 [Yaml] fix lexing strings containing escaped quotation characters (xabbuh) + * bug #39180 [Serializer] Fix denormalizing scalar with UnwrappingDenormalizer (camilledejoye) + * bug #38597 [PhpUnitBridge] Fix qualification of deprecations triggered by the debug class loader (fancyweb) + * bug #39160 [Console] Use a partial buffer in SymfonyStyle (jderusse) + * bug #39168 [Console] Fix console closing tag (jderusse) + * bug #39155 [VarDumper] fix casting resources turned into objects on PHP 8 (nicolas-grekas) + * bug #39131 [Cache] Fix CI because of Couchbase version (jderusse) + * bug #39115 [HttpClient] don't fallback to HTTP/1.1 when HTTP/2 streams break (nicolas-grekas) + * bug #33763 [Yaml] fix lexing nested sequences/mappings (xabbuh) + * bug #39083 [Dotenv] Check if method inheritEnvironmentVariables exists (Chi-teck) + * bug #39094 [Ldap] Fix undefined variable $con (derrabus) + * bug #39091 [Config] Recheck glob brace support after GlobResource was serialized (wouterj) + * bug #39092 Fix critical extension when reseting paged control (jderusse) + * bug #38614 [HttpFoundation] Fix for virtualhosts based on URL path (mvorisek) + * bug #39072 [FrameworkBundle] [Notifier] fix firebase transport factory DI tag type (xabbuh) + * bug #38387 [Validator] prevent hash collisions caused by reused object hashes (fancyweb, xabbuh) + * bug #38999 [DependencyInjection] autoconfigure behavior describing tags on decorators (xabbuh) + * bug #39058 [DependencyInjection] Fix circular detection with multiple paths (jderusse) + * bug #39059 [Filesystem] fix cleaning up tmp files when dumpFile() fails (nicolas-grekas) + * bug #38628 [DoctrineBridge] indexBy could reference to association columns (juanmiguelbesada) + * bug #39021 [DependencyInjection] Optimize circular collection by removing flattening (jderusse) + * bug #39031 [Ldap] Fix pagination (jderusse) + * bug #39038 [DoctrineBridge] also reset id readers (xabbuh) + * bug #39026 [Messenger] Fix DBAL deprecations in PostgreSqlConnection (chalasr) + * bug #39025 [DoctrineBridge] Fix DBAL deprecations in middlewares (derrabus) + * bug #38991 [Console] Fix ANSI when stdErr is not a tty (jderusse) + * bug #38980 [DependencyInjection] Fix circular reference with Factory + Lazy Iterrator (jderusse) + * bug #38977 [HttpClient] Check status code before decoding content in TraceableResponse (chalasr) + * bug #38971 [PhpUnitBridge] fix replaying skipped tests (nicolas-grekas) + * bug #38910 [HttpKernel] Fix session initialized several times (jderusse) + * bug #38882 [DependencyInjection] Improve performances in CircualReference detection (jderusse) + * bug #38950 [Process] Dont test TTY if there is no TTY support (Nyholm) + * bug #38921 [PHPUnitBridge] Fixed crash on Windows with PHP 8 (villfa) + * bug #38869 [SecurityBundle] inject only compatible token storage implementations for usage tracking (xabbuh) + * bug #38894 [HttpKernel] Remove Symfony 3 compatibility code (derrabus) + * bug #38895 [PhpUnitBridge] Fix wrong check for exporter in ConstraintTrait (alcaeus) + * bug #38879 [Cache] Fixed expiry could be int in ChainAdapter due to race conditions (phamviet) + * bug #38867 [FrameworkBundle] Fixing TranslationUpdateCommand failure when using "--no-backup" (liarco) + * bug #38856 [Cache] Add missing use statement (fabpot) + * 5.1.8 (2020-10-28) * bug #38713 [DI] Fix Preloader exception when preloading a class with an unknown parent/interface (rgeraads) diff --git a/CHANGELOG-5.2.md b/CHANGELOG-5.2.md index 2a83a9f8b5649..ff8c81c4481b7 100644 --- a/CHANGELOG-5.2.md +++ b/CHANGELOG-5.2.md @@ -7,6 +7,30 @@ in 5.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v5.2.0...v5.2.1 +* 5.2.0 (2020-11-30) + + * feature #39213 [Security] [DX] Automatically add PasswordUpgradeBadge + default support() impl in AbstractFormLoginAuthenticator (wouterj) + * bug #39166 [Messenger] Fix mssql compatibility for doctrine transport. (bill moll) + * bug #39210 [DoctrineBridge] Fix form EntityType with filter on UID (jderusse) + * bug #39211 [HttpClient] fix binding to network interfaces (nicolas-grekas) + * bug #39129 [DependencyInjection] Fix circular in DI with lazy + byContruct loop (jderusse) + * feature #39153 [Security] Automatically register custom authenticator as entry_point (if supported) (wouterj) + * bug #39068 [DependencyInjection][Translator] Silent deprecation triggered by libxml_disable_entity_loader (jderusse) + * bug #39119 [Form] prevent duplicated error message for file upload limits (xabbuh) + * bug #39099 [Form] ignore the pattern attribute for textareas (xabbuh) + * feature #39118 [DoctrineBridge] Require doctrine/persistence 2 (greg0ire) + * feature #39128 [HttpFoundation] Deprecate BinaryFileResponse::create() (derrabus) + * bug #39154 [Yaml] fix lexing strings containing escaped quotation characters (xabbuh) + * bug #39187 [Security] Support for SwitchUserToken instances serialized with 4.4/5.1 (derrabus) + * bug #39180 [Serializer] Fix denormalizing scalar with UnwrappingDenormalizer (camilledejoye) + * bug #38597 [PhpUnitBridge] Fix qualification of deprecations triggered by the debug class loader (fancyweb) + * bug #39160 [Console] Use a partial buffer in SymfonyStyle (jderusse) + * bug #39168 [Console] Fix console closing tag (jderusse) + * bug #39155 [VarDumper] fix casting resources turned into objects on PHP 8 (nicolas-grekas) + * bug #39131 [Cache] Fix CI because of Couchbase version (jderusse) + * bug #39115 [HttpClient] don't fallback to HTTP/1.1 when HTTP/2 streams break (nicolas-grekas) + * bug #33763 [Yaml] fix lexing nested sequences/mappings (xabbuh) + * 5.2.0-RC2 (2020-11-21) * bug #39113 [DoctrineBridge] drop binary variants of UID types (nicolas-grekas) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5d3ae522834d7..3bed011d06243 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -12,9 +12,10 @@ Symfony is the result of the work of many people who made the code better - Robin Chalas (chalas_r) - Christophe Coevoet (stof) - Kévin Dunglas (dunglas) - - Jordi Boggiano (seldaek) - Maxime Steinhausser (ogizanagi) + - Jordi Boggiano (seldaek) - Victor Berchet (victor) + - Alexander M. Turek (derrabus) - Grégoire Pineau (lyrixx) - Ryan Weaver (weaverryan) - Javier Eguiluz (javier.eguiluz) @@ -22,33 +23,32 @@ Symfony is the result of the work of many people who made the code better - Jakub Zalas (jakubzalas) - Johannes S (johannes) - Kris Wallsmith (kriswallsmith) - - Alexander M. Turek (derrabus) - Wouter de Jong (wouterj) - Yonel Ceruto (yonelceruto) - Thomas Calvet (fancyweb) - Hugo Hamon (hhamon) + - Jérémy DERUSSÉ (jderusse) - Abdellatif Ait boudad (aitboudad) - Samuel ROZE (sroze) - Romain Neutron (romain) - Pascal Borreli (pborreli) - - Jérémy DERUSSÉ (jderusse) - Joseph Bielawski (stloyd) - Karma Dordrak (drak) - Jules Pietri (heah) - Lukas Kahwe Smith (lsmith) - Martin Hasoň (hason) - Hamza Amrouche (simperfit) + - Tobias Nyholm (tobias) - Jeremy Mikola (jmikola) - Jean-François Simon (jfsimon) - Benjamin Eberlei (beberlei) - Igor Wiedler (igorw) - - Tobias Nyholm (tobias) - Eriksen Costa (eriksencosta) - Guilhem Niot (energetick) - Sarah Khalil (saro0h) - Jonathan Wage (jwage) - - Lynn van der Berg (kjarli) - Jan Schädlich (jschaedl) + - Lynn van der Berg (kjarli) - Matthias Pigulla (mpdude) - Diego Saint Esteben (dosten) - Pierre du Plessis (pierredup) @@ -56,13 +56,13 @@ Symfony is the result of the work of many people who made the code better - William Durand (couac) - Valentin Udaltsov (vudaltsov) - ornicar + - Grégoire Paris (greg0ire) - Dany Maillard (maidmaid) - Francis Besset (francisbesset) - stealth35 ‏ (stealth35) - Alexander Mols (asm89) - Kevin Bond (kbond) - Konstantin Myakshin (koc) - - Grégoire Paris (greg0ire) - Bulat Shakirzyanov (avalanche123) - Saša Stamenković (umpirsky) - Peter Rehm (rpet) @@ -116,17 +116,17 @@ Symfony is the result of the work of many people who made the code better - Tim Nagel (merk) - Chris Wilkinson (thewilkybarkid) - Brice BERNARD (brikou) + - Alexander Schranz (alexander-schranz) - marc.weistroff - Tomáš Votruba (tomas_votruba) - Peter Kokot (maastermedia) - Lars Strojny (lstrojny) - lenar - Alexander Schwenn (xelaris) + - Massimiliano Arione (garak) - Włodzimierz Gajda (gajdaw) - - Alexander Schranz (alexander-schranz) - Oskar Stark (oskarstark) - Adrien Brault (adrienbrault) - - Massimiliano Arione (garak) - Jacob Dreesen (jdreesen) - Florian Voutzinos (florianv) - Teoh Han Hui (teohhanhui) @@ -139,11 +139,11 @@ Symfony is the result of the work of many people who made the code better - excelwebzone - Gordon Franke (gimler) - Joel Wurtz (brouznouf) + - Antoine Makdessi (amakdessi) - Fabien Pennequin (fabienpennequin) - Julien Falque (julienfalque) - Théo FIDRY (theofidry) - Eric GELOEN (gelo) - - Antoine Makdessi (amakdessi) - Jannik Zschiesche (apfelbox) - jeremyFreeAgent (jeremyfreeagent) - Robert Schönthal (digitalkaoz) @@ -166,6 +166,7 @@ Symfony is the result of the work of many people who made the code better - Guilherme Blanco (guilhermeblanco) - SpacePossum - Pablo Godel (pgodel) + - Andreas Braun - Matthieu Napoli (mnapoli) - Richard van Laak (rvanlaak) - Jérémie Augustin (jaugustin) @@ -175,7 +176,6 @@ Symfony is the result of the work of many people who made the code better - Rafael Dohms (rdohms) - jwdeitch - Ahmed TAILOULOUTE (ahmedtai) - - Andreas Braun - Mikael Pajunen - Arman Hosseini (arman) - Niels Keurentjes (curry684) @@ -295,7 +295,9 @@ Symfony is the result of the work of many people who made the code better - Lorenz Schori - Sébastien Lavoie (lavoiesl) - Dariusz + - Farhad Safarov (safarov) - Michael Babker (mbabker) + - Thomas Lallement (raziel057) - Francois Zaninotto - Colin O'Dell (colinodell) - Alexander Kotynia (olden) @@ -307,21 +309,25 @@ Symfony is the result of the work of many people who made the code better - Danny Berger (dpb587) - zairig imad (zairigimad) - Antonio J. García Lagar (ajgarlag) + - Alessandro Lai (jean85) - Adam Prager (padam87) - Benoît Burnichon (bburnichon) - Maciej Malarz (malarzm) - Roman Marintšenko (inori) - Xavier Montaña Carreras (xmontana) + - Timothée Barray (tyx) - Mickaël Andrieu (mickaelandrieu) - Xavier Perez - Arjen Brouwer (arjenjb) - Katsuhiro OGAWA - Patrick McDougle (patrick-mcdougle) + - Rokas Mikalkėnas (rokasm) - Marc Weistroff (futurecat) - Alif Rachmawadi - Anton Chernikov (anton_ch1989) - Kristen Gilden (kgilden) - Pierre-Yves LEBECQ (pylebecq) + - Benjamin Leveque (benji07) - Jordan Samouh (jordansamouh) - Jakub Kucharovic (jkucharovic) - Loick Piera (pyrech) @@ -337,6 +343,7 @@ Symfony is the result of the work of many people who made the code better - Ray - Chekote - Thomas Adam + - Chi-teck - Jhonny Lidfors (jhonne) - Diego Agulló (aeoris) - jdhoek @@ -348,7 +355,6 @@ Symfony is the result of the work of many people who made the code better - Wodor Wodorski - Timo Bakx (timobakx) - Joe Bennett (kralos) - - Thomas Lallement (raziel057) - soyuka - Giorgio Premi - renanbr @@ -362,7 +368,6 @@ Symfony is the result of the work of many people who made the code better - Alexander Menshchikov (zmey_kk) - Emanuele Panzeri (thepanz) - Kim Hemsø Rasmussen (kimhemsoe) - - Alessandro Lai (jean85) - Langlet Vincent (deviling) - Pascal Luna (skalpa) - Wouter Van Hecke @@ -375,6 +380,7 @@ Symfony is the result of the work of many people who made the code better - Christian Schmidt - Patrick Landolt (scube) - MatTheCat + - Denis Brumann (dbrumann) - Bohan Yang (brentybh) - Vilius Grigaliūnas - David Badura (davidbadura) @@ -383,8 +389,6 @@ Symfony is the result of the work of many people who made the code better - Chris Smith (cs278) - Thomas Bisignani (toma) - Florian Klein (docteurklein) - - Timothée Barray (tyx) - - Benjamin Leveque (benji07) - Manuel Kiessling (manuelkiessling) - Alexey Kopytko (sanmai) - Atsuhiro KUBO (iteman) @@ -404,7 +408,6 @@ Symfony is the result of the work of many people who made the code better - Emmanuel BORGES (eborges78) - Aurelijus Valeiša (aurelijus) - Jan Decavele (jandc) - - Chi-teck - Gustavo Piltcher - Jesse Rushlow (geeshoe) - Stepan Tanasiychuk (stfalcon) @@ -459,19 +462,22 @@ Symfony is the result of the work of many people who made the code better - Thomas Perez (scullwm) - Felix Labrecque - Yaroslav Kiliba + - Ben Hakim - Terje Bråten - Gonzalo Vilaseca (gonzalovilaseca) + - Marco Petersen (ocrampete16) - Markus Fasselt (digilist) - Daniel STANCU - Robbert Klarenbeek (robbertkl) - Eric Masoero (eric-masoero) - Ion Bazan (ionbazan) - - Denis Brumann (dbrumann) + - Vitalii Ekert (comrade42) - JhonnyL - Clara van Miert - Haralan Dobrev (hkdobrev) - hossein zolfi (ocean) - Clément Gautier (clementgautier) + - Jeroen Noten (jeroennoten) - Bastien Jaillot (bastnic) - Dāvis Zālītis (k0d3r1s) - Sanpi @@ -485,7 +491,6 @@ Symfony is the result of the work of many people who made the code better - Dimitri Gritsajuk (ottaviano) - Kirill chEbba Chebunin (chebba) - - - Rokas Mikalkėnas (rokasm) - Greg Thornton (xdissent) - Alex Bowers - Philipp Cordes @@ -551,7 +556,6 @@ Symfony is the result of the work of many people who made the code better - Nate Wiebe (natewiebe13) - Marcin Szepczynski (czepol) - Mohammad Emran Hasan (phpfour) - - Farhad Safarov - Dmitriy Mamontov (mamontovdmitriy) - Jan Schumann - Niklas Fiekas @@ -600,18 +604,20 @@ Symfony is the result of the work of many people who made the code better - Arkadius Stefanski (arkadius) - Tim Goudriaan (codedmonkey) - Jonas Flodén (flojon) + - AnneKir - Soner Sayakci - Tobias Weichart + - Miro Michalicka - Tarmo Leppänen (tarlepp) - Marcin Sikoń (marphi) - M. Vondano - Dominik Zogg (dominik.zogg) - Marek Pietrzak + - Tavo Nieves J - Luc Vieillescazes (iamluc) - Lukáš Holeczy (holicz) - franek (franek) - Raulnet - - Marco Petersen (ocrampete16) - Christian Wahler - Giso Stallenberg (gisostallenberg) - Gintautas Miselis @@ -660,6 +666,7 @@ Symfony is the result of the work of many people who made the code better - Roy Van Ginneken (rvanginneken) - ondrowan - Barry vd. Heuvel (barryvdh) + - Michael Voříšek - Evan S Kaufman (evanskaufman) - mcben - Jérôme Vieilledent (lolautruche) @@ -758,6 +765,7 @@ Symfony is the result of the work of many people who made the code better - Fred Cox - vitaliytv - Philippe Segatori + - fd6130 (fdtvui) - Dalibor Karlović (dkarlovi) - Andrey Sevastianov - Sebastian Blum @@ -803,8 +811,10 @@ Symfony is the result of the work of many people who made the code better - Jérôme Tamarelle (jtamarelle-prismamedia) - Geoffrey Brier (geoffrey-brier) - Alexandre Parent + - Roger Guasch (rogerguasch) - Vladimir Tsykun - Dustin Dobervich (dustin10) + - Luis Tacón (lutacon) - dantleech - Philipp Kolesnikov - Anne-Sophie Bachelard (annesophie) @@ -828,6 +838,7 @@ Symfony is the result of the work of many people who made the code better - Stefan Warman - Tristan Maindron (tmaindron) - Behnoush Norouzali (behnoush) + - Marko H. Tamminen (gzumba) - Wesley Lancel - Xavier Briand (xavierbriand) - Ke WANG (yktd26) @@ -853,8 +864,10 @@ Symfony is the result of the work of many people who made the code better - Michael Devery (mickadoo) - Antoine Corcy - Ahmed Ashraf (ahmedash95) + - Luca Saba (lucasaba) - Sascha Grossenbacher - Szijarto Tamas + - Thomas P - Robin Lehrmann (robinlehrmann) - Catalin Dan - Jaroslav Kuba @@ -918,6 +931,7 @@ Symfony is the result of the work of many people who made the code better - Peter Ward - Davide Borsatto (davide.borsatto) - Julien DIDIER (juliendidier) + - Randy Geraads - Dominik Ritter (dritter) - Andreas Leathley (iquito) - Sebastian Grodzicki (sgrodzicki) @@ -927,7 +941,6 @@ Symfony is the result of the work of many people who made the code better - Baldur Rensch (brensch) - Pierre Rineau - Fritz Michael Gschwantner - - Jeroen Noten (jeroennoten) - Vladyslav Petrovych - Alex Xandra Albert Sim - Carson Full @@ -1045,12 +1058,10 @@ Symfony is the result of the work of many people who made the code better - Daniel Gorgan - Tony Malzhacker - Mathieu MARCHOIS - - Tavo Nieves J - Cyril Quintin (cyqui) - Gerard van Helden (drm) - flack (flack) - Johnny Peck (johnnypeck) - - Michael Voříšek - Stefan Kruppa - Ivan Menshykov - David Romaní @@ -1058,6 +1069,7 @@ Symfony is the result of the work of many people who made the code better - Gustavo Falco (gfalco) - Matt Robinson (inanimatt) - Kristof Van Cauwenbergh (kristofvc) + - Marco Lipparini (liarco) - Peter Bowyer (pbowyer) - Aleksey Podskrebyshev - Calin Mihai Pristavu @@ -1095,6 +1107,7 @@ Symfony is the result of the work of many people who made the code better - Don Pinkster - Maksim Muruev - Emil Einarsson + - Anderson Müller - 243083df - Thibault Duplessis - Rimas Kudelis @@ -1195,7 +1208,9 @@ Symfony is the result of the work of many people who made the code better - Pieter - Michael Tibben - Hallison Boaventura (hallisonboaventura) + - Mas Iting - Billie Thompson + - Albion Bame (abame) - Ganesh Chandrasekaran - Sander Marechal - Franz Wilding (killerpoke) @@ -1216,11 +1231,15 @@ Symfony is the result of the work of many people who made the code better - Nicolas Martin (cocorambo) - Tom Panier (neemzy) - Fred Cox + - luffy1727 - Luciano Mammino (loige) - fabios - Sander Coolen (scoolen) + - Amirreza Shafaat (amirrezashafaat) - Laurent Clouet + - Adoni Pavlakis (adoni) - Nicolas Le Goff (nlegoff) + - Ahmed EBEN HASSINE (famas23) - Ben Oman - Chris de Kok - Eduard Bulava (nonanerz) @@ -1229,6 +1248,7 @@ Symfony is the result of the work of many people who made the code better - Guillaume (guill) - Igor Timoshenko (igor.timoshenko) - Manuele Menozzi + - “teerasak” - Anton Babenko (antonbabenko) - Irmantas Šiupšinskas (irmantas) - Benoit Mallo @@ -1238,6 +1258,7 @@ Symfony is the result of the work of many people who made the code better - pizzaminded - Arnaud PETITPAS (apetitpa) - Ken Stanley + - ivan - Zachary Tong (polyfractal) - linh - Guilherme Augusto Henschel @@ -1279,8 +1300,10 @@ Symfony is the result of the work of many people who made the code better - Danijel Obradović - Pablo Borowicz - Arjan Keeman + - Bruno Rodrigues de Araujo (brunosinister) - Máximo Cuadros (mcuadros) - Lukas Mencl + - Jacek Wilczyński (jacekwilczynski) - tamirvs - gauss - julien.galenski @@ -1342,7 +1365,6 @@ Symfony is the result of the work of many people who made the code better - Martijn Evers - Philipp Fritsche - tarlepp - - Luca Saba (lucasaba) - Benjamin Paap (benjaminpaap) - Claus Due (namelesscoder) - Christian @@ -1449,6 +1471,7 @@ Symfony is the result of the work of many people who made the code better - Makdessi Alex - James Gilliland - fduch (fduch) + - Juan Miguel Besada Vidal (soutlink) - Stuart Fyfe - David de Boer (ddeboer) - Eno Mullaraj (emullaraj) @@ -1677,7 +1700,6 @@ Symfony is the result of the work of many people who made the code better - WedgeSama - Hugo Sales - Felds Liscia - - Randy Geraads - Chihiro Adachi (chihiro-adachi) - Raphaëll Roussel - Tadcka @@ -1693,7 +1715,6 @@ Symfony is the result of the work of many people who made the code better - Emmanuel Vella (emmanuel.vella) - Guillaume BRETOU (guiguiboy) - Carsten Nielsen (phreaknerd) - - Roger Guasch (rogerguasch) - Jay Severson - Benny Born - Emirald Mateli @@ -1727,6 +1748,7 @@ Symfony is the result of the work of many people who made the code better - Michael Dowling (mtdowling) - Karlos Presumido (oneko) - Tony Vermeiren (tony) + - Bart Wach - Jos Elstgeest - Thomas Counsell - BilgeXA @@ -1772,7 +1794,6 @@ Symfony is the result of the work of many people who made the code better - Pablo Ogando Ferreira - Thomas Ploch - Simon Neidhold - - Ben Hakim - Valentin VALCIU - Jeremiah VALERIE - Julien Menth @@ -1826,8 +1847,8 @@ Symfony is the result of the work of many people who made the code better - Antonio Peric-Mazar (antonioperic) - César Suárez (csuarez) - Bjorn Twachtmann (dotbjorn) + - Marek Víger (freezy) - Tobias Genberg (lorceroth) - - Luis Tacón (lutacon) - Nicolas Badey (nico-b) - Shane Preece (shane) - Johannes Goslar @@ -1996,6 +2017,7 @@ Symfony is the result of the work of many people who made the code better - Felix Marezki - Normunds - Luiz “Felds” Liscia + - Johan - Thomas Rothe - Adrien Wilmet - Martin @@ -2095,6 +2117,8 @@ Symfony is the result of the work of many people who made the code better - Ali Tavafi - Trevor Suarez - gedrox + - hugovms + - Viet Pham - Alan Bondarchuk - Pchol - dropfen @@ -2200,7 +2224,6 @@ Symfony is the result of the work of many people who made the code better - Marin Nicolae - Alessandro Loffredo - Ian Phillips - - Marco Lipparini - Haritz - Matthieu Prat - Grummfy @@ -2246,7 +2269,6 @@ Symfony is the result of the work of many people who made the code better - Gyula Szucs - Tomas Liubinas - Alex - - Thomas P - Jan Hort - Klaas Naaijkens - Daniel González Cerviño @@ -2410,8 +2432,10 @@ Symfony is the result of the work of many people who made the code better - Daniel Bannert - Karim Miladi - Michael Genereux + - Wojciech Kania - patrick-mcdougle - Dariusz Czech + - Bruno Baguette - Jack Wright - MrNicodemuz - Anonymous User @@ -2430,7 +2454,6 @@ Symfony is the result of the work of many people who made the code better - n-aleha - Talha Zekeriya Durmuş - Anatol Belski - - Anderson Müller - Şəhriyar İmanov - Alexis BOYER - Kaipi Yann @@ -2485,6 +2508,7 @@ Symfony is the result of the work of many people who made the code better - Alex Nostadt - Michael Squires - Egor Gorbachev + - Fabien Villepinte - Derek Stephen McLean - Norman Soetbeer - zorn diff --git a/UPGRADE-5.2.md b/UPGRADE-5.2.md index e62963fe2d50b..c49e55445e34d 100644 --- a/UPGRADE-5.2.md +++ b/UPGRADE-5.2.md @@ -44,6 +44,7 @@ HttpFoundation * Deprecated not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()`; wrap your filter in a closure instead. * Deprecated the `Request::HEADER_X_FORWARDED_ALL` constant, use either `Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO` or `Request::HEADER_X_FORWARDED_AWS_ELB` or `Request::HEADER_X_FORWARDED_TRAEFIK`constants instead. + * Deprecated `BinaryFileResponse::create()`, use `__construct()` instead Lock ---- diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md index 6e84d2c0dee2f..ba4be59b39a9d 100644 --- a/UPGRADE-6.0.md +++ b/UPGRADE-6.0.md @@ -64,8 +64,8 @@ HttpFoundation -------------- * Removed `Response::create()`, `JsonResponse::create()`, - `RedirectResponse::create()`, and `StreamedResponse::create()` methods (use - `__construct()` instead) + `RedirectResponse::create()`, `StreamedResponse::create()` and + `BinaryFileResponse::create()` methods (use `__construct()` instead) * Not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()` throws an `InvalidArgumentException`; wrap your filter in a closure instead. * Removed the `Request::HEADER_X_FORWARDED_ALL` constant, use either `Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO` or `Request::HEADER_X_FORWARDED_AWS_ELB` or `Request::HEADER_X_FORWARDED_TRAEFIK`constants instead. diff --git a/composer.json b/composer.json index 3ca90e56d1d68..765c2d6577360 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "php": ">=7.2.5", "ext-xml": "*", "doctrine/event-manager": "~1.0", - "doctrine/persistence": "^1.3|^2", + "doctrine/persistence": "^2", "twig/twig": "^2.10|^3.0", "psr/cache": "~1.0", "psr/container": "^1.0", @@ -116,7 +116,7 @@ "doctrine/collections": "~1.0", "doctrine/data-fixtures": "^1.1", "doctrine/dbal": "^2.10|^3.0", - "doctrine/orm": "~2.4,>=2.4.5", + "doctrine/orm": "^2.7.3", "doctrine/doctrine-bundle": "^2.0", "guzzlehttp/promises": "^1.3.1", "masterminds/html5": "^2.6", diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 0d6a214c92b68..efefb28f85bba 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\Type; use Doctrine\ORM\QueryBuilder; /** @@ -74,7 +75,7 @@ public function getEntitiesByIds(string $identifier, array $values) // Guess type $entity = current($qb->getRootEntities()); $metadata = $qb->getEntityManager()->getClassMetadata($entity); - if (\in_array($metadata->getTypeOfField($identifier), ['integer', 'bigint', 'smallint'])) { + if (\in_array($type = $metadata->getTypeOfField($identifier), ['integer', 'bigint', 'smallint'])) { $parameterType = Connection::PARAM_INT_ARRAY; // Filter out non-integer values (e.g. ""). If we don't, some @@ -82,13 +83,23 @@ public function getEntitiesByIds(string $identifier, array $values) $values = array_values(array_filter($values, function ($v) { return (string) $v === (string) (int) $v || ctype_digit($v); })); - } elseif (\in_array($metadata->getTypeOfField($identifier), ['uuid', 'guid'])) { + } elseif (\in_array($type, ['ulid', 'uuid', 'guid'])) { $parameterType = Connection::PARAM_STR_ARRAY; // Like above, but we just filter out empty strings. $values = array_values(array_filter($values, function ($v) { return '' !== (string) $v; })); + + // Convert values into right type + if (Type::hasType($type)) { + $doctrineType = Type::getType($type); + $platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform(); + foreach ($values as &$value) { + $value = $doctrineType->convertToDatabaseValue($value, $platform); + } + unset($value); + } } else { $parameterType = Connection::PARAM_STR_ARRAY; } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 21b78bada3019..e8e3e71b585cc 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -269,5 +269,3 @@ private function getCachedEntityLoader(ObjectManager $manager, $queryBuilder, st return $this->entityLoaders[$hash] ?? ($this->entityLoaders[$hash] = $this->getLoader($manager, $queryBuilder, $class)); } } - -interface_exists(ObjectManager::class); diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 7cbe648f9b868..6627630675b53 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -96,5 +96,3 @@ private function parameterToArray(Parameter $parameter): array return [$parameter->getName(), $parameter->getType(), $parameter->getValue()]; } } - -interface_exists(ObjectManager::class); diff --git a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php index defc2cb2af438..b49b373444c75 100644 --- a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php @@ -153,6 +153,3 @@ private function getClassMetadata(): ClassMetadata return $this->getObjectManager()->getClassMetadata($this->classOrAlias); } } - -interface_exists(ObjectManager::class); -interface_exists(ObjectRepository::class); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UlidIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UlidIdEntity.php new file mode 100644 index 0000000000000..3ee909fe4bfc5 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/UlidIdEntity.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Fixtures; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; + +/** @Entity */ +class UlidIdEntity +{ + /** @Id @Column(type="ulid") */ + protected $id; + + public function __construct($id) + { + $this->id = $id; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php index d846df62c8da9..1702532aa699e 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php @@ -12,13 +12,25 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\GuidType; +use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Version; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; +use Symfony\Bridge\Doctrine\Types\UlidType; +use Symfony\Bridge\Doctrine\Types\UuidType; +use Symfony\Component\Uid\Uuid; class ORMQueryBuilderLoaderTest extends TestCase { + protected function tearDown(): void + { + if (Type::hasType('uuid')) { + Type::overrideType('uuid', GuidType::class); + } + } + public function testIdentifierTypeIsStringArray() { $this->checkIdentifierType('Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity', Connection::PARAM_STR_ARRAY); @@ -131,6 +143,51 @@ public function testFilterEmptyUuids($entityClass) $loader->getEntitiesByIds('id', ['71c5fd46-3f16-4abb-bad7-90ac1e654a2d', '', 'b98e8e11-2897-44df-ad24-d2627eb7f499']); } + /** + * @dataProvider provideUidEntityClasses + */ + public function testFilterUid($entityClass) + { + if (Type::hasType('uuid')) { + Type::overrideType('uuid', UuidType::class); + } else { + Type::addType('uuid', UuidType::class); + } + if (!Type::hasType('ulid')) { + Type::addType('ulid', UlidType::class); + } + + $em = DoctrineTestHelper::createTestEntityManager(); + + $query = $this->getMockBuilder('QueryMock') + ->setMethods(['setParameter', 'getResult', 'getSql', '_doExecute']) + ->getMock(); + + $query + ->method('getResult') + ->willReturn([]); + + $query->expects($this->once()) + ->method('setParameter') + ->with('ORMQueryBuilderLoader_getEntitiesByIds_id', [Uuid::fromString('71c5fd46-3f16-4abb-bad7-90ac1e654a2d')->toBinary(), Uuid::fromString('b98e8e11-2897-44df-ad24-d2627eb7f499')->toBinary()], Connection::PARAM_STR_ARRAY) + ->willReturn($query); + + $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->setConstructorArgs([$em]) + ->setMethods(['getQuery']) + ->getMock(); + + $qb->expects($this->once()) + ->method('getQuery') + ->willReturn($query); + + $qb->select('e') + ->from($entityClass, 'e'); + + $loader = new ORMQueryBuilderLoader($qb); + $loader->getEntitiesByIds('id', ['71c5fd46-3f16-4abb-bad7-90ac1e654a2d', '', 'b98e8e11-2897-44df-ad24-d2627eb7f499']); + } + public function testEmbeddedIdentifierName() { if (Version::compare('2.5.0') > 0) { @@ -176,4 +233,12 @@ public function provideGuidEntityClasses() ['Symfony\Bridge\Doctrine\Tests\Fixtures\UuidIdEntity'], ]; } + + public function provideUidEntityClasses() + { + return [ + ['Symfony\Bridge\Doctrine\Tests\Fixtures\UuidIdEntity'], + ['Symfony\Bridge\Doctrine\Tests\Fixtures\UlidIdEntity'], + ]; + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php index 36aace3ed843d..fde2341bc9ebe 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/UlidTypeTest.php @@ -31,7 +31,11 @@ final class UlidTypeTest extends TestCase public static function setUpBeforeClass(): void { - Type::addType('ulid', UlidType::class); + if (Type::hasType('ulid')) { + Type::overrideType('ulid', UlidType::class); + } else { + Type::addType('ulid', UlidType::class); + } } protected function setUp(): void diff --git a/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php index 45e2ee0d5dc71..d6bf714627a1d 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Types/UuidTypeTest.php @@ -31,7 +31,11 @@ final class UuidTypeTest extends TestCase public static function setUpBeforeClass(): void { - Type::addType('uuid', UuidType::class); + if (Type::hasType('uuid')) { + Type::overrideType('uuid', UuidType::class); + } else { + Type::addType('uuid', UuidType::class); + } } protected function setUp(): void diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 48be4f9215efa..fee16fa8ec2d0 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -18,7 +18,7 @@ "require": { "php": ">=7.2.5", "doctrine/event-manager": "~1.0", - "doctrine/persistence": "^1.3|^2", + "doctrine/persistence": "^2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.15", @@ -48,7 +48,7 @@ "doctrine/collections": "~1.0", "doctrine/data-fixtures": "^1.1", "doctrine/dbal": "^2.10|^3.0", - "doctrine/orm": "^2.6.3" + "doctrine/orm": "^2.7.3" }, "conflict": { "doctrine/dbal": "<2.10", diff --git a/src/Symfony/Bridge/PhpUnit/.gitignore b/src/Symfony/Bridge/PhpUnit/.gitignore index c49a5d8df5c65..9d8c4aadaf9f5 100644 --- a/src/Symfony/Bridge/PhpUnit/.gitignore +++ b/src/Symfony/Bridge/PhpUnit/.gitignore @@ -1,3 +1,4 @@ vendor/ composer.lock phpunit.xml +Tests/DeprecationErrorHandler/fake_vendor/symfony/error-handler/DebugClassLoader.php diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php index 797ed145bb6b9..85d776638d3bb 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php @@ -13,6 +13,8 @@ use PHPUnit\Util\Test; use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerFor; +use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader; +use Symfony\Component\ErrorHandler\DebugClassLoader; /** * @internal @@ -58,6 +60,18 @@ public function __construct($message, array $trace, $file) } $this->trace = $trace; + + if ('trigger_error' === ($trace[1]['function'] ?? null) + && (DebugClassLoader::class === ($class = $trace[2]['class'] ?? null) || LegacyDebugClassLoader::class === $class) + && 'checkClass' === ($trace[2]['function'] ?? null) + && null !== ($extraFile = $trace[2]['args'][1] ?? null) + && '' !== $extraFile + && false !== $extraFile = realpath($extraFile) + ) { + $this->getOriginalFilesStack(); + array_splice($this->originalFilesStack, 2, 1, $extraFile); + } + $this->message = $message; $i = \count($this->trace); while (1 < $i && $this->lineShouldBeSkipped($this->trace[--$i])) { diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_autoload.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_autoload.phpt new file mode 100644 index 0000000000000..781027e84fe66 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_autoload.phpt @@ -0,0 +1,51 @@ +--TEST-- +Test that a deprecation from the DebugClassLoader on a vendor class autoload triggered by an app class is considered indirect. +--FILE-- + +--EXPECTF-- +Remaining indirect deprecation notices (1) + + 1x: The "acme\lib\ExtendsDeprecatedClassFromOtherVendor" class extends "fcy\lib\DeprecatedClass" that is deprecated. + 1x in BarService::__construct from App\Services diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/BarService.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/BarService.php new file mode 100644 index 0000000000000..868de5bd443db --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/BarService.php @@ -0,0 +1,13 @@ + [__DIR__.'/../../fake_app/'], 'acme\\lib\\' => [__DIR__.'/../acme/lib/'], 'bar\\lib\\' => [__DIR__.'/../bar/lib/'], + 'fcy\\lib\\' => [__DIR__.'/../fcy/lib/'], ]; } public function loadClass($className) + { + if ($file = $this->findFile($className)) { + require $file; + } + } + + public function findFile($class) { foreach ($this->getPrefixesPsr4() as $prefix => $baseDirs) { - if (strpos($className, $prefix) !== 0) { + if (strpos($class, $prefix) !== 0) { continue; } foreach ($baseDirs as $baseDir) { - $file = str_replace([$prefix, '\\'], [$baseDir, '/'], $className.'.php'); + $file = str_replace([$prefix, '\\'], [$baseDir, '/'], $class.'.php'); if (file_exists($file)) { - require $file; + return $file; } } } + + return false; } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php new file mode 100644 index 0000000000000..f6672cea20400 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php @@ -0,0 +1,10 @@ +=5.5.9" }, "require-dev": { - "symfony/deprecation-contracts": "^2.1" + "symfony/deprecation-contracts": "^2.1", + "symfony/error-handler": "^4.4|^5.0" }, "suggest": { "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php index 0a9319472552b..73ec1c18277e2 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php @@ -2393,7 +2393,7 @@ public function testTextarea() $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], '/textarea [@name="name"] - [@pattern="foo"] + [not(@pattern)] [@class="my&class form-control"] [.="foo&bar"] ' diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 06475d199c1bd..f9caf53de6cd0 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -27,7 +27,7 @@ "symfony/asset": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/finder": "^4.4|^5.0", - "symfony/form": "^5.1", + "symfony/form": "^5.1.9", "symfony/http-foundation": "^4.4|^5.0", "symfony/http-kernel": "^4.4|^5.0", "symfony/mime": "^5.2", diff --git a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php index 6e44657cf8181..40381e34aa310 100644 --- a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php +++ b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php @@ -65,7 +65,7 @@ public function getKernel() /** * Gets the profile associated with the current Response. * - * @return HttpProfile|false A Profile instance + * @return HttpProfile|false|null A Profile instance */ public function getProfile() { diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 2fe6c20335385..4e1ccb8d2b9fb 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -8,6 +8,8 @@ CHANGELOG * Added `SortFirewallListenersPass` to make the execution order of firewall listeners configurable by leveraging `Symfony\Component\Security\Http\Firewall\FirewallListenerInterface` * Added ability to use comma separated ip address list for `security.access_control` + * [BC break] Removed `EntryPointFactoryInterface`, authenticators must now implement `AuthenticationEntryPointInterface` if + they require autoregistration of a Security entry point. 5.1.0 ----- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterEntryPointPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterEntryPointPass.php new file mode 100644 index 0000000000000..d76e7ecdbcb72 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterEntryPointPass.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; + +/** + * @author Wouter de Jong + */ +class RegisterEntryPointPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasParameter('security.firewalls')) { + return; + } + + $firewalls = $container->getParameter('security.firewalls'); + foreach ($firewalls as $firewallName) { + if (!$container->hasDefinition('security.authenticator.manager.'.$firewallName) || !$container->hasParameter('security.'.$firewallName.'._indexed_authenticators')) { + continue; + } + + $entryPoints = []; + $indexedAuthenticators = $container->getParameter('security.'.$firewallName.'._indexed_authenticators'); + // this is a compile-only parameter, removing it cleans up space and avoids unintended usage + $container->getParameterBag()->remove('security.'.$firewallName.'._indexed_authenticators'); + foreach ($indexedAuthenticators as $key => $authenticatorId) { + if (!$container->has($authenticatorId)) { + continue; + } + + $definition = $container->findDefinition($authenticatorId); + if (is_a($definition->getClass(), AuthenticationEntryPointInterface::class, true)) { + $entryPoints[$key] = $authenticatorId; + } + } + + if (!$entryPoints) { + return; + } + + $config = $container->getDefinition('security.firewall.map.config.'.$firewallName); + $configuredEntryPoint = $config->getArgument(7); + + if (null !== $configuredEntryPoint) { + // allow entry points to be configured by authenticator key (e.g. "http_basic") + $entryPoint = $entryPoints[$configuredEntryPoint] ?? $configuredEntryPoint; + } elseif (1 === \count($entryPoints)) { + $entryPoint = array_shift($entryPoints); + } else { + $entryPointNames = []; + foreach ($entryPoints as $key => $serviceId) { + $entryPointNames[] = is_numeric($key) ? $serviceId : $key; + } + + throw new InvalidConfigurationException(sprintf('Because you have multiple authenticators in firewall "%s", you need to set the "entry_point" key to one of your authenticators ("%s") or a service ID implementing "%s". The "entry_point" determines what should happen (e.g. redirect to "/login") when an anonymous user tries to access a protected page.', $firewallName, implode('", "', $entryPointNames), AuthenticationEntryPointInterface::class)); + } + + $config->replaceArgument(7, $entryPoint); + $container->getDefinition('security.exception_listener.'.$firewallName)->replaceArgument(4, new Reference($entryPoint)); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php index cb65f31fe5efb..a94c988d6308e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AuthenticatorFactoryInterface.php @@ -16,7 +16,7 @@ /** * @author Wouter de Jong * - * @experimental in 5.1 + * @experimental in 5.2 */ interface AuthenticatorFactoryInterface { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php index d9245b0616022..67294b3111d63 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/CustomAuthenticatorFactory.php @@ -19,7 +19,7 @@ * @author Wouter de Jong * * @internal - * @experimental in Symfony 5.1 + * @experimental in 5.2 */ class CustomAuthenticatorFactory implements AuthenticatorFactoryInterface, SecurityFactoryInterface { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php deleted file mode 100644 index f352e14755652..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; - -use Symfony\Component\DependencyInjection\ContainerBuilder; - -/** - * @author Wouter de Jong - * - * @experimental in 5.1 - */ -interface EntryPointFactoryInterface -{ - /** - * Register the entry point on the container and returns the service ID. - * - * This does not mean that the entry point is also used. This is managed - * by the "entry_point" firewall setting. - */ - public function registerEntryPoint(ContainerBuilder $container, string $firewallName, array $config): ?string; -} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 76dde71424a9c..3f4f6a16909b1 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -25,7 +25,7 @@ * * @internal */ -class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface, EntryPointFactoryInterface +class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface { public function __construct() { @@ -94,12 +94,7 @@ protected function createListener(ContainerBuilder $container, string $id, array protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId) { - return $this->registerEntryPoint($container, $id, $config); - } - - public function registerEntryPoint(ContainerBuilder $container, string $firewallName, array $config): string - { - $entryPointId = 'security.authentication.form_entry_point.'.$firewallName; + $entryPointId = 'security.authentication.form_entry_point.'.$id; $container ->setDefinition($entryPointId, new ChildDefinition('security.authentication.form_entry_point')) ->addArgument(new Reference('security.http_utils')) @@ -118,13 +113,17 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal $authenticatorId = 'security.authenticator.form_login.'.$firewallName; $options = array_intersect_key($config, $this->options); - $container + $authenticator = $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login')) ->replaceArgument(1, new Reference($userProviderId)) ->replaceArgument(2, new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config))) ->replaceArgument(3, new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config))) ->replaceArgument(4, $options); + if ($options['use_forward'] ?? false) { + $authenticator->addMethodCall('setHttpKernel', [new Reference('http_kernel')]); + } + return $authenticatorId; } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php index 51a2b3bb97f54..f60666e9dc772 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php @@ -27,7 +27,7 @@ * * @internal */ -class GuardAuthenticationFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface, EntryPointFactoryInterface +class GuardAuthenticationFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { public function getPosition() { @@ -102,6 +102,10 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal $userProvider = new Reference($userProviderId); $authenticatorIds = []; + if (isset($config['entry_point'])) { + throw new InvalidConfigurationException('The "security.firewall.'.$firewallName.'.guard.entry_point" option has no effect in the new authenticator system, configure "security.firewall.'.$firewallName.'.entry_point" instead.'); + } + $guardAuthenticatorIds = $config['authenticators']; foreach ($guardAuthenticatorIds as $i => $guardAuthenticatorId) { $container->setDefinition($authenticatorIds[] = 'security.authenticator.guard.'.$firewallName.'.'.$i, new Definition(GuardBridgeAuthenticator::class)) @@ -114,15 +118,6 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal return $authenticatorIds; } - public function registerEntryPoint(ContainerBuilder $container, string $firewallName, array $config): ?string - { - try { - return $this->determineEntryPoint(null, $config); - } catch (\LogicException $e) { - throw new InvalidConfigurationException(sprintf('Because you have multiple guard authenticators, you need to set the "entry_point" key to one of your authenticators (%s).', implode(', ', $config['authenticators']))); - } - } - private function determineEntryPoint(?string $defaultEntryPointId, array $config): string { if ($defaultEntryPointId) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index 718076ced42f7..784878b9ed775 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -23,7 +23,7 @@ * * @internal */ -class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface, EntryPointFactoryInterface +class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface { public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint) { @@ -38,7 +38,11 @@ public function create(ContainerBuilder $container, string $id, array $config, s // entry point $entryPointId = $defaultEntryPoint; if (null === $entryPointId) { - $entryPointId = $this->registerEntryPoint($container, $id, $config); + $entryPointId = 'security.authentication.basic_entry_point.'.$id; + $container + ->setDefinition($entryPointId, new ChildDefinition('security.authentication.basic_entry_point')) + ->addArgument($config['realm']) + ; } // listener @@ -81,15 +85,4 @@ public function addConfiguration(NodeDefinition $node) ->end() ; } - - public function registerEntryPoint(ContainerBuilder $container, string $firewallName, array $config): string - { - $entryPointId = 'security.authentication.basic_entry_point.'.$firewallName; - $container - ->setDefinition($entryPointId, new ChildDefinition('security.authentication.basic_entry_point')) - ->addArgument($config['realm']) - ; - - return $entryPointId; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 4d0232d9217b2..3c84bf34072d0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface; -use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\EntryPointFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; @@ -40,7 +39,6 @@ use Symfony\Component\Security\Core\User\ChainUserProvider; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; -use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; use Symfony\Component\Security\Http\Event\CheckPassportEvent; use Twig\Extension\AbstractExtension; @@ -567,15 +565,13 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri $authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider); if (\is_array($authenticators)) { - foreach ($authenticators as $i => $authenticator) { + foreach ($authenticators as $authenticator) { $authenticationProviders[] = $authenticator; + $entryPoints[] = $authenticator; } } else { $authenticationProviders[] = $authenticators; - } - - if ($factory instanceof EntryPointFactoryInterface && ($entryPoint = $factory->registerEntryPoint($container, $id, $firewall[$key]))) { - $entryPoints[$key] = $entryPoint; + $entryPoints[$key] = $authenticators; } } else { [$provider, $listenerId, $defaultEntryPoint] = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); @@ -596,16 +592,8 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri } } - if ($entryPoints) { - // we can be sure the authenticator system is enabled - if (null !== $defaultEntryPoint) { - $defaultEntryPoint = $entryPoints[$defaultEntryPoint] ?? $defaultEntryPoint; - } elseif (1 === \count($entryPoints)) { - $defaultEntryPoint = current($entryPoints); - } else { - throw new InvalidConfigurationException(sprintf('Because you have multiple authenticators in firewall "%s", you need to set the "entry_point" key to one of your authenticators (%s) or a service ID implementing "%s". The "entry_point" determines what should happen (e.g. redirect to "/login") when an anonymous user tries to access a protected page.', $id, implode(', ', $entryPoints), AuthenticationEntryPointInterface::class)); - } - } + // the actual entry point is configured by the RegisterEntryPointPass + $container->setParameter('security.'.$id.'._indexed_authenticators', $entryPoints); if (false === $hasListeners && !$this->authenticatorManagerEnabled) { throw new InvalidConfigurationException(sprintf('No authentication listener registered for firewall "%s".', $id)); diff --git a/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php index ab2dded7989a0..ba4af81acd603 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/UserAuthenticator.php @@ -27,7 +27,7 @@ * @author Wouter de Jong * * @final - * @experimental in Symfony 5.1 + * @experimental in 5.2 */ class UserAuthenticator implements UserAuthenticatorInterface { diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 27ee29df549b9..409d116228622 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -15,6 +15,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfFeaturesPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterEntryPointPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterGlobalSecurityEventListenersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; @@ -77,6 +78,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new RegisterCsrfFeaturesPass()); $container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200); $container->addCompilerPass(new RegisterLdapLocatorPass()); + $container->addCompilerPass(new RegisterEntryPointPass(), PassConfig::TYPE_BEFORE_REMOVING); // must be registered after RegisterListenersPass (in the FrameworkBundle) $container->addCompilerPass(new RegisterGlobalSecurityEventListenersPass(), PassConfig::TYPE_BEFORE_REMOVING, -200); // execute after ResolveChildDefinitionsPass optimization pass, to ensure class names are set diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php index 7d6f5f6591278..8dd7617d6d03a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php @@ -178,9 +178,6 @@ public function testAuthenticatorSystemCreate() $authenticators = $factory->createAuthenticator($container, $firewallName, $config, $userProviderId); $this->assertEquals('security.authenticator.guard.my_firewall.0', $authenticators[0]); - $entryPointId = $factory->registerEntryPoint($container, $firewallName, $config, null); - $this->assertEquals('authenticator123', $entryPointId); - $authenticatorDefinition = $container->getDefinition('security.authenticator.guard.my_firewall.0'); $this->assertEquals(GuardBridgeAuthenticator::class, $authenticatorDefinition->getClass()); $this->assertEquals('authenticator123', (string) $authenticatorDefinition->getArgument(0)); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 616abed29e65b..61e6287768cf6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -438,6 +438,7 @@ public function testSwitchUserWithSeveralDefinedProvidersButNoFirewallRootProvid public function testAuthenticatorManagerEnabledEntryPoint(array $firewall, $entryPointId) { $container = $this->getRawContainer(); + $container->register(AppCustomAuthenticator::class); $container->loadFromExtension('security', [ 'enable_authenticator_manager' => true, 'providers' => [ @@ -458,9 +459,9 @@ public function testAuthenticatorManagerEnabledEntryPoint(array $firewall, $entr public function provideEntryPointFirewalls() { // only one entry point available - yield [['http_basic' => true], 'security.authentication.basic_entry_point.main']; + yield [['http_basic' => true], 'security.authenticator.http_basic.main']; // explicitly configured by authenticator key - yield [['form_login' => true, 'http_basic' => true, 'entry_point' => 'form_login'], 'security.authentication.form_entry_point.main']; + yield [['form_login' => true, 'http_basic' => true, 'entry_point' => 'form_login'], 'security.authenticator.form_login.main']; // explicitly configured another service yield [['form_login' => true, 'entry_point' => EntryPointStub::class], EntryPointStub::class]; // no entry point required @@ -469,14 +470,7 @@ public function provideEntryPointFirewalls() // only one guard authenticator entry point available yield [[ 'guard' => ['authenticators' => [AppCustomAuthenticator::class]], - ], AppCustomAuthenticator::class]; - // explicitly configured guard authenticator entry point - yield [[ - 'guard' => [ - 'authenticators' => [AppCustomAuthenticator::class, NullAuthenticator::class], - 'entry_point' => NullAuthenticator::class, - ], - ], NullAuthenticator::class]; + ], 'security.authenticator.guard.main.0']; } /** @@ -507,12 +501,7 @@ public function provideEntryPointRequiredData() // more than one entry point available and not explicitly set yield [ ['http_basic' => true, 'form_login' => true], - '/^Because you have multiple authenticators in firewall "main", you need to set the "entry_point" key to one of your authenticators/', - ]; - // more than one guard entry point available and not explicitly set - yield [ - ['guard' => ['authenticators' => [AppCustomAuthenticator::class, NullAuthenticator::class]]], - '/^Because you have multiple guard authenticators, you need to set the "entry_point" key to one of your authenticators/', + '/Because you have multiple authenticators in firewall "main", you need to set the "entry_point" key to one of your authenticators \("form_login", "http_basic"\) or a service ID implementing/', ]; } @@ -537,6 +526,7 @@ public function testAlwaysAuthenticateBeforeGrantingCannotBeTrueWithAuthenticato public function testConfigureCustomAuthenticator(array $firewall, array $expectedAuthenticators) { $container = $this->getRawContainer(); + $container->register(TestAuthenticator::class); $container->loadFromExtension('security', [ 'enable_authenticator_manager' => true, 'providers' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index ba7d29010d62e..852643c7881e7 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -25,7 +25,7 @@ "symfony/polyfill-php80": "^1.15", "symfony/security-core": "^5.2", "symfony/security-csrf": "^4.4|^5.0", - "symfony/security-guard": "^5.1", + "symfony/security-guard": "^5.2", "symfony/security-http": "^5.2" }, "require-dev": { diff --git a/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php b/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php index a0e8f4027181c..36667f2a0dfb9 100644 --- a/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/CouchbaseBucketAdapter.php @@ -42,7 +42,7 @@ class CouchbaseBucketAdapter extends AbstractAdapter public function __construct(\CouchbaseBucket $bucket, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null) { if (!static::isSupported()) { - throw new CacheException('Couchbase >= 2.6.0 is required.'); + throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); } $this->maxIdLength = static::MAX_KEY_LENGTH; @@ -66,7 +66,7 @@ public static function createConnection($servers, array $options = []): \Couchba } if (!static::isSupported()) { - throw new CacheException('Couchbase >= 2.6.0 is required.'); + throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); } set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); }); @@ -125,7 +125,7 @@ public static function createConnection($servers, array $options = []): \Couchba public static function isSupported(): bool { - return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>='); + return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<'); } private static function getOptions(string $options): array diff --git a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php index 120d0d94c0cc5..c72d6710f22e9 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/CouchbaseBucketAdapterTest.php @@ -16,7 +16,8 @@ use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter; /** - * @requires extension couchbase 2.6.0 + * @requires extension couchbase <3.0.0 + * @requires extension couchbase >=2.6.0 * @group integration * * @author Antonio Jose Cerezo Aranda @@ -32,6 +33,10 @@ class CouchbaseBucketAdapterTest extends AdapterTestCase public static function setupBeforeClass(): void { + if (!CouchbaseBucketAdapter::isSupported()) { + self::markTestSkipped('Couchbase >= 2.6.0 < 3.0.0 is required.'); + } + self::$client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache', ['username' => getenv('COUCHBASE_USER'), 'password' => getenv('COUCHBASE_PASS')] ); diff --git a/src/Symfony/Component/Console/Formatter/OutputFormatter.php b/src/Symfony/Component/Console/Formatter/OutputFormatter.php index edc6a1d71d6f9..4ad248868dbfc 100644 --- a/src/Symfony/Component/Console/Formatter/OutputFormatter.php +++ b/src/Symfony/Component/Console/Formatter/OutputFormatter.php @@ -25,6 +25,14 @@ class OutputFormatter implements WrappableOutputFormatterInterface private $styles = []; private $styleStack; + public function __clone() + { + $this->styleStack = clone $this->styleStack; + foreach ($this->styles as $key => $value) { + $this->styles[$key] = clone $value; + } + } + /** * Escapes "<" special char in given text. * diff --git a/src/Symfony/Component/Console/Output/TrimmedBufferOutput.php b/src/Symfony/Component/Console/Output/TrimmedBufferOutput.php new file mode 100644 index 0000000000000..a03aa835f0086 --- /dev/null +++ b/src/Symfony/Component/Console/Output/TrimmedBufferOutput.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * A BufferedOutput that keeps only the last N chars. + * + * @author Jérémy Derussé + */ +class TrimmedBufferOutput extends Output +{ + private $maxLength; + private $buffer = ''; + + public function __construct( + int $maxLength, + ?int $verbosity = self::VERBOSITY_NORMAL, + bool $decorated = false, + OutputFormatterInterface $formatter = null + ) { + if ($maxLength <= 0) { + throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength)); + } + + parent::__construct($verbosity, $decorated, $formatter); + $this->maxLength = $maxLength; + } + + /** + * Empties buffer and returns its content. + * + * @return string + */ + public function fetch() + { + $content = $this->buffer; + $this->buffer = ''; + + return $content; + } + + /** + * {@inheritdoc} + */ + protected function doWrite($message, $newline) + { + $this->buffer .= $message; + + if ($newline) { + $this->buffer .= \PHP_EOL; + } + + $this->buffer = substr($this->buffer, 0 - $this->maxLength); + } +} diff --git a/src/Symfony/Component/Console/Style/SymfonyStyle.php b/src/Symfony/Component/Console/Style/SymfonyStyle.php index ba8626f74b0c1..b00a8544bc436 100644 --- a/src/Symfony/Component/Console/Style/SymfonyStyle.php +++ b/src/Symfony/Component/Console/Style/SymfonyStyle.php @@ -21,8 +21,8 @@ use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\TrimmedBufferOutput; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; @@ -46,7 +46,7 @@ class SymfonyStyle extends OutputStyle public function __construct(InputInterface $input, OutputInterface $output) { $this->input = $input; - $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter()); + $this->bufferedOutput = new TrimmedBufferOutput(\DIRECTORY_SEPARATOR === '\\' ? 4 : 2, $output->getVerbosity(), false, clone $output->getFormatter()); // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); @@ -454,9 +454,8 @@ private function autoPrependText(): void private function writeBuffer(string $message, bool $newLine, int $type): void { - // We need to know if the two last chars are PHP_EOL - // Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer - $this->bufferedOutput->write(substr($message, -4), $newLine, $type); + // We need to know if the last chars are PHP_EOL + $this->bufferedOutput->write($message, $newLine, $type); } private function createBlock(iterable $messages, string $type = null, string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php new file mode 100644 index 0000000000000..6b47969eeeba6 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_20.php @@ -0,0 +1,13 @@ +setDecorated(true); + $output = new SymfonyStyle($input, $output); + $output->write('do you want something'); + $output->writeln('?'); +}; diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_20.txt b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_20.txt new file mode 100644 index 0000000000000..c082985309229 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_20.txt @@ -0,0 +1 @@ +do you want something? diff --git a/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php b/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php index 943b94172a609..16bb2baec4ac7 100644 --- a/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php +++ b/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php @@ -14,8 +14,10 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Tester\CommandTester; @@ -115,4 +117,18 @@ public function testGetErrorStyleUsesTheCurrentOutputIfNoErrorOutputIsAvailable( $this->assertInstanceOf(SymfonyStyle::class, $style->getErrorStyle()); } + + public function testMemoryConsumption() + { + $io = new SymfonyStyle(new ArrayInput([]), new NullOutput()); + $str = 'teststr'; + $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); + $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); + $start = memory_get_usage(); + for ($i = 0; $i < 100; ++$i) { + $io->writeln($str, SymfonyStyle::VERBOSITY_QUIET); + } + + $this->assertSame(0, memory_get_usage() - $start); + } } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php index beb488236c069..f7dbe6c8a35a4 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AnalyzeServiceReferencesPass.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Argument\ArgumentInterface; +use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; @@ -35,6 +36,7 @@ class AnalyzeServiceReferencesPass extends AbstractRecursivePass private $hasProxyDumper; private $lazy; private $byConstructor; + private $byFactory; private $definitions; private $aliases; @@ -58,6 +60,7 @@ public function process(ContainerBuilder $container) $this->graph->clear(); $this->lazy = false; $this->byConstructor = false; + $this->byFactory = false; $this->definitions = $container->getDefinitions(); $this->aliases = $container->getAliases(); @@ -79,7 +82,7 @@ protected function processValue($value, bool $isRoot = false) $inExpression = $this->inExpression(); if ($value instanceof ArgumentInterface) { - $this->lazy = true; + $this->lazy = !$this->byFactory || !$value instanceof IteratorArgument; parent::processValue($value->getValues()); $this->lazy = $lazy; @@ -129,7 +132,11 @@ protected function processValue($value, bool $isRoot = false) $byConstructor = $this->byConstructor; $this->byConstructor = $isRoot || $byConstructor; + + $byFactory = $this->byFactory; + $this->byFactory = true; $this->processValue($value->getFactory()); + $this->byFactory = $byFactory; $this->processValue($value->getArguments()); $properties = $value->getProperties(); diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 9e71d955a227e..5376f38cc83ad 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -440,7 +440,7 @@ private function collectCircularReferences(string $sourceId, array $edges, array foreach ($edges as $edge) { $node = $edge->getDestNode(); $id = $node->getId(); - if (!($definition = $node->getValue()) instanceof Definition || $sourceId === $id || ($edge->isLazy() && ($this->proxyDumper ?? $this->getProxyDumper())->isProxyCandidate($definition)) || $edge->isWeak()) { + if ($sourceId === $id || !$node->getValue() instanceof Definition || $edge->isLazy() || $edge->isWeak()) { continue; } diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index e4620912cb874..42103275f5391 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -624,14 +624,13 @@ public function validateSchema(\DOMDocument $dom) EOF ; - if (\LIBXML_VERSION < 20900) { + if ($this->shouldEnableEntityLoader()) { $disableEntities = libxml_disable_entity_loader(false); $valid = @$dom->schemaValidateSource($source); libxml_disable_entity_loader($disableEntities); } else { $valid = @$dom->schemaValidateSource($source); } - foreach ($tmpfiles as $tmpfile) { @unlink($tmpfile); } @@ -639,6 +638,36 @@ public function validateSchema(\DOMDocument $dom) return $valid; } + private function shouldEnableEntityLoader(): bool + { + // Version prior to 8.0 can be enabled without deprecation + if (\PHP_VERSION_ID < 80000) { + return true; + } + + static $dom, $schema; + if (null === $dom) { + $dom = new \DOMDocument(); + $dom->loadXML(''); + + $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); + register_shutdown_function(static function () use ($tmpfile) { + @unlink($tmpfile); + }); + $schema = ' + + +'; + file_put_contents($tmpfile, ' + + + +'); + } + + return !@$dom->schemaValidateSource($schema); + } + private function validateAlias(\DOMElement $alias, string $file) { foreach ($alias->attributes as $name => $node) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 005ea3230ad40..c35dfd68b368b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1395,12 +1395,18 @@ public function testAlmostCircular($visibility) $container = include __DIR__.'/Fixtures/containers/container_almost_circular.php'; $container->compile(); + $entityManager = $container->get('doctrine.entity_manager'); + $this->assertEquals(new \stdClass(), $entityManager); + $pA = $container->get('pA'); $this->assertEquals(new \stdClass(), $pA); $logger = $container->get('monolog.logger'); $this->assertEquals(new \stdClass(), $logger->handler); + $logger_inline = $container->get('monolog_inline.logger'); + $this->assertEquals(new \stdClass(), $logger_inline->handler); + $foo = $container->get('foo'); $this->assertSame($foo, $foo->bar->foobar->foo); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index e68b4ceb228c3..c5c5217386f12 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -1054,12 +1054,18 @@ public function testAlmostCircular($visibility) $container = new $container(); + $entityManager = $container->get('doctrine.entity_manager'); + $this->assertEquals(new \stdClass(), $entityManager); + $pA = $container->get('pA'); $this->assertEquals(new \stdClass(), $pA); $logger = $container->get('monolog.logger'); $this->assertEquals(new \stdClass(), $logger->handler); + $logger_inline = $container->get('monolog_inline.logger'); + $this->assertEquals(new \stdClass(), $logger_inline->handler); + $foo = $container->get('foo'); $this->assertSame($foo, $foo->bar->foobar->foo); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_almost_circular.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_almost_circular.php index 6a0b2da766cdd..8dd05316969f2 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_almost_circular.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/containers/container_almost_circular.php @@ -1,5 +1,6 @@ register('doctrine.config', 'stdClass')->setPublic(false) + ->setProperty('resolver', new Reference('doctrine.entity_listener_resolver')) + ->setProperty('flag', 'ok'); + +$container->register('doctrine.entity_manager', 'stdClass')->setPublic(true) + ->setFactory([FactoryChecker::class, 'create']) + ->addArgument(new Reference('doctrine.config')); +$container->register('doctrine.entity_listener_resolver', 'stdClass')->setPublic($public) + ->addArgument(new IteratorArgument([new Reference('doctrine.listener')])); +$container->register('doctrine.listener', 'stdClass')->setPublic($public) + ->addArgument(new Reference('doctrine.entity_manager')); + // multiple path detection $container->register('pA', 'stdClass')->setPublic(true) @@ -42,6 +57,27 @@ $container->register('monolog.logger_2', 'stdClass')->setPublic($public) ->setProperty('handler', new Reference('mailer.transport')); +// monolog-like + handler that require monolog with inlined factory + +$container->register('monolog_inline.logger', 'stdClass')->setPublic(true) + ->setProperty('handler', new Reference('mailer_inline.mailer')); + +$container->register('mailer_inline.mailer', 'stdClass')->setPublic(false) + ->addArgument( + (new Definition('stdClass')) + ->setFactory([new Reference('mailer_inline.transport_factory'), 'create']) + ); + +$container->register('mailer_inline.transport_factory', FactoryCircular::class)->setPublic($public) + ->addArgument(new TaggedIteratorArgument('mailer_inline.transport')); + +$container->register('mailer_inline.transport_factory.amazon', 'stdClass')->setPublic($public) + ->addArgument(new Reference('monolog_inline.logger_2')) + ->addTag('mailer.transport'); + +$container->register('monolog_inline.logger_2', 'stdClass')->setPublic($public) + ->setProperty('handler', new Reference('mailer_inline.mailer')); + // same visibility for deps $container->register('foo', FooCircular::class)->setPublic(true) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php index b22e8bcffc744..39ad97f990e93 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/classes.php @@ -128,6 +128,18 @@ public function create() } } +class FactoryChecker +{ + public static function create($config) + { + if (!isset($config->flag)) { + throw new \LogicException('The injected config must contain a "flag" property.'); + } + + return new stdClass(); + } +} + class FoobarCircular { public function __construct(FooCircular $foo) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php index 6d9985af3265b..f20be40568b0b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_private.php @@ -25,6 +25,7 @@ public function __construct() 'baz6' => 'getBaz6Service', 'connection' => 'getConnectionService', 'connection2' => 'getConnection2Service', + 'doctrine.entity_manager' => 'getDoctrine_EntityManagerService', 'foo' => 'getFooService', 'foo2' => 'getFoo2Service', 'foo5' => 'getFoo5Service', @@ -37,6 +38,7 @@ public function __construct() 'manager2' => 'getManager2Service', 'manager3' => 'getManager3Service', 'monolog.logger' => 'getMonolog_LoggerService', + 'monolog_inline.logger' => 'getMonologInline_LoggerService', 'pA' => 'getPAService', 'root' => 'getRootService', 'subscriber' => 'getSubscriberService', @@ -69,6 +71,9 @@ public function getRemovedIds(): array 'connection4' => true, 'dispatcher' => true, 'dispatcher2' => true, + 'doctrine.config' => true, + 'doctrine.entity_listener_resolver' => true, + 'doctrine.listener' => true, 'foo4' => true, 'foobar' => true, 'foobar2' => true, @@ -82,8 +87,12 @@ public function getRemovedIds(): array 'mailer.transport' => true, 'mailer.transport_factory' => true, 'mailer.transport_factory.amazon' => true, + 'mailer_inline.mailer' => true, + 'mailer_inline.transport_factory' => true, + 'mailer_inline.transport_factory.amazon' => true, 'manager4' => true, 'monolog.logger_2' => true, + 'monolog_inline.logger_2' => true, 'multiuse1' => true, 'pB' => true, 'pC' => true, @@ -182,6 +191,22 @@ protected function getConnection2Service() return $instance; } + /** + * Gets the public 'doctrine.entity_manager' shared service. + * + * @return \stdClass + */ + protected function getDoctrine_EntityManagerService() + { + $a = new \stdClass(); + $a->resolver = new \stdClass(new RewindableGenerator(function () { + yield 0 => ($this->privates['doctrine.listener'] ?? $this->getDoctrine_ListenerService()); + }, 1)); + $a->flag = 'ok'; + + return $this->services['doctrine.entity_manager'] = \FactoryChecker::create($a); + } + /** * Gets the public 'foo' shared service. * @@ -375,6 +400,20 @@ protected function getMonolog_LoggerService() return $instance; } + /** + * Gets the public 'monolog_inline.logger' shared service. + * + * @return \stdClass + */ + protected function getMonologInline_LoggerService() + { + $this->services['monolog_inline.logger'] = $instance = new \stdClass(); + + $instance->handler = ($this->privates['mailer_inline.mailer'] ?? $this->getMailerInline_MailerService()); + + return $instance; + } + /** * Gets the public 'pA' shared service. * @@ -445,6 +484,16 @@ protected function getBar6Service() return $this->privates['bar6'] = new \stdClass($a); } + /** + * Gets the private 'doctrine.listener' shared service. + * + * @return \stdClass + */ + protected function getDoctrine_ListenerService() + { + return $this->privates['doctrine.listener'] = new \stdClass(($this->services['doctrine.entity_manager'] ?? $this->getDoctrine_EntityManagerService())); + } + /** * Gets the private 'level5' shared service. * @@ -470,7 +519,8 @@ protected function getMailer_TransportService() { return $this->privates['mailer.transport'] = (new \FactoryCircular(new RewindableGenerator(function () { yield 0 => ($this->privates['mailer.transport_factory.amazon'] ?? $this->getMailer_TransportFactory_AmazonService()); - }, 1)))->create(); + yield 1 => $this->getMailerInline_TransportFactory_AmazonService(); + }, 2)))->create(); } /** @@ -489,6 +539,31 @@ protected function getMailer_TransportFactory_AmazonService() return $instance; } + /** + * Gets the private 'mailer_inline.mailer' shared service. + * + * @return \stdClass + */ + protected function getMailerInline_MailerService() + { + return $this->privates['mailer_inline.mailer'] = new \stdClass((new \FactoryCircular(new RewindableGenerator(function () { + return new \EmptyIterator(); + }, 0)))->create()); + } + + /** + * Gets the private 'mailer_inline.transport_factory.amazon' shared service. + * + * @return \stdClass + */ + protected function getMailerInline_TransportFactory_AmazonService() + { + $a = new \stdClass(); + $a->handler = ($this->privates['mailer_inline.mailer'] ?? $this->getMailerInline_MailerService()); + + return new \stdClass($a); + } + /** * Gets the private 'manager4' shared service. * diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php index 2e5d5281e0b79..666ac0a876995 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_almost_circular_public.php @@ -30,6 +30,9 @@ public function __construct() 'connection4' => 'getConnection4Service', 'dispatcher' => 'getDispatcherService', 'dispatcher2' => 'getDispatcher2Service', + 'doctrine.entity_listener_resolver' => 'getDoctrine_EntityListenerResolverService', + 'doctrine.entity_manager' => 'getDoctrine_EntityManagerService', + 'doctrine.listener' => 'getDoctrine_ListenerService', 'foo' => 'getFooService', 'foo2' => 'getFoo2Service', 'foo4' => 'getFoo4Service', @@ -45,11 +48,15 @@ public function __construct() 'mailer.transport' => 'getMailer_TransportService', 'mailer.transport_factory' => 'getMailer_TransportFactoryService', 'mailer.transport_factory.amazon' => 'getMailer_TransportFactory_AmazonService', + 'mailer_inline.transport_factory' => 'getMailerInline_TransportFactoryService', + 'mailer_inline.transport_factory.amazon' => 'getMailerInline_TransportFactory_AmazonService', 'manager' => 'getManagerService', 'manager2' => 'getManager2Service', 'manager3' => 'getManager3Service', 'monolog.logger' => 'getMonolog_LoggerService', 'monolog.logger_2' => 'getMonolog_Logger2Service', + 'monolog_inline.logger' => 'getMonologInline_LoggerService', + 'monolog_inline.logger_2' => 'getMonologInline_Logger2Service', 'pA' => 'getPAService', 'pB' => 'getPBService', 'pC' => 'getPCService', @@ -80,12 +87,14 @@ public function getRemovedIds(): array 'bar6' => true, 'config' => true, 'config2' => true, + 'doctrine.config' => true, 'level2' => true, 'level3' => true, 'level4' => true, 'level5' => true, 'level6' => true, 'logger2' => true, + 'mailer_inline.mailer' => true, 'manager4' => true, 'multiuse1' => true, 'subscriber2' => true, @@ -257,6 +266,42 @@ protected function getDispatcher2Service($lazyLoad = true) return $instance; } + /** + * Gets the public 'doctrine.entity_listener_resolver' shared service. + * + * @return \stdClass + */ + protected function getDoctrine_EntityListenerResolverService() + { + return $this->services['doctrine.entity_listener_resolver'] = new \stdClass(new RewindableGenerator(function () { + yield 0 => ($this->services['doctrine.listener'] ?? $this->getDoctrine_ListenerService()); + }, 1)); + } + + /** + * Gets the public 'doctrine.entity_manager' shared service. + * + * @return \stdClass + */ + protected function getDoctrine_EntityManagerService() + { + $a = new \stdClass(); + $a->resolver = ($this->services['doctrine.entity_listener_resolver'] ?? $this->getDoctrine_EntityListenerResolverService()); + $a->flag = 'ok'; + + return $this->services['doctrine.entity_manager'] = \FactoryChecker::create($a); + } + + /** + * Gets the public 'doctrine.listener' shared service. + * + * @return \stdClass + */ + protected function getDoctrine_ListenerService() + { + return $this->services['doctrine.listener'] = new \stdClass(($this->services['doctrine.entity_manager'] ?? $this->getDoctrine_EntityManagerService())); + } + /** * Gets the public 'foo' shared service. * @@ -450,13 +495,7 @@ protected function getLoggerService() */ protected function getMailer_TransportService() { - $a = ($this->services['mailer.transport_factory'] ?? $this->getMailer_TransportFactoryService()); - - if (isset($this->services['mailer.transport'])) { - return $this->services['mailer.transport']; - } - - return $this->services['mailer.transport'] = $a->create(); + return $this->services['mailer.transport'] = ($this->services['mailer.transport_factory'] ?? $this->getMailer_TransportFactoryService())->create(); } /** @@ -468,7 +507,8 @@ protected function getMailer_TransportFactoryService() { return $this->services['mailer.transport_factory'] = new \FactoryCircular(new RewindableGenerator(function () { yield 0 => ($this->services['mailer.transport_factory.amazon'] ?? $this->getMailer_TransportFactory_AmazonService()); - }, 1)); + yield 1 => ($this->services['mailer_inline.transport_factory.amazon'] ?? $this->getMailerInline_TransportFactory_AmazonService()); + }, 2)); } /** @@ -478,13 +518,29 @@ protected function getMailer_TransportFactoryService() */ protected function getMailer_TransportFactory_AmazonService() { - $a = ($this->services['monolog.logger_2'] ?? $this->getMonolog_Logger2Service()); + return $this->services['mailer.transport_factory.amazon'] = new \stdClass(($this->services['monolog.logger_2'] ?? $this->getMonolog_Logger2Service())); + } - if (isset($this->services['mailer.transport_factory.amazon'])) { - return $this->services['mailer.transport_factory.amazon']; - } + /** + * Gets the public 'mailer_inline.transport_factory' shared service. + * + * @return \FactoryCircular + */ + protected function getMailerInline_TransportFactoryService() + { + return $this->services['mailer_inline.transport_factory'] = new \FactoryCircular(new RewindableGenerator(function () { + return new \EmptyIterator(); + }, 0)); + } - return $this->services['mailer.transport_factory.amazon'] = new \stdClass($a); + /** + * Gets the public 'mailer_inline.transport_factory.amazon' shared service. + * + * @return \stdClass + */ + protected function getMailerInline_TransportFactory_AmazonService() + { + return $this->services['mailer_inline.transport_factory.amazon'] = new \stdClass(($this->services['monolog_inline.logger_2'] ?? $this->getMonologInline_Logger2Service())); } /** @@ -563,6 +619,34 @@ protected function getMonolog_Logger2Service() return $instance; } + /** + * Gets the public 'monolog_inline.logger' shared service. + * + * @return \stdClass + */ + protected function getMonologInline_LoggerService() + { + $this->services['monolog_inline.logger'] = $instance = new \stdClass(); + + $instance->handler = ($this->privates['mailer_inline.mailer'] ?? $this->getMailerInline_MailerService()); + + return $instance; + } + + /** + * Gets the public 'monolog_inline.logger_2' shared service. + * + * @return \stdClass + */ + protected function getMonologInline_Logger2Service() + { + $this->services['monolog_inline.logger_2'] = $instance = new \stdClass(); + + $instance->handler = ($this->privates['mailer_inline.mailer'] ?? $this->getMailerInline_MailerService()); + + return $instance; + } + /** * Gets the public 'pA' shared service. * @@ -692,6 +776,16 @@ protected function getLevel5Service() return $instance; } + /** + * Gets the private 'mailer_inline.mailer' shared service. + * + * @return \stdClass + */ + protected function getMailerInline_MailerService() + { + return $this->privates['mailer_inline.mailer'] = new \stdClass(($this->services['mailer_inline.transport_factory'] ?? $this->getMailerInline_TransportFactoryService())->create()); + } + /** * Gets the private 'manager4' shared service. * diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php index be8af57828e05..d3b3eb6da1fa3 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php @@ -12,8 +12,8 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FileUploadError; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; @@ -169,7 +169,7 @@ private function getFileUploadError(int $errorCode) $message = strtr($messageTemplate, $messageParameters); } - return new FormError($message, $messageTemplate, $messageParameters); + return new FileUploadError($message, $messageTemplate, $messageParameters); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/TextareaType.php b/src/Symfony/Component/Form/Extension/Core/Type/TextareaType.php index 7db19d8aedc65..173b7ef53c8a2 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/TextareaType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/TextareaType.php @@ -23,6 +23,7 @@ class TextareaType extends AbstractType public function buildView(FormView $view, FormInterface $form, array $options) { $view->vars['pattern'] = null; + unset($view->vars['attr']['pattern']); } /** diff --git a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php index 8aa75be2d7245..2f4a3a6a5a01e 100644 --- a/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php +++ b/src/Symfony/Component/Form/Extension/Validator/ViolationMapper/ViolationMapper.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Form\Extension\Validator\ViolationMapper; +use Symfony\Component\Form\FileUploadError; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormRendererInterface; @@ -18,6 +19,7 @@ use Symfony\Component\PropertyAccess\PropertyPathBuilder; use Symfony\Component\PropertyAccess\PropertyPathIterator; use Symfony\Component\PropertyAccess\PropertyPathIteratorInterface; +use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Contracts\Translation\TranslatorInterface; @@ -131,6 +133,23 @@ public function mapViolation(ConstraintViolation $violation, FormInterface $form // Only add the error if the form is synchronized if ($this->acceptsErrors($scope)) { + if ($violation->getConstraint() instanceof File && (string) \UPLOAD_ERR_INI_SIZE === $violation->getCode()) { + $errorsTarget = $scope; + + while (null !== $errorsTarget->getParent() && $errorsTarget->getConfig()->getErrorBubbling()) { + $errorsTarget = $errorsTarget->getParent(); + } + + $errors = $errorsTarget->getErrors(); + $errorsTarget->clearErrors(); + + foreach ($errors as $error) { + if (!$error instanceof FileUploadError) { + $errorsTarget->addError($error); + } + } + } + $message = $violation->getMessage(); $messageTemplate = $violation->getMessageTemplate(); diff --git a/src/Symfony/Component/Form/FileUploadError.php b/src/Symfony/Component/Form/FileUploadError.php new file mode 100644 index 0000000000000..20142b20337ea --- /dev/null +++ b/src/Symfony/Component/Form/FileUploadError.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Form; + +/** + * @internal + */ +class FileUploadError extends FormError +{ +} diff --git a/src/Symfony/Component/Form/Resources/translations/validators.ro.xlf b/src/Symfony/Component/Form/Resources/translations/validators.ro.xlf index 25abab3b6f148..a7dc62b579c6b 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.ro.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.ro.xlf @@ -4,7 +4,7 @@ This form should not contain extra fields. - Aceast formular nu ar trebui să conțină câmpuri suplimentare. + Acest formular nu ar trebui să conțină câmpuri suplimentare. The uploaded file was too large. Please try to upload a smaller file. @@ -12,8 +12,128 @@ The CSRF token is invalid. Please try to resubmit the form. - Token-ul CSRF este invalid. Vă rugăm să trimiteți formularul incă o dată. + Token-ul CSRF este invalid. Vă rugăm să retrimiteți formularul. + + + This value is not a valid HTML5 color. + Această valoare nu este un cod de culoare HTML5 valid. + + + Please enter a valid birthdate. + Vă rugăm să introduceți o dată de naștere validă. + + + The selected choice is invalid. + Valoarea selectată este invalidă. + + + The collection is invalid. + Colecția nu este validă. + + + Please select a valid color. + Vă rugăm să selectați o culoare validă. + + + Please select a valid country. + Vă rugăm să selectați o țară validă. + + + Please select a valid currency. + Vă rugăm să selectați o monedă validă. + + + Please choose a valid date interval. + Vă rugăm să selectați un interval de zile valid. + + + Please enter a valid date and time. + Vă rugăm să introduceți o dată și o oră validă. + + + Please enter a valid date. + Vă rugăm să introduceți o dată validă. + + + Please select a valid file. + Vă rugăm să selectați un fișier valid. + + + The hidden field is invalid. + Câmpul ascuns este invalid. + + + Please enter an integer. + Vă rugăm să introduceți un număr întreg. + + + Please select a valid language. + Vă rugăm să selectați o limbă validă. + + + Please select a valid locale. + Vă rugăm să selectați o setare locală validă. + + + Please enter a valid money amount. + Vă rugăm să introduceți o valoare monetară corectă. + + + Please enter a number. + Vă rugăm să introduceți un număr. + + + The password is invalid. + Parola nu este validă. + + + Please enter a percentage value. + Vă rugăm să introduceți o valoare procentuală. + + + The values do not match. + Valorile nu coincid. + + + Please enter a valid time. + Vă rugăm să introduceți o oră validă. + + + Please select a valid timezone. + Vă rugăm să selectați un fus orar valid. + + + Please enter a valid URL. + Vă rugăm să introduceți un URL valid. + + + Please enter a valid search term. + Vă rugăm să introduceți un termen de căutare valid. + + + Please provide a valid phone number. + Vă rugăm să introduceți un număr de telefon valid. + + + The checkbox has an invalid value. + Bifa nu are o valoare validă. + + + Please enter a valid email address. + Vă rugăm să introduceți o adresă de email validă. + + + Please select a valid option. + Vă rugăm să selectați o opțiune validă. + + + Please select a valid range. + Vă rugăm să selectați un interval valid. + + + Please enter a valid week. + Vă rugăm să introduceți o săptămână validă. - \ No newline at end of file + diff --git a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php index cd39b50db440d..4931ccb6a3ed7 100644 --- a/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractLayoutTest.php @@ -2045,7 +2045,7 @@ public function testTextarea() $this->assertWidgetMatchesXpath($form->createView(), [], '/textarea [@name="name"] - [@pattern="foo"] + [not(@pattern)] [.="foo&bar"] ' ); diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php index 89a2c4c57c03d..70dec5c308b74 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/ViolationMapper/ViolationMapperTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\DataMapper\DataMapper; use Symfony\Component\Form\Extension\Validator\ViolationMapper\ViolationMapper; +use Symfony\Component\Form\FileUploadError; use Symfony\Component\Form\Form; use Symfony\Component\Form\FormConfigBuilder; use Symfony\Component\Form\FormError; @@ -25,6 +26,7 @@ use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\Tests\Extension\Validator\ViolationMapper\Fixtures\Issue; use Symfony\Component\PropertyAccess\PropertyPath; +use Symfony\Component\Validator\Constraints\File; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -83,6 +85,7 @@ protected function getForm($name = 'name', $propertyPath = null, $dataClass = nu $config->setPropertyPath($propertyPath); $config->setCompound(true); $config->setDataMapper(new DataMapper()); + $config->setErrorBubbling($options['error_bubbling'] ?? false); if (!$synchronized) { $config->addViewTransformer(new CallbackTransformer( @@ -1759,4 +1762,93 @@ public function testTranslatorNotCalledWithoutLabel() $violation = new ConstraintViolation('Message without label', null, [], null, 'data.name', null); $this->mapper->mapViolation($violation, $parent); } + + public function testFileUploadErrorIsNotRemovedIfNoFileSizeConstraintViolationWasRaised() + { + $form = $this->getForm('form'); + $form->addError(new FileUploadError( + 'The file is too large. Allowed maximum size is 2 MB.', + 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.', + [ + '{{ limit }}' => '2', + '{{ suffix }}' => 'MB', + ] + )); + + $this->mapper->mapViolation($this->getConstraintViolation('data'), $form); + + $this->assertCount(2, $form->getErrors()); + } + + public function testFileUploadErrorIsRemovedIfFileSizeConstraintViolationWasRaised() + { + $form = $this->getForm('form'); + $form->addError(new FileUploadError( + 'The file is too large. Allowed maximum size is 2 MB.', + 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.', + [ + '{{ limit }}' => '2', + '{{ suffix }}' => 'MB', + ] + )); + + $violation = new ConstraintViolation( + 'The file is too large (3 MB). Allowed maximum size is 2 MB.', + 'The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.', + [ + '{{ limit }}' => '2', + '{{ size }}' => '3', + '{{ suffix }}' => 'MB', + ], + '', + 'data', + null, + null, + (string) \UPLOAD_ERR_INI_SIZE, + new File() + ); + $this->mapper->mapViolation($this->getConstraintViolation('data'), $form); + $this->mapper->mapViolation($violation, $form); + + $this->assertCount(2, $form->getErrors()); + } + + public function testFileUploadErrorIsRemovedIfFileSizeConstraintViolationWasRaisedOnFieldWithErrorBubbling() + { + $parent = $this->getForm('parent'); + $child = $this->getForm('child', 'file', null, [], false, true, [ + 'error_bubbling' => true, + ]); + $parent->add($child); + $child->addError(new FileUploadError( + 'The file is too large. Allowed maximum size is 2 MB.', + 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.', + [ + '{{ limit }}' => '2', + '{{ suffix }}' => 'MB', + ] + )); + + $violation = new ConstraintViolation( + 'The file is too large (3 MB). Allowed maximum size is 2 MB.', + 'The file is too large ({{ size }} {{ suffix }}). Allowed maximum size is {{ limit }} {{ suffix }}.', + [ + '{{ limit }}' => '2', + '{{ size }}' => '3', + '{{ suffix }}' => 'MB', + ], + null, + 'data.file', + null, + null, + (string) \UPLOAD_ERR_INI_SIZE, + new File() + ); + $this->mapper->mapViolation($this->getConstraintViolation('data'), $parent); + $this->mapper->mapViolation($this->getConstraintViolation('data.file'), $parent); + $this->mapper->mapViolation($violation, $parent); + + $this->assertCount(3, $parent->getErrors()); + $this->assertCount(0, $child->getErrors()); + } } diff --git a/src/Symfony/Component/Form/Tests/Resources/TranslationFilesTest.php b/src/Symfony/Component/Form/Tests/Resources/TranslationFilesTest.php index 53b2cee448805..5a9669e92b424 100644 --- a/src/Symfony/Component/Form/Tests/Resources/TranslationFilesTest.php +++ b/src/Symfony/Component/Form/Tests/Resources/TranslationFilesTest.php @@ -29,6 +29,21 @@ public function testTranslationFileIsValid($filePath) $this->assertCount(0, $errors, sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); } + /** + * @dataProvider provideTranslationFiles + * @group Legacy + */ + public function testTranslationFileIsValidWithoutEntityLoader($filePath) + { + $document = new \DOMDocument(); + $document->loadXML(file_get_contents($filePath)); + libxml_disable_entity_loader(true); + + $errors = XliffUtils::validateSchema($document); + + $this->assertCount(0, $errors, sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); + } + public function provideTranslationFiles() { return array_map( diff --git a/src/Symfony/Component/HttpClient/AmpHttpClient.php b/src/Symfony/Component/HttpClient/AmpHttpClient.php index 0877b8883bef7..b53d636b9cf2f 100644 --- a/src/Symfony/Component/HttpClient/AmpHttpClient.php +++ b/src/Symfony/Component/HttpClient/AmpHttpClient.php @@ -82,6 +82,15 @@ public function request(string $method, string $url, array $options = []): Respo throw new \LogicException('You cannot use the "proxy" option as the "amphp/http-tunnel" package is not installed. Try running "composer require amphp/http-tunnel".'); } + if ($options['bindto']) { + if (0 === strpos($options['bindto'], 'if!')) { + throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.'); + } + if (0 === strpos($options['bindto'], 'host!')) { + $options['bindto'] = substr($options['bindto'], 5); + } + } + if ('' !== $options['body'] && 'POST' === $method && !isset($options['normalized_headers']['content-type'])) { $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded'; } @@ -141,7 +150,7 @@ public function stream($responses, float $timeout = null): ResponseStreamInterfa if ($responses instanceof AmpResponse) { $responses = [$responses]; } elseif (!is_iterable($responses)) { - throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of AmpResponse objects, %s given.', __METHOD__, get_debug_type($responses))); + throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AmpResponse objects, "%s" given.', __METHOD__, get_debug_type($responses))); } return new ResponseStream(AmpResponse::stream($responses, $timeout)); diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 445cc062221c9..014534bba9dcc 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -272,7 +272,7 @@ public function request(string $method, string $url, array $options = []): Respo if ($options['bindto']) { if (file_exists($options['bindto'])) { $curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto']; - } elseif (preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) { + } elseif (0 !== strpos($options['bindto'], 'if!') && preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) { $curlopts[\CURLOPT_INTERFACE] = $matches[1]; $curlopts[\CURLOPT_LOCALPORT] = $matches[2]; } else { diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index 3ea79650dc4e4..5ff1fffdef1e7 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -67,8 +67,16 @@ public function request(string $method, string $url, array $options = []): Respo { [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions); - if ($options['bindto'] && file_exists($options['bindto'])) { - throw new TransportException(__CLASS__.' cannot bind to local Unix sockets, use e.g. CurlHttpClient instead.'); + if ($options['bindto']) { + if (file_exists($options['bindto'])) { + throw new TransportException(__CLASS__.' cannot bind to local Unix sockets, use e.g. CurlHttpClient instead.'); + } + if (0 === strpos($options['bindto'], 'if!')) { + throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.'); + } + if (0 === strpos($options['bindto'], 'host!')) { + $options['bindto'] = substr($options['bindto'], 5); + } } $options['body'] = self::getBodyAsString($options['body']); diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index f0370e9c00be0..1812bf9c70202 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -306,10 +306,7 @@ private static function perform(ClientState $multi, array &$responses = null): v curl_multi_remove_handle($multi->handle, $ch); $waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor); - - if ('1' === $waitFor[1]) { - curl_setopt($ch, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_1); - } + curl_setopt($ch, \CURLOPT_FORBID_REUSE, true); if (0 === curl_multi_add_handle($multi->handle, $ch)) { continue; diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index 930ea23f39091..b2e2d9e40b55c 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -65,9 +65,13 @@ public function __construct($file, int $status = 200, array $headers = [], bool * @param bool $autoLastModified Whether the Last-Modified header should be automatically set * * @return static + * + * @deprecated since Symfony 5.2, use __construct() instead. */ public static function create($file = null, int $status = 200, array $headers = [], bool $public = true, string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) { + trigger_deprecation('symfony/http-foundation', '5.2', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class); + return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified); } diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index fef21b8723594..a5ba6f720672e 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * added `RateLimiter\RequestRateLimiterInterface` and `RateLimiter\AbstractRequestRateLimiter` * deprecated not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()`; wrap your filter in a closure instead. * Deprecated the `Request::HEADER_X_FORWARDED_ALL` constant, use either `HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO` or `HEADER_X_FORWARDED_AWS_ELB` or `HEADER_X_FORWARDED_TRAEFIK` constants instead. + * Deprecated `BinaryFileResponse::create()`, use `__construct()` instead 5.1.0 diff --git a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php index 2efbc2b8aec88..31d6bc2c007bb 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpFoundation\Tests; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\File\Stream; use Symfony\Component\HttpFoundation\Request; @@ -19,6 +20,8 @@ class BinaryFileResponseTest extends ResponseTestCase { + use ExpectDeprecationTrait; + public function testConstruction() { $file = __DIR__.'/../README.md'; @@ -29,6 +32,26 @@ public function testConstruction() $this->assertTrue($response->headers->has('Last-Modified')); $this->assertFalse($response->headers->has('Content-Disposition')); + $response = new BinaryFileResponse($file, 404, [], true, ResponseHeaderBag::DISPOSITION_INLINE); + $this->assertEquals(404, $response->getStatusCode()); + $this->assertFalse($response->headers->has('ETag')); + $this->assertEquals('inline; filename=README.md', $response->headers->get('Content-Disposition')); + } + + /** + * @group legacy + */ + public function testConstructionLegacy() + { + $file = __DIR__.'/../README.md'; + $this->expectDeprecation('Since symfony/http-foundation 5.2: The "Symfony\Component\HttpFoundation\BinaryFileResponse::create()" method is deprecated, use "new Symfony\Component\HttpFoundation\BinaryFileResponse()" instead.'); + $response = BinaryFileResponse::create($file, 404, ['X-Header' => 'Foo'], true, null, true, true); + $this->assertEquals(404, $response->getStatusCode()); + $this->assertEquals('Foo', $response->headers->get('X-Header')); + $this->assertTrue($response->headers->has('ETag')); + $this->assertTrue($response->headers->has('Last-Modified')); + $this->assertFalse($response->headers->has('Content-Disposition')); + $response = BinaryFileResponse::create($file, 404, [], true, ResponseHeaderBag::DISPOSITION_INLINE); $this->assertEquals(404, $response->getStatusCode()); $this->assertFalse($response->headers->has('ETag')); @@ -83,7 +106,7 @@ public function testSetContentDispositionGeneratesSafeFallbackFilenameForWrongly */ public function testRequests($requestRange, $offset, $length, $responseRange) { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); + $response = (new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']))->setAutoEtag(); // do a request to get the ETag $request = Request::create('/'); @@ -115,7 +138,7 @@ public function testRequests($requestRange, $offset, $length, $responseRange) */ public function testRequestsWithoutEtag($requestRange, $offset, $length, $responseRange) { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); + $response = new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); // do a request to get the LastModified $request = Request::create('/'); @@ -156,7 +179,7 @@ public function provideRanges() public function testRangeRequestsWithoutLastModifiedDate() { // prevent auto last modified - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'], true, null, false, false); + $response = new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'], true, null, false, false); // prepare a request for a range of the testing file $request = Request::create('/'); @@ -177,7 +200,7 @@ public function testRangeRequestsWithoutLastModifiedDate() */ public function testFullFileRequests($requestRange) { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); + $response = (new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']))->setAutoEtag(); // prepare a request for a range of the testing file $request = Request::create('/'); @@ -213,7 +236,7 @@ public function testRangeOnPostMethod() { $request = Request::create('/', 'POST'); $request->headers->set('Range', 'bytes=10-20'); - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); + $response = new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); $file = fopen(__DIR__.'/File/Fixtures/test.gif', 'r'); $data = fread($file, 35); @@ -231,7 +254,7 @@ public function testRangeOnPostMethod() public function testUnpreparedResponseSendsFullFile() { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200); + $response = new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200); $data = file_get_contents(__DIR__.'/File/Fixtures/test.gif'); @@ -247,7 +270,7 @@ public function testUnpreparedResponseSendsFullFile() */ public function testInvalidRequests($requestRange) { - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream'])->setAutoEtag(); + $response = (new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']))->setAutoEtag(); // prepare a request for a range of the testing file $request = Request::create('/'); @@ -278,7 +301,7 @@ public function testXSendfile($file) $request->headers->set('X-Sendfile-Type', 'X-Sendfile'); BinaryFileResponse::trustXSendfileTypeHeader(); - $response = BinaryFileResponse::create($file, 200, ['Content-Type' => 'application/octet-stream']); + $response = new BinaryFileResponse($file, 200, ['Content-Type' => 'application/octet-stream']); $response->prepare($request); $this->expectOutputString(''); @@ -338,7 +361,7 @@ public function testDeleteFileAfterSend() public function testAcceptRangeOnUnsafeMethods() { $request = Request::create('/', 'POST'); - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); + $response = new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); $response->prepare($request); $this->assertEquals('none', $response->headers->get('Accept-Ranges')); @@ -347,7 +370,7 @@ public function testAcceptRangeOnUnsafeMethods() public function testAcceptRangeNotOverriden() { $request = Request::create('/', 'POST'); - $response = BinaryFileResponse::create(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); + $response = new BinaryFileResponse(__DIR__.'/File/Fixtures/test.gif', 200, ['Content-Type' => 'application/octet-stream']); $response->headers->set('Accept-Ranges', 'foo'); $response->prepare($request); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index aa069bb7e952e..285507a21ec6c 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -74,12 +74,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - const VERSION = '5.2.0-RC2'; + const VERSION = '5.2.0'; const VERSION_ID = 50200; const MAJOR_VERSION = 5; const MINOR_VERSION = 2; const RELEASE_VERSION = 0; - const EXTRA_VERSION = 'RC2'; + const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '07/2021'; const END_OF_LIFE = '07/2021'; diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php index 9497353c5d242..daf95b5fbbceb 100644 --- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php +++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Transport/Connection.php @@ -16,6 +16,7 @@ use Doctrine\DBAL\Driver\Result as DriverResult; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Exception\TableNotFoundException; +use Doctrine\DBAL\LockMode; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Result; use Doctrine\DBAL\Schema\Comparator; @@ -159,9 +160,23 @@ public function get(): ?array ->orderBy('available_at', 'ASC') ->setMaxResults(1); + // Append pessimistic write lock to FROM clause if db platform supports it + $sql = $query->getSQL(); + if (($fromPart = $query->getQueryPart('from')) && + ($table = $fromPart[0]['table'] ?? null) && + ($alias = $fromPart[0]['alias'] ?? null) + ) { + $fromClause = sprintf('%s %s', $table, $alias); + $sql = str_replace( + sprintf('FROM %s WHERE', $fromClause), + sprintf('FROM %s WHERE', $this->driverConnection->getDatabasePlatform()->appendLockHint($fromClause, LockMode::PESSIMISTIC_WRITE)), + $sql + ); + } + // use SELECT ... FOR UPDATE to lock table $stmt = $this->executeQuery( - $query->getSQL().' '.$this->driverConnection->getDatabasePlatform()->getWriteLockSQL(), + $sql.' '.$this->driverConnection->getDatabasePlatform()->getWriteLockSQL(), $query->getParameters(), $query->getParameterTypes() ); diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherDumper.php b/src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherDumper.php index e77d24aecf9c1..402ac51351d55 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherDumper.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/CompiledUrlMatcherDumper.php @@ -242,7 +242,7 @@ private function compileStaticRoutes(array $staticRoutes, array &$conditions): a * Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases. * * Last but not least: - * - Because it is not possibe to mix unicode/non-unicode patterns in a single regexp, several of them can be generated. + * - Because it is not possible to mix unicode/non-unicode patterns in a single regexp, several of them can be generated. * - The same regexp can be used several times when the logic in the switch rejects the match. When this happens, the * matching-but-failing subpattern is excluded by replacing its name by "(*F)", which forces a failure-to-match. * To ease this backlisting operation, the name of subpatterns is also the string offset where the replacement should occur. diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/SwitchUserToken.php b/src/Symfony/Component/Security/Core/Authentication/Token/SwitchUserToken.php index e575999374893..ccccb5b51c04b 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Token/SwitchUserToken.php +++ b/src/Symfony/Component/Security/Core/Authentication/Token/SwitchUserToken.php @@ -59,7 +59,12 @@ public function __serialize(): array */ public function __unserialize(array $data): void { - [$this->originalToken, $this->originatedFromUri, $parentData] = $data; + if (3 > \count($data)) { + // Support for tokens serialized with version 5.1 or lower of symfony/security-core. + [$this->originalToken, $parentData] = $data; + } else { + [$this->originalToken, $this->originatedFromUri, $parentData] = $data; + } $parentData = \is_array($parentData) ? $parentData : unserialize($parentData); parent::__unserialize($parentData); } diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf index f35a2bb815878..1462e650e9c4b 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.ro.xlf @@ -62,6 +62,14 @@ Account is locked. Contul este blocat. + + Too many failed login attempts, please try again later. + Prea multe încercări de autentificare eșuate, vă rugăm să încercați mai târziu. + + + Invalid or expired login link. + Link de autentificare invalid sau expirat. + diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Fixtures/switch-user-token-4.4.txt b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Fixtures/switch-user-token-4.4.txt new file mode 100644 index 0000000000000..7b3f7c40920db Binary files /dev/null and b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Fixtures/switch-user-token-4.4.txt differ diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/SwitchUserTokenTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/SwitchUserTokenTest.php index 00f1ac984a868..8138f7659639b 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/SwitchUserTokenTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/SwitchUserTokenTest.php @@ -84,4 +84,18 @@ public function testSerializeNullImpersonateUrl() $this->assertNull($unserializedToken->getOriginatedFromUri()); } + + public function testUnserializeOldToken() + { + /** @var SwitchUserToken $token */ + $token = unserialize(file_get_contents(__DIR__.'/Fixtures/switch-user-token-4.4.txt')); + + self::assertInstanceOf(SwitchUserToken::class, $token); + self::assertInstanceOf(UsernamePasswordToken::class, $token->getOriginalToken()); + self::assertSame('john', $token->getUsername()); + self::assertSame(['foo' => 'bar'], $token->getCredentials()); + self::assertSame('main', $token->getFirewallName()); + self::assertEquals(['ROLE_USER'], $token->getRoleNames()); + self::assertNull($token->getOriginatedFromUri()); + } } diff --git a/src/Symfony/Component/Security/Core/Tests/Resources/TranslationFilesTest.php b/src/Symfony/Component/Security/Core/Tests/Resources/TranslationFilesTest.php index 2402b0199824f..4255e91d926b8 100644 --- a/src/Symfony/Component/Security/Core/Tests/Resources/TranslationFilesTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Resources/TranslationFilesTest.php @@ -29,6 +29,20 @@ public function testTranslationFileIsValid($filePath) $this->assertCount(0, $errors, sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); } + /** + * @dataProvider provideTranslationFiles + */ + public function testTranslationFileIsValidWithoutEntityLoader($filePath) + { + $document = new \DOMDocument(); + $document->loadXML(file_get_contents($filePath)); + libxml_disable_entity_loader(true); + + $errors = XliffUtils::validateSchema($document); + + $this->assertCount(0, $errors, sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); + } + public function provideTranslationFiles() { return array_map( diff --git a/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php b/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php index 6d51428c99e33..1d7cec6c6ecfa 100644 --- a/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php +++ b/src/Symfony/Component/Security/Guard/Authenticator/GuardBridgeAuthenticator.php @@ -29,6 +29,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; +use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; /** * This authenticator is used to bridge Guard authenticators with @@ -38,7 +39,7 @@ * * @internal */ -class GuardBridgeAuthenticator implements InteractiveAuthenticatorInterface +class GuardBridgeAuthenticator implements InteractiveAuthenticatorInterface, AuthenticationEntryPointInterface { private $guard; private $userProvider; @@ -49,6 +50,11 @@ public function __construct(GuardAuthenticatorInterface $guard, UserProviderInte $this->userProvider = $userProvider; } + public function start(Request $request, AuthenticationException $authException = null) + { + return $this->guard->start($request, $authException); + } + public function supports(Request $request): ?bool { return $this->guard->supports($request); diff --git a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php index 2b6328ea3507b..318fd7bd21193 100644 --- a/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php +++ b/src/Symfony/Component/Security/Http/Authentication/AuthenticatorManager.php @@ -22,7 +22,6 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; @@ -219,10 +218,6 @@ private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, $this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN); } - if ($passport instanceof AnonymousPassport) { - return $response; - } - $this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $passport, $authenticatedToken, $request, $response, $this->firewallName)); return $loginSuccessEvent->getResponse(); diff --git a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php index 24c7405ea91a8..aeaf1d17cd193 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/AbstractLoginFormAuthenticator.php @@ -32,6 +32,20 @@ abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator impl */ abstract protected function getLoginUrl(Request $request): string; + /** + * {@inheritdoc} + * + * Override to change the request conditions that have to be + * matched in order to handle the login form submit. + * + * This default implementation handles all POST requests to the + * login path (@see getLoginUrl()). + */ + public function supports(Request $request): bool + { + return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getPathInfo(); + } + /** * Override to change what happens after a bad username/password is submitted. */ diff --git a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php index 6e22839497151..246e4894b1abc 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php +++ b/src/Symfony/Component/Security/Http/Authenticator/FormLoginAuthenticator.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -49,6 +50,7 @@ class FormLoginAuthenticator extends AbstractLoginFormAuthenticator private $successHandler; private $failureHandler; private $options; + private $httpKernel; public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options) { @@ -146,4 +148,24 @@ private function getCredentials(Request $request): array return $credentials; } + + public function setHttpKernel(HttpKernelInterface $httpKernel): void + { + $this->httpKernel = $httpKernel; + } + + public function start(Request $request, AuthenticationException $authException = null): Response + { + if (!$this->options['use_forward']) { + return parent::start($request, $authException); + } + + $subRequest = $this->httpUtils->createRequest($request, $this->options['login_path']); + $response = $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + if (200 === $response->getStatusCode()) { + $response->setStatusCode(401); + } + + return $response; + } } diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php deleted file mode 100644 index 678745eea00a9..0000000000000 --- a/src/Symfony/Component/Security/Http/Authenticator/Passport/AnonymousPassport.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Http\Authenticator\Passport; - -/** - * A passport used during anonymous authentication. - * - * @author Wouter de Jong - * - * @internal - * @experimental in 5.2 - */ -class AnonymousPassport implements PassportInterface -{ - use PassportTrait; -} diff --git a/src/Symfony/Component/Security/Http/EventListener/CheckCredentialsListener.php b/src/Symfony/Component/Security/Http/EventListener/CheckCredentialsListener.php index e2d3f86b8f5b3..c1f649b089ce0 100644 --- a/src/Symfony/Component/Security/Http/EventListener/CheckCredentialsListener.php +++ b/src/Symfony/Component/Security/Http/EventListener/CheckCredentialsListener.php @@ -14,6 +14,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface; @@ -65,6 +66,10 @@ public function checkPassport(CheckPassportEvent $event): void $badge->markResolved(); + if (!$passport->hasBadge(PasswordUpgradeBadge::class)) { + $passport->addBadge(new PasswordUpgradeBadge($presentedPassword)); + } + return; } diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php index 5dc411bef092e..e903dcd22cbf6 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/CheckCredentialsListenerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; @@ -113,6 +114,54 @@ public function testNoCredentialsBadgeProvided() $this->listener->checkPassport($event); } + public function testAddsPasswordUpgradeBadge() + { + $encoder = $this->createMock(PasswordEncoderInterface::class); + $encoder->expects($this->any())->method('isPasswordValid')->with('encoded-password', 'ThePa$$word')->willReturn(true); + + $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->identicalTo($this->user))->willReturn($encoder); + + $passport = new Passport(new UserBadge('wouter', function () { return $this->user; }), new PasswordCredentials('ThePa$$word')); + $this->listener->checkPassport($this->createEvent($passport)); + + $this->assertTrue($passport->hasBadge(PasswordUpgradeBadge::class)); + $this->assertEquals('ThePa$$word', $passport->getBadge(PasswordUpgradeBadge::class)->getAndErasePlaintextPassword()); + } + + public function testAddsNoPasswordUpgradeBadgeIfItAlreadyExists() + { + $encoder = $this->createMock(PasswordEncoderInterface::class); + $encoder->expects($this->any())->method('isPasswordValid')->with('encoded-password', 'ThePa$$word')->willReturn(true); + + $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->identicalTo($this->user))->willReturn($encoder); + + $passport = $this->getMockBuilder(Passport::class) + ->setMethods(['addBadge']) + ->setConstructorArgs([new UserBadge('wouter', function () { return $this->user; }), new PasswordCredentials('ThePa$$word'), [new PasswordUpgradeBadge('ThePa$$word')]]) + ->getMock(); + + $passport->expects($this->never())->method('addBadge')->with($this->isInstanceOf(PasswordUpgradeBadge::class)); + + $this->listener->checkPassport($this->createEvent($passport)); + } + + public function testAddsNoPasswordUpgradeBadgeIfPasswordIsInvalid() + { + $encoder = $this->createMock(PasswordEncoderInterface::class); + $encoder->expects($this->any())->method('isPasswordValid')->with('encoded-password', 'ThePa$$word')->willReturn(false); + + $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->identicalTo($this->user))->willReturn($encoder); + + $passport = $this->getMockBuilder(Passport::class) + ->setMethods(['addBadge']) + ->setConstructorArgs([new UserBadge('wouter', function () { return $this->user; }), new PasswordCredentials('ThePa$$word'), [new PasswordUpgradeBadge('ThePa$$word')]]) + ->getMock(); + + $passport->expects($this->never())->method('addBadge')->with($this->isInstanceOf(PasswordUpgradeBadge::class)); + + $this->listener->checkPassport($this->createEvent($passport)); + } + private function createEvent($passport) { return new CheckPassportEvent($this->createMock(AuthenticatorInterface::class), $passport); diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php index b43aebde96aab..95f99de8d0fde 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php @@ -16,9 +16,7 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; -use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; use Symfony\Component\Security\Http\Event\CheckPassportEvent; use Symfony\Component\Security\Http\EventListener\UserProviderListener; @@ -61,7 +59,6 @@ public function testNotOverrideUserLoader($passport) public function provideCompletePassports() { - yield [new AnonymousPassport()]; yield [new SelfValidatingPassport(new UserBadge('wouter', function () {}))]; } diff --git a/src/Symfony/Component/Semaphore/composer.json b/src/Symfony/Component/Semaphore/composer.json index 5d38d07acaa9b..e540ee0807af2 100644 --- a/src/Symfony/Component/Semaphore/composer.json +++ b/src/Symfony/Component/Semaphore/composer.json @@ -20,7 +20,7 @@ } ], "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "psr/log": "~1.0" }, "require-dev": { diff --git a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php index 33892277389e5..854400b538e0d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php @@ -35,6 +35,8 @@ class ArrayDenormalizer implements ContextAwareDenormalizerInterface, Serializer * {@inheritdoc} * * @throws NotNormalizableValueException + * + * @return array */ public function denormalize($data, string $type, string $format = null, array $context = []) { diff --git a/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php index 4fcee91bda1c3..6be2d44c31a10 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ConstraintViolationListNormalizer.php @@ -41,6 +41,8 @@ public function __construct($defaultContext = [], NameConverterInterface $nameCo /** * {@inheritdoc} + * + * @return array */ public function normalize($object, string $format = null, array $context = []) { diff --git a/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php index 979288cf6ace3..a2f57b57e4a61 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DataUriNormalizer.php @@ -47,6 +47,8 @@ public function __construct(MimeTypeGuesserInterface $mimeTypeGuesser = null) /** * {@inheritdoc} + * + * @return string */ public function normalize($object, string $format = null, array $context = []) { @@ -88,6 +90,8 @@ public function supportsNormalization($data, string $format = null) * * @throws InvalidArgumentException * @throws NotNormalizableValueException + * + * @return \SplFileInfo */ public function denormalize($data, string $type, string $format = null, array $context = []) { diff --git a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php index de028b34341f9..8bfb4c1675428 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateIntervalNormalizer.php @@ -37,6 +37,8 @@ public function __construct(array $defaultContext = []) * {@inheritdoc} * * @throws InvalidArgumentException + * + * @return string */ public function normalize($object, string $format = null, array $context = []) { @@ -68,6 +70,8 @@ public function hasCacheableSupportsMethod(): bool * * @throws InvalidArgumentException * @throws UnexpectedValueException + * + * @return \DateInterval */ public function denormalize($data, string $type, string $format = null, array $context = []) { diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index c12e23470a445..05aad160e3b2c 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -45,6 +45,8 @@ public function __construct(array $defaultContext = []) * {@inheritdoc} * * @throws InvalidArgumentException + * + * @return string */ public function normalize($object, string $format = null, array $context = []) { @@ -75,6 +77,8 @@ public function supportsNormalization($data, string $format = null) * {@inheritdoc} * * @throws NotNormalizableValueException + * + * @return \DateTimeInterface */ public function denormalize($data, string $type, string $format = null, array $context = []) { diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php index 2ff3f18200fb5..af262ebaad70e 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeZoneNormalizer.php @@ -25,6 +25,8 @@ class DateTimeZoneNormalizer implements NormalizerInterface, DenormalizerInterfa * {@inheritdoc} * * @throws InvalidArgumentException + * + * @return string */ public function normalize($object, string $format = null, array $context = []) { @@ -47,6 +49,8 @@ public function supportsNormalization($data, string $format = null) * {@inheritdoc} * * @throws NotNormalizableValueException + * + * @return \DateTimeZone */ public function denormalize($data, string $type, string $format = null, array $context = []) { diff --git a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php index 51821de2a06d8..a39181fcf37c7 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php @@ -37,6 +37,8 @@ public function __construct(bool $debug = false, array $defaultContext = []) /** * {@inheritdoc} + * + * @return array */ public function normalize($exception, string $format = null, array $context = []) { diff --git a/src/Symfony/Component/Serializer/Serializer.php b/src/Symfony/Component/Serializer/Serializer.php index b8c33a2fe56c5..6414caf900472 100644 --- a/src/Symfony/Component/Serializer/Serializer.php +++ b/src/Symfony/Component/Serializer/Serializer.php @@ -189,7 +189,10 @@ public function normalize($data, string $format = null, array $context = []) */ public function denormalize($data, string $type, string $format = null, array $context = []) { - if (isset(self::SCALAR_TYPES[$type])) { + $normalizer = $this->getDenormalizer($data, $type, $format, $context); + + // Check for a denormalizer first, e.g. the data is wrapped + if (!$normalizer && isset(self::SCALAR_TYPES[$type])) { if (!('is_'.$type)($data)) { throw new NotNormalizableValueException(sprintf('Data expected to be of type "%s" ("%s" given).', $type, get_debug_type($data))); } @@ -201,7 +204,7 @@ public function denormalize($data, string $type, string $format = null, array $c throw new LogicException('You must register at least one normalizer to be able to denormalize objects.'); } - if ($normalizer = $this->getDenormalizer($data, $type, $format, $context)) { + if ($normalizer) { return $normalizer->denormalize($data, $type, $format, $context); } diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index 36cd098af8720..3f27877840143 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -613,6 +613,13 @@ public function testDeserializeInconsistentScalarArray() $serializer->deserialize('["42"]', 'int[]', 'json'); } + public function testDeserializeWrappedScalar() + { + $serializer = new Serializer([new UnwrappingDenormalizer()], ['json' => new JsonEncoder()]); + + $this->assertSame(42, $serializer->deserialize('{"wrapper": 42}', 'int', 'json', [UnwrappingDenormalizer::UNWRAP_PATH => '[wrapper]'])); + } + private function serializerWithClassDiscriminator() { $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); diff --git a/src/Symfony/Component/Translation/Util/XliffUtils.php b/src/Symfony/Component/Translation/Util/XliffUtils.php index a8c05c2244d47..e4373a7d5ba1b 100644 --- a/src/Symfony/Component/Translation/Util/XliffUtils.php +++ b/src/Symfony/Component/Translation/Util/XliffUtils.php @@ -61,21 +61,18 @@ public static function validateSchema(\DOMDocument $dom): array { $xliffVersion = static::getVersionNumber($dom); $internalErrors = libxml_use_internal_errors(true); - if (\LIBXML_VERSION < 20900) { + if ($shouldEnable = self::shouldEnableEntityLoader()) { $disableEntities = libxml_disable_entity_loader(false); } - - $isValid = @$dom->schemaValidateSource(self::getSchema($xliffVersion)); - if (!$isValid) { - if (\LIBXML_VERSION < 20900) { + try { + $isValid = @$dom->schemaValidateSource(self::getSchema($xliffVersion)); + if (!$isValid) { + return self::getXmlErrors($internalErrors); + } + } finally { + if ($shouldEnable) { libxml_disable_entity_loader($disableEntities); } - - return self::getXmlErrors($internalErrors); - } - - if (\LIBXML_VERSION < 20900) { - libxml_disable_entity_loader($disableEntities); } $dom->normalizeDocument(); @@ -86,6 +83,36 @@ public static function validateSchema(\DOMDocument $dom): array return []; } + private static function shouldEnableEntityLoader(): bool + { + // Version prior to 8.0 can be enabled without deprecation + if (\PHP_VERSION_ID < 80000) { + return true; + } + + static $dom, $schema; + if (null === $dom) { + $dom = new \DOMDocument(); + $dom->loadXML(''); + + $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); + register_shutdown_function(static function () use ($tmpfile) { + @unlink($tmpfile); + }); + $schema = ' + + +'; + file_put_contents($tmpfile, ' + + + +'); + } + + return !@$dom->schemaValidateSource($schema); + } + public static function getErrorsAsString(array $xmlErrors): string { $errorsAsString = ''; diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf index e5cf660686358..0244ee4f398ba 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf @@ -128,7 +128,7 @@ This value should be a valid number. - Este valor deveria de ser um número válido. + Este valor deveria ser um número válido. This file is not a valid image. @@ -176,27 +176,27 @@ This value should be the user's current password. - Este valor deveria de ser a password atual do utilizador. + Este valor deveria ser a senha atual do usuário. This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters. - Este valor tem de ter exatamente {{ limit }} carateres. + Este valor deve possuir exatamente {{ limit }} caracteres. The file was only partially uploaded. - Só foi enviado parte do ficheiro. + Só foi enviada uma parte do arquivo. No file was uploaded. - Nenhum ficheiro foi enviado. + Nenhum arquivo foi enviado. No temporary folder was configured in php.ini. - Não existe nenhum directório temporária configurado no ficheiro php.ini. + Não existe uma pasta temporária configurada no arquivo php.ini. Cannot write temporary file to disk. - Não foi possível escrever ficheiros temporários no disco. + Não foi possível escrever os arquivos temporários no disco. A PHP extension caused the upload to fail. @@ -292,15 +292,15 @@ The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed. - A imagem está orientada à paisagem ({{ width }}x{{ height }}px). Imagens orientadas à paisagem não são permitidas. + A imagem está em orientação de paisagem ({{ width }}x{{ height }}px). Imagens orientadas em paisagem não são permitidas. The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed. - A imagem está orientada ao retrato ({{ width }}x{{ height }}px). Imagens orientadas ao retrato não são permitidas. + A imagem está em orientação de retrato ({{ width }}x{{ height }}px). Imagens orientadas em retrato não são permitidas. An empty file is not allowed. - Ficheiro vazio não é permitido. + Um arquivo vazio não é permitido. The host could not be resolved. diff --git a/src/Symfony/Component/Validator/Tests/Resources/TranslationFilesTest.php b/src/Symfony/Component/Validator/Tests/Resources/TranslationFilesTest.php index 894ae55f10567..6e0620b517563 100644 --- a/src/Symfony/Component/Validator/Tests/Resources/TranslationFilesTest.php +++ b/src/Symfony/Component/Validator/Tests/Resources/TranslationFilesTest.php @@ -29,6 +29,20 @@ public function testTranslationFileIsValid($filePath) $this->assertCount(0, $errors, sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); } + /** + * @dataProvider provideTranslationFiles + */ + public function testTranslationFileIsValidWithoutEntityLoader($filePath) + { + $document = new \DOMDocument(); + $document->loadXML(file_get_contents($filePath)); + libxml_disable_entity_loader(true); + + $errors = XliffUtils::validateSchema($document); + + $this->assertCount(0, $errors, sprintf('"%s" is invalid:%s', $filePath, \PHP_EOL.implode(\PHP_EOL, array_column($errors, 'message')))); + } + public function provideTranslationFiles() { return array_map( diff --git a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php index ee9abd346c3ff..739e069934550 100644 --- a/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php +++ b/src/Symfony/Component/VarDumper/Cloner/AbstractCloner.php @@ -152,7 +152,10 @@ abstract class AbstractCloner implements ClonerInterface ':dba' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'], ':dba persistent' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castDba'], + + 'GdImage' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castGd'], ':gd' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castGd'], + ':mysql link' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castMysqlLink'], ':pgsql large object' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castLargeObject'], ':pgsql link' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castLink'], @@ -160,9 +163,14 @@ abstract class AbstractCloner implements ClonerInterface ':pgsql result' => ['Symfony\Component\VarDumper\Caster\PgSqlCaster', 'castResult'], ':process' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castProcess'], ':stream' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStream'], + + 'OpenSSLCertificate' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castOpensslX509'], ':OpenSSL X.509' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castOpensslX509'], + ':persistent stream' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStream'], ':stream-context' => ['Symfony\Component\VarDumper\Caster\ResourceCaster', 'castStreamContext'], + + 'XmlParser' => ['Symfony\Component\VarDumper\Caster\XmlResourceCaster', 'castXml'], ':xml' => ['Symfony\Component\VarDumper\Caster\XmlResourceCaster', 'castXml'], 'RdKafka' => ['Symfony\Component\VarDumper\Caster\RdKafkaCaster', 'castRdKafka'], diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index f98fcd69795c8..f0ce2aaef047b 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -358,7 +358,7 @@ private function doParse(string $value, int $flags) } try { - return Inline::parse($this->parseQuotedString($this->currentLine), $flags, $this->refs); + return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); @@ -371,7 +371,7 @@ private function doParse(string $value, int $flags) } try { - $parsedMapping = Inline::parse($this->lexInlineMapping($this->currentLine), $flags, $this->refs); + $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs); while ($this->moveToNextLine()) { if (!$this->isCurrentLineEmpty()) { @@ -392,7 +392,7 @@ private function doParse(string $value, int $flags) } try { - $parsedSequence = Inline::parse($this->lexInlineSequence($this->currentLine), $flags, $this->refs); + $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs); while ($this->moveToNextLine()) { if (!$this->isCurrentLineEmpty()) { @@ -667,6 +667,11 @@ private function getNextEmbedBlock(int $indentation = null, bool $inSequence = f return implode("\n", $data); } + private function hasMoreLines(): bool + { + return (\count($this->lines) - 1) > $this->currentLineNb; + } + /** * Moves the parser to the next line. */ @@ -744,59 +749,63 @@ private function parseValue(string $value, int $flags, string $context) try { if ('' !== $value && '{' === $value[0]) { - return Inline::parse($this->lexInlineMapping($value), $flags, $this->refs); + $cursor = \strlen($this->currentLine) - \strlen($value); + + return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs); } elseif ('' !== $value && '[' === $value[0]) { - return Inline::parse($this->lexInlineSequence($value), $flags, $this->refs); + $cursor = \strlen($this->currentLine) - \strlen($value); + + return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs); } - $quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null; + switch ($value[0] ?? '') { + case '"': + case "'": + $cursor = \strlen($this->currentLine) - \strlen($value); + $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs); - // do not take following lines into account when the current line is a quoted single line value - if (null !== $quotation && self::preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) { - return Inline::parse($value, $flags, $this->refs); - } + if (isset($this->currentLine[$cursor]) && preg_replace('/\s*#.*$/A', '', substr($this->currentLine, $cursor))) { + throw new ParseException(sprintf('Unexpected characters near "%s".', substr($this->currentLine, $cursor))); + } - $lines = []; + return $parsedValue; + default: + $lines = []; - while ($this->moveToNextLine()) { - // unquoted strings end before the first unindented line - if (null === $quotation && 0 === $this->getCurrentLineIndentation()) { - $this->moveToPreviousLine(); + while ($this->moveToNextLine()) { + // unquoted strings end before the first unindented line + if (0 === $this->getCurrentLineIndentation()) { + $this->moveToPreviousLine(); - break; - } + break; + } - $lines[] = trim($this->currentLine); + $lines[] = trim($this->currentLine); + } - // quoted string values end with a line that is terminated with the quotation character - $escapedLine = str_replace(['\\\\', '\\"'], '', $this->currentLine); - if ('' !== $escapedLine && $escapedLine[-1] === $quotation) { - break; - } - } + for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) { + if ('' === $lines[$i]) { + $value .= "\n"; + $previousLineBlank = true; + } elseif ($previousLineBlank) { + $value .= $lines[$i]; + $previousLineBlank = false; + } else { + $value .= ' '.$lines[$i]; + $previousLineBlank = false; + } + } - for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) { - if ('' === $lines[$i]) { - $value .= "\n"; - $previousLineBlank = true; - } elseif ($previousLineBlank) { - $value .= $lines[$i]; - $previousLineBlank = false; - } else { - $value .= ' '.$lines[$i]; - $previousLineBlank = false; - } - } + Inline::$parsedLineNumber = $this->getRealCurrentLineNb(); - Inline::$parsedLineNumber = $this->getRealCurrentLineNb(); + $parsedValue = Inline::parse($value, $flags, $this->refs); - $parsedValue = Inline::parse($value, $flags, $this->refs); + if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) { + throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename); + } - if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) { - throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename); + return $parsedValue; } - - return $parsedValue; } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); @@ -1145,107 +1154,153 @@ private function getLineTag(string $value, int $flags, bool $nextLineCheck = tru throw new ParseException(sprintf('Tags support is not enabled. You must use the flag "Yaml::PARSE_CUSTOM_TAGS" to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename); } - private function parseQuotedString(string $yaml): ?string + private function lexInlineQuotedString(int &$cursor = 0): string { - if ('' === $yaml || ('"' !== $yaml[0] && "'" !== $yaml[0])) { - throw new \InvalidArgumentException(sprintf('"%s" is not a quoted string.', $yaml)); - } - - $lines = [$yaml]; + $quotation = $this->currentLine[$cursor]; + $value = $quotation; + ++$cursor; - while ($this->moveToNextLine()) { - $lines[] = $this->currentLine; + $previousLineWasNewline = true; + $previousLineWasTerminatedWithBackslash = false; + $lineNumber = 0; - if (!$this->isCurrentLineEmpty() && $yaml[0] === $this->currentLine[-1]) { - break; + do { + if (++$lineNumber > 1) { + $cursor += strspn($this->currentLine, ' ', $cursor); } - } - - $value = ''; - for ($i = 0, $linesCount = \count($lines), $previousLineWasNewline = false, $previousLineWasTerminatedWithBackslash = false; $i < $linesCount; ++$i) { - $trimmedLine = trim($lines[$i]); - if ('' === $trimmedLine) { + if ($this->isCurrentLineBlank()) { $value .= "\n"; } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) { $value .= ' '; } - if ('' !== $trimmedLine && '\\' === $lines[$i][-1]) { - $value .= ltrim(substr($lines[$i], 0, -1)); - } elseif ('' !== $trimmedLine) { - $value .= $trimmedLine; + for (; \strlen($this->currentLine) > $cursor; ++$cursor) { + switch ($this->currentLine[$cursor]) { + case '\\': + if (isset($this->currentLine[++$cursor])) { + $value .= '\\'.$this->currentLine[$cursor]; + } + + break; + case $quotation: + ++$cursor; + + if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) { + $value .= "''"; + break; + } + + return $value.$quotation; + default: + $value .= $this->currentLine[$cursor]; + } } - if ('' === $trimmedLine) { + if ($this->isCurrentLineBlank()) { $previousLineWasNewline = true; $previousLineWasTerminatedWithBackslash = false; - } elseif ('\\' === $lines[$i][-1]) { + } elseif ('\\' === $this->currentLine[-1]) { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = true; } else { $previousLineWasNewline = false; $previousLineWasTerminatedWithBackslash = false; } - } - return $value; + if ($this->hasMoreLines()) { + $cursor = 0; + } + } while ($this->moveToNextLine()); + + throw new ParseException('Malformed inline YAML string'); } - private function lexInlineMapping(string $yaml): string + private function lexUnquotedString(int &$cursor): string { - if ('' === $yaml || '{' !== $yaml[0]) { - throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml)); - } - - for ($i = 1; isset($yaml[$i]) && '}' !== $yaml[$i]; ++$i) { - } - - if (isset($yaml[$i]) && '}' === $yaml[$i]) { - return $yaml; - } + $offset = $cursor; + $cursor += strcspn($this->currentLine, '[]{},: ', $cursor); - $lines = [$yaml]; - - while ($this->moveToNextLine()) { - $lines[] = $this->currentLine; - } + return substr($this->currentLine, $offset, $cursor - $offset); + } - return implode("\n", $lines); + private function lexInlineMapping(int &$cursor = 0): string + { + return $this->lexInlineStructure($cursor, '}'); } - private function lexInlineSequence(string $yaml): string + private function lexInlineSequence(int &$cursor = 0): string { - if ('' === $yaml || '[' !== $yaml[0]) { - throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml)); - } + return $this->lexInlineStructure($cursor, ']'); + } - for ($i = 1; isset($yaml[$i]) && ']' !== $yaml[$i]; ++$i) { - } + private function lexInlineStructure(int &$cursor, string $closingTag): string + { + $value = $this->currentLine[$cursor]; + ++$cursor; - if (isset($yaml[$i]) && ']' === $yaml[$i]) { - return $yaml; - } + do { + $this->consumeWhitespaces($cursor); + + while (isset($this->currentLine[$cursor])) { + switch ($this->currentLine[$cursor]) { + case '"': + case "'": + $value .= $this->lexInlineQuotedString($cursor); + break; + case ':': + case ',': + $value .= $this->currentLine[$cursor]; + ++$cursor; + break; + case '{': + $value .= $this->lexInlineMapping($cursor); + break; + case '[': + $value .= $this->lexInlineSequence($cursor); + break; + case $closingTag: + $value .= $this->currentLine[$cursor]; + ++$cursor; + + return $value; + case '#': + break 2; + default: + $value .= $this->lexUnquotedString($cursor); + } - $value = $yaml; + if ($this->consumeWhitespaces($cursor)) { + $value .= ' '; + } + } - while ($this->moveToNextLine()) { - for ($i = 1; isset($this->currentLine[$i]) && ']' !== $this->currentLine[$i]; ++$i) { + if ($this->hasMoreLines()) { + $cursor = 0; } + } while ($this->moveToNextLine()); + + throw new ParseException('Malformed inline YAML string'); + } - $trimmedValue = trim($this->currentLine); + private function consumeWhitespaces(int &$cursor): bool + { + $whitespacesConsumed = 0; - if ('' !== $trimmedValue && '#' === $trimmedValue[0]) { - continue; - } + do { + $whitespaceOnlyTokenLength = strspn($this->currentLine, ' ', $cursor); + $whitespacesConsumed += $whitespaceOnlyTokenLength; + $cursor += $whitespaceOnlyTokenLength; - $value .= $trimmedValue; + if (isset($this->currentLine[$cursor])) { + return 0 < $whitespacesConsumed; + } - if (isset($this->currentLine[$i]) && ']' === $this->currentLine[$i]) { - break; + if ($this->hasMoreLines()) { + $cursor = 0; } - } + } while ($this->moveToNextLine()); - return $value; + return 0 < $whitespacesConsumed; } } diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php index 6e8b997313251..c31646d512b31 100644 --- a/src/Symfony/Component/Yaml/Tests/ParserTest.php +++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php @@ -1569,6 +1569,54 @@ public function testParseMultiLineUnquotedString() $this->assertSame(['foo' => 'bar baz foobar foo', 'bar' => 'baz'], $this->parser->parse($yaml)); } + /** + * @dataProvider escapedQuotationCharactersInQuotedStrings + */ + public function testParseQuotedStringContainingEscapedQuotationCharacters(string $yaml, array $expected) + { + $this->assertSame($expected, $this->parser->parse($yaml)); + } + + public function escapedQuotationCharactersInQuotedStrings() + { + return [ + 'single quoted string' => [ + << [ + [ + 'message' => 'No emails received before timeout - Address: \'test@testemail.company.com\' Keyword: \'Your Order confirmation\' ttl: 50', + 'outcome' => 'failed', + ], + ], + ], + ], + 'double quoted string' => [ + << [ + [ + 'message' => 'No emails received before timeout - Address: "test@testemail.company.com" Keyword: "Your Order confirmation" ttl: 50', + 'outcome' => 'failed', + ], + ], + ], + ], + ]; + } + public function testParseMultiLineString() { $this->assertEquals("foo bar\nbaz", $this->parser->parse("foo\nbar\n\nbaz")); @@ -1659,6 +1707,16 @@ public function inlineNotationSpanningMultipleLinesProvider(): array 'foo': 'bar', 'bar': 'baz' } +YAML + , + ], + 'mapping with unquoted strings and values' => [ + ['foo' => 'bar', 'bar' => 'baz'], + << [ + ['foo', 'bar'], + << [ + [ + 'foo' => [ + 'bar' => 'foobar', + ], + ], + << [ + [ + 'foo', + [ + 'bar', + 'baz', + ], + ], + << [ + [ + ['entry1', []], + ['entry2'], + ], + << [ ['foo' => ['bar', 'foobar'], 'bar' => ['baz']], << [ + [ + 'foobar' => [ + 'foo', + 'bar', + ], + 'bar' => 'baz', + ], + << [ [ 'foo' => [ @@ -1823,6 +1944,110 @@ public function inlineNotationSpanningMultipleLinesProvider(): array foo: 'bar baz' +YAML + ], + 'mixed mapping with inline notation having separated lines' => [ + [ + 'map' => [ + 'key' => 'value', + 'a' => 'b', + ], + 'param' => 'some', + ], + << [ + [ + 'map' => [ + 'key' => 'value', + 'a' => 'b', + ], + 'param' => 'some', + ], + << [ + [ + 'map' => [ + 'key' => 'value', + 'a' => 'b', + ], + 'param' => 'some', + ], + << [ + [ + [']'], + ['}'], + ['ba[r'], + ['[ba]r'], + ['bar]'], + ['foo' => 'bar{'], + ['foo' => 'b{ar}'], + ['foo' => 'bar}'], + ], + << [ + [ + ['te"st'], + ['test'], + ["te'st"], + ['te"st]'], + ['te"st'], + ['test'], + ["te'st"], + ['te"st]'], + ], + <<assertEquals(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS)); } + public function testInvalidInlineSequenceContainingStringWithEscapedQuotationCharacter() + { + $this->expectException(ParseException::class); + + $this->parser->parse('["\\"]'); + } + /** * @dataProvider taggedValuesProvider */