From 2ee1df31cdcac0aeb879b8e3c8feb9624ade5134 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 22 May 2014 16:26:18 +0200 Subject: [PATCH 1/5] updated CHANGELOG for 2.3.14 --- CHANGELOG-2.3.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG-2.3.md b/CHANGELOG-2.3.md index c8b2938fd2..d1ee162f3e 100644 --- a/CHANGELOG-2.3.md +++ b/CHANGELOG-2.3.md @@ -7,6 +7,36 @@ in 2.3 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/v2.3.0...v2.3.1 +* 2.3.14 (2014-05-22) + + * bug #10849 [WIP][Finder] Fix wrong implementation on sortable callback comparator (ProPheT777) + * bug #10929 [Process] Add validation on Process input (romainneutron) + * bug #10958 [DomCrawler] Fixed filterXPath() chaining loosing the parent DOM nodes (stof, robbertkl) + * bug #10953 [HttpKernel] fixed file uploads in functional tests without file selected (realmfoo) + * bug #10937 [HttpKernel] Fix "absolute path" when we look to the cache directory (BenoitLeveque) + * bug #10908 [HttpFoundation] implement session locking for PDO (Tobion) + * bug #10894 [HttpKernel] removed absolute paths from the generated container (fabpot) + * bug #10926 [DomCrawler] Fixed the initial state for options without value attribute (stof) + * bug #10925 [DomCrawler] Fixed the handling of boolean attributes in ChoiceFormField (stof) + * bug #10777 [Form] Automatically add step attribute to HTML5 time widgets to display seconds if needed (tucksaun) + * bug #10909 [PropertyAccess] Fixed plurals for -ves words (csarrazi) + * bug #10899 Explicitly define the encoding. (jakzal) + * bug #10897 [Console] Fix a console test (jakzal) + * bug #10896 [HttpKernel] Fixed cache behavior when TTL has expired and a default "global" TTL is defined (alquerci, fabpot) + * bug #10841 [DomCrawler] Fixed image input case sensitive (geoffrey-brier) + * bug #10714 [Console]Improve formatter for double-width character (denkiryokuhatsuden) + * bug #10872 [Form] Fixed TrimListenerTest as of PHP 5.5 (webmozart) + * bug #10762 [BrowserKit] Allow URLs that don't contain a path when creating a cookie from a string (thewilkybarkid) + * bug #10863 [Security] Add check for supported attributes in AclVoter (artursvonda) + * bug #10833 [TwigBridge][Transchoice] set %count% from the current context. (aitboudad) + * bug #10820 [WebProfilerBundle] Fixed profiler seach/homepage with empty token (tucksaun) + * bug #10815 Fixed issue #5427 (umpirsky) + * bug #10817 [Debug] fix #10313: FlattenException not found (nicolas-grekas) + * bug #10803 [Debug] fix ErrorHandlerTest when context is not an array (nicolas-grekas) + * bug #10801 [Debug] ErrorHandler: remove $GLOBALS from context in PHP5.3 fix #10292 (nicolas-grekas) + * bug #10797 [HttpFoundation] Allow File instance to be passed to BinaryFileResponse (anlutro) + * bug #10643 [TwigBridge] Removed strict check when found variables inside a translation (goetas) + * 2.3.13 (2014-04-27) * bug #10789 [Console] Fixed the rendering of exceptions on HHVM with a terminal width (stof) From 5a66baf22c5bb98694fffed7b2e380d94f7d6c0f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 22 May 2014 16:26:40 +0200 Subject: [PATCH 2/5] update CONTRIBUTORS for 2.3.14 --- CONTRIBUTORS.md | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d67d7509e1..63dd89eab6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -37,12 +37,12 @@ Symfony2 is the result of the work of many people who made the code better - Francis Besset (francisbesset) - Miha Vrhovnik - Henrik Bjørnskov (henrikbjorn) + - Saša Stamenković (umpirsky) - Konstantin Kudryashov (everzet) - Bilal Amarni (bamarni) - Florin Patan (florinpatan) - - Saša Stamenković (umpirsky) - - Eric Clemmons (ericclemmons) - Wouter De Jong (wouterj) + - Eric Clemmons (ericclemmons) - Deni - Henrik Westphal (snc) - Dariusz Górecki (canni) @@ -61,6 +61,7 @@ Symfony2 is the result of the work of many people who made the code better - Antoine Hérault (herzult) - Toni Uebernickel (havvg) - Arnaud Le Blanc (arnaud-lb) + - Nicolas Grekas (nicolas-grekas) - Brice BERNARD (brikou) - Luis Cordova (cordoval) - Tim Nagel (merk) @@ -80,11 +81,11 @@ Symfony2 is the result of the work of many people who made the code better - Juti Noppornpitak (shiroyuki) - Sebastian Hörl (blogsh) - Hidenori Goto (hidenorigoto) + - Ait Boudad Abdellatif (aitboudad) - Daniel Gomes (danielcsgomes) - Peter Kokot (maastermedia) - Jérémie Augustin (jaugustin) - David Buchmann (dbu) - - Ait Boudad Abdellatif (aitboudad) - Jérôme Tamarelle (gromnan) - Tigran Azatyan (tigranazatyan) - Javier Eguiluz (javier.eguiluz) @@ -99,7 +100,6 @@ Symfony2 is the result of the work of many people who made the code better - Jonathan Ingram (jonathaningram) - Artur Kotyrba - Guilherme Blanco (guilhermeblanco) - - Nicolas Grekas (nicolas-grekas) - Pablo Godel (pgodel) - Eric GELOEN (gelo) - Dmitrii Chekaliuk (lazyhammer) @@ -114,6 +114,7 @@ Symfony2 is the result of the work of many people who made the code better - Benjamin Dulau (dbenjamin) - Andreas Hucks (meandmymonkey) - Noel Guilbert (noel) + - Charles Sarrazin (csarrazi) - bronze1man - Larry Garfield (crell) - Martin Schuhfuß (usefulthink) @@ -132,7 +133,9 @@ Symfony2 is the result of the work of many people who made the code better - Andréia Bohner (andreia) - Joel Wurtz (brouznouf) - Rui Marinho (ruimarinho) + - sun (sun) - Julien Brochet (mewt) + - Tugdual Saunier (tucksaun) - Sergey Linnik (linniksa) - Marcel Beerta (mazen) - Francois Zaninotto @@ -152,7 +155,6 @@ Symfony2 is the result of the work of many people who made the code better - Félix Labrecque (woodspire) - Christian Flothmann (xabbuh) - GordonsLondon - - sun (sun) - Jan Sorgalla (jsor) - Ray - Chekote @@ -175,6 +177,7 @@ Symfony2 is the result of the work of many people who made the code better - Lars Strojny (lstrojny) - Bertrand Zuchuat (garfield-fr) - Gabor Toth (tgabi333) + - realmfoo - Thomas Tourlourat (armetiz) - Andrey Esaulov (andremaha) - Grégoire Passault (gregwar) @@ -193,6 +196,7 @@ Symfony2 is the result of the work of many people who made the code better - Ricard Clau (ricardclau) - Erin Millard - Matthew Lewinski (lewinski) + - alquerci - Francesco Levorato - Vitaliy Zakharov (zakharovvi) - Gyula Sallai (salla) @@ -205,6 +209,7 @@ Symfony2 is the result of the work of many people who made the code better - Sébastien Lavoie (lavoiesl) - Terje Bråten - Kristen Gilden (kgilden) + - Robbert Klarenbeek (robbertkl) - hossein zolfi (ocean) - Eduardo Gulias (egulias) - giulio de donato (liuggio) @@ -217,7 +222,6 @@ Symfony2 is the result of the work of many people who made the code better - Costin Bereveanu (schniper) - Loïc Chardonnet (gnusat) - Marek Kalnik (marekkalnik) - - realmfoo - Tamas Szijarto - Pavel Volokitin (pvolok) - Tobias Naumann (tna) @@ -252,7 +256,6 @@ Symfony2 is the result of the work of many people who made the code better - Olivier Dolbeau (odolbeau) - Roumen Damianoff (roumen) - Tobias Sjösten (tobiassjosten) - - alquerci - vagrant - Asier Illarramendi (doup) - Kévin Dunglas (dunglas) @@ -260,12 +263,10 @@ Symfony2 is the result of the work of many people who made the code better - Vitaliy Tverdokhlib (vitaliytv) - Dirk Pahl (dirkaholic) - cedric lombardot (cedriclombardot) - - Charles Sarrazin (csarrazi) - Jonas Flodén (flojon) - Marcin Sikoń (marphi) - franek (franek) - Adam Harvey - - Robbert Klarenbeek (robbertkl) - François-Xavier de Guillebon (de-gui_f) - boombatower - Fabrice Bernhard (fabriceb) @@ -294,6 +295,7 @@ Symfony2 is the result of the work of many people who made the code better - Erik Trapman (eriktrapman) - De Cock Xavier (xdecock) - Alex Pott + - Issei Murasawa (issei_m) - Norbert Orzechowicz (norzechowicz) - Matthijs van den Bos (matthijs) - Nils Adermann (naderman) @@ -312,6 +314,7 @@ Symfony2 is the result of the work of many people who made the code better - Ned Schwartz - Ziumin - Lenar Lõhmus + - julien pauli (jpauli) - Zach Badgett (zachbadgett) - Aurélien Fredouelle - Karoly Negyesi (chx) @@ -343,6 +346,7 @@ Symfony2 is the result of the work of many people who made the code better - Kamil Kokot (pamil) - Florian Lonqueu-Brochard (florianlb) - Rostyslav Kinash + - Daisuke Ohata - Vincent Simonin - Christian Schmidt - Stefan Warman @@ -392,10 +396,12 @@ Symfony2 is the result of the work of many people who made the code better - Josiah (josiah) - John Bohn (jbohn) - Jakub Škvára (jskvara) + - Chris Wilkinson (thewilkybarkid) - Andrew Hilobok (hilobok) - Christian Soronellas (theunic) - Jérôme Vieilledent (lolautruche) - Degory Valentine + - Benoit Lévêque (benoit_leveque) - hacfi (hifi) - Krzysiek Łabuś - Xavier Lacot (xavier) @@ -405,7 +411,6 @@ Symfony2 is the result of the work of many people who made the code better - Jayson Xu (superjavason) - Jaik Dean (jaikdean) - Jan Prieser - - Issei Murasawa (issei_m) - James Michael DuPont - Tom Klingenberg - Christopher Hall (mythmakr) @@ -419,8 +424,8 @@ Symfony2 is the result of the work of many people who made the code better - Abhoryo - Fabian Vogler (fabian) - Maksim Kotlyar (makasim) - - Tugdual Saunier (tucksaun) - Neil Ferreira + - Dmitry Parnas (parnas) - Tony Malzhacker - Cyril Quintin (cyqui) - Gerard van Helden (drm) @@ -429,7 +434,6 @@ Symfony2 is the result of the work of many people who made the code better - Aleksey Podskrebyshev - David Marín Carreño (davefx) - Jörn Lang (j.lang) - - julien pauli (jpauli) - mwsaz - Benoît Bourgeois - corphi @@ -537,6 +541,7 @@ Symfony2 is the result of the work of many people who made the code better - Vincent AUBERT (vincent) - Benoit Garret - DerManoMann + - Asmir Mustafic (goetas) - Marcin Chwedziak - Roland Franssen (ro0) - Maciej Malarz @@ -581,6 +586,7 @@ Symfony2 is the result of the work of many people who made the code better - Jeroen van den Enden (stoefke) - Quique Porta (quiqueporta) - Tomasz Szymczyk (karion) + - Arturs Vonda - ConneXNL - Aharon Perkel - Abdul.Mohsen B. A. A @@ -588,8 +594,10 @@ Symfony2 is the result of the work of many people who made the code better - Cédric Girard (enk_) - Oriol Mangas Abellan (oriolman) - Sebastian Göttschkes (sgoettschkes) + - Ross Tuck - Kévin Gomez (kevin) - Ludek Stepan + - Geoffrey Brier - Aaron Stephens (astephens) - Balázs Benyó (duplabe) - Erika Heidi Reinaldo (erikaheidi) @@ -612,7 +620,6 @@ Symfony2 is the result of the work of many people who made the code better - Sergey Kolodyazhnyy - George Giannoulopoulos - Daniel Richter (richtermeister) - - Chris Wilkinson (thewilkybarkid) - ChrisC - Ilya Biryukov - Jason Desrosiers @@ -638,6 +645,7 @@ Symfony2 is the result of the work of many people who made the code better - Nathaniel Catchpole - Adrien Samson (adriensamson) - Samuel Gordalina (gordalina) + - Max Romanovsky (maxromanovsky) - Timothy Anido (xanido) - Sebastian Krebs - Rick Prent @@ -670,7 +678,6 @@ Symfony2 is the result of the work of many people who made the code better - Jonathan Gough - Benjamin Bender - Konrad Mohrfeldt - - Benoit Lévêque (benoit_leveque) - kor3k kor3k (kor3k) - Stelian Mocanita (stelian) - Flavian (2much) @@ -800,6 +807,7 @@ Symfony2 is the result of the work of many people who made the code better - Grzegorz Łukaszewicz (newicz) - Robert Campbell - Matt Lehner + - Ruben Kruiswijk - Alex Pods - timaschew - Ian Phillips @@ -870,6 +878,7 @@ Symfony2 is the result of the work of many people who made the code better - Brian Debuire - Sylvain Lorinet - klyk50 + - Andreas Lutro - jc - BenjaminBeck - Aurelijus Rožėnas @@ -892,6 +901,7 @@ Symfony2 is the result of the work of many people who made the code better - andreabreu98 - Thomas Schulz - Michael Schneider + - n-aleha - Kaipi Yann - Sam Williams - James Michael DuPont @@ -912,7 +922,6 @@ Symfony2 is the result of the work of many people who made the code better - Abdulkadir N. A. - Sema - Thorsten Hallwas - - Daisuke Ohata - Michael Squires - Matt Janssen - Peter Gribanov @@ -944,6 +953,7 @@ Symfony2 is the result of the work of many people who made the code better - Bill Hance (billhance) - Bernd Matzner (bmatzner) - Chris Sedlmayr (catchamonkey) + - Choong Wei Tjeng (choonge) - Kousuke Ebihara (co3k) - Loïc Vernet (coil) - Christoph Schaefer (cvschaefer) @@ -957,6 +967,7 @@ Symfony2 is the result of the work of many people who made the code better - Yohan Giarelli (frequence-web) - Massimiliano Arione (garak) - Ghazy Ben Ahmed (ghazy) + - Arash Tabriziyan (ghost098) - ibasaw (ibasaw) - Vladislav Krupenkin (ideea) - joris de wit (jdewit) @@ -985,6 +996,7 @@ Symfony2 is the result of the work of many people who made the code better - André Filipe Gonçalves Neves (seven) - Andrea Giuliano (shark) - Julien Sanchez (sumbobyboys) + - Guillermo Gisinger (t3chn0r) - Markus Tacker (tacker) - Tyler Stroud (tystr) - Víctor Mateo (victormateo) From a037f31de80d9cc896cb9fb391cc5f1bf81696fa Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 22 May 2014 16:27:03 +0200 Subject: [PATCH 3/5] updated VERSION for 2.3.14 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 5fbdddbf0c..74758773fb 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -60,12 +60,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.3.14-DEV'; + const VERSION = '2.3.14'; const VERSION_ID = '20314'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '3'; const RELEASE_VERSION = '14'; - const EXTRA_VERSION = 'DEV'; + const EXTRA_VERSION = ''; /** * Constructor. From 08bc4d920359a161409ca4d02d169cd8a66a7b9a Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 22 May 2014 18:17:54 +0200 Subject: [PATCH 4/5] bumped Symfony version to 2.3.15 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 74758773fb..e772a031fc 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -60,12 +60,12 @@ abstract class Kernel implements KernelInterface, TerminableInterface protected $startTime; protected $loadClassCache; - const VERSION = '2.3.14'; - const VERSION_ID = '20314'; + const VERSION = '2.3.15-DEV'; + const VERSION_ID = '20315'; const MAJOR_VERSION = '2'; const MINOR_VERSION = '3'; - const RELEASE_VERSION = '14'; - const EXTRA_VERSION = ''; + const RELEASE_VERSION = '15'; + const EXTRA_VERSION = 'DEV'; /** * Constructor. From 57a64a0fa60b4e14c890b70260a4077099b64f07 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 22 May 2014 18:20:26 +0200 Subject: [PATCH 5/5] Revert "bug #10908 [HttpFoundation] implement session locking for PDO (Tobion)" This reverts commit 8c71454f47bbcdf82693a0501acac3e8fe6e08cf, reversing changes made to 735e9a4768962b6d5098733c347cff0df6b9cd36. --- .../stubs/SessionHandlerInterface.php | 12 - .../Storage/Handler/PdoSessionHandler.php | 326 +++++------------- .../Storage/Handler/PdoSessionHandlerTest.php | 114 +++--- 3 files changed, 120 insertions(+), 332 deletions(-) diff --git a/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php b/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php index 9557135bcf..24280e38fc 100644 --- a/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php +++ b/src/Symfony/Component/HttpFoundation/Resources/stubs/SessionHandlerInterface.php @@ -12,14 +12,6 @@ /** * SessionHandlerInterface for PHP < 5.4 * - * The order in which these methods are invoked by PHP are: - * 1. open [session_start] - * 2. read - * 3. gc [optional depending on probability settings: gc_probability / gc_divisor] - * 4. destroy [optional when session_regenerate_id(true) is used] - * 5. write [session_write_close] or destroy [session_destroy] - * 6. close - * * Extensive documentation can be found at php.net, see links: * * @see http://php.net/sessionhandlerinterface @@ -27,7 +19,6 @@ * @see http://php.net/session-set-save-handler * * @author Drak - * @author Tobias Schultze */ interface SessionHandlerInterface { @@ -66,9 +57,6 @@ interface SessionHandlerInterface /** * Writes the session data to the storage. * - * Care, the session ID passed to write() can be different from the one previously - * received in read() when the session ID changed due to session_regenerate_id(). - * * @see http://php.net/sessionhandlerinterface.write * * @param string $sessionId Session ID , see http://php.net/function.session-id diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index aabd123a3e..e81db4de85 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -12,19 +12,7 @@ namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; /** - * Session handler using a PDO connection to read and write data. - * - * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements - * locking of sessions to prevent loss of data by concurrent access to the same session. - * This means requests for the same session will wait until the other one finished. - * PHPs internal files session handler also works this way. - * - * Attention: Since SQLite does not support row level locks but locks the whole database, - * it means only one session can be accessed at a time. Even different sessions would wait - * for another to finish. So saving session in SQLite should only be considered for - * development or prototypes. - * - * @see http://php.net/sessionhandlerinterface + * PdoSessionHandler. * * @author Fabien Potencier * @author Michael Williams @@ -37,11 +25,6 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private $pdo; - /** - * @var string Database driver - */ - private $driver; - /** * @var string Table name */ @@ -62,50 +45,39 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private $timeCol; - /** - * @var bool Whether a transaction is active - */ - private $inTransaction = false; - - /** - * @var bool Whether gc() has been called - */ - private $gcCalled = false; - /** * Constructor. * * List of available options: - * * db_table: The name of the table [default: sessions] + * * db_table: The name of the table [required] * * db_id_col: The column where to store the session id [default: sess_id] * * db_data_col: The column where to store the session data [default: sess_data] * * db_time_col: The column where to store the timestamp [default: sess_time] * - * @param \PDO $pdo A \PDO instance - * @param array $options An associative array of DB options + * @param \PDO $pdo A \PDO instance + * @param array $dbOptions An associative array of DB options * - * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION + * @throws \InvalidArgumentException When "db_table" option is not provided */ - public function __construct(\PDO $pdo, array $options = array()) + public function __construct(\PDO $pdo, array $dbOptions = array()) { + if (!array_key_exists('db_table', $dbOptions)) { + throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.'); + } if (\PDO::ERRMODE_EXCEPTION !== $pdo->getAttribute(\PDO::ATTR_ERRMODE)) { throw new \InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION))', __CLASS__)); } - $this->pdo = $pdo; - $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); - - $options = array_replace(array( - 'db_table' => 'sessions', + $dbOptions = array_merge(array( 'db_id_col' => 'sess_id', 'db_data_col' => 'sess_data', 'db_time_col' => 'sess_time', - ), $options); + ), $dbOptions); - $this->table = $options['db_table']; - $this->idCol = $options['db_id_col']; - $this->dataCol = $options['db_data_col']; - $this->timeCol = $options['db_time_col']; + $this->table = $dbOptions['db_table']; + $this->idCol = $dbOptions['db_id_col']; + $this->dataCol = $dbOptions['db_data_col']; + $this->timeCol = $dbOptions['db_time_col']; } /** @@ -113,56 +85,14 @@ class PdoSessionHandler implements \SessionHandlerInterface */ public function open($savePath, $sessionName) { - $this->gcCalled = false; - return true; } /** * {@inheritdoc} */ - public function read($sessionId) + public function close() { - $this->beginTransaction(); - - try { - $this->lockSession($sessionId); - - // We need to make sure we do not return session data that is already considered garbage according - // to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. - $maxlifetime = (int) ini_get('session.gc_maxlifetime'); - - $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id AND $this->timeCol > :time"; - - $stmt = $this->pdo->prepare($sql); - $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); - $stmt->execute(); - - // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed - $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); - - if ($sessionRows) { - return base64_decode($sessionRows[0][0]); - } - - return ''; - } catch (\PDOException $e) { - $this->rollback(); - - throw $e; - } - } - - /** - * {@inheritdoc} - */ - public function gc($maxlifetime) - { - // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. - // This way, pruning expired sessions does not block them from being started while the current session is used. - $this->gcCalled = true; - return true; } @@ -179,14 +109,56 @@ class PdoSessionHandler implements \SessionHandlerInterface $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $stmt->execute(); } catch (\PDOException $e) { - $this->rollback(); - - throw $e; + throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete a session: %s', $e->getMessage()), 0, $e); } return true; } + /** + * {@inheritdoc} + */ + public function gc($maxlifetime) + { + // delete the session records that have expired + $sql = "DELETE FROM $this->table WHERE $this->timeCol < :time"; + + try { + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); + $stmt->execute(); + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete expired sessions: %s', $e->getMessage()), 0, $e); + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function read($sessionId) + { + $sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id"; + + try { + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->execute(); + + // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed + $sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM); + + if ($sessionRows) { + return base64_decode($sessionRows[0][0]); + } + + return ''; + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e); + } + } + /** * {@inheritdoc} */ @@ -195,10 +167,8 @@ class PdoSessionHandler implements \SessionHandlerInterface // Session data can contain non binary safe characters so we need to encode it. $encoded = base64_encode($data); - // The session ID can be different from the one previously received in read() - // when the session ID changed due to session_regenerate_id(). So we have to - // do an insert or update even if we created a row in read() for locking. // We use a MERGE SQL query when supported by the database. + // Otherwise we have to use a transactional DELETE followed by INSERT to prevent duplicate entries under high concurrency. try { $mergeSql = $this->getMergeSql(); @@ -213,18 +183,15 @@ class PdoSessionHandler implements \SessionHandlerInterface return true; } - $updateStmt = $this->pdo->prepare( - "UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id" - ); - $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $updateStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); - $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); - $updateStmt->execute(); + $this->pdo->beginTransaction(); + + try { + $deleteStmt = $this->pdo->prepare( + "DELETE FROM $this->table WHERE $this->idCol = :id" + ); + $deleteStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $deleteStmt->execute(); - // Since we have a lock on the session, this is safe to do. Otherwise it would be prone to - // race conditions in high concurrency. And if it's a regenerated session ID it should be - // unique anyway. - if (!$updateStmt->rowCount()) { $insertStmt = $this->pdo->prepare( "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)" ); @@ -232,153 +199,18 @@ class PdoSessionHandler implements \SessionHandlerInterface $insertStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); $insertStmt->execute(); - } - } catch (\PDOException $e) { - $this->rollback(); - throw $e; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function close() - { - $this->commit(); - - if ($this->gcCalled) { - $maxlifetime = (int) ini_get('session.gc_maxlifetime'); - - // delete the session records that have expired - $sql = "DELETE FROM $this->table WHERE $this->timeCol <= :time"; - - $stmt = $this->pdo->prepare($sql); - $stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT); - $stmt->execute(); - } - - return true; - } - - /** - * Helper method to begin a transaction. - * - * Since SQLite does not support row level locks, we have to acquire a reserved lock - * on the database immediately. Because of https://bugs.php.net/42766 we have to create - * such a transaction manually which also means we cannot use PDO::commit or - * PDO::rollback or PDO::inTransaction for SQLite. - */ - private function beginTransaction() - { - if ($this->inTransaction) { - $this->rollback(); - - throw new \BadMethodCallException( - 'Session handler methods have been invoked in wrong sequence. ' . - 'Expected sequence: open() -> read() -> destroy() / write() -> close()'); - } - - if ('sqlite' === $this->driver) { - $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); - } else { - $this->pdo->beginTransaction(); - } - $this->inTransaction = true; - } - - /** - * Helper method to commit a transaction. - */ - private function commit() - { - if ($this->inTransaction) { - try { - // commit read-write transaction which also releases the lock - if ('sqlite' === $this->driver) { - $this->pdo->exec('COMMIT'); - } else { - $this->pdo->commit(); - } - $this->inTransaction = false; + $this->pdo->commit(); } catch (\PDOException $e) { - $this->rollback(); + $this->pdo->rollback(); throw $e; } - } - } - - /** - * Helper method to rollback a transaction. - */ - private function rollback() - { - // We only need to rollback if we are in a transaction. Otherwise the resulting - // error would hide the real problem why rollback was called. We might not be - // in a transaction when two callbacks (e.g. destroy and write) are invoked that - // both fail. - if ($this->inTransaction) { - if ('sqlite' === $this->driver) { - $this->pdo->exec('ROLLBACK'); - } else { - $this->pdo->rollback(); - } - $this->inTransaction = false; - } - } - - /** - * Exclusively locks the row so other concurrent requests on the same session will block. - * - * This prevents loss of data by keeping the data consistent between read() and write(). - * We do not use SELECT FOR UPDATE because it does not lock non-existent rows. And a following - * INSERT when not found can result in a deadlock for one connection. - * - * @param string $sessionId Session ID - */ - private function lockSession($sessionId) - { - switch ($this->driver) { - case 'mysql': - // will also lock the row when actually nothing got updated (id = id) - $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "ON DUPLICATE KEY UPDATE $this->idCol = $this->idCol"; - break; - case 'oci': - // DUAL is Oracle specific dummy table - $sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . - "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol"; - break; - case 'sqlsrv': - // MS SQL Server requires MERGE be terminated by semicolon - $sql = "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " . - "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol;"; - break; - case 'pgsql': - // obtain an exclusive transaction level advisory lock - $sql = 'SELECT pg_advisory_xact_lock(:lock_id)'; - $stmt = $this->pdo->prepare($sql); - $stmt->bindValue(':lock_id', hexdec(substr($sessionId, 0, 15)), \PDO::PARAM_INT); - $stmt->execute(); - - return; - default: - return; + } catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e); } - // We create a DML lock for the session by inserting empty data or updating the row. - // This is safer than an application level advisory lock because it also prevents concurrent modification - // of the session from other parts of the application. - $stmt = $this->pdo->prepare($sql); - $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); - $stmt->bindValue(':data', '', \PDO::PARAM_STR); - $stmt->bindValue(':time', time(), \PDO::PARAM_INT); - $stmt->execute(); + return true; } /** @@ -388,7 +220,9 @@ class PdoSessionHandler implements \SessionHandlerInterface */ private function getMergeSql() { - switch ($this->driver) { + $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + + switch ($driver) { case 'mysql': return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)"; @@ -396,12 +230,12 @@ class PdoSessionHandler implements \SessionHandlerInterface // DUAL is Oracle specific dummy table return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time"; + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data"; case 'sqlsrv': // MS SQL Server requires MERGE be terminated by semicolon return "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " . "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;"; + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data;"; case 'sqlite': return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"; } diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php index 32195f96f4..14e2dba526 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -29,107 +29,73 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase $this->pdo->exec($sql); } - /** - * @expectedException \InvalidArgumentException - */ + public function testIncompleteOptions() + { + $this->setExpectedException('InvalidArgumentException'); + $storage = new PdoSessionHandler($this->pdo, array()); + } + public function testWrongPdoErrMode() { - $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); + $pdo = new \PDO("sqlite::memory:"); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); + $pdo->exec("CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)"); - $storage = new PdoSessionHandler($this->pdo); + $this->setExpectedException('InvalidArgumentException'); + $storage = new PdoSessionHandler($pdo, array('db_table' => 'sessions')); } - /** - * @expectedException \RuntimeException - */ - public function testInexistentTable() + public function testWrongTableOptionsWrite() { - $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'inexistent_table')); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->write('id', 'data'); - $storage->close(); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name')); + $this->setExpectedException('RuntimeException'); + $storage->write('foo', 'bar'); } - public function testReadWriteRead() + public function testWrongTableOptionsRead() { - $storage = new PdoSessionHandler($this->pdo); - $storage->open('', 'sid'); - $this->assertSame('', $storage->read('id'), 'New session returns empty string data'); - $storage->write('id', 'data'); - $storage->close(); - - $storage->open('', 'sid'); - $this->assertSame('data', $storage->read('id'), 'Written value can be read back correctly'); - $storage->close(); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name')); + $this->setExpectedException('RuntimeException'); + $storage->read('foo', 'bar'); } - /** - * Simulates session_regenerate_id(true) which will require an INSERT or UPDATE (replace) - */ - public function testWriteDifferentSessionIdThanRead() + public function testWriteRead() { - $storage = new PdoSessionHandler($this->pdo); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->destroy('id'); - $storage->write('new_id', 'data_of_new_session_id'); - $storage->close(); - - $storage->open('', 'sid'); - $this->assertSame('data_of_new_session_id', $storage->read('new_id'), 'Data of regenerated session id is available'); - $storage->close(); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + $storage->write('foo', 'bar'); + $this->assertEquals('bar', $storage->read('foo'), 'written value can be read back correctly'); } - /** - * @expectedException \BadMethodCallException - */ - public function testWrongUsage() + public function testMultipleInstances() { - $storage = new PdoSessionHandler($this->pdo); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->read('id'); + $storage1 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + $storage1->write('foo', 'bar'); + + $storage2 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + $this->assertEquals('bar', $storage2->read('foo'), 'values persist between instances'); } public function testSessionDestroy() { - $storage = new PdoSessionHandler($this->pdo); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); + $storage->write('foo', 'bar'); + $this->assertCount(1, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->write('id', 'data'); - $storage->close(); - $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + $storage->destroy('foo'); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->destroy('id'); - $storage->close(); - $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); - - $storage->open('', 'sid'); - $this->assertSame('', $storage->read('id'), 'Destroyed session returns empty string'); - $storage->close(); + $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); } public function testSessionGC() { - $previousLifeTime = ini_set('session.gc_maxlifetime', 0); - $storage = new PdoSessionHandler($this->pdo); + $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions')); - $storage->open('', 'sid'); - $storage->read('id'); - $storage->write('id', 'data'); - $storage->close(); - $this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + $storage->write('foo', 'bar'); + $storage->write('baz', 'bar'); - $storage->open('', 'sid'); - $this->assertSame('', $storage->read('id'), 'Session already considered garbage, so not returning data even if it is not pruned yet'); - $storage->gc(0); - $storage->close(); - $this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn()); + $this->assertCount(2, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); - ini_set('session.gc_maxlifetime', $previousLifeTime); + $storage->gc(-1); + $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll()); } }