diff --git a/db/migrations/2018_08_30_000000_create_log_entries_table.php b/db/migrations/2018_08_30_000000_create_log_entries_table.php index 68815434..bef78712 100644 --- a/db/migrations/2018_08_30_000000_create_log_entries_table.php +++ b/db/migrations/2018_08_30_000000_create_log_entries_table.php @@ -17,12 +17,14 @@ class CreateLogEntriesTable extends Migration $table->timestamp('created_at')->nullable(); }); - $this->schema->getConnection()->unprepared(' - INSERT INTO log_entries (`id`, `level`, `message`, `created_at`) - SELECT `id`, `level`, `message`, FROM_UNIXTIME(`timestamp`) FROM LogEntries - '); + if ($this->schema->hasTable('LogEntries')) { + $this->schema->getConnection()->unprepared(' + INSERT INTO log_entries (`id`, `level`, `message`, `created_at`) + SELECT `id`, `level`, `message`, FROM_UNIXTIME(`timestamp`) FROM LogEntries + '); - $this->schema->dropIfExists('LogEntries'); + $this->schema->drop('LogEntries'); + } } /** diff --git a/db/migrations/2018_09_11_000000_create_sessions_table.php b/db/migrations/2018_09_11_000000_create_sessions_table.php index 0af96d33..33a9f569 100644 --- a/db/migrations/2018_09_11_000000_create_sessions_table.php +++ b/db/migrations/2018_09_11_000000_create_sessions_table.php @@ -13,8 +13,7 @@ class CreateSessionsTable extends Migration $this->schema->create('sessions', function (Blueprint $table) { $table->string('id')->unique(); $table->text('payload'); - $table->integer('last_activity'); - $table->integer('lifetime'); + $table->dateTime('last_activity')->useCurrent(); }); } diff --git a/src/Database/Migration/Migrate.php b/src/Database/Migration/Migrate.php index cec8bc4a..9c6d3e43 100644 --- a/src/Database/Migration/Migrate.php +++ b/src/Database/Migration/Migrate.php @@ -76,6 +76,21 @@ class Migrate } } + /** + * Setup migration tables + */ + public function initMigration() + { + if ($this->schema->hasTable($this->table)) { + return; + } + + $this->schema->create($this->table, function (Blueprint $table) { + $table->increments('id'); + $table->string('migration'); + }); + } + /** * Get all migrated migrations * @@ -155,21 +170,6 @@ class Migrate return glob($dir . '/*_*.php'); } - /** - * Setup migration tables - */ - protected function initMigration() - { - if ($this->schema->hasTable($this->table)) { - return; - } - - $this->schema->create($this->table, function (Blueprint $table) { - $table->increments('id'); - $table->string('migration'); - }); - } - /** * Init a table query * diff --git a/src/Http/SessionHandlers/AbstractHandler.php b/src/Http/SessionHandlers/AbstractHandler.php new file mode 100644 index 00000000..135d0d43 --- /dev/null +++ b/src/Http/SessionHandlers/AbstractHandler.php @@ -0,0 +1,75 @@ +name = $name; + $this->sessionPath = $sessionPath; + + return true; + } + + /** + * Shutdown the session handler + * + * @return bool + */ + public function close(): bool + { + return true; + } + + /** + * Remove old sessions + * + * @param int $maxLifetime + * @return bool + */ + public function gc($maxLifetime): bool + { + return true; + } + + /** + * Read session data + * + * @param string $id + * @return string + */ + abstract public function read($id): string; + + /** + * Write session data + * + * @param string $id + * @param string $data + * @return bool + */ + abstract public function write($id, $data): bool; + + /** + * Delete a session + * + * @param string $id + * @return bool + */ + abstract public function destroy($id): bool; +} diff --git a/src/Http/SessionHandlers/DatabaseHandler.php b/src/Http/SessionHandlers/DatabaseHandler.php new file mode 100644 index 00000000..8df70287 --- /dev/null +++ b/src/Http/SessionHandlers/DatabaseHandler.php @@ -0,0 +1,108 @@ +database = $database; + } + + /** + * {@inheritdoc} + */ + public function read($id): string + { + $session = $this->getQuery() + ->where('id', '=', $id) + ->first(); + + return $session ? $session->payload : ''; + } + + /** + * {@inheritdoc} + */ + public function write($id, $data): bool + { + $values = [ + 'payload' => $data, + 'last_activity' => $this->getCurrentTimestamp(), + ]; + + $session = $this->getQuery() + ->where('id', '=', $id) + ->first(); + + if (!$session) { + return $this->getQuery() + ->insert($values + [ + 'id' => $id, + ]); + } + + $this->getQuery() + ->where('id', '=', $id) + ->update($values); + + // The update return can't be used directly because it won't change if the second call is in the same second + return true; + } + + /** + * {@inheritdoc} + */ + public function destroy($id): bool + { + $this->getQuery() + ->where('id', '=', $id) + ->delete(); + + return true; + } + + /** + * {@inheritdoc} + */ + public function gc($maxLifetime): bool + { + $timestamp = $this->getCurrentTimestamp(-$maxLifetime); + + $this->getQuery() + ->where('last_activity', '<', $timestamp) + ->delete(); + + return true; + } + + /** + * @return QueryBuilder + */ + protected function getQuery(): QueryBuilder + { + return $this->database + ->getConnection() + ->table('sessions'); + } + + /** + * Format the SQL timestamp + * + * @param int $diff + * @return string + */ + protected function getCurrentTimestamp(int $diff = 0): string + { + return date('Y-m-d H:i:s', strtotime(sprintf('%+d seconds', $diff))); + } +} diff --git a/src/Http/SessionServiceProvider.php b/src/Http/SessionServiceProvider.php index 66ff18cc..c2e09624 100644 --- a/src/Http/SessionServiceProvider.php +++ b/src/Http/SessionServiceProvider.php @@ -4,8 +4,8 @@ namespace Engelsystem\Http; use Engelsystem\Config\Config; use Engelsystem\Container\ServiceProvider; +use Engelsystem\Http\SessionHandlers\DatabaseHandler; use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; @@ -45,20 +45,9 @@ class SessionServiceProvider extends ServiceProvider $sessionConfig = $config->get('session'); $handler = null; - $driver = $sessionConfig['driver']; - - switch ($driver) { + switch ($sessionConfig['driver']) { case 'pdo': - $handler = $this->app->make(PdoSessionHandler::class, [ - 'pdoOrDsn' => $this->app->get('db.pdo'), - 'options' => [ - 'db_table' => 'sessions', - 'db_id_col' => 'id', - 'db_data_col' => 'payload', - 'db_lifetime_col' => 'lifetime', - 'db_time_col' => 'last_activity', - ], - ]); + $handler = $this->app->make(DatabaseHandler::class); break; } diff --git a/tests/Unit/HasDatabase.php b/tests/Unit/HasDatabase.php new file mode 100644 index 00000000..d69f0a3a --- /dev/null +++ b/tests/Unit/HasDatabase.php @@ -0,0 +1,47 @@ +addConnection(['driver' => 'sqlite', 'database' => ':memory:']); + + $connection = $dbManager->getConnection(); + $connection->getPdo()->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->database = new Database($connection); + + $app = new Application(); + $app->instance(Database::class, $this->database); + $app->register(MigrationServiceProvider::class); + + /** @var Migrate $migration */ + $migration = $app->get('db.migration'); + $migration->initMigration(); + + $this->database + ->getConnection() + ->table('migrations') + ->insert([ + ['migration' => '2018_01_01_000001_import_install_sql'], + ['migration' => '2018_01_01_000002_import_update_sql'], + ]); + + $migration->run(__DIR__ . '/../../db/migrations'); + } +} diff --git a/tests/Unit/Http/SessionHandlers/AbstractHandlerTest.php b/tests/Unit/Http/SessionHandlers/AbstractHandlerTest.php new file mode 100644 index 00000000..bfd2e883 --- /dev/null +++ b/tests/Unit/Http/SessionHandlers/AbstractHandlerTest.php @@ -0,0 +1,44 @@ +open('/foo/bar', '1337asd098hkl7654'); + + $this->assertTrue($return); + $this->assertEquals('1337asd098hkl7654', $handler->getName()); + $this->assertEquals('/foo/bar', $handler->getSessionPath()); + } + + /** + * @covers \Engelsystem\Http\SessionHandlers\AbstractHandler::close + */ + public function testClose() + { + $handler = new ArrayHandler(); + $return = $handler->close(); + + $this->assertTrue($return); + } + + /** + * @covers \Engelsystem\Http\SessionHandlers\AbstractHandler::gc + */ + public function testGc() + { + $handler = new ArrayHandler(); + $return = $handler->gc(60 * 60 * 24); + + $this->assertTrue($return); + } +} diff --git a/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php b/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php new file mode 100644 index 00000000..ea4f3701 --- /dev/null +++ b/tests/Unit/Http/SessionHandlers/DatabaseHandlerTest.php @@ -0,0 +1,95 @@ +database); + $this->assertEquals('', $handler->read('foo')); + + $this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', CURRENT_TIMESTAMP)"); + $this->assertEquals('Lorem Ipsum', $handler->read('foo')); + } + + /** + * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::write + * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::getCurrentTimestamp + */ + public function testWrite() + { + $handler = new DatabaseHandler($this->database); + + foreach (['Lorem Ipsum', 'Dolor Sit!'] as $data) { + $this->assertTrue($handler->write('foo', $data)); + + $return = $this->database->select('SELECT * FROM sessions WHERE id = :id', ['id' => 'foo']); + $this->assertCount(1, $return); + + $return = array_shift($return); + $this->assertEquals($data, $return->payload); + } + } + + /** + * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::destroy + */ + public function testDestroy() + { + $this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', CURRENT_TIMESTAMP)"); + $this->database->insert("INSERT INTO sessions VALUES ('bar', 'Dolor Sit', CURRENT_TIMESTAMP)"); + + $handler = new DatabaseHandler($this->database); + $this->assertTrue($handler->destroy('batz')); + + $return = $this->database->select('SELECT * FROM sessions'); + $this->assertCount(2, $return); + + $this->assertTrue($handler->destroy('bar')); + + $return = $this->database->select('SELECT * FROM sessions'); + $this->assertCount(1, $return); + + $return = array_shift($return); + $this->assertEquals('foo', $return->id); + } + + /** + * @covers \Engelsystem\Http\SessionHandlers\DatabaseHandler::gc + */ + public function testGc() + { + $this->database->insert("INSERT INTO sessions VALUES ('foo', 'Lorem Ipsum', '2000-01-01 01:00')"); + $this->database->insert("INSERT INTO sessions VALUES ('bar', 'Dolor Sit', '3000-01-01 01:00')"); + + $handler = new DatabaseHandler($this->database); + + $this->assertTrue($handler->gc(60 * 60)); + + $return = $this->database->select('SELECT * FROM sessions'); + $this->assertCount(1, $return); + + $return = array_shift($return); + $this->assertEquals('bar', $return->id); + } + + /** + * Prepare tests + */ + protected function setUp() + { + $this->initDatabase(); + } +} diff --git a/tests/Unit/Http/SessionHandlers/Stub/ArrayHandler.php b/tests/Unit/Http/SessionHandlers/Stub/ArrayHandler.php new file mode 100644 index 00000000..4d37da48 --- /dev/null +++ b/tests/Unit/Http/SessionHandlers/Stub/ArrayHandler.php @@ -0,0 +1,59 @@ +content[$id])) { + return $this->content[$id]; + } + + return ''; + } + + /** + * {@inheritdoc} + */ + public function write($id, $data): bool + { + $this->content[$id] = $data; + + return true; + } + + /** + * {@inheritdoc} + */ + public function destroy($id): bool + { + unset($this->content[$id]); + + return true; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getSessionPath(): string + { + return $this->sessionPath; + } +} diff --git a/tests/Unit/Http/SessionServiceProviderTest.php b/tests/Unit/Http/SessionServiceProviderTest.php index 5e4575b3..dd0e538f 100644 --- a/tests/Unit/Http/SessionServiceProviderTest.php +++ b/tests/Unit/Http/SessionServiceProviderTest.php @@ -4,12 +4,11 @@ namespace Engelsystem\Test\Unit\Http; use Engelsystem\Config\Config; use Engelsystem\Http\Request; +use Engelsystem\Http\SessionHandlers\DatabaseHandler; use Engelsystem\Http\SessionServiceProvider; use Engelsystem\Test\Unit\ServiceProviderTest; -use PDO; use PHPUnit_Framework_MockObject_MockObject as MockObject; use Symfony\Component\HttpFoundation\Session\Session; -use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface as StorageInterface; @@ -26,7 +25,7 @@ class SessionServiceProviderTest extends ServiceProviderTest $sessionStorage = $this->getMockForAbstractClass(StorageInterface::class); $sessionStorage2 = $this->getMockForAbstractClass(StorageInterface::class); - $pdoSessionHandler = $this->getMockBuilder(PdoSessionHandler::class) + $databaseHandler = $this->getMockBuilder(DatabaseHandler::class) ->disableOriginalConstructor() ->getMock(); @@ -41,10 +40,6 @@ class SessionServiceProviderTest extends ServiceProviderTest /** @var Config|MockObject $config */ $config = $this->createMock(Config::class); - /** @var PDO|MockObject $pdo */ - $pdo = $this->getMockBuilder(PDO::class) - ->disableOriginalConstructor() - ->getMock(); $serviceProvider->expects($this->exactly(3)) ->method('isCli') @@ -60,22 +55,10 @@ class SessionServiceProviderTest extends ServiceProviderTest ['options' => ['cookie_httponly' => true, 'name' => 'session'], 'handler' => null] ], [Session::class], - [ - PdoSessionHandler::class, - [ - 'pdoOrDsn' => $pdo, - 'options' => [ - 'db_table' => 'sessions', - 'db_id_col' => 'id', - 'db_data_col' => 'payload', - 'db_lifetime_col' => 'lifetime', - 'db_time_col' => 'last_activity', - ], - ] - ], + [DatabaseHandler::class], [ NativeSessionStorage::class, - ['options' => ['cookie_httponly' => true, 'name' => 'foobar'], 'handler' => $pdoSessionHandler] + ['options' => ['cookie_httponly' => true, 'name' => 'foobar'], 'handler' => $databaseHandler] ], [Session::class] ) @@ -84,7 +67,7 @@ class SessionServiceProviderTest extends ServiceProviderTest $session, $sessionStorage2, $session, - $pdoSessionHandler, + $databaseHandler, $sessionStorage2, $session ); @@ -96,14 +79,13 @@ class SessionServiceProviderTest extends ServiceProviderTest ['session', $session] ); - $app->expects($this->exactly(6)) + $app->expects($this->exactly(5)) ->method('get') ->withConsecutive( ['request'], ['config'], ['request'], ['config'], - ['db.pdo'], ['request'] ) ->willReturnOnConsecutiveCalls( @@ -111,7 +93,6 @@ class SessionServiceProviderTest extends ServiceProviderTest $config, $request, $config, - $pdo, $request );