Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e79229d

Browse files
committed
[HttpFoundation] allow different lifetime per session
1 parent af1bb1f commit e79229d

File tree

2 files changed

+79
-61
lines changed

2 files changed

+79
-61
lines changed

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ class PdoSessionHandler implements \SessionHandlerInterface
4242
private $pdo;
4343

4444
/**
45-
* @var string|null|false DNS string or null for session.save_path or false when lazy connection disabled
45+
* @var string|null|false DSN string or null for session.save_path or false when lazy connection disabled
4646
*/
47-
private $dns = false;
47+
private $dsn = false;
4848

4949
/**
5050
* @var string Database driver
@@ -66,6 +66,11 @@ class PdoSessionHandler implements \SessionHandlerInterface
6666
*/
6767
private $dataCol;
6868

69+
/**
70+
* @var string Column for lifetime
71+
*/
72+
private $lifetimeCol;
73+
6974
/**
7075
* @var string Column for timestamp
7176
*/
@@ -100,40 +105,43 @@ class PdoSessionHandler implements \SessionHandlerInterface
100105
* Constructor.
101106
*
102107
* You can either pass an existing database connection as PDO instance or
103-
* pass a DNS string that will be used to lazy-connect to the database
108+
* pass a DSN string that will be used to lazy-connect to the database
104109
* when the session is actually used. Furthermore it's possible to pass null
105-
* which will then use the session.save_path ini setting as PDO DNS parameter.
110+
* which will then use the session.save_path ini setting as PDO DSN parameter.
106111
*
107112
* List of available options:
108113
* * db_table: The name of the table [default: sessions]
109114
* * db_id_col: The column where to store the session id [default: sess_id]
110115
* * db_data_col: The column where to store the session data [default: sess_data]
116+
* * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime]
111117
* * db_time_col: The column where to store the timestamp [default: sess_time]
112118
* * db_username: The username when lazy-connect [default: '']
113119
* * db_password: The password when lazy-connect [default: '']
114120
* * db_connection_options: An array of driver-specific connection options [default: array()]
115121
*
116-
* @param \PDO|string|null $pdoOrDns A \PDO instance or DNS string or null
122+
* @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null
117123
* @param array $options An associative array of DB options
118124
*
119125
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
120126
*/
121-
public function __construct($pdoOrDns, array $options = array())
127+
public function __construct($pdoOrDsn, array $options = array())
122128
{
123-
if ($pdoOrDns instanceof \PDO) {
124-
if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDns->getAttribute(\PDO::ATTR_ERRMODE)) {
129+
if ($pdoOrDsn instanceof \PDO) {
130+
if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
125131
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__));
126132
}
127133

128-
$this->pdo = $pdoOrDns;
134+
$this->pdo = $pdoOrDsn;
135+
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
129136
} else {
130-
$this->dns = $pdoOrDns;
137+
$this->dsn = $pdoOrDsn;
131138
}
132139

133140
$options = array_replace(array(
134141
'db_table' => 'sessions',
135142
'db_id_col' => 'sess_id',
136143
'db_data_col' => 'sess_data',
144+
'db_lifetime_col' => 'sess_lifetime',
137145
'db_time_col' => 'sess_time',
138146
'db_username' => '',
139147
'db_password' => '',
@@ -143,6 +151,7 @@ public function __construct($pdoOrDns, array $options = array())
143151
$this->table = $options['db_table'];
144152
$this->idCol = $options['db_id_col'];
145153
$this->dataCol = $options['db_data_col'];
154+
$this->lifetimeCol = $options['db_lifetime_col'];
146155
$this->timeCol = $options['db_time_col'];
147156
$this->username = $options['db_username'];
148157
$this->password = $options['db_password'];
@@ -156,10 +165,10 @@ public function open($savePath, $sessionName)
156165
{
157166
$this->gcCalled = false;
158167
if (null === $this->pdo) {
159-
$this->pdo = new \PDO($this->dns ?: $savePath, $this->username, $this->password, $this->connectionOptions);
168+
$this->pdo = new \PDO($this->dsn ?: $savePath, $this->username, $this->password, $this->connectionOptions);
160169
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
170+
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
161171
}
162-
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
163172

164173
return true;
165174
}
@@ -176,13 +185,12 @@ public function read($sessionId)
176185

177186
// We need to make sure we do not return session data that is already considered garbage according
178187
// to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes.
179-
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
180188

181-
$sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id AND $this->timeCol > :time";
189+
$sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id AND $this->lifetimeCol + $this->timeCol >= :time";
182190

183191
$stmt = $this->pdo->prepare($sql);
184192
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
185-
$stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT);
193+
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
186194
$stmt->execute();
187195

188196
// We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed
@@ -239,24 +247,28 @@ public function write($sessionId, $data)
239247
// do an insert or update even if we created a row in read() for locking.
240248
// We use a single MERGE SQL query when supported by the database.
241249

250+
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
251+
242252
try {
243253
$mergeSql = $this->getMergeSql();
244254

245255
if (null !== $mergeSql) {
246256
$mergeStmt = $this->pdo->prepare($mergeSql);
247257
$mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
248258
$mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
259+
$mergeStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
249260
$mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
250261
$mergeStmt->execute();
251262

252263
return true;
253264
}
254265

255266
$updateStmt = $this->pdo->prepare(
256-
"UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id"
267+
"UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id"
257268
);
258269
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
259270
$updateStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
271+
$updateStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
260272
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
261273
$updateStmt->execute();
262274

@@ -270,10 +282,11 @@ public function write($sessionId, $data)
270282
if (!$updateStmt->rowCount()) {
271283
try {
272284
$insertStmt = $this->pdo->prepare(
273-
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"
285+
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)"
274286
);
275287
$insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
276-
$insertStmt->bindParam(':data', $encoded, \PDO::PARAM_LOB);
288+
$insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
289+
$insertStmt->bindParam(':lifetime', $maxlifetime, \PDO::PARAM_INT);
277290
$insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
278291
$insertStmt->execute();
279292
} catch (\PDOException $e) {
@@ -302,17 +315,15 @@ public function close()
302315
$this->commit();
303316

304317
if ($this->gcCalled) {
305-
$maxlifetime = (int) ini_get('session.gc_maxlifetime');
306-
307318
// delete the session records that have expired
308-
$sql = "DELETE FROM $this->table WHERE $this->timeCol <= :time";
319+
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time";
309320

310321
$stmt = $this->pdo->prepare($sql);
311-
$stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT);
322+
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
312323
$stmt->execute();
313324
}
314325

315-
if (false !== $this->dns) {
326+
if (false !== $this->dsn) {
316327
$this->pdo = null;
317328
}
318329

@@ -329,20 +340,14 @@ public function close()
329340
*/
330341
private function beginTransaction()
331342
{
332-
if ($this->inTransaction) {
333-
$this->rollback();
334-
335-
throw new \BadMethodCallException(
336-
'Session handler methods have been invoked in wrong sequence. '.
337-
'Expected sequence: open() -> read() -> destroy() / write() -> close()');
338-
}
339-
340-
if ('sqlite' === $this->driver) {
341-
$this->pdo->exec('BEGIN IMMEDIATE TRANSACTION');
342-
} else {
343-
$this->pdo->beginTransaction();
343+
if (!$this->inTransaction) {
344+
if ('sqlite' === $this->driver) {
345+
$this->pdo->exec('BEGIN IMMEDIATE TRANSACTION');
346+
} else {
347+
$this->pdo->beginTransaction();
348+
}
349+
$this->inTransaction = true;
344350
}
345-
$this->inTransaction = true;
346351
}
347352

348353
/**
@@ -400,20 +405,20 @@ private function lockSession($sessionId)
400405
switch ($this->driver) {
401406
case 'mysql':
402407
// will also lock the row when actually nothing got updated (id = id)
403-
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
408+
$sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
404409
"ON DUPLICATE KEY UPDATE $this->idCol = $this->idCol";
405410
break;
406411
case 'oci':
407412
// DUAL is Oracle specific dummy table
408413
$sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
409-
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
414+
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
410415
"WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol";
411416
break;
412417
// todo: implement locking for SQL Server < 2008
413418
case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
414419
// MS SQL Server requires MERGE be terminated by semicolon
415420
$sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
416-
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
421+
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
417422
"WHEN MATCHED THEN UPDATE SET $this->idCol = $this->idCol;";
418423
break;
419424
case 'pgsql':
@@ -434,6 +439,7 @@ private function lockSession($sessionId)
434439
$stmt = $this->pdo->prepare($sql);
435440
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
436441
$stmt->bindValue(':data', '', \PDO::PARAM_STR);
442+
$stmt->bindValue(':lifetime', 0, \PDO::PARAM_INT);
437443
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
438444
$stmt->execute();
439445
}
@@ -447,21 +453,21 @@ private function getMergeSql()
447453
{
448454
switch ($this->driver) {
449455
case 'mysql':
450-
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
451-
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)";
456+
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
457+
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
452458
case 'oci':
453459
// DUAL is Oracle specific dummy table
454460
return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
455-
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
456-
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time";
461+
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
462+
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time";
457463
case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
458464
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
459465
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
460466
return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
461-
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
462-
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;";
467+
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time) ".
468+
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time;";
463469
case 'sqlite':
464-
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)";
470+
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
465471
}
466472
}
467473

src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ protected function setUp()
2525

2626
$this->pdo = new \PDO('sqlite::memory:');
2727
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
28-
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data BLOB, sess_time INTEGER)';
28+
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data BLOB, sess_lifetime MEDIUMINT, sess_time INTEGER)';
2929
$this->pdo->exec($sql);
3030
}
3131

@@ -59,7 +59,7 @@ public function testWithLazyDnsConnection()
5959
}
6060

6161
$pdo = new \PDO('sqlite:' . $dbFile);
62-
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data BLOB, sess_time INTEGER)';
62+
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data BLOB, sess_lifetime MEDIUMINT, sess_time INTEGER)';
6363
$pdo->exec($sql);
6464
$pdo = null;
6565

@@ -86,7 +86,7 @@ public function testWithLazySavePathConnection()
8686
}
8787

8888
$pdo = new \PDO('sqlite:' . $dbFile);
89-
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data BLOB, sess_time INTEGER)';
89+
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data BLOB, sess_lifetime MEDIUMINT, sess_time INTEGER)';
9090
$pdo->exec($sql);
9191
$pdo = null;
9292

@@ -138,18 +138,24 @@ public function testWriteDifferentSessionIdThanRead()
138138
$storage->open('', 'sid');
139139
$data = $storage->read('new_id');
140140
$storage->close();
141+
141142
$this->assertSame('data_of_new_session_id', $data, 'Data of regenerated session id is available');
142143
}
143144

144-
/**
145-
* @expectedException \BadMethodCallException
146-
*/
147-
public function testWrongUsage()
145+
public function testWrongUsageStillWorks()
148146
{
147+
// wrong method sequence that should no happen, but still works
149148
$storage = new PdoSessionHandler($this->pdo);
149+
$storage->write('id', 'data');
150+
$storage->write('other_id', 'other_data');
151+
$storage->destroy('inexistent');
150152
$storage->open('', 'sid');
151-
$storage->read('id');
152-
$storage->read('id');
153+
$data = $storage->read('id');
154+
$otherData = $storage->read('other_id');
155+
$storage->close();
156+
157+
$this->assertSame('data', $data);
158+
$this->assertSame('other_data', $otherData);
153159
}
154160

155161
public function testSessionDestroy()
@@ -176,29 +182,35 @@ public function testSessionDestroy()
176182

177183
public function testSessionGC()
178184
{
179-
$previousLifeTime = ini_set('session.gc_maxlifetime', 0);
185+
$previousLifeTime = ini_set('session.gc_maxlifetime', 1000);
180186
$storage = new PdoSessionHandler($this->pdo);
181187

182188
$storage->open('', 'sid');
183189
$storage->read('id');
184190
$storage->write('id', 'data');
185191
$storage->close();
186-
$this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
187192

188193
$storage->open('', 'sid');
189-
$data = $storage->read('id');
190-
$storage->gc(0);
194+
$storage->read('gc_id');
195+
ini_set('session.gc_maxlifetime', -1); // test that you can set lifetime of a session after it has been read
196+
$storage->write('gc_id', 'data');
197+
$storage->close();
198+
$this->assertEquals(2, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn(), 'No session pruned because gc not called');
199+
200+
$storage->open('', 'sid');
201+
$data = $storage->read('gc_id');
202+
$storage->gc(-1);
191203
$storage->close();
192204

193205
ini_set('session.gc_maxlifetime', $previousLifeTime);
194206

195207
$this->assertSame('', $data, 'Session already considered garbage, so not returning data even if it is not pruned yet');
196-
$this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
208+
$this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn(), 'Expired session is pruned');
197209
}
198210

199211
public function testGetConnection()
200212
{
201-
$storage = new PdoSessionHandler($this->pdo, array('db_table' => 'sessions'), array());
213+
$storage = new PdoSessionHandler($this->pdo);
202214

203215
$method = new \ReflectionMethod($storage, 'getConnection');
204216
$method->setAccessible(true);

0 commit comments

Comments
 (0)