From 518c456477b131ed890e2c2fe6708d72c8869c0d Mon Sep 17 00:00:00 2001 From: Steven Ngesera Date: Sat, 23 Nov 2024 06:53:44 +0300 Subject: [PATCH] Revamp db session storage to work in exclusive locking mode like native file session storage works --- config/database.php | 2 +- lib/session_db.php | 106 +++++++++++++++++++++++++++++++++++++ scripts/setup_database.php | 5 +- 3 files changed, 111 insertions(+), 2 deletions(-) diff --git a/config/database.php b/config/database.php index cbd0d43f12..fae0540a3e 100644 --- a/config/database.php +++ b/config/database.php @@ -75,7 +75,7 @@ | CREATE TABLE hm_user_session (hm_id varchar(250) primary key not null, data text, date timestamp); | | MySQL or SQLite: - | CREATE TABLE hm_user_session (hm_id varchar(180), data longblob, date timestamp, primary key (hm_id)); + | CREATE TABLE hm_user_session (hm_id varchar(180), data longblob, lock INTEGER DEFAULT 0, date timestamp, primary key (hm_id)); | | | DB Authentication diff --git a/lib/session_db.php b/lib/session_db.php index d606eb2f46..81d0398cb1 100644 --- a/lib/session_db.php +++ b/lib/session_db.php @@ -18,6 +18,16 @@ class Hm_DB_Session extends Hm_PHP_Session { /* DB handle */ protected $dbh; + /* + * Database driver type from site config + */ + private $db_driver; + + /* + * Timeout for acquiring locks (seconds) + */ + private $lock_timeout = 10; + /** * Create a new session * @return boolean|integer|array @@ -31,6 +41,7 @@ public function insert_session_row() { * @return bool true on success */ public function connect() { + $this->db_driver = $this->site_config->get('db_driver', false); return ($this->dbh = Hm_DB::connect($this->site_config)) ? true : false; } @@ -66,12 +77,17 @@ public function start_new($request) { */ public function start_existing($key) { $this->session_key = $key; + if (!$this->acquire_lock($key)) { + Hm_Debug::add('DB SESSION: Failed to acquire lock'); + return; + } $data = $this->get_session_data($key); if (is_array($data)) { Hm_Debug::add('LOGGED IN'); $this->active = true; $this->data = $data; } + $this->release_lock($key); } /** @@ -169,4 +185,94 @@ public function db_start($request) { } } } + + /** + * Acquire a lock for the session (unified for all DB types) + * @param string $key session key + * @return bool true if lock acquired, false otherwise + */ + private function acquire_lock($key) { + $lock_name = 'session_lock_' . substr(hash('sha256', $key), 0, 51); + $query = ''; + $params = []; + + switch ($this->db_driver) { + case 'mysql': + $query = 'SELECT GET_LOCK(:lock_name, :timeout)'; + $params = [':lock_name' => $lock_name, ':timeout' => $this->lock_timeout]; + break; + + case 'pgsql': + $query = 'SELECT pg_try_advisory_lock(:hash_key)'; + $params = [':hash_key' => crc32($lock_name)]; + break; + + case 'sqlite': + $query = 'UPDATE hm_user_session SET lock=1 WHERE hm_id=? AND lock=0'; + $params = [$key]; + break; + + default: + Hm_Debug::add('DB SESSION: Unsupported db_driver for locking: ' . $this->db_driver); + return false; + } + + $result = Hm_DB::execute($this->dbh, $query, $params); + if ($this->db_driver == 'mysql') { + return isset($result['GET_LOCK(?, ?)']) && $result['GET_LOCK(?, ?)'] == 1; + } + if ($this->db_driver == 'pgsql') { + return isset($result['pg_try_advisory_lock']) && $result['pg_try_advisory_lock'] === true; + } + + if ($this->db_driver == 'sqlite') { + return isset($result[0]) && $result[0] == 1; + } + return false; + } + + /** + * Release a lock for the session (unified for all DB types) + * @param string $key session key + * @return bool true if lock released, false otherwise + */ + private function release_lock($key) { + $query = ''; + $params = []; + + $lock_name = "session_lock_" . substr(hash('sha256', $key), 0, 51); + switch ($this->db_driver) { + case 'mysql': + $query = 'SELECT RELEASE_LOCK(:lock_name)'; + $params = [':lock_name' => $lock_name]; + break; + + case 'pgsql': + $query = 'SELECT pg_advisory_unlock(:hash_key)'; + $params = [':hash_key' => crc32($lock_name)]; + break; + + case 'sqlite': + $query = 'UPDATE hm_user_session SET lock=0 WHERE hm_id=?'; + $params = [$key]; + break; + + default: + Hm_Debug::add('DB SESSION: Unsupported db_driver for unlocking: ' . $this->db_driver); + return false; + } + $result = Hm_DB::execute($this->dbh, $query, $params); + if ($this->db_driver == 'mysql') { + return isset($result['GET_LOCK(?, ?)']) && $result['GET_LOCK(?, ?)'] == 1; + } + if ($this->db_driver == 'pgsql') { + return isset($result['pg_advisory_unlock']) && $result['pg_advisory_unlock'] === true; + } + + if ($this->db_driver == 'sqlite') { + return isset($result[0]) && $result[0] == 1; + } + Hm_Debug::add('DB SESSION: Lock release failed. Query: ' . $query . ' Parameters: ' . json_encode($params)); + return false; + } } diff --git a/scripts/setup_database.php b/scripts/setup_database.php index 3c575e5caf..f18418daa3 100755 --- a/scripts/setup_database.php +++ b/scripts/setup_database.php @@ -86,8 +86,11 @@ if (strcasecmp($session_type, 'DB')==0) { printf("Creating database table hm_user_session ...\n"); - if ($db_driver == 'mysql' || $db_driver == 'sqlite') { + if ($db_driver == 'mysql') { $stmt = "{$create_table} hm_user_session (hm_id varchar(255), data longblob, date timestamp, primary key (hm_id));"; + } elseif($db_driver == 'sqlite') { + //0 means unlocked, 1 means locked + $stmt = "{$create_table} hm_user_session (hm_id varchar(255), data longblob, lock INTEGER DEFAULT 0, date timestamp, primary key (hm_id));"; } elseif ($db_driver == 'pgsql') { $stmt = "{$create_table} hm_user_session (hm_id varchar(255) primary key not null, data text, date timestamp);"; } else {