. */ declare(strict_types=1); namespace Fisharebest\Webtrees; use function session_status; use Symfony\Component\HttpFoundation\Request; /** * Session handling */ class Session { /** * Start a session * * @return void */ public static function start() { $domain = ''; $path = parse_url(WT_BASE_URL, PHP_URL_PATH); $secure = parse_url(WT_BASE_URL, PHP_URL_SCHEME) === 'https'; $httponly = true; // Paths containing UTF-8 characters need special handling. $path = implode('/', array_map('rawurlencode', explode('/', $path))); self::setSaveHandler(); session_name('WT_SESSION'); session_register_shutdown(); session_set_cookie_params(0, $path, $domain, $secure, $httponly); session_start(); // A new session? Prevent session fixation attacks by choosing a new session ID. if (!self::get('initiated')) { self::regenerate(true); self::put('initiated', true); } } /** * Read a value from the session * * @param string $name * @param mixed $default * * @return mixed */ public static function get(string $name, $default = null) { return $_SESSION[$name] ?? $default; } /** * Write a value to the session * * @param string $name * @param mixed $value * * @return void */ public static function put(string $name, $value) { $_SESSION[$name] = $value; } /** * Remove a value from the session * * @param string $name * * @return void */ public static function forget(string $name) { unset($_SESSION[$name]); } /** * Does a session variable exist? * * @param string $name * * @return bool */ public static function has(string $name): bool { return isset($_SESSION[$name]); } /** * Remove all stored data from the session. * * @return void */ public static function clear() { $_SESSION = []; } /** * After any change in authentication level, we should use a new session ID. * * @param bool $destroy * * @return void */ public static function regenerate(bool $destroy = false) { if ($destroy) { self::clear(); } if (session_status() === PHP_SESSION_ACTIVE) { session_regenerate_id($destroy); } } /** * Set an explicit session ID. Typically used for search robots. * * @param string $id * * @return void */ public static function setId(string $id) { session_id($id); } /** * Initialise our session save handler * * @return void */ private static function setSaveHandler() { session_set_save_handler( function (): bool { return Session::open(); }, function (): bool { return Session::close(); }, function (string $id): string { return Session::read($id); }, function (string $id, string $data): bool { return Session::write($id, $data); }, function (string $id): bool { return Session::destroy($id); }, function (int $maxlifetime): bool { return Session::gc($maxlifetime); } ); } /** * For session_set_save_handler() * * @return bool */ private static function close(): bool { return true; } /** * For session_set_save_handler() * * @param string $id * * @return bool */ private static function destroy(string $id): bool { Database::prepare( "DELETE FROM `##session` WHERE session_id = :session_id" )->execute([ 'session_id' => $id, ]); return true; } /** * For session_set_save_handler() * * @param int $maxlifetime * * @return bool */ private static function gc(int $maxlifetime): bool { Database::prepare( "DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)" )->execute([ 'maxlifetime' => $maxlifetime, ]); return true; } /** * For session_set_save_handler() * * @return bool */ private static function open(): bool { return true; } /** * For session_set_save_handler() * * @param string $id * * @return string */ private static function read(string $id): string { return (string) Database::prepare( "SELECT session_data FROM `##session` WHERE session_id = :session_id" )->execute([ 'session_id' => $id, ])->fetchOne(); } /** * For session_set_save_handler() * * @param string $id * @param string $data * * @return bool */ private static function write(string $id, string $data): bool { $request = Request::createFromGlobals(); // Only update the session table once per minute, unless the session data has actually changed. Database::prepare( "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" . " VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" . " ON DUPLICATE KEY UPDATE" . " user_id = VALUES(user_id)," . " ip_address = VALUES(ip_address)," . " session_data = VALUES(session_data)," . " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)" )->execute([ 'session_id' => $id, 'user_id' => (int) Auth::id(), 'ip_address' => $request->getClientIp(), 'data' => $data, ]); return true; } /** * Cross-Site Request Forgery tokens - ensure that the user is submitting * a form that was generated by the current session. * * @return string */ public static function getCsrfToken(): string { if (!Session::has('CSRF_TOKEN')) { $charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789'; $csrf_token = ''; for ($n = 0; $n < 32; ++$n) { $csrf_token .= substr($charset, random_int(0, 61), 1); } Session::put('CSRF_TOKEN', $csrf_token); } return Session::get('CSRF_TOKEN'); } }