* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Framework\Session; use Framework\Log\Logger; use Framework\Log\LogLevel; use SensitiveParameter; /** * Class SaveHandler. * * @see https://www.php.net/manual/en/class.sessionhandler.php * @see https://gist.github.com/mindplay-dk/623bdd50c1b4c0553cd3 * @see https://www.cloudways.com/blog/setup-redis-as-session-handler-php/#sessionlifecycle * * @package session */ abstract class SaveHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface { /** * The configurations used by the save handler. * * @var array */ protected array $config; /** * The current data fingerprint. * * @var string */ protected string $fingerprint; /** * The lock id or false if is not locked. * * @var false|string */ protected false | string $lockId = false; /** * Tells if the session exists (if was read). * * @var bool */ protected bool $sessionExists = false; /** * The current session ID. * * @var string|null */ protected ?string $sessionId; /** * The Logger instance or null if it was not set. * * @var Logger|null */ protected ?Logger $logger; /** * It says that the handler object was set with an externally created object. * * If this is true, the handler object should not be changed or removed (in the close method). * * Some functions, such as `session_regenerate_id`, call the close method and then open again! * * @var bool */ protected bool $setByExternal = false; /** * SessionSaveHandler constructor. * * @param array $config * @param Logger|null $logger */ public function __construct( #[SensitiveParameter] array $config = [], ?Logger $logger = null ) { $this->prepareConfig($config); $this->logger = $logger; } /** * Prepare configurations to be used by the save handler. * * @param array $config Custom configs * * @codeCoverageIgnore */ protected function prepareConfig(#[SensitiveParameter] array $config) : void { $this->config = $config; } /** * @return array */ public function getConfig() : array { return $this->config; } /** * Log a message if the Logger is set. * * @param string $message The message to log * @param LogLevel $level The log level */ protected function log(string $message, LogLevel $level = LogLevel::ERROR) : void { $this->logger?->log($level, $message); } /** * Set the data fingerprint. * * @param string $data The data to set the new fingerprint */ protected function setFingerprint(string $data) : void { $this->fingerprint = $this->makeFingerprint($data); } /** * Make the fingerprint value. * * @param string $data The data to get the fingerprint * * @return string The fingerprint hash */ private function makeFingerprint(string $data) : string { return \hash('xxh3', $data); } /** * Tells if the data has the same current fingerprint. * * @param string $data The data to compare * * @return bool True if the fingerprints are the same, otherwise false */ protected function hasSameFingerprint(string $data) : bool { return $this->fingerprint === $this->makeFingerprint($data); } /** * Get the maxlifetime (TTL) used by cache handlers or locking. * * NOTE: It will use the `maxlifetime` config or the ini value of * `session.gc_maxlifetime` as fallback. * * @return int The maximum lifetime of a session in seconds */ protected function getMaxlifetime() : int { return (int) ($this->config['maxlifetime'] ?? \ini_get('session.gc_maxlifetime')); } /** * Get the remote IP address. * * @return string */ protected function getIP() : string { return $_SERVER['REMOTE_ADDR'] ?? ''; } /** * Get the HTTP User-Agent. * * @return string */ protected function getUA() : string { return $_SERVER['HTTP_USER_AGENT'] ?? ''; } protected function getKeySuffix() : string { $suffix = ''; if ($this->config['match_ip']) { $suffix .= ':' . $this->getIP(); } if ($this->config['match_ua']) { $suffix .= ':' . $this->getUA(); } if ($suffix) { $suffix = \hash('xxh3', $suffix); } return $suffix; } /** * Validate session id. * * @param string $id The session id * * @see https://www.php.net/manual/en/sessionupdatetimestamphandlerinterface.validateid.php * @see https://wiki.php.net/rfc/deprecations_php_8_4#sessionsid_length_and_sessionsid_bits_per_character * * @return bool Returns TRUE if the id is valid, otherwise FALSE */ public function validateId($id) : bool { if (\PHP_VERSION_ID >= 80400) { return (bool) \preg_match('#\A[0-9a-f]{32}\z#', $id); } $bits = \ini_get('session.sid_bits_per_character') ?: 5; $length = \ini_get('session.sid_length') ?: 40; $bitsRegex = [ 4 => '[0-9a-f]', 5 => '[0-9a-v]', 6 => '[0-9a-zA-Z,-]', ]; return isset($bitsRegex[$bits]) && \preg_match('#\A' . $bitsRegex[$bits] . '{' . $length . '}\z#', $id); } /** * Initialize the session. * * @param string $path The path where to store/retrieve the session * @param string $name The session name * * @see https://www.php.net/manual/en/sessionhandlerinterface.open.php * * @return bool Returns TRUE on success, FALSE on failure */ abstract public function open($path, $name) : bool; /** * Read session data. * * @param string $id The session id to read data for * * @see https://www.php.net/manual/en/sessionhandlerinterface.read.php * * @return string Returns an encoded string of the read data. * If nothing was read, it returns an empty string */ abstract public function read($id) : string; /** * Write session data. * * @param string $id The session id * @param string $data The encoded session data. This data is the result * of the PHP internally encoding the $_SESSION superglobal to a serialized * string and passing it as this parameter. * * NOTE: Sessions can use an alternative serialization method * * @see https://www.php.net/manual/en/sessionhandlerinterface.write.php * * @return bool Returns TRUE on success, FALSE on failure */ abstract public function write($id, $data) : bool; /** * Update the timestamp of a session. * * @param string $id The session id * @param string $data The encoded session data. This data is the result * of the PHP internally encoding the $_SESSION superglobal to a serialized * string and passing it as this parameter. * * NOTE: Sessions can use an alternative serialization method * * @see https://www.php.net/manual/en/sessionupdatetimestamphandlerinterface.updatetimestamp.php * * @return bool Returns TRUE on success, FALSE on failure */ abstract public function updateTimestamp($id, $data) : bool; /** * Close the session. * * @see https://www.php.net/manual/en/sessionhandlerinterface.close.php * * @return bool Returns TRUE on success, FALSE on failure */ abstract public function close() : bool; /** * Destroy a session. * * @param string $id The session ID being destroyed * * @see https://www.php.net/manual/en/sessionhandlerinterface.destroy.php * * @return bool Returns TRUE on success, FALSE on failure */ abstract public function destroy($id) : bool; /** * Cleanup old sessions. * * @param int $max_lifetime Sessions that have not updated for * the last $maxLifetime seconds will be removed * * @see https://www.php.net/manual/en/sessionhandlerinterface.gc.php * * @return false|int Returns the number of deleted session data for success, * false for failure */ abstract public function gc($max_lifetime) : false | int; /** * Acquire a lock for a session id. * * @param string $id The session id * * @return bool Returns TRUE on success, FALSE on failure */ abstract protected function lock(string $id) : bool; /** * Unlock the current session lock id. * * @return bool Returns TRUE on success, FALSE on failure */ abstract protected function unlock() : bool; }