diff options
Diffstat (limited to 'inc/session.inc.php')
-rw-r--r-- | inc/session.inc.php | 180 |
1 files changed, 125 insertions, 55 deletions
diff --git a/inc/session.inc.php b/inc/session.inc.php index 6204c98c..ccb878cd 100644 --- a/inc/session.inc.php +++ b/inc/session.inc.php @@ -1,19 +1,21 @@ <?php -require_once('config.php'); +declare(strict_types=1); -@mkdir(CONFIG_SESSION_DIR, 0700, true); -@chmod(CONFIG_SESSION_DIR, 0700); -if (!is_writable(CONFIG_SESSION_DIR)) die('Config error: Session Path not writable!'); +require_once('config.php'); class Session { private static $sid = false; private static $data = false; + private static $dataChanged = false; + private static $userId = 0; + private static $updateSessionDateline = false; - private static function generateSessionId($salt) + private static function generateSessionId(string $salt): void { - if (self::$sid !== false) Util::traceError('Error: Asked to generate session id when already set.'); + if (self::$sid !== false) + ErrorHandler::traceError('Error: Asked to generate session id when already set.'); self::$sid = sha1($salt . ',' . mt_rand(0, 65535) . $_SERVER['REMOTE_ADDR'] @@ -27,26 +29,42 @@ class Session ); } - public static function create($salt = '') + public static function create(string $salt, int $userId, bool $fixedAddress): void { self::generateSessionId($salt); - self::$data = array(); + self::$data = []; + self::$userId = $userId; + Database::exec("INSERT INTO session (sid, userid, dateline, lastip, fixedip, data) + VALUES (:sid, :userid, 0, '', :fixedip, '')", [ + 'sid' => self::$sid, + 'userid' => $userId, + 'fixedip' => $fixedAddress ? 1 : 0, + ]); + self::setupSessionAccounting(true); } - public static function load() + public static function load(): bool { // Try to load session id from cookie - if (!self::loadSessionId()) return false; - // Succeded, now try to load session data. If successful, job is done - if (self::readSessionData()) return true; + 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::delete(); + self::$sid = false; return false; } - public static function get($key) + public static function getUserId(): int { - if (!isset(self::$data[$key]) || !is_array(self::$data[$key])) return false; + 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]; } @@ -55,80 +73,132 @@ class Session * @param mixed $value data to store for key, false = delete * @param int|false $validMinutes validity in minutes, or false = forever */ - public static function set($key, $value, $validMinutes = false) + public static function set(string $key, $value, $validMinutes = 60): void { - if (self::$data === false) Util::traceError('Tried to set session data with no active session'); + 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() + private static function loadSessionId(): bool { - if (self::$sid !== false) die('Error: Asked to load session id when already set.'); - if (empty($_COOKIE['sid'])) return false; + 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; + if (empty($id)) + return false; self::$sid = $id; return true; } - public static function delete() + public static function delete(): void { - if (self::$sid === false) return; - @unlink(self::getSessionFile()); + if (self::$sid === false) + return; + Database::exec("DELETE FROM session WHERE sid = :sid", + ['sid' => self::$sid]); self::deleteCookie(); self::$sid = false; self::$data = false; } - public static function deleteCookie() + /** + * 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 { - Util::clearCookie('sid'); + if (self::$sid === false) + return; + Database::exec("DELETE FROM session WHERE sid <> :sid AND userid = :uid", + ['sid' => self::$sid, 'uid' => self::$userId]); } - - private static function getSessionFile() + + public static function deleteCookie(): void { - if (self::$sid === false) Util::traceError('Error: Tried to access session file when no session id was set.'); - return CONFIG_SESSION_DIR . '/' . self::$sid; + Util::clearCookie('sid'); } - private static function readSessionData() + private static function readSessionData(): bool { - if (self::$data !== false) Util::traceError('Tried to call read session data twice'); - $sessionfile = self::getSessionFile(); - if (!is_readable($sessionfile) || filemtime($sessionfile) + CONFIG_SESSION_TIMEOUT < time()) { - @unlink($sessionfile); - return false; - } - self::$data = @unserialize(@file_get_contents($sessionfile)); - if (self::$data === false) - return false; + 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(); - $save = false; + 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]); - $save = true; + self::$dataChanged = true; } } - if ($save) { - self::save(); - } return true; } - - public static function save() + + private static function setupSessionAccounting(bool $cookie): void { - if (self::$sid === false || self::$data === false) return; //Util::traceError('Called saveSession with no active session'); - $sessionfile = self::getSessionFile(); - $ret = @file_put_contents($sessionfile, @serialize(self::$data)); - if (!$ret) Util::traceError('Storing session data in ' . $sessionfile . ' failed.'); - Util::clearCookie('sid'); - $ret = setcookie('sid', self::$sid, time() + CONFIG_SESSION_TIMEOUT, null, null, !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off', true); - if (!$ret) Util::traceError('Error: Could not set Cookie for Client (headers already sent)'); + 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); + } + +} |