. */ declare(strict_types=1); namespace Fisharebest\Webtrees; use Illuminate\Support\Str; use Psr\Http\Message\ServerRequestInterface; use function array_map; use function explode; use function implode; use function is_string; use function parse_url; use function rawurlencode; use function session_name; use function session_regenerate_id; use function session_register_shutdown; use function session_set_cookie_params; use function session_set_save_handler; use function session_start; use function session_status; use function session_write_close; use const PHP_SESSION_ACTIVE; use const PHP_URL_HOST; use const PHP_URL_PATH; use const PHP_URL_SCHEME; /** * Session handling */ class Session { // Use the secure prefix with HTTPS. private const SESSION_NAME = 'WT2_SESSION'; private const SECURE_SESSION_NAME = '__Secure-WT-ID'; /** * Start a session * * @param ServerRequestInterface $request * * @return void */ public static function start(ServerRequestInterface $request): void { // Store sessions in the database session_set_save_handler(new SessionDatabaseHandler($request)); $url = Validator::attributes($request)->string('base_url'); $secure = parse_url($url, PHP_URL_SCHEME) === 'https'; $domain = (string) parse_url($url, PHP_URL_HOST); $path = (string) parse_url($url, PHP_URL_PATH); // Paths containing UTF-8 characters need special handling. $path = implode('/', array_map(static fn (string $x): string => rawurlencode($x), explode('/', $path))); session_name($secure ? self::SECURE_SESSION_NAME : self::SESSION_NAME); session_register_shutdown(); session_set_cookie_params([ 'lifetime' => 0, 'path' => $path . '/', 'domain' => $domain, 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax', ]); session_start(); // A new session? Prevent session fixation attacks by choosing a new session ID. if (self::get('initiated') !== true) { self::regenerate(true); self::put('initiated', true); } } /** * Save/close the session. This releases the session lock. * Closing early can help concurrent connections. */ public static function save(): void { if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } } /** * 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; } /** * Read a value from the session and remove it. * * @param string $name * * @return mixed */ public static function pull(string $name) { $value = self::get($name); self::forget($name); return $value; } /** * 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): void { if ($destroy) { self::clear(); } if (session_status() === PHP_SESSION_ACTIVE) { session_regenerate_id($destroy); } } /** * Remove all stored data from the session. * * @return void */ public static function clear(): void { $_SESSION = []; } /** * Write a value to the session * * @param string $name * @param mixed $value * * @return void */ public static function put(string $name, $value): void { $_SESSION[$name] = $value; } /** * Remove a value from the session * * @param string $name * * @return void */ public static function forget(string $name): void { unset($_SESSION[$name]); } /** * 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 { $csrf_token = self::get('CSRF_TOKEN'); if (is_string($csrf_token)) { return $csrf_token; } $csrf_token = Str::random(32); self::put('CSRF_TOKEN', $csrf_token); return $csrf_token; } /** * Does a session variable exist? * * @param string $name * * @return bool */ public static function has(string $name): bool { return isset($_SESSION[$name]); } }