self::$sid, 'userid' => $userId, 'fixedip' => $fixedAddress ? 1 : 0, ]); self::setupSessionAccounting(true); } public static function load(): bool { // Try to load session id from cookie if (!self::loadSessionId()) return false; // Succeeded, now try to load session data. If successful, job is done if (self::readSessionData()) return true; // Loading session data failed self::$sid = false; return false; } public static function getUserId(): int { return self::$userId; } public static function get(string $key) { if (!isset(self::$data[$key]) || !is_array(self::$data[$key])) return false; return self::$data[$key][0]; } /** * @param string $key key of entry * @param mixed $value data to store for key, false = delete * @param int|false $validMinutes validity in minutes, or false = forever */ public static function set(string $key, $value, $validMinutes = 60): void { if (self::$data === false) ErrorHandler::traceError('Tried to set session data with no active session'); if ($value === false) { unset(self::$data[$key]); } else { self::$data[$key] = [$value, $validMinutes === false ? false : time() + $validMinutes * 60]; } self::$dataChanged = true; } private static function loadSessionId(): bool { if (self::$sid !== false) ErrorHandler::traceError('Error: Asked to load session id when already set.'); if (empty($_COOKIE['sid'])) return false; $id = preg_replace('/[^a-zA-Z0-9]/', '', $_COOKIE['sid']); if (empty($id)) return false; self::$sid = $id; return true; } public static function delete(): void { if (self::$sid === false) return; Database::exec("DELETE FROM session WHERE sid = :sid", ['sid' => self::$sid]); self::deleteCookie(); self::$sid = false; self::$data = false; } /** * Kill all sessions of currently logged-in user. This can be used as * a security measure if the user suspects that a session left open on * another device could be/is being abused. */ public static function deleteAllButCurrent(): void { if (self::$sid === false) return; Database::exec("DELETE FROM session WHERE sid <> :sid AND userid = :uid", ['sid' => self::$sid, 'uid' => self::$userId]); } public static function deleteCookie(): void { Util::clearCookie('sid'); } private static function readSessionData(): bool { if (self::$data !== false) ErrorHandler::traceError('Tried to call read session data twice'); $row = Database::queryFirst("SELECT userid, dateline, lastip, fixedip, data FROM session WHERE sid = :sid", ['sid' => self::$sid]); $now = time(); if ($row === false || $row['dateline'] < $now) { self::delete(); return false; } if ($row['fixedip'] && $row['lastip'] !== $_SERVER['REMOTE_ADDR']) { return false; // Ignore but don't invalidate } // Refresh cookie if appropriate self::setupSessionAccounting(Request::isGet() && $row['dateline'] + 86400 < $now + CONFIG_SESSION_TIMEOUT); self::$userId = (int)$row['userid']; self::$data = @json_decode($row['data'], true); if (!is_array(self::$data)) { self::$data = []; } foreach (array_keys(self::$data) as $key) { if (self::$data[$key][1] !== false && self::$data[$key][1] < $now) { unset(self::$data[$key]); self::$dataChanged = true; } } return true; } private static function setupSessionAccounting(bool $cookie): void { if ($cookie) { self::$updateSessionDateline = true; $ret = setcookie('sid', self::$sid, time() + CONFIG_SESSION_TIMEOUT, '', '', !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); if (!$ret) ErrorHandler::traceError('Error: Could not set Cookie for Client (headers already sent)'); } register_shutdown_function(function () { self::saveOnShutdown(); }); } private static function saveOnShutdown(): void { $now = time(); $args = ['lastip' => $_SERVER['REMOTE_ADDR']]; if (self::$updateSessionDateline) { $args['dateline'] = $now + CONFIG_SESSION_TIMEOUT; } if (self::$dataChanged) { $args['data'] = json_encode(self::$data); } self::saveData($args); } public static function saveExtraData(): void { if (!self::$dataChanged) return; self::saveData(['data' => json_encode(self::$data)]); self::$dataChanged = false; } private static function saveData(array $args): void { $query = "UPDATE session SET " . implode(', ', array_map(function ($key) { return "$key = :$key"; }, array_keys($args))) . " WHERE sid = :sid"; $args['sid'] = self::$sid; Database::exec($query, $args); } }