<?php
declare(strict_types=1);
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(string $salt): void
{
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']
. mt_rand(0, 65535)
. $_SERVER['REMOTE_PORT']
. mt_rand(0, 65535)
. $_SERVER['HTTP_USER_AGENT']
. mt_rand(0, 65535)
. microtime(true)
. mt_rand(0, 65535)
);
}
public static function create(string $salt, int $userId, bool $fixedAddress): void
{
self::generateSessionId($salt);
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(): 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);
}
}