From 0cbd3326051d8107c2eb3bba7ddb81ee70aae6b4 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 19 Aug 2021 11:27:06 +0800 Subject: [PATCH] Make the session handlers all compatible with SessionHandlerInterface --- rector.php | 5 + system/Session/Handlers/ArrayHandler.php | 60 ++++---- system/Session/Handlers/BaseHandler.php | 6 +- system/Session/Handlers/DatabaseHandler.php | 101 ++++++------- system/Session/Handlers/FileHandler.php | 145 +++++++++---------- system/Session/Handlers/MemcachedHandler.php | 97 ++++++------- system/Session/Handlers/RedisHandler.php | 108 +++++++------- 7 files changed, 233 insertions(+), 289 deletions(-) diff --git a/rector.php b/rector.php index 7ed60ffb1382..a3db0f19c5ac 100644 --- a/rector.php +++ b/rector.php @@ -111,6 +111,11 @@ RecastingRemovalRector::class => [ __DIR__ . '/tests/system/Entity/EntityTest.php', ], + + // session handlers have the gc() method with underscored parameter `$max_lifetime` + UnderscoreToCamelCaseVariableNameRector::class => [ + __DIR__ . '/system/Session/Handlers', + ], ]); // auto import fully qualified class names diff --git a/system/Session/Handlers/ArrayHandler.php b/system/Session/Handlers/ArrayHandler.php index f3db062eb7fe..31951a788e65 100644 --- a/system/Session/Handlers/ArrayHandler.php +++ b/system/Session/Handlers/ArrayHandler.php @@ -11,7 +11,7 @@ namespace CodeIgniter\Session\Handlers; -use Exception; +use ReturnTypeWillChange; /** * Session handler using static array for storage. @@ -22,51 +22,43 @@ class ArrayHandler extends BaseHandler protected static $cache = []; /** - * Open + * Re-initialize existing session, or creates a new one. * - * Ensures we have an initialized database connection. - * - * @param string $savePath Path to session files' directory - * @param string $name Session cookie name - * - * @throws Exception + * @param string $path The path where to store/retrieve the session + * @param string $name The session name */ - public function open($savePath, $name): bool + public function open($path, $name): bool { return true; } /** - * Read + * Reads the session data from the session storage, and returns the results. * - * Reads session data and acquires a lock + * @param string $id The session ID * - * @param string $sessionID Session ID - * - * @return string Serialized session data + * @return false|string Returns an encoded string of the read data. + * If nothing was read, it must return false. */ - public function read($sessionID): string + #[ReturnTypeWillChange] + public function read($id) { return ''; } /** - * Write - * - * Writes (create / update) session data + * Writes the session data to the session storage. * - * @param string $sessionID Session ID - * @param string $sessionData Serialized session data + * @param string $id The session ID + * @param string $data The encoded session data */ - public function write($sessionID, $sessionData): bool + public function write($id, $data): bool { return true; } /** - * Close - * - * Releases locks and closes file descriptor. + * Closes the current session. */ public function close(): bool { @@ -74,26 +66,26 @@ public function close(): bool } /** - * Destroy - * - * Destroys the current session. + * Destroys a session * - * @param string $sessionID + * @param string $id The session ID being destroyed */ - public function destroy($sessionID): bool + public function destroy($id): bool { return true; } /** - * Garbage Collector + * Cleans up expired sessions. * - * Deletes expired sessions + * @param int $max_lifetime Sessions that have not updated + * for the last max_lifetime seconds will be removed. * - * @param int $maxlifetime Maximum lifetime of sessions + * @return false|int Returns the number of deleted sessions on success, or false on failure. */ - public function gc($maxlifetime): bool + #[ReturnTypeWillChange] + public function gc($max_lifetime) { - return true; + return 1; } } diff --git a/system/Session/Handlers/BaseHandler.php b/system/Session/Handlers/BaseHandler.php index e5663abc5f85..984b0fe7c3f6 100644 --- a/system/Session/Handlers/BaseHandler.php +++ b/system/Session/Handlers/BaseHandler.php @@ -100,9 +100,6 @@ abstract class BaseHandler implements SessionHandlerInterface */ protected $ipAddress; - /** - * Constructor - */ public function __construct(AppConfig $config, string $ipAddress) { $this->cookiePrefix = $config->cookiePrefix; @@ -155,12 +152,11 @@ protected function releaseLock(): bool } /** - * Fail - * * Drivers other than the 'files' one don't (need to) use the * session.save_path INI setting, but that leads to confusing * error messages emitted by PHP when open() or write() fail, * as the message contains session.save_path ... + * * To work around the problem, the drivers will call this method * so that the INI is set just in time for the error message to * be properly generated. diff --git a/system/Session/Handlers/DatabaseHandler.php b/system/Session/Handlers/DatabaseHandler.php index 01c127e1e125..18da544d2873 100644 --- a/system/Session/Handlers/DatabaseHandler.php +++ b/system/Session/Handlers/DatabaseHandler.php @@ -15,7 +15,7 @@ use CodeIgniter\Session\Exceptions\SessionException; use Config\App as AppConfig; use Config\Database; -use Exception; +use ReturnTypeWillChange; /** * Session handler using current Database for storage @@ -58,27 +58,24 @@ class DatabaseHandler extends BaseHandler protected $rowExists = false; /** - * Constructor + * @throws SessionException */ public function __construct(AppConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); - - // Determine Table $this->table = $config->sessionSavePath; if (empty($this->table)) { throw SessionException::forMissingDatabaseTable(); } - // Get DB Connection // @phpstan-ignore-next-line $this->DBGroup = $config->sessionDBGroup ?? config(Database::class)->defaultGroup; $this->db = Database::connect($this->DBGroup); - // Determine Database type $driver = strtolower(get_class($this->db)); + if (strpos($driver, 'mysql') !== false) { $this->platform = 'mysql'; } elseif (strpos($driver, 'postgre') !== false) { @@ -87,16 +84,12 @@ public function __construct(AppConfig $config, string $ipAddress) } /** - * Open - * - * Ensures we have an initialized database connection. - * - * @param string $savePath Path to session files' directory - * @param string $name Session cookie name + * Re-initialize existing session, or creates a new one. * - * @throws Exception + * @param string $path The path where to store/retrieve the session + * @param string $name The session name */ - public function open($savePath, $name): bool + public function open($path, $name): bool { if (empty($this->db->connID)) { $this->db->initialize(); @@ -106,30 +99,29 @@ public function open($savePath, $name): bool } /** - * Read + * Reads the session data from the session storage, and returns the results. * - * Reads session data and acquires a lock + * @param string $id The session ID * - * @param string $sessionID Session ID - * - * @return string Serialized session data + * @return false|string Returns an encoded string of the read data. + * If nothing was read, it must return false. */ - public function read($sessionID): string + #[ReturnTypeWillChange] + public function read($id) { - if ($this->lockSession($sessionID) === false) { + if ($this->lockSession($id) === false) { $this->fingerprint = md5(''); return ''; } - // Needed by write() to detect session_regenerate_id() calls if (! isset($this->sessionID)) { - $this->sessionID = $sessionID; + $this->sessionID = $id; } $builder = $this->db->table($this->table) ->select($this->platform === 'postgre' ? "encode(data, 'base64') AS data" : 'data') - ->where('id', $sessionID); + ->where('id', $id); if ($this->matchIP) { $builder = $builder->where('ip_address', $this->ipAddress); @@ -160,70 +152,63 @@ public function read($sessionID): string } /** - * Write - * - * Writes (create / update) session data + * Writes the session data to the session storage. * - * @param string $sessionID Session ID - * @param string $sessionData Serialized session data + * @param string $id The session ID + * @param string $data The encoded session data */ - public function write($sessionID, $sessionData): bool + public function write($id, $data): bool { if ($this->lock === false) { return $this->fail(); } - // Was the ID regenerated? - if ($sessionID !== $this->sessionID) { + if ($this->sessionID !== $id) { $this->rowExists = false; - $this->sessionID = $sessionID; + $this->sessionID = $id; } if ($this->rowExists === false) { $insertData = [ - 'id' => $sessionID, + 'id' => $id, 'ip_address' => $this->ipAddress, 'timestamp' => 'now()', - 'data' => $this->platform === 'postgre' ? '\x' . bin2hex($sessionData) : $sessionData, + 'data' => $this->platform === 'postgre' ? '\x' . bin2hex($data) : $data, ]; if (! $this->db->table($this->table)->insert($insertData)) { return $this->fail(); } - $this->fingerprint = md5($sessionData); + $this->fingerprint = md5($data); $this->rowExists = true; return true; } - $builder = $this->db->table($this->table)->where('id', $sessionID); + $builder = $this->db->table($this->table)->where('id', $id); if ($this->matchIP) { $builder = $builder->where('ip_address', $this->ipAddress); } - $updateData = [ - 'timestamp' => 'now()', - ]; + $updateData = ['timestamp' => 'now()']; - if ($this->fingerprint !== md5($sessionData)) { - $updateData['data'] = ($this->platform === 'postgre') ? '\x' . bin2hex($sessionData) : $sessionData; + if ($this->fingerprint !== md5($data)) { + $updateData['data'] = ($this->platform === 'postgre') ? '\x' . bin2hex($data) : $data; } if (! $builder->update($updateData)) { return $this->fail(); } - $this->fingerprint = md5($sessionData); + $this->fingerprint = md5($data); return true; } /** - * Close - * - * Releases locks and closes file descriptor. + * Closes the current session. */ public function close(): bool { @@ -231,16 +216,14 @@ public function close(): bool } /** - * Destroy - * - * Destroys the current session. + * Destroys a session * - * @param string $sessionID + * @param string $id The session ID being destroyed */ - public function destroy($sessionID): bool + public function destroy($id): bool { if ($this->lock) { - $builder = $this->db->table($this->table)->where('id', $sessionID); + $builder = $this->db->table($this->table)->where('id', $id); if ($this->matchIP) { $builder = $builder->where('ip_address', $this->ipAddress); @@ -261,18 +244,20 @@ public function destroy($sessionID): bool } /** - * Garbage Collector + * Cleans up expired sessions. * - * Deletes expired sessions + * @param int $max_lifetime Sessions that have not updated + * for the last max_lifetime seconds will be removed. * - * @param int $maxlifetime Maximum lifetime of sessions + * @return false|int Returns the number of deleted sessions on success, or false on failure. */ - public function gc($maxlifetime): bool + #[ReturnTypeWillChange] + public function gc($max_lifetime) { $separator = $this->platform === 'postgre' ? '\'' : ' '; - $interval = implode($separator, ['', "{$maxlifetime} second", '']); + $interval = implode($separator, ['', "{$max_lifetime} second", '']); - return $this->db->table($this->table)->delete("timestamp < now() - INTERVAL {$interval}") ? true : $this->fail(); + return $this->db->table($this->table)->delete("timestamp < now() - INTERVAL {$interval}") ? 1 : $this->fail(); } /** diff --git a/system/Session/Handlers/FileHandler.php b/system/Session/Handlers/FileHandler.php index 0f5653739dc3..9a6d2de7eb34 100644 --- a/system/Session/Handlers/FileHandler.php +++ b/system/Session/Handlers/FileHandler.php @@ -13,7 +13,7 @@ use CodeIgniter\Session\Exceptions\SessionException; use Config\App as AppConfig; -use Exception; +use ReturnTypeWillChange; /** * Session handler using file system for storage @@ -62,9 +62,6 @@ class FileHandler extends BaseHandler */ protected $sessionIDRegex = ''; - /** - * Constructor - */ public function __construct(AppConfig $config, string $ipAddress) { parent::__construct($config, $ipAddress); @@ -88,70 +85,67 @@ public function __construct(AppConfig $config, string $ipAddress) } /** - * Open + * Re-initialize existing session, or creates a new one. * - * Sanitizes the save_path directory. + * @param string $path The path where to store/retrieve the session + * @param string $name The session name * - * @param string $savePath Path to session files' directory - * @param string $name Session cookie name - * - * @throws Exception + * @throws SessionException */ - public function open($savePath, $name): bool + public function open($path, $name): bool { - if (! is_dir($savePath)) { - if (! mkdir($savePath, 0700, true)) { - throw SessionException::forInvalidSavePath($this->savePath); - } - } elseif (! is_writable($savePath)) { + if (! is_dir($path) && ! mkdir($path, 0700, true)) { + throw SessionException::forInvalidSavePath($this->savePath); + } + + if (! is_writable($path)) { throw SessionException::forWriteProtectedSavePath($this->savePath); } - $this->savePath = $savePath; - $this->filePath = $this->savePath . '/' - . $name // we'll use the session cookie name as a prefix to avoid collisions - . ($this->matchIP ? md5($this->ipAddress) : ''); + $this->savePath = $path; + + // we'll use the session name as prefix to avoid collisions + $this->filePath = $this->savePath . '/' . $name . ($this->matchIP ? md5($this->ipAddress) : ''); return true; } /** - * Read + * Reads the session data from the session storage, and returns the results. * - * Reads session data and acquires a lock + * @param string $id The session ID * - * @param string $sessionID Session ID - * - * @return bool|string Serialized session data + * @return false|string Returns an encoded string of the read data. + * If nothing was read, it must return false. */ - public function read($sessionID) + #[ReturnTypeWillChange] + public function read($id) { // This might seem weird, but PHP 5.6 introduced session_reset(), // which re-reads session data if ($this->fileHandle === null) { - $this->fileNew = ! is_file($this->filePath . $sessionID); + $this->fileNew = ! is_file($this->filePath . $id); - if (($this->fileHandle = fopen($this->filePath . $sessionID, 'c+b')) === false) { - $this->logger->error("Session: Unable to open file '" . $this->filePath . $sessionID . "'."); + if (($this->fileHandle = fopen($this->filePath . $id, 'c+b')) === false) { + $this->logger->error("Session: Unable to open file '" . $this->filePath . $id . "'."); return false; } if (flock($this->fileHandle, LOCK_EX) === false) { - $this->logger->error("Session: Unable to obtain lock for file '" . $this->filePath . $sessionID . "'."); + $this->logger->error("Session: Unable to obtain lock for file '" . $this->filePath . $id . "'."); fclose($this->fileHandle); $this->fileHandle = null; return false; } - // Needed by write() to detect session_regenerate_id() calls if (! isset($this->sessionID)) { - $this->sessionID = $sessionID; + $this->sessionID = $id; } if ($this->fileNew) { - chmod($this->filePath . $sessionID, 0600); + chmod($this->filePath . $id, 0600); $this->fingerprint = md5(''); return ''; @@ -160,43 +154,42 @@ public function read($sessionID) rewind($this->fileHandle); } - $sessionData = ''; - clearstatcache(); // Address https://github.com/codeigniter4/CodeIgniter4/issues/2056 + $data = ''; + $buffer = 0; + clearstatcache(); // Address https://github.com/codeigniter4/CodeIgniter4/issues/2056 - for ($read = 0, $length = filesize($this->filePath . $sessionID); $read < $length; $read += strlen($buffer)) { + for ($read = 0, $length = filesize($this->filePath . $id); $read < $length; $read += strlen($buffer)) { if (($buffer = fread($this->fileHandle, $length - $read)) === false) { break; } - $sessionData .= $buffer; + $data .= $buffer; } - $this->fingerprint = md5($sessionData); + $this->fingerprint = md5($data); - return $sessionData; + return $data; } /** - * Write - * - * Writes (create / update) session data + * Writes the session data to the session storage. * - * @param string $sessionID Session ID - * @param string $sessionData Serialized session data + * @param string $id The session ID + * @param string $data The encoded session data */ - public function write($sessionID, $sessionData): bool + public function write($id, $data): bool { // If the two IDs don't match, we have a session_regenerate_id() call - if ($sessionID !== $this->sessionID) { - $this->sessionID = $sessionID; + if ($id !== $this->sessionID) { + $this->sessionID = $id; } if (! is_resource($this->fileHandle)) { return false; } - if ($this->fingerprint === md5($sessionData)) { - return ($this->fileNew) ? true : touch($this->filePath . $sessionID); + if ($this->fingerprint === md5($data)) { + return ($this->fileNew) ? true : touch($this->filePath . $id); } if (! $this->fileNew) { @@ -204,32 +197,30 @@ public function write($sessionID, $sessionData): bool rewind($this->fileHandle); } - if (($length = strlen($sessionData)) > 0) { + if (($length = strlen($data)) > 0) { $result = null; for ($written = 0; $written < $length; $written += $result) { - if (($result = fwrite($this->fileHandle, substr($sessionData, $written))) === false) { + if (($result = fwrite($this->fileHandle, substr($data, $written))) === false) { break; } } if (! is_int($result)) { - $this->fingerprint = md5(substr($sessionData, 0, $written)); + $this->fingerprint = md5(substr($data, 0, $written)); $this->logger->error('Session: Unable to write data.'); return false; } } - $this->fingerprint = md5($sessionData); + $this->fingerprint = md5($data); return true; } /** - * Close - * - * Releases locks and closes file descriptor. + * Closes the current session. */ public function close(): bool { @@ -239,45 +230,45 @@ public function close(): bool $this->fileHandle = null; $this->fileNew = false; - - return true; } return true; } /** - * Destroy + * Destroys a session * - * Destroys the current session. - * - * @param string $sessionId Session ID + * @param string $id The session ID being destroyed */ - public function destroy($sessionId): bool + public function destroy($id): bool { if ($this->close()) { - return is_file($this->filePath . $sessionId) - ? (unlink($this->filePath . $sessionId) && $this->destroyCookie()) : true; + return is_file($this->filePath . $id) + ? (unlink($this->filePath . $id) && $this->destroyCookie()) + : true; } if ($this->filePath !== null) { clearstatcache(); - return is_file($this->filePath . $sessionId) - ? (unlink($this->filePath . $sessionId) && $this->destroyCookie()) : true; + return is_file($this->filePath . $id) + ? (unlink($this->filePath . $id) && $this->destroyCookie()) + : true; } return false; } /** - * Garbage Collector + * Cleans up expired sessions. * - * Deletes expired sessions + * @param int $max_lifetime Sessions that have not updated + * for the last max_lifetime seconds will be removed. * - * @param int $maxlifetime Maximum lifetime of sessions + * @return false|int Returns the number of deleted sessions on success, or false on failure. */ - public function gc($maxlifetime): bool + #[ReturnTypeWillChange] + public function gc($max_lifetime) { if (! is_dir($this->savePath) || ($directory = opendir($this->savePath)) === false) { $this->logger->debug("Session: Garbage collector couldn't list files under directory '" . $this->savePath . "'."); @@ -285,17 +276,17 @@ public function gc($maxlifetime): bool return false; } - $ts = time() - $maxlifetime; + $ts = time() - $max_lifetime; - $pattern = $this->matchIP === true - ? '[0-9a-f]{32}' - : ''; + $pattern = $this->matchIP === true ? '[0-9a-f]{32}' : ''; $pattern = sprintf( '#\A%s' . $pattern . $this->sessionIDRegex . '\z#', preg_quote($this->cookieName, '#') ); + $collected = 0; + while (($file = readdir($directory)) !== false) { // If the filename doesn't match this pattern, it's either not a session file or is not ours if (! preg_match($pattern, $file) @@ -307,11 +298,12 @@ public function gc($maxlifetime): bool } unlink($this->savePath . DIRECTORY_SEPARATOR . $file); + $collected++; } closedir($directory); - return true; + return $collected; } /** @@ -328,7 +320,6 @@ protected function configureSessionIDRegex() ini_set('session.sid_length', (string) $SIDLength); } - // Yes, 4,5,6 are the only known possible values as of 2016-10-27 switch ($bitsPerCharacter) { case 4: $this->sessionIDRegex = '[0-9a-f]'; diff --git a/system/Session/Handlers/MemcachedHandler.php b/system/Session/Handlers/MemcachedHandler.php index dab3e61d4b06..5e963bf5048a 100644 --- a/system/Session/Handlers/MemcachedHandler.php +++ b/system/Session/Handlers/MemcachedHandler.php @@ -14,6 +14,7 @@ use CodeIgniter\Session\Exceptions\SessionException; use Config\App as AppConfig; use Memcached; +use ReturnTypeWillChange; /** * Session handler using Memcache for persistence @@ -49,8 +50,6 @@ class MemcachedHandler extends BaseHandler protected $sessionExpiration = 7200; /** - * Constructor - * * @throws SessionException */ public function __construct(AppConfig $config, string $ipAddress) @@ -73,14 +72,12 @@ public function __construct(AppConfig $config, string $ipAddress) } /** - * Open - * - * Sanitizes save_path and initializes connections. + * Re-initialize existing session, or creates a new one. * - * @param string $savePath Server path(s) - * @param string $name Session cookie name, unused + * @param string $path The path where to store/retrieve the session + * @param string $name The session name */ - public function open($savePath, $name): bool + public function open($path, $name): bool { $this->memcached = new Memcached(); $this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); // required for touch() usage @@ -91,8 +88,7 @@ public function open($savePath, $name): bool $serverList[] = $server['host'] . ':' . $server['port']; } - if (! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->savePath, $matches, PREG_SET_ORDER) - ) { + if (! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->savePath, $matches, PREG_SET_ORDER)) { $this->memcached = null; $this->logger->error('Session: Invalid Memcached save path format: ' . $this->savePath); @@ -124,60 +120,57 @@ public function open($savePath, $name): bool } /** - * Read - * - * Reads session data and acquires a lock + * Reads the session data from the session storage, and returns the results. * - * @param string $sessionID Session ID + * @param string $id The session ID * - * @return string Serialized session data + * @return false|string Returns an encoded string of the read data. + * If nothing was read, it must return false. */ - public function read($sessionID): string + #[ReturnTypeWillChange] + public function read($id) { - if (isset($this->memcached) && $this->lockSession($sessionID)) { - // Needed by write() to detect session_regenerate_id() calls + if (isset($this->memcached) && $this->lockSession($id)) { if (! isset($this->sessionID)) { - $this->sessionID = $sessionID; + $this->sessionID = $id; } - $sessionData = (string) $this->memcached->get($this->keyPrefix . $sessionID); - $this->fingerprint = md5($sessionData); + $data = (string) $this->memcached->get($this->keyPrefix . $id); + + $this->fingerprint = md5($data); - return $sessionData; + return $data; } return ''; } /** - * Write - * - * Writes (create / update) session data + * Writes the session data to the session storage. * - * @param string $sessionID Session ID - * @param string $sessionData Serialized session data + * @param string $id The session ID + * @param string $data The encoded session data */ - public function write($sessionID, $sessionData): bool + public function write($id, $data): bool { if (! isset($this->memcached)) { return false; } - // Was the ID regenerated? - if ($sessionID !== $this->sessionID) { - if (! $this->releaseLock() || ! $this->lockSession($sessionID)) { + if ($this->sessionID !== $id) { + if (! $this->releaseLock() || ! $this->lockSession($id)) { return false; } $this->fingerprint = md5(''); - $this->sessionID = $sessionID; + $this->sessionID = $id; } if (isset($this->lockKey)) { $this->memcached->replace($this->lockKey, time(), 300); - if ($this->fingerprint !== ($fingerprint = md5($sessionData))) { - if ($this->memcached->set($this->keyPrefix . $sessionID, $sessionData, $this->sessionExpiration)) { + if ($this->fingerprint !== ($fingerprint = md5($data))) { + if ($this->memcached->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) { $this->fingerprint = $fingerprint; return true; @@ -186,16 +179,14 @@ public function write($sessionID, $sessionData): bool return false; } - return $this->memcached->touch($this->keyPrefix . $sessionID, $this->sessionExpiration); + return $this->memcached->touch($this->keyPrefix . $id, $this->sessionExpiration); } return false; } /** - * Close - * - * Releases locks and closes connection. + * Closes the current session. */ public function close(): bool { @@ -217,16 +208,14 @@ public function close(): bool } /** - * Destroy - * - * Destroys the current session. + * Destroys a session * - * @param string $sessionId Session ID + * @param string $id The session ID being destroyed */ - public function destroy($sessionId): bool + public function destroy($id): bool { if (isset($this->memcached, $this->lockKey)) { - $this->memcached->delete($this->keyPrefix . $sessionId); + $this->memcached->delete($this->keyPrefix . $id); return $this->destroyCookie(); } @@ -235,22 +224,21 @@ public function destroy($sessionId): bool } /** - * Garbage Collector + * Cleans up expired sessions. * - * Deletes expired sessions + * @param int $max_lifetime Sessions that have not updated + * for the last max_lifetime seconds will be removed. * - * @param int $maxlifetime Maximum lifetime of sessions + * @return false|int Returns the number of deleted sessions on success, or false on failure. */ - public function gc($maxlifetime): bool + #[ReturnTypeWillChange] + public function gc($max_lifetime) { - // Not necessary, Memcached takes care of that. - return true; + return 1; } /** - * Get lock - * - * Acquires an (emulated) lock. + * Acquires an emulated lock. * * @param string $sessionID Session ID */ @@ -260,7 +248,6 @@ protected function lockSession(string $sessionID): bool return $this->memcached->replace($this->lockKey, time(), 300); } - // 30 attempts to obtain a lock, in case another request already has it $lockKey = $this->keyPrefix . $sessionID . ':lock'; $attempt = 0; @@ -293,8 +280,6 @@ protected function lockSession(string $sessionID): bool } /** - * Release lock - * * Releases a previously acquired lock */ protected function releaseLock(): bool diff --git a/system/Session/Handlers/RedisHandler.php b/system/Session/Handlers/RedisHandler.php index c066813b7969..8ed7b8c052b8 100644 --- a/system/Session/Handlers/RedisHandler.php +++ b/system/Session/Handlers/RedisHandler.php @@ -13,9 +13,9 @@ use CodeIgniter\Session\Exceptions\SessionException; use Config\App as AppConfig; -use Exception; use Redis; use RedisException; +use ReturnTypeWillChange; /** * Session handler using Redis for persistence @@ -58,9 +58,7 @@ class RedisHandler extends BaseHandler protected $sessionExpiration = 7200; /** - * Constructor - * - * @throws Exception + * @throws SessionException */ public function __construct(AppConfig $config, string $ipAddress) { @@ -72,8 +70,8 @@ public function __construct(AppConfig $config, string $ipAddress) if (preg_match('#(?:tcp://)?([^:?]+)(?:\:(\d+))?(\?.+)?#', $this->savePath, $matches)) { if (! isset($matches[3])) { - $matches[3] = ''; - } // Just to avoid undefined index notices below + $matches[3] = ''; // Just to avoid undefined index notices below + } $this->savePath = [ 'host' => $matches[1], @@ -98,14 +96,12 @@ public function __construct(AppConfig $config, string $ipAddress) } /** - * Open + * Re-initialize existing session, or creates a new one. * - * Sanitizes save_path and initializes connection. - * - * @param string $savePath Server path - * @param string $name Session cookie name, unused + * @param string $path The path where to store/retrieve the session + * @param string $name The session name */ - public function open($savePath, $name): bool + public function open($path, $name): bool { if (empty($this->savePath)) { return false; @@ -129,62 +125,63 @@ public function open($savePath, $name): bool } /** - * Read + * Reads the session data from the session storage, and returns the results. * - * Reads session data and acquires a lock + * @param string $id The session ID * - * @param string $sessionID Session ID - * - * @return string Serialized session data + * @return false|string Returns an encoded string of the read data. + * If nothing was read, it must return false. */ - public function read($sessionID): string + #[ReturnTypeWillChange] + public function read($id) { - if (isset($this->redis) && $this->lockSession($sessionID)) { - // Needed by write() to detect session_regenerate_id() calls + if (isset($this->redis) && $this->lockSession($id)) { if (! isset($this->sessionID)) { - $this->sessionID = $sessionID; + $this->sessionID = ${$id}; } - $sessionData = $this->redis->get($this->keyPrefix . $sessionID); - is_string($sessionData) ? $this->keyExists = true : $sessionData = ''; + $data = $this->redis->get($this->keyPrefix . $id); + + if (is_string($data)) { + $this->keyExists = true; + } else { + $data = ''; + } - $this->fingerprint = md5($sessionData); + $this->fingerprint = md5($data); - return $sessionData; + return $data; } return ''; } /** - * Write + * Writes the session data to the session storage. * - * Writes (create / update) session data - * - * @param string $sessionID Session ID - * @param string $sessionData Serialized session data + * @param string $id The session ID + * @param string $data The encoded session data */ - public function write($sessionID, $sessionData): bool + public function write($id, $data): bool { if (! isset($this->redis)) { return false; } - // Was the ID regenerated? - if ($sessionID !== $this->sessionID) { - if (! $this->releaseLock() || ! $this->lockSession($sessionID)) { + if ($this->sessionID !== $id) { + if (! $this->releaseLock() || ! $this->lockSession($id)) { return false; } $this->keyExists = false; - $this->sessionID = $sessionID; + $this->sessionID = $id; } if (isset($this->lockKey)) { $this->redis->expire($this->lockKey, 300); - if ($this->fingerprint !== ($fingerprint = md5($sessionData)) || $this->keyExists === false) { - if ($this->redis->set($this->keyPrefix . $sessionID, $sessionData, $this->sessionExpiration)) { + if ($this->fingerprint !== ($fingerprint = md5($data)) || $this->keyExists === false) { + if ($this->redis->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) { $this->fingerprint = $fingerprint; $this->keyExists = true; @@ -194,22 +191,21 @@ public function write($sessionID, $sessionData): bool return false; } - return $this->redis->expire($this->keyPrefix . $sessionID, $this->sessionExpiration); + return $this->redis->expire($this->keyPrefix . $id, $this->sessionExpiration); } return false; } /** - * Close - * - * Releases locks and closes connection. + * Closes the current session. */ public function close(): bool { if (isset($this->redis)) { try { $pingReply = $this->redis->ping(); + // @phpstan-ignore-next-line if (($pingReply === true) || ($pingReply === '+PONG')) { if (isset($this->lockKey)) { @@ -233,16 +229,14 @@ public function close(): bool } /** - * Destroy - * - * Destroys the current session. + * Destroys a session * - * @param string $sessionID + * @param string $id The session ID being destroyed */ - public function destroy($sessionID): bool + public function destroy($id): bool { if (isset($this->redis, $this->lockKey)) { - if (($result = $this->redis->del($this->keyPrefix . $sessionID)) !== 1) { + if (($result = $this->redis->del($this->keyPrefix . $id)) !== 1) { $this->logger->debug('Session: Redis::del() expected to return 1, got ' . var_export($result, true) . ' instead.'); } @@ -253,22 +247,21 @@ public function destroy($sessionID): bool } /** - * Garbage Collector + * Cleans up expired sessions. * - * Deletes expired sessions + * @param int $max_lifetime Sessions that have not updated + * for the last max_lifetime seconds will be removed. * - * @param int $maxlifetime Maximum lifetime of sessions + * @return false|int Returns the number of deleted sessions on success, or false on failure. */ - public function gc($maxlifetime): bool + #[ReturnTypeWillChange] + public function gc($max_lifetime) { - // Not necessary, Redis takes care of that. - return true; + return 1; } /** - * Get lock - * - * Acquires an (emulated) lock. + * Acquires an emulated lock. * * @param string $sessionID Session ID */ @@ -281,7 +274,6 @@ protected function lockSession(string $sessionID): bool return $this->redis->expire($this->lockKey, 300); } - // 30 attempts to obtain a lock, in case another request already has it $lockKey = $this->keyPrefix . $sessionID . ':lock'; $attempt = 0; @@ -318,8 +310,6 @@ protected function lockSession(string $sessionID): bool } /** - * Release lock - * * Releases a previously acquired lock */ protected function releaseLock(): bool