Skip to content

Commit

Permalink
[HttpFoundation] add create table method to pdo session handler
Browse files Browse the repository at this point in the history
  • Loading branch information
Tobion committed Sep 29, 2014
1 parent e79229d commit 182a5d3
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 51 deletions.
Expand Up @@ -26,7 +26,8 @@
*
* Session data is a binary string that can contain non-printable characters like the null byte.
* For this reason it must be saved in a binary column in the database like BLOB in MySQL.
* Saving it in a character column could corrupt the data.
* Saving it in a character column could corrupt the data. You can use createTable()
* to initialize a correctly defined table.
*
* @see http://php.net/sessionhandlerinterface
*
Expand Down Expand Up @@ -158,16 +159,58 @@ public function __construct($pdoOrDsn, array $options = array())
$this->connectionOptions = $options['db_connection_options'];
}

/**
* Creates the table to store sessions which can be called once for setup.
*
* Session ID is saved in a VARCHAR(128) column because that is enough even for
* a 512 bit configured session.hash_function like Whirlpool. Session data is
* saved in a BLOB. One could also use a shorter inlined varbinary column
* if one was sure the data fits into it.
*
* @throws \PDOException When the table already exists
* @throws \DomainException When an unsupported PDO driver is used
*/
public function createTable()
{
// connect if we are not yet
$this->getConnection();

switch ($this->driver) {
case 'mysql':
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol INTEGER NOT NULL) ENGINE = InnoDB";
break;
case 'sqlite':
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol MEDIUMINT NOT NULL, $this->timeCol 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)";
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)";
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)";
break;
default:
throw new \DomainException(sprintf('"%s" does not currently support PDO driver "%s".', __CLASS__, $this->driver));
}

try {
$this->pdo->exec($sql);
} catch (\PDOException $e) {
$this->rollback();

throw $e;
}
}

/**
* {@inheritdoc}
*/
public function open($savePath, $sessionName)
{
$this->gcCalled = false;
if (null === $this->pdo) {
$this->pdo = new \PDO($this->dsn ?: $savePath, $this->username, $this->password, $this->connectionOptions);
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
$this->connect($this->dsn ?: $savePath);
}

return true;
Expand Down Expand Up @@ -315,6 +358,8 @@ public function close()
$this->commit();

if ($this->gcCalled) {
$this->gcCalled = false;

// delete the session records that have expired
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol < :time";

Expand All @@ -330,6 +375,18 @@ public function close()
return true;
}

/**
* Lazy-connects to the database.
*
* @param string $dsn DSN string
*/
private function connect($dsn)
{
$this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions);
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
}

/**
* Helper method to begin a transaction.
*
Expand Down Expand Up @@ -399,6 +456,8 @@ private function rollback()
* INSERT when not found can result in a deadlock for one connection.
*
* @param string $sessionId Session ID
*
* @throws \DomainException When an unsupported PDO driver is used
*/
private function lockSession($sessionId)
{
Expand Down Expand Up @@ -429,8 +488,10 @@ private function lockSession($sessionId)
$stmt->execute();

return;
case 'sqlite':
return; // we already locked when starting transaction
default:
return;
throw new \DomainException(sprintf('"%s" does not currently support PDO driver "%s".', __CLASS__, $this->driver));
}

// We create a DML lock for the session by inserting empty data or updating the row.
Expand Down Expand Up @@ -478,6 +539,10 @@ private function getMergeSql()
*/
protected function getConnection()
{
if (null === $this->pdo) {
$this->connect($this->dsn ?: ini_get('session.save_path'));
}

return $this->pdo;
}
}
Expand Up @@ -15,55 +15,78 @@

class PdoSessionHandlerTest extends \PHPUnit_Framework_TestCase
{
private $pdo;
private $dbFile;

protected function setUp()
{
if (!class_exists('PDO') || !in_array('sqlite', \PDO::getAvailableDrivers())) {
$this->markTestSkipped('This test requires SQLite support in your environment');
}
}

protected function tearDown()
{
// make sure the temporary database file is deleted when it has been created (even when a test fails)
if ($this->dbFile) {
@unlink($this->dbFile);
}
}

protected function getPersistentSqliteDsn()
{
$this->dbFile = tempnam(sys_get_temp_dir(), 'sf2_sqlite_sessions');

$this->pdo = new \PDO('sqlite::memory:');
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data BLOB, sess_lifetime MEDIUMINT, sess_time INTEGER)';
$this->pdo->exec($sql);
return 'sqlite:' . $this->dbFile;
}

protected function getMemorySqlitePdo()
{
$pdo = new \PDO('sqlite::memory:');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$storage = new PdoSessionHandler($pdo);
$storage->createTable();

return $pdo;
}

/**
* @expectedException \InvalidArgumentException
*/
public function testWrongPdoErrMode()
{
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT);
$pdo = $this->getMemorySqlitePdo();
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT);

$storage = new PdoSessionHandler($this->pdo);
$storage = new PdoSessionHandler($pdo);
}

/**
* @expectedException \RuntimeException
*/
public function testInexistentTable()
{
$storage = new PdoSessionHandler($this->pdo, array('db_table' => 'inexistent_table'));
$storage = new PdoSessionHandler($this->getMemorySqlitePdo(), array('db_table' => 'inexistent_table'));
$storage->open('', 'sid');
$storage->read('id');
$storage->write('id', 'data');
$storage->close();
}

public function testWithLazyDnsConnection()
/**
* @expectedException \RuntimeException
*/
public function testCreateTableTwice()
{
$dbFile = tempnam(sys_get_temp_dir(), 'sf2_sqlite_sessions');
if (file_exists($dbFile)) {
@unlink($dbFile);
}
$storage = new PdoSessionHandler($this->getMemorySqlitePdo());
$storage->createTable();
}

$pdo = new \PDO('sqlite:' . $dbFile);
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data BLOB, sess_lifetime MEDIUMINT, sess_time INTEGER)';
$pdo->exec($sql);
$pdo = null;
public function testWithLazyDsnConnection()
{
$dsn = $this->getPersistentSqliteDsn();

$storage = new PdoSessionHandler('sqlite:' . $dbFile);
$storage = new PdoSessionHandler($dsn);
$storage->createTable();
$storage->open('', 'sid');
$data = $storage->read('id');
$storage->write('id', 'data');
Expand All @@ -74,43 +97,32 @@ public function testWithLazyDnsConnection()
$data = $storage->read('id');
$storage->close();
$this->assertSame('data', $data, 'Written value can be read back correctly');

@unlink($dbFile);
}

public function testWithLazySavePathConnection()
{
$dbFile = tempnam(sys_get_temp_dir(), 'sf2_sqlite_sessions');
if (file_exists($dbFile)) {
@unlink($dbFile);
}
$dsn = $this->getPersistentSqliteDsn();

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

// Open is called with what ini_set('session.save_path', 'sqlite:' . $dbFile) would mean
// Open is called with what ini_set('session.save_path', $dsn) would mean
$storage = new PdoSessionHandler(null);
$storage->open('sqlite:' . $dbFile, 'sid');
$storage->open($dsn, 'sid');
$storage->createTable();
$data = $storage->read('id');
$storage->write('id', 'data');
$storage->close();
$this->assertSame('', $data, 'New session returns empty string data');

$storage->open('sqlite:' . $dbFile, 'sid');
$storage->open($dsn, 'sid');
$data = $storage->read('id');
$storage->close();
$this->assertSame('data', $data, 'Written value can be read back correctly');

@unlink($dbFile);
}

public function testReadWriteReadWithNullByte()
{
$sessionData = 'da' . "\0" . 'ta';

$storage = new PdoSessionHandler($this->pdo);
$storage = new PdoSessionHandler($this->getMemorySqlitePdo());
$storage->open('', 'sid');
$readData = $storage->read('id');
$storage->write('id', $sessionData);
Expand All @@ -128,7 +140,7 @@ public function testReadWriteReadWithNullByte()
*/
public function testWriteDifferentSessionIdThanRead()
{
$storage = new PdoSessionHandler($this->pdo);
$storage = new PdoSessionHandler($this->getMemorySqlitePdo());
$storage->open('', 'sid');
$storage->read('id');
$storage->destroy('id');
Expand All @@ -139,13 +151,13 @@ public function testWriteDifferentSessionIdThanRead()
$data = $storage->read('new_id');
$storage->close();

$this->assertSame('data_of_new_session_id', $data, 'Data of regenerated session id is available');
$this->assertSame('data_of_new_session_id', $data, 'Data of regenerated session id is available');
}

public function testWrongUsageStillWorks()
{
// wrong method sequence that should no happen, but still works
$storage = new PdoSessionHandler($this->pdo);
$storage = new PdoSessionHandler($this->getMemorySqlitePdo());
$storage->write('id', 'data');
$storage->write('other_id', 'other_data');
$storage->destroy('inexistent');
Expand All @@ -160,19 +172,20 @@ public function testWrongUsageStillWorks()

public function testSessionDestroy()
{
$storage = new PdoSessionHandler($this->pdo);
$pdo = $this->getMemorySqlitePdo();
$storage = new PdoSessionHandler($pdo);

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

$storage->open('', 'sid');
$storage->read('id');
$storage->destroy('id');
$storage->close();
$this->assertEquals(0, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
$this->assertEquals(0, $pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());

$storage->open('', 'sid');
$data = $storage->read('id');
Expand All @@ -183,7 +196,8 @@ public function testSessionDestroy()
public function testSessionGC()
{
$previousLifeTime = ini_set('session.gc_maxlifetime', 1000);
$storage = new PdoSessionHandler($this->pdo);
$pdo = $this->getMemorySqlitePdo();
$storage = new PdoSessionHandler($pdo);

$storage->open('', 'sid');
$storage->read('id');
Expand All @@ -195,7 +209,7 @@ public function testSessionGC()
ini_set('session.gc_maxlifetime', -1); // test that you can set lifetime of a session after it has been read
$storage->write('gc_id', 'data');
$storage->close();
$this->assertEquals(2, $this->pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn(), 'No session pruned because gc not called');
$this->assertEquals(2, $pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn(), 'No session pruned because gc not called');

$storage->open('', 'sid');
$data = $storage->read('gc_id');
Expand All @@ -205,12 +219,22 @@ public function testSessionGC()
ini_set('session.gc_maxlifetime', $previousLifeTime);

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

public function testGetConnection()
{
$storage = new PdoSessionHandler($this->pdo);
$storage = new PdoSessionHandler($this->getMemorySqlitePdo());

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

$this->assertInstanceOf('\PDO', $method->invoke($storage));
}

public function testGetConnectionConnectsIfNeeded()
{
$storage = new PdoSessionHandler('sqlite::memory:');

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

0 comments on commit 182a5d3

Please sign in to comment.