Skip to content

Commit

Permalink
refactor: Database Session Handler
Browse files Browse the repository at this point in the history
  • Loading branch information
kenjis committed Feb 15, 2022
1 parent b6866de commit 5a93531
Show file tree
Hide file tree
Showing 7 changed files with 374 additions and 109 deletions.
20 changes: 19 additions & 1 deletion system/Config/Services.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
use CodeIgniter\Router\RouteCollectionInterface;
use CodeIgniter\Router\Router;
use CodeIgniter\Security\Security;
use CodeIgniter\Session\Handlers\Database\MySQLiHandler;
use CodeIgniter\Session\Handlers\Database\PostgreHandler;
use CodeIgniter\Session\Handlers\DatabaseHandler;
use CodeIgniter\Session\Session;
use CodeIgniter\Throttle\Throttler;
use CodeIgniter\Typography\Typography;
Expand All @@ -58,6 +61,7 @@
use Config\App;
use Config\Cache;
use Config\ContentSecurityPolicy as CSPConfig;
use Config\Database;
use Config\Email as EmailConfig;
use Config\Encryption as EncryptionConfig;
use Config\Exceptions as ExceptionsConfig;
Expand Down Expand Up @@ -585,7 +589,21 @@ public static function session(?App $config = null, bool $getShared = true)
$logger = AppServices::logger();

$driverName = $config->sessionDriver;
$driver = new $driverName($config, AppServices::request()->getIPAddress());

if ($driverName === DatabaseHandler::class) {
$DBGroup = $config->sessionDBGroup ?? config(Database::class)->defaultGroup;
$db = Database::connect($DBGroup);

$driver = $db->getPlatform();

if ($driver === 'MySQLi') {
$driverName = MySQLiHandler::class;
} elseif ($driver === 'Postgre') {
$driverName = PostgreHandler::class;
}
}

$driver = new $driverName($config, AppServices::request()->getIPAddress());
$driver->setLogger($logger);

$session = new Session($driver, $config);
Expand Down
53 changes: 53 additions & 0 deletions system/Session/Handlers/Database/MySQLiHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Session\Handlers\Database;

use CodeIgniter\Session\Handlers\DatabaseHandler;

/**
* Session handler for MySQLi
*/
class MySQLiHandler extends DatabaseHandler
{
/**
* Lock the session.
*/
protected function lockSession(string $sessionID): bool
{
$arg = md5($sessionID . ($this->matchIP ? '_' . $this->ipAddress : ''));
if ($this->db->query("SELECT GET_LOCK('{$arg}', 300) AS ci_session_lock")->getRow()->ci_session_lock) {
$this->lock = $arg;

return true;
}

return $this->fail();
}

/**
* Releases the lock, if any.
*/
protected function releaseLock(): bool
{
if (! $this->lock) {
return true;
}

if ($this->db->query("SELECT RELEASE_LOCK('{$this->lock}') AS ci_session_lock")->getRow()->ci_session_lock) {
$this->lock = false;

return true;
}

return $this->fail();
}
}
175 changes: 175 additions & 0 deletions system/Session/Handlers/Database/PostgreHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Session\Handlers\Database;

use CodeIgniter\Session\Handlers\DatabaseHandler;
use ReturnTypeWillChange;

/**
* Session handler for Postgre
*/
class PostgreHandler extends DatabaseHandler
{
/**
* Reads the session data from the session storage, and returns the results.
*
* @param string $id The session ID
*
* @return false|string Returns an encoded string of the read data.
* If nothing was read, it must return false.
*/
#[ReturnTypeWillChange]
public function read($id)
{
if ($this->lockSession($id) === false) {
$this->fingerprint = md5('');

return '';
}

if (! isset($this->sessionID)) {
$this->sessionID = $id;
}

$builder = $this->db->table($this->table)
->select("encode(data, 'base64') AS data")
->where('id', $id);

if ($this->matchIP) {
$builder = $builder->where('ip_address', $this->ipAddress);
}

$result = $builder->get()->getRow();

if ($result === null) {
// PHP7 will reuse the same SessionHandler object after
// ID regeneration, so we need to explicitly set this to
// FALSE instead of relying on the default ...
$this->rowExists = false;
$this->fingerprint = md5('');

return '';
}

$result = is_bool($result) ? '' : base64_decode(rtrim($result->data), true);

$this->fingerprint = md5($result);
$this->rowExists = true;

return $result;
}

/**
* Writes the session data to the session storage.
*
* @param string $id The session ID
* @param string $data The encoded session data
*/
public function write($id, $data): bool
{
if ($this->lock === false) {
return $this->fail();
}

if ($this->sessionID !== $id) {
$this->rowExists = false;
$this->sessionID = $id;
}

if ($this->rowExists === false) {
$insertData = [
'id' => $id,
'ip_address' => $this->ipAddress,
'data' => '\x' . bin2hex($data),
];

if (! $this->db->table($this->table)->set('timestamp', 'now()', false)->insert($insertData)) {
return $this->fail();
}

$this->fingerprint = md5($data);
$this->rowExists = true;

return true;
}

$builder = $this->db->table($this->table)->where('id', $id);

if ($this->matchIP) {
$builder = $builder->where('ip_address', $this->ipAddress);
}

$updateData = [];

if ($this->fingerprint !== md5($data)) {
$updateData['data'] = '\x' . bin2hex($data);
}

if (! $builder->set('timestamp', 'now()', false)->update($updateData)) {
return $this->fail();
}

$this->fingerprint = md5($data);

return true;
}

/**
* Cleans up expired sessions.
*
* @param int $max_lifetime Sessions that have not updated
* for the last max_lifetime seconds will be removed.
*
* @return false|int Returns the number of deleted sessions on success, or false on failure.
*/
#[ReturnTypeWillChange]
public function gc($max_lifetime)
{
$separator = '\'';
$interval = implode($separator, ['', "{$max_lifetime} second", '']);

return $this->db->table($this->table)->where('timestamp <', "now() - INTERVAL {$interval}", false)->delete() ? 1 : $this->fail();
}

/**
* Lock the session.
*/
protected function lockSession(string $sessionID): bool
{
$arg = "hashtext('{$sessionID}')" . ($this->matchIP ? ", hashtext('{$this->ipAddress}')" : '');
if ($this->db->simpleQuery("SELECT pg_advisory_lock({$arg})")) {
$this->lock = $arg;

return true;
}

return $this->fail();
}

/**
* Releases the lock, if any.
*/
protected function releaseLock(): bool
{
if (! $this->lock) {
return true;
}

if ($this->db->simpleQuery("SELECT pg_advisory_unlock({$this->lock})")) {
$this->lock = false;

return true;
}

return $this->fail();
}
}
Loading

0 comments on commit 5a93531

Please sign in to comment.