diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index 028286d74c1a..9b5d5def1468 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -1106,4 +1106,24 @@ UPGRADE FROM 2.x to 3.0 ### HttpFoundation -* `Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface` no longer implements the `IteratorAggregate` interface. Use the `all()` method instead of iterating over the flash bag. + * `Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface` no longer implements the `IteratorAggregate` interface. Use the `all()` method instead of iterating over the flash bag. + * The schema of the `PdoSessionHandler` to store sessions in a database changed. + The following changes must be made to your `session` table: + + - Create a new `sess_expiry` column. In MySQL this would be: + + ```sql + ALTER TABLE `session` ADD `sess_expiry` INTEGER UNSIGNED NOT NULL; + ``` + + - Migrate the old data to the `sess_expiry` column. In MySQL this would be: + + ```sql + UPDATE `session` SET `sess_expiry` = `sess_time` + `sess_lifetime`; + ``` + + - Remove the `sess_time` and `sess_lifetime` columns. In MySQL this would be: + + ```sql + ALTER TABLE `session` DROP COLUMN `sess_time`, DROP COLUMN `sess_lifetime`; + ``` diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index dcdeb4ebf966..0291d6d9c4a8 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.0.0 +----- + + * PdoSessionHandler: removed the `lifetime` and `timestamp` columns in favor of `expiry` column + 2.6.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php index 32b73968db68..6f97fc52072d 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php @@ -96,14 +96,9 @@ class PdoSessionHandler implements \SessionHandlerInterface private $dataCol = 'sess_data'; /** - * @var string Column for lifetime + * @var string Column for expiry */ - private $lifetimeCol = 'sess_lifetime'; - - /** - * @var string Column for timestamp - */ - private $timeCol = 'sess_time'; + private $expiryCol = 'sess_expiry'; /** * @var string Username when lazy-connect @@ -126,7 +121,7 @@ class PdoSessionHandler implements \SessionHandlerInterface private $lockMode = self::LOCK_TRANSACTIONAL; /** - * It's an array to support multiple reads before closing which is manual, non-standard usage + * It's an array to support multiple reads before closing which is manual, non-standard usage. * * @var \PDOStatement[] An array of statements to release advisory locks */ @@ -159,8 +154,7 @@ class PdoSessionHandler implements \SessionHandlerInterface * * db_table: The name of the table [default: sessions] * * 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_lifetime_col: The column where to store the lifetime [default: sess_lifetime] - * * db_time_col: The column where to store the timestamp [default: sess_time] + * * db_expiry_col: The column where to store the expiry timestamp [default: sess_expiry] * * db_username: The username when lazy-connect [default: ''] * * db_password: The password when lazy-connect [default: ''] * * db_connection_options: An array of driver-specific connection options [default: array()] @@ -187,8 +181,7 @@ public function __construct($pdoOrDsn = null, array $options = array()) $this->table = isset($options['db_table']) ? $options['db_table'] : $this->table; $this->idCol = isset($options['db_id_col']) ? $options['db_id_col'] : $this->idCol; $this->dataCol = isset($options['db_data_col']) ? $options['db_data_col'] : $this->dataCol; - $this->lifetimeCol = isset($options['db_lifetime_col']) ? $options['db_lifetime_col'] : $this->lifetimeCol; - $this->timeCol = isset($options['db_time_col']) ? $options['db_time_col'] : $this->timeCol; + $this->expiryCol = isset($options['db_expiry_col']) ? $options['db_expiry_col'] : $this->expiryCol; $this->username = isset($options['db_username']) ? $options['db_username'] : $this->username; $this->password = isset($options['db_password']) ? $options['db_password'] : $this->password; $this->connectionOptions = isset($options['db_connection_options']) ? $options['db_connection_options'] : $this->connectionOptions; @@ -218,19 +211,19 @@ public function createTable() // - trailing space removal // - case-insensitivity // - language processing like é == e - $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB"; + $sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->expiryCol INTEGER UNSIGNED NOT NULL) COLLATE utf8_bin, ENGINE = InnoDB"; break; case 'sqlite': - $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + $sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->expiryCol INTEGER NOT NULL)"; break; case 'pgsql': - $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->expiryCol INTEGER NOT NULL)"; break; case 'oci': - $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->expiryCol INTEGER NOT NULL)"; break; case 'sqlsrv': - $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)"; + $sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->expiryCol INTEGER NOT NULL)"; break; default: throw new \DomainException(sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); @@ -331,20 +324,18 @@ public function write($sessionId, $data) $mergeStmt = $this->pdo->prepare($mergeSql); $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); - $mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); - $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $mergeStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); $mergeStmt->execute(); return true; } $updateStmt = $this->pdo->prepare( - "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id" + "UPDATE $this->table SET $this->dataCol = :data, $this->expiryCol = :expiry WHERE $this->idCol = :id" ); $updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $updateStmt->bindParam(':data', $data, \PDO::PARAM_LOB); - $updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); - $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $updateStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); $updateStmt->execute(); // When MERGE is not supported, like in Postgres, we have to use this approach that can result in @@ -355,12 +346,11 @@ public function write($sessionId, $data) if (!$updateStmt->rowCount()) { try { $insertStmt = $this->pdo->prepare( - "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)" + "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry)" ); $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB); - $insertStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT); - $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $insertStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); $insertStmt->execute(); } catch (\PDOException $e) { // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys @@ -395,7 +385,7 @@ public function close() $this->gcCalled = false; // delete the session records that have expired - $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time"; + $sql = "DELETE FROM $this->table WHERE $this->expiryCol < :time"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':time', time(), \PDO::PARAM_INT); @@ -515,7 +505,7 @@ private function doRead($sessionId) $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); if ($sessionRows) { - if ($sessionRows[0][1] + $sessionRows[0][2] < time()) { + if ($sessionRows[0][1] < time()) { $this->sessionExpired = true; return ''; @@ -529,12 +519,11 @@ private function doRead($sessionId) // until other connections to the session are committed. try { $insertStmt = $this->pdo->prepare( - "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)" + "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry)" ); $insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); $insertStmt->bindValue(':data', '', \PDO::PARAM_LOB); - $insertStmt->bindValue(':lifetime', 0, \PDO::PARAM_INT); - $insertStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $insertStmt->bindValue(':expiry', 0, \PDO::PARAM_INT); $insertStmt->execute(); } catch (\PDOException $e) { // Catch duplicate key error because other connection created the session already. @@ -638,9 +627,9 @@ private function getSelectSql() case 'mysql': case 'oci': case 'pgsql': - return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; + return "SELECT $this->dataCol, $this->expiryCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; case 'sqlsrv': - return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; + return "SELECT $this->dataCol, $this->expiryCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; case 'sqlite': // we already locked when starting transaction break; @@ -649,7 +638,7 @@ private function getSelectSql() } } - return "SELECT $this->dataCol, $this->lifetimeCol, $this->timeCol FROM $this->table WHERE $this->idCol = :id"; + return "SELECT $this->dataCol, $this->expiryCol FROM $this->table WHERE $this->idCol = :id"; } /** @@ -661,26 +650,26 @@ private function getMergeSql() { switch ($this->driver) { case 'mysql': - return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". - "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; + return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry) ". + "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->expiryCol = VALUES($this->expiryCol)"; case 'oci': // 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->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time"; + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->expiryCol = :expiry"; case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='): // MERGE is only available since SQL Server 2008 and must be terminated by semicolon // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ". - "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ". - "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time;"; + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->expiryCol = :expiry;"; case 'sqlite': - return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"; + return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->expiryCol) VALUES (:id, :data, :expiry)"; } } /** - * Return a PDO instance + * Return a PDO instance. * * @return \PDO */ 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 ba06fccff910..bce06d832cbe 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php @@ -144,7 +144,7 @@ public function testReadConvertsStreamToString() $stream = $this->createStream($content); $pdo->prepareResult->expects($this->once())->method('fetchAll') - ->will($this->returnValue(array(array($stream, 42, time())))); + ->will($this->returnValue(array(array($stream, 42 + time())))); $storage = new PdoSessionHandler($pdo); $result = $storage->read('foo'); @@ -205,7 +205,7 @@ public function testReadingRequiresExactlySameId() } /** - * Simulates session_regenerate_id(true) which will require an INSERT or UPDATE (replace) + * Simulates session_regenerate_id(true) which will require an INSERT or UPDATE (replace). */ public function testWriteDifferentSessionIdThanRead() {