Merge branch '2.4'

* 2.4:
  Revert "bug #10908 [HttpFoundation] implement session locking for PDO (Tobion)"
  bumped Symfony version to 2.3.15
  updated VERSION for 2.3.14
  update CONTRIBUTORS for 2.3.14
  updated CHANGELOG for 2.3.14
This commit is contained in:
Fabien Potencier 2014-05-22 18:21:05 +02:00
commit 26bc81e5a0
5 changed files with 177 additions and 347 deletions

View File

@ -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 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 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) * 2.3.13 (2014-04-27)
* bug #10789 [Console] Fixed the rendering of exceptions on HHVM with a terminal width (stof) * bug #10789 [Console] Fixed the rendering of exceptions on HHVM with a terminal width (stof)

View File

@ -37,12 +37,12 @@ Symfony2 is the result of the work of many people who made the code better
- Francis Besset (francisbesset) - Francis Besset (francisbesset)
- Miha Vrhovnik - Miha Vrhovnik
- Henrik Bjørnskov (henrikbjorn) - Henrik Bjørnskov (henrikbjorn)
- Saša Stamenković (umpirsky)
- Konstantin Kudryashov (everzet) - Konstantin Kudryashov (everzet)
- Bilal Amarni (bamarni) - Bilal Amarni (bamarni)
- Florin Patan (florinpatan) - Florin Patan (florinpatan)
- Saša Stamenković (umpirsky)
- Eric Clemmons (ericclemmons)
- Wouter De Jong (wouterj) - Wouter De Jong (wouterj)
- Eric Clemmons (ericclemmons)
- Deni - Deni
- Henrik Westphal (snc) - Henrik Westphal (snc)
- Dariusz Górecki (canni) - 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) - Antoine Hérault (herzult)
- Toni Uebernickel (havvg) - Toni Uebernickel (havvg)
- Arnaud Le Blanc (arnaud-lb) - Arnaud Le Blanc (arnaud-lb)
- Nicolas Grekas (nicolas-grekas)
- Brice BERNARD (brikou) - Brice BERNARD (brikou)
- Luis Cordova (cordoval) - Luis Cordova (cordoval)
- Tim Nagel (merk) - 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) - Juti Noppornpitak (shiroyuki)
- Sebastian Hörl (blogsh) - Sebastian Hörl (blogsh)
- Hidenori Goto (hidenorigoto) - Hidenori Goto (hidenorigoto)
- Ait Boudad Abdellatif (aitboudad)
- Daniel Gomes (danielcsgomes) - Daniel Gomes (danielcsgomes)
- Peter Kokot (maastermedia) - Peter Kokot (maastermedia)
- Jérémie Augustin (jaugustin) - Jérémie Augustin (jaugustin)
- David Buchmann (dbu) - David Buchmann (dbu)
- Ait Boudad Abdellatif (aitboudad)
- Jérôme Tamarelle (gromnan) - Jérôme Tamarelle (gromnan)
- Tigran Azatyan (tigranazatyan) - Tigran Azatyan (tigranazatyan)
- Javier Eguiluz (javier.eguiluz) - 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) - Jonathan Ingram (jonathaningram)
- Artur Kotyrba - Artur Kotyrba
- Guilherme Blanco (guilhermeblanco) - Guilherme Blanco (guilhermeblanco)
- Nicolas Grekas (nicolas-grekas)
- Pablo Godel (pgodel) - Pablo Godel (pgodel)
- Eric GELOEN (gelo) - Eric GELOEN (gelo)
- Dmitrii Chekaliuk (lazyhammer) - 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) - Benjamin Dulau (dbenjamin)
- Andreas Hucks (meandmymonkey) - Andreas Hucks (meandmymonkey)
- Noel Guilbert (noel) - Noel Guilbert (noel)
- Charles Sarrazin (csarrazi)
- bronze1man - bronze1man
- Larry Garfield (crell) - Larry Garfield (crell)
- Martin Schuhfuß (usefulthink) - 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) - Andréia Bohner (andreia)
- Joel Wurtz (brouznouf) - Joel Wurtz (brouznouf)
- Rui Marinho (ruimarinho) - Rui Marinho (ruimarinho)
- sun (sun)
- Julien Brochet (mewt) - Julien Brochet (mewt)
- Tugdual Saunier (tucksaun)
- Sergey Linnik (linniksa) - Sergey Linnik (linniksa)
- Marcel Beerta (mazen) - Marcel Beerta (mazen)
- Francois Zaninotto - 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) - Félix Labrecque (woodspire)
- Christian Flothmann (xabbuh) - Christian Flothmann (xabbuh)
- GordonsLondon - GordonsLondon
- sun (sun)
- Jan Sorgalla (jsor) - Jan Sorgalla (jsor)
- Ray - Ray
- Chekote - Chekote
@ -175,6 +177,7 @@ Symfony2 is the result of the work of many people who made the code better
- Lars Strojny (lstrojny) - Lars Strojny (lstrojny)
- Bertrand Zuchuat (garfield-fr) - Bertrand Zuchuat (garfield-fr)
- Gabor Toth (tgabi333) - Gabor Toth (tgabi333)
- realmfoo
- Thomas Tourlourat (armetiz) - Thomas Tourlourat (armetiz)
- Andrey Esaulov (andremaha) - Andrey Esaulov (andremaha)
- Grégoire Passault (gregwar) - 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) - Ricard Clau (ricardclau)
- Erin Millard - Erin Millard
- Matthew Lewinski (lewinski) - Matthew Lewinski (lewinski)
- alquerci
- Francesco Levorato - Francesco Levorato
- Vitaliy Zakharov (zakharovvi) - Vitaliy Zakharov (zakharovvi)
- Gyula Sallai (salla) - 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) - Sébastien Lavoie (lavoiesl)
- Terje Bråten - Terje Bråten
- Kristen Gilden (kgilden) - Kristen Gilden (kgilden)
- Robbert Klarenbeek (robbertkl)
- hossein zolfi (ocean) - hossein zolfi (ocean)
- Eduardo Gulias (egulias) - Eduardo Gulias (egulias)
- giulio de donato (liuggio) - 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) - Costin Bereveanu (schniper)
- Loïc Chardonnet (gnusat) - Loïc Chardonnet (gnusat)
- Marek Kalnik (marekkalnik) - Marek Kalnik (marekkalnik)
- realmfoo
- Tamas Szijarto - Tamas Szijarto
- Pavel Volokitin (pvolok) - Pavel Volokitin (pvolok)
- Tobias Naumann (tna) - 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) - Olivier Dolbeau (odolbeau)
- Roumen Damianoff (roumen) - Roumen Damianoff (roumen)
- Tobias Sjösten (tobiassjosten) - Tobias Sjösten (tobiassjosten)
- alquerci
- vagrant - vagrant
- Asier Illarramendi (doup) - Asier Illarramendi (doup)
- Kévin Dunglas (dunglas) - 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) - Vitaliy Tverdokhlib (vitaliytv)
- Dirk Pahl (dirkaholic) - Dirk Pahl (dirkaholic)
- cedric lombardot (cedriclombardot) - cedric lombardot (cedriclombardot)
- Charles Sarrazin (csarrazi)
- Jonas Flodén (flojon) - Jonas Flodén (flojon)
- Marcin Sikoń (marphi) - Marcin Sikoń (marphi)
- franek (franek) - franek (franek)
- Adam Harvey - Adam Harvey
- Robbert Klarenbeek (robbertkl)
- François-Xavier de Guillebon (de-gui_f) - François-Xavier de Guillebon (de-gui_f)
- boombatower - boombatower
- Fabrice Bernhard (fabriceb) - 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) - Erik Trapman (eriktrapman)
- De Cock Xavier (xdecock) - De Cock Xavier (xdecock)
- Alex Pott - Alex Pott
- Issei Murasawa (issei_m)
- Norbert Orzechowicz (norzechowicz) - Norbert Orzechowicz (norzechowicz)
- Matthijs van den Bos (matthijs) - Matthijs van den Bos (matthijs)
- Nils Adermann (naderman) - Nils Adermann (naderman)
@ -312,6 +314,7 @@ Symfony2 is the result of the work of many people who made the code better
- Ned Schwartz - Ned Schwartz
- Ziumin - Ziumin
- Lenar Lõhmus - Lenar Lõhmus
- julien pauli (jpauli)
- Zach Badgett (zachbadgett) - Zach Badgett (zachbadgett)
- Aurélien Fredouelle - Aurélien Fredouelle
- Karoly Negyesi (chx) - 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) - Kamil Kokot (pamil)
- Florian Lonqueu-Brochard (florianlb) - Florian Lonqueu-Brochard (florianlb)
- Rostyslav Kinash - Rostyslav Kinash
- Daisuke Ohata
- Vincent Simonin - Vincent Simonin
- Christian Schmidt - Christian Schmidt
- Stefan Warman - Stefan Warman
@ -392,10 +396,12 @@ Symfony2 is the result of the work of many people who made the code better
- Josiah (josiah) - Josiah (josiah)
- John Bohn (jbohn) - John Bohn (jbohn)
- Jakub Škvára (jskvara) - Jakub Škvára (jskvara)
- Chris Wilkinson (thewilkybarkid)
- Andrew Hilobok (hilobok) - Andrew Hilobok (hilobok)
- Christian Soronellas (theunic) - Christian Soronellas (theunic)
- Jérôme Vieilledent (lolautruche) - Jérôme Vieilledent (lolautruche)
- Degory Valentine - Degory Valentine
- Benoit Lévêque (benoit_leveque)
- hacfi (hifi) - hacfi (hifi)
- Krzysiek Łabuś - Krzysiek Łabuś
- Xavier Lacot (xavier) - 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) - Jayson Xu (superjavason)
- Jaik Dean (jaikdean) - Jaik Dean (jaikdean)
- Jan Prieser - Jan Prieser
- Issei Murasawa (issei_m)
- James Michael DuPont - James Michael DuPont
- Tom Klingenberg - Tom Klingenberg
- Christopher Hall (mythmakr) - Christopher Hall (mythmakr)
@ -419,8 +424,8 @@ Symfony2 is the result of the work of many people who made the code better
- Abhoryo - Abhoryo
- Fabian Vogler (fabian) - Fabian Vogler (fabian)
- Maksim Kotlyar (makasim) - Maksim Kotlyar (makasim)
- Tugdual Saunier (tucksaun)
- Neil Ferreira - Neil Ferreira
- Dmitry Parnas (parnas)
- Tony Malzhacker - Tony Malzhacker
- Cyril Quintin (cyqui) - Cyril Quintin (cyqui)
- Gerard van Helden (drm) - 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 - Aleksey Podskrebyshev
- David Marín Carreño (davefx) - David Marín Carreño (davefx)
- Jörn Lang (j.lang) - Jörn Lang (j.lang)
- julien pauli (jpauli)
- mwsaz - mwsaz
- Benoît Bourgeois - Benoît Bourgeois
- corphi - corphi
@ -537,6 +541,7 @@ Symfony2 is the result of the work of many people who made the code better
- Vincent AUBERT (vincent) - Vincent AUBERT (vincent)
- Benoit Garret - Benoit Garret
- DerManoMann - DerManoMann
- Asmir Mustafic (goetas)
- Marcin Chwedziak - Marcin Chwedziak
- Roland Franssen (ro0) - Roland Franssen (ro0)
- Maciej Malarz - 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) - Jeroen van den Enden (stoefke)
- Quique Porta (quiqueporta) - Quique Porta (quiqueporta)
- Tomasz Szymczyk (karion) - Tomasz Szymczyk (karion)
- Arturs Vonda
- ConneXNL - ConneXNL
- Aharon Perkel - Aharon Perkel
- Abdul.Mohsen B. A. A - 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_) - Cédric Girard (enk_)
- Oriol Mangas Abellan (oriolman) - Oriol Mangas Abellan (oriolman)
- Sebastian Göttschkes (sgoettschkes) - Sebastian Göttschkes (sgoettschkes)
- Ross Tuck
- Kévin Gomez (kevin) - Kévin Gomez (kevin)
- Ludek Stepan - Ludek Stepan
- Geoffrey Brier
- Aaron Stephens (astephens) - Aaron Stephens (astephens)
- Balázs Benyó (duplabe) - Balázs Benyó (duplabe)
- Erika Heidi Reinaldo (erikaheidi) - 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 - Sergey Kolodyazhnyy
- George Giannoulopoulos - George Giannoulopoulos
- Daniel Richter (richtermeister) - Daniel Richter (richtermeister)
- Chris Wilkinson (thewilkybarkid)
- ChrisC - ChrisC
- Ilya Biryukov - Ilya Biryukov
- Jason Desrosiers - Jason Desrosiers
@ -638,6 +645,7 @@ Symfony2 is the result of the work of many people who made the code better
- Nathaniel Catchpole - Nathaniel Catchpole
- Adrien Samson (adriensamson) - Adrien Samson (adriensamson)
- Samuel Gordalina (gordalina) - Samuel Gordalina (gordalina)
- Max Romanovsky (maxromanovsky)
- Timothy Anido (xanido) - Timothy Anido (xanido)
- Sebastian Krebs - Sebastian Krebs
- Rick Prent - Rick Prent
@ -670,7 +678,6 @@ Symfony2 is the result of the work of many people who made the code better
- Jonathan Gough - Jonathan Gough
- Benjamin Bender - Benjamin Bender
- Konrad Mohrfeldt - Konrad Mohrfeldt
- Benoit Lévêque (benoit_leveque)
- kor3k kor3k (kor3k) - kor3k kor3k (kor3k)
- Stelian Mocanita (stelian) - Stelian Mocanita (stelian)
- Flavian (2much) - Flavian (2much)
@ -800,6 +807,7 @@ Symfony2 is the result of the work of many people who made the code better
- Grzegorz Łukaszewicz (newicz) - Grzegorz Łukaszewicz (newicz)
- Robert Campbell - Robert Campbell
- Matt Lehner - Matt Lehner
- Ruben Kruiswijk
- Alex Pods - Alex Pods
- timaschew - timaschew
- Ian Phillips - Ian Phillips
@ -870,6 +878,7 @@ Symfony2 is the result of the work of many people who made the code better
- Brian Debuire - Brian Debuire
- Sylvain Lorinet - Sylvain Lorinet
- klyk50 - klyk50
- Andreas Lutro
- jc - jc
- BenjaminBeck - BenjaminBeck
- Aurelijus Rožėnas - Aurelijus Rožėnas
@ -892,6 +901,7 @@ Symfony2 is the result of the work of many people who made the code better
- andreabreu98 - andreabreu98
- Thomas Schulz - Thomas Schulz
- Michael Schneider - Michael Schneider
- n-aleha
- Kaipi Yann - Kaipi Yann
- Sam Williams - Sam Williams
- James Michael DuPont - 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. - Abdulkadir N. A.
- Sema - Sema
- Thorsten Hallwas - Thorsten Hallwas
- Daisuke Ohata
- Michael Squires - Michael Squires
- Matt Janssen - Matt Janssen
- Peter Gribanov - Peter Gribanov
@ -944,6 +953,7 @@ Symfony2 is the result of the work of many people who made the code better
- Bill Hance (billhance) - Bill Hance (billhance)
- Bernd Matzner (bmatzner) - Bernd Matzner (bmatzner)
- Chris Sedlmayr (catchamonkey) - Chris Sedlmayr (catchamonkey)
- Choong Wei Tjeng (choonge)
- Kousuke Ebihara (co3k) - Kousuke Ebihara (co3k)
- Loïc Vernet (coil) - Loïc Vernet (coil)
- Christoph Schaefer (cvschaefer) - 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) - Yohan Giarelli (frequence-web)
- Massimiliano Arione (garak) - Massimiliano Arione (garak)
- Ghazy Ben Ahmed (ghazy) - Ghazy Ben Ahmed (ghazy)
- Arash Tabriziyan (ghost098)
- ibasaw (ibasaw) - ibasaw (ibasaw)
- Vladislav Krupenkin (ideea) - Vladislav Krupenkin (ideea)
- joris de wit (jdewit) - 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) - André Filipe Gonçalves Neves (seven)
- Andrea Giuliano (shark) - Andrea Giuliano (shark)
- Julien Sanchez (sumbobyboys) - Julien Sanchez (sumbobyboys)
- Guillermo Gisinger (t3chn0r)
- Markus Tacker (tacker) - Markus Tacker (tacker)
- Tyler Stroud (tystr) - Tyler Stroud (tystr)
- Víctor Mateo (victormateo) - Víctor Mateo (victormateo)

View File

@ -12,14 +12,6 @@
/** /**
* SessionHandlerInterface for PHP < 5.4 * 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: * Extensive documentation can be found at php.net, see links:
* *
* @see http://php.net/sessionhandlerinterface * @see http://php.net/sessionhandlerinterface
@ -27,7 +19,6 @@
* @see http://php.net/session-set-save-handler * @see http://php.net/session-set-save-handler
* *
* @author Drak <drak@zikula.org> * @author Drak <drak@zikula.org>
* @author Tobias Schultze <http://tobion.de>
*/ */
interface SessionHandlerInterface interface SessionHandlerInterface
{ {
@ -66,9 +57,6 @@ interface SessionHandlerInterface
/** /**
* Writes the session data to the storage. * 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 * @see http://php.net/sessionhandlerinterface.write
* *
* @param string $sessionId Session ID , see http://php.net/function.session-id * @param string $sessionId Session ID , see http://php.net/function.session-id

View File

@ -12,19 +12,7 @@
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
/** /**
* Session handler using a PDO connection to read and write data. * PdoSessionHandler.
*
* 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
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* @author Michael Williams <michael.williams@funsational.com> * @author Michael Williams <michael.williams@funsational.com>
@ -37,11 +25,6 @@ class PdoSessionHandler implements \SessionHandlerInterface
*/ */
private $pdo; private $pdo;
/**
* @var string Database driver
*/
private $driver;
/** /**
* @var string Table name * @var string Table name
*/ */
@ -62,50 +45,39 @@ class PdoSessionHandler implements \SessionHandlerInterface
*/ */
private $timeCol; private $timeCol;
/**
* @var bool Whether a transaction is active
*/
private $inTransaction = false;
/**
* @var bool Whether gc() has been called
*/
private $gcCalled = false;
/** /**
* Constructor. * Constructor.
* *
* List of available options: * 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_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_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] * * db_time_col: The column where to store the timestamp [default: sess_time]
* *
* @param \PDO $pdo A \PDO instance * @param \PDO $pdo A \PDO instance
* @param array $options An associative array of DB options * @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)) { 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__)); 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->pdo = $pdo;
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); $dbOptions = array_merge(array(
$options = array_replace(array(
'db_table' => 'sessions',
'db_id_col' => 'sess_id', 'db_id_col' => 'sess_id',
'db_data_col' => 'sess_data', 'db_data_col' => 'sess_data',
'db_time_col' => 'sess_time', 'db_time_col' => 'sess_time',
), $options); ), $dbOptions);
$this->table = $options['db_table']; $this->table = $dbOptions['db_table'];
$this->idCol = $options['db_id_col']; $this->idCol = $dbOptions['db_id_col'];
$this->dataCol = $options['db_data_col']; $this->dataCol = $dbOptions['db_data_col'];
$this->timeCol = $options['db_time_col']; $this->timeCol = $dbOptions['db_time_col'];
} }
/** /**
@ -113,56 +85,14 @@ class PdoSessionHandler implements \SessionHandlerInterface
*/ */
public function open($savePath, $sessionName) public function open($savePath, $sessionName)
{ {
$this->gcCalled = false;
return true; return true;
} }
/** /**
* {@inheritdoc} * {@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; return true;
} }
@ -179,14 +109,56 @@ class PdoSessionHandler implements \SessionHandlerInterface
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
$stmt->execute(); $stmt->execute();
} catch (\PDOException $e) { } catch (\PDOException $e) {
$this->rollback(); throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete a session: %s', $e->getMessage()), 0, $e);
throw $e;
} }
return true; 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} * {@inheritdoc}
*/ */
@ -195,10 +167,8 @@ class PdoSessionHandler implements \SessionHandlerInterface
// Session data can contain non binary safe characters so we need to encode it. // Session data can contain non binary safe characters so we need to encode it.
$encoded = base64_encode($data); $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. // 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 { try {
$mergeSql = $this->getMergeSql(); $mergeSql = $this->getMergeSql();
@ -213,18 +183,15 @@ class PdoSessionHandler implements \SessionHandlerInterface
return true; return true;
} }
$updateStmt = $this->pdo->prepare( $this->pdo->beginTransaction();
"UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id"
); try {
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $deleteStmt = $this->pdo->prepare(
$updateStmt->bindParam(':data', $encoded, \PDO::PARAM_STR); "DELETE FROM $this->table WHERE $this->idCol = :id"
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); );
$updateStmt->execute(); $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( $insertStmt = $this->pdo->prepare(
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)" "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->bindParam(':data', $encoded, \PDO::PARAM_STR);
$insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
$insertStmt->execute(); $insertStmt->execute();
}
} catch (\PDOException $e) {
$this->rollback();
throw $e; $this->pdo->commit();
}
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;
} catch (\PDOException $e) { } catch (\PDOException $e) {
$this->rollback(); $this->pdo->rollback();
throw $e; throw $e;
} }
} } catch (\PDOException $e) {
} throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $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;
} }
// We create a DML lock for the session by inserting empty data or updating the row. return true;
// 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();
} }
/** /**
@ -388,7 +220,9 @@ class PdoSessionHandler implements \SessionHandlerInterface
*/ */
private function getMergeSql() private function getMergeSql()
{ {
switch ($this->driver) { $driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
switch ($driver) {
case 'mysql': case 'mysql':
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " . 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)"; "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 // DUAL is Oracle specific dummy table
return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " . 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 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': case 'sqlsrv':
// MS SQL Server requires MERGE be terminated by semicolon // 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) " . 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 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': case 'sqlite':
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"; return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)";
} }

View File

@ -29,108 +29,74 @@ class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
$this->pdo->exec($sql); $this->pdo->exec($sql);
} }
/** public function testIncompleteOptions()
* @expectedException \InvalidArgumentException {
*/ $this->setExpectedException('InvalidArgumentException');
$storage = new PdoSessionHandler($this->pdo, array());
}
public function testWrongPdoErrMode() 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'));
} }
/** public function testWrongTableOptionsWrite()
* @expectedException \RuntimeException
*/
public function testInexistentTable()
{ {
$storage = new PdoSessionHandler($this->pdo, array('db_table' => 'inexistent_table')); $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name'));
$storage->open('', 'sid'); $this->setExpectedException('RuntimeException');
$storage->read('id'); $storage->write('foo', 'bar');
$storage->write('id', 'data');
$storage->close();
} }
public function testReadWriteRead() public function testWrongTableOptionsRead()
{ {
$storage = new PdoSessionHandler($this->pdo); $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'bad_name'));
$storage->open('', 'sid'); $this->setExpectedException('RuntimeException');
$this->assertSame('', $storage->read('id'), 'New session returns empty string data'); $storage->read('foo', 'bar');
$storage->write('id', 'data');
$storage->close();
$storage->open('', 'sid');
$this->assertSame('data', $storage->read('id'), 'Written value can be read back correctly');
$storage->close();
} }
/** public function testWriteRead()
* Simulates session_regenerate_id(true) which will require an INSERT or UPDATE (replace)
*/
public function testWriteDifferentSessionIdThanRead()
{ {
$storage = new PdoSessionHandler($this->pdo); $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions'));
$storage->open('', 'sid'); $storage->write('foo', 'bar');
$storage->read('id'); $this->assertEquals('bar', $storage->read('foo'), 'written value can be read back correctly');
$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();
} }
/** public function testMultipleInstances()
* @expectedException \BadMethodCallException
*/
public function testWrongUsage()
{ {
$storage = new PdoSessionHandler($this->pdo); $storage1 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions'));
$storage->open('', 'sid'); $storage1->write('foo', 'bar');
$storage->read('id');
$storage->read('id'); $storage2 = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions'));
$this->assertEquals('bar', $storage2->read('foo'), 'values persist between instances');
} }
public function testSessionDestroy() 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->destroy('foo');
$storage->read('id');
$storage->write('id', 'data');
$storage->close();
$this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
$storage->open('', 'sid'); $this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll());
$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();
} }
public function testSessionGC() public function testSessionGC()
{ {
$previousLifeTime = ini_set('session.gc_maxlifetime', 0); $storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions'));
$storage = new PdoSessionHandler($this->pdo);
$storage->open('', 'sid'); $storage->write('foo', 'bar');
$storage->read('id'); $storage->write('baz', 'bar');
$storage->write('id', 'data');
$storage->close();
$this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
$storage->open('', 'sid'); $this->assertCount(2, $this->pdo->query('SELECT * FROM sessions')->fetchAll());
$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());
ini_set('session.gc_maxlifetime', $previousLifeTime); $storage->gc(-1);
$this->assertCount(0, $this->pdo->query('SELECT * FROM sessions')->fetchAll());
} }
public function testGetConnection() public function testGetConnection()