131bc7874SGreg Roach<?php 23976b470SGreg Roach 331bc7874SGreg Roach/** 431bc7874SGreg Roach * webtrees: online genealogy 5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team 631bc7874SGreg Roach * This program is free software: you can redistribute it and/or modify 731bc7874SGreg Roach * it under the terms of the GNU General Public License as published by 831bc7874SGreg Roach * the Free Software Foundation, either version 3 of the License, or 931bc7874SGreg Roach * (at your option) any later version. 1031bc7874SGreg Roach * This program is distributed in the hope that it will be useful, 1131bc7874SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 1231bc7874SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1331bc7874SGreg Roach * GNU General Public License for more details. 1431bc7874SGreg Roach * You should have received a copy of the GNU General Public License 1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 1631bc7874SGreg Roach */ 17fcfa147eSGreg Roach 18e7f56f2aSGreg Roachdeclare(strict_types=1); 19e7f56f2aSGreg Roach 2076692c8bSGreg Roachnamespace Fisharebest\Webtrees; 2131bc7874SGreg Roach 22deb4b685SGreg Roachuse Illuminate\Support\Str; 236ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface; 243976b470SGreg Roach 254d7dd147SGreg Roachuse function array_map; 264d7dd147SGreg Roachuse function explode; 274d7dd147SGreg Roachuse function implode; 28d8809d62SGreg Roachuse function is_string; 294d7dd147SGreg Roachuse function parse_url; 301a9891fdSGreg Roachuse function rawurlencode; 314d7dd147SGreg Roachuse function session_name; 324d7dd147SGreg Roachuse function session_regenerate_id; 334d7dd147SGreg Roachuse function session_register_shutdown; 344d7dd147SGreg Roachuse function session_set_cookie_params; 354d7dd147SGreg Roachuse function session_set_save_handler; 364d7dd147SGreg Roachuse function session_start; 37fe3a9054SGreg Roachuse function session_status; 38dc6b8e0eSGreg Roachuse function session_write_close; 393976b470SGreg Roach 40dc6b8e0eSGreg Roachuse const PHP_SESSION_ACTIVE; 414d7dd147SGreg Roachuse const PHP_URL_HOST; 424d7dd147SGreg Roachuse const PHP_URL_PATH; 434d7dd147SGreg Roachuse const PHP_URL_SCHEME; 444c891c40SGreg Roach 4531bc7874SGreg Roach/** 4642af74e7SGreg Roach * Session handling 4731bc7874SGreg Roach */ 48c1010edaSGreg Roachclass Session 49c1010edaSGreg Roach{ 50a51e953eSGreg Roach // Use the secure prefix with HTTPS. 51*e873f434SGreg Roach private const string SESSION_NAME = 'WT2_SESSION'; 52*e873f434SGreg Roach private const string SECURE_SESSION_NAME = '__Secure-WT-ID'; 53dc6b8e0eSGreg Roach 5431bc7874SGreg Roach /** 5531bc7874SGreg Roach * Start a session 56c7ff4153SGreg Roach * 574d7dd147SGreg Roach * @param ServerRequestInterface $request 584d7dd147SGreg Roach * 59c7ff4153SGreg Roach * @return void 6031bc7874SGreg Roach */ 614d7dd147SGreg Roach public static function start(ServerRequestInterface $request): void 62c1010edaSGreg Roach { 634d7dd147SGreg Roach // Store sessions in the database 644d7dd147SGreg Roach session_set_save_handler(new SessionDatabaseHandler($request)); 654d7dd147SGreg Roach 66b55cbc6bSGreg Roach $url = Validator::attributes($request)->string('base_url'); 674d7dd147SGreg Roach $secure = parse_url($url, PHP_URL_SCHEME) === 'https'; 684831cc24SGreg Roach $domain = (string) parse_url($url, PHP_URL_HOST); 694831cc24SGreg Roach $path = (string) parse_url($url, PHP_URL_PATH); 7008df3d18SGreg Roach 7108df3d18SGreg Roach // Paths containing UTF-8 characters need special handling. 7205babb96SGreg Roach $path = implode('/', array_map(static fn (string $x): string => rawurlencode($x), explode('/', $path))); 7308df3d18SGreg Roach 74a51e953eSGreg Roach session_name($secure ? self::SECURE_SESSION_NAME : self::SESSION_NAME); 7531bc7874SGreg Roach session_register_shutdown(); 7618c571c3SGreg Roach session_set_cookie_params([ 7718c571c3SGreg Roach 'lifetime' => 0, 7818c571c3SGreg Roach 'path' => $path . '/', 7918c571c3SGreg Roach 'domain' => $domain, 8018c571c3SGreg Roach 'secure' => $secure, 8118c571c3SGreg Roach 'httponly' => true, 8218c571c3SGreg Roach 'samesite' => 'Lax', 8318c571c3SGreg Roach ]); 8431bc7874SGreg Roach session_start(); 8508df3d18SGreg Roach 8608df3d18SGreg Roach // A new session? Prevent session fixation attacks by choosing a new session ID. 87b262b3d3SGreg Roach if (self::get('initiated') !== true) { 8808df3d18SGreg Roach self::regenerate(true); 8908df3d18SGreg Roach self::put('initiated', true); 9008df3d18SGreg Roach } 9131bc7874SGreg Roach } 9231bc7874SGreg Roach 9331bc7874SGreg Roach /** 94dc6b8e0eSGreg Roach * Save/close the session. This releases the session lock. 95dc6b8e0eSGreg Roach * Closing early can help concurrent connections. 96dc6b8e0eSGreg Roach */ 97dc6b8e0eSGreg Roach public static function save(): void 98dc6b8e0eSGreg Roach { 99dc6b8e0eSGreg Roach if (session_status() === PHP_SESSION_ACTIVE) { 100dc6b8e0eSGreg Roach session_write_close(); 101dc6b8e0eSGreg Roach } 102dc6b8e0eSGreg Roach } 103dc6b8e0eSGreg Roach 104dc6b8e0eSGreg Roach /** 1056ccdf4f0SGreg Roach * Read a value from the session 10657514a4fSGreg Roach * 1076ccdf4f0SGreg Roach * @param string $name 1086ccdf4f0SGreg Roach * @param mixed $default 1096ccdf4f0SGreg Roach * 1106ccdf4f0SGreg Roach * @return mixed 11157514a4fSGreg Roach */ 1126ccdf4f0SGreg Roach public static function get(string $name, $default = null) 113c1010edaSGreg Roach { 1146ccdf4f0SGreg Roach return $_SESSION[$name] ?? $default; 11557514a4fSGreg Roach } 11657514a4fSGreg Roach 11757514a4fSGreg Roach /** 118fd8bd3bcSGreg Roach * Read a value from the session and remove it. 119fd8bd3bcSGreg Roach * 120fd8bd3bcSGreg Roach * @param string $name 121fd8bd3bcSGreg Roach * 122fd8bd3bcSGreg Roach * @return mixed 123fd8bd3bcSGreg Roach */ 124d8809d62SGreg Roach public static function pull(string $name) 125fd8bd3bcSGreg Roach { 126d8809d62SGreg Roach $value = self::get($name); 127fd8bd3bcSGreg Roach self::forget($name); 128fd8bd3bcSGreg Roach 129fd8bd3bcSGreg Roach return $value; 130fd8bd3bcSGreg Roach } 131fd8bd3bcSGreg Roach 132fd8bd3bcSGreg Roach /** 1336ccdf4f0SGreg Roach * After any change in authentication level, we should use a new session ID. 13457514a4fSGreg Roach * 1356ccdf4f0SGreg Roach * @param bool $destroy 13657514a4fSGreg Roach * 1376ccdf4f0SGreg Roach * @return void 13857514a4fSGreg Roach */ 1396ccdf4f0SGreg Roach public static function regenerate(bool $destroy = false): void 140c1010edaSGreg Roach { 1416ccdf4f0SGreg Roach if ($destroy) { 1426ccdf4f0SGreg Roach self::clear(); 1436ccdf4f0SGreg Roach } 1446ccdf4f0SGreg Roach 1456ccdf4f0SGreg Roach if (session_status() === PHP_SESSION_ACTIVE) { 1466ccdf4f0SGreg Roach session_regenerate_id($destroy); 1476ccdf4f0SGreg Roach } 14857514a4fSGreg Roach } 14957514a4fSGreg Roach 15057514a4fSGreg Roach /** 1516ccdf4f0SGreg Roach * Remove all stored data from the session. 1526ccdf4f0SGreg Roach * 1536ccdf4f0SGreg Roach * @return void 1546ccdf4f0SGreg Roach */ 1556ccdf4f0SGreg Roach public static function clear(): void 1566ccdf4f0SGreg Roach { 1576ccdf4f0SGreg Roach $_SESSION = []; 1586ccdf4f0SGreg Roach } 1596ccdf4f0SGreg Roach 1606ccdf4f0SGreg Roach /** 1616ccdf4f0SGreg Roach * Write a value to the session 1626ccdf4f0SGreg Roach * 1636ccdf4f0SGreg Roach * @param string $name 1646ccdf4f0SGreg Roach * @param mixed $value 1656ccdf4f0SGreg Roach * 1666ccdf4f0SGreg Roach * @return void 1676ccdf4f0SGreg Roach */ 1686ccdf4f0SGreg Roach public static function put(string $name, $value): void 1696ccdf4f0SGreg Roach { 1706ccdf4f0SGreg Roach $_SESSION[$name] = $value; 1716ccdf4f0SGreg Roach } 1726ccdf4f0SGreg Roach 1736ccdf4f0SGreg Roach /** 1746ccdf4f0SGreg Roach * Remove a value from the session 1756ccdf4f0SGreg Roach * 1766ccdf4f0SGreg Roach * @param string $name 1776ccdf4f0SGreg Roach * 1786ccdf4f0SGreg Roach * @return void 1796ccdf4f0SGreg Roach */ 1806ccdf4f0SGreg Roach public static function forget(string $name): void 1816ccdf4f0SGreg Roach { 1826ccdf4f0SGreg Roach unset($_SESSION[$name]); 1836ccdf4f0SGreg Roach } 1846ccdf4f0SGreg Roach 1856ccdf4f0SGreg Roach /** 186a45f9889SGreg Roach * Cross-Site Request Forgery tokens - ensure that the user is submitting 187a45f9889SGreg Roach * a form that was generated by the current session. 188a45f9889SGreg Roach * 189a45f9889SGreg Roach * @return string 190a45f9889SGreg Roach */ 1918f53f488SRico Sonntag public static function getCsrfToken(): string 192a45f9889SGreg Roach { 193d8809d62SGreg Roach $csrf_token = self::get('CSRF_TOKEN'); 194d8809d62SGreg Roach 195d8809d62SGreg Roach if (is_string($csrf_token)) { 196d8809d62SGreg Roach return $csrf_token; 197a45f9889SGreg Roach } 198a45f9889SGreg Roach 199d8809d62SGreg Roach $csrf_token = Str::random(32); 200d8809d62SGreg Roach 201d8809d62SGreg Roach self::put('CSRF_TOKEN', $csrf_token); 202d8809d62SGreg Roach 203d8809d62SGreg Roach return $csrf_token; 204a45f9889SGreg Roach } 2056ccdf4f0SGreg Roach 2066ccdf4f0SGreg Roach /** 2076ccdf4f0SGreg Roach * Does a session variable exist? 2086ccdf4f0SGreg Roach * 2096ccdf4f0SGreg Roach * @param string $name 2106ccdf4f0SGreg Roach * 2116ccdf4f0SGreg Roach * @return bool 2126ccdf4f0SGreg Roach */ 2136ccdf4f0SGreg Roach public static function has(string $name): bool 2146ccdf4f0SGreg Roach { 2156ccdf4f0SGreg Roach return isset($_SESSION[$name]); 2166ccdf4f0SGreg Roach } 21731bc7874SGreg Roach} 218