1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use Illuminate\Support\Str; 23use Psr\Http\Message\ServerRequestInterface; 24 25use function array_map; 26use function explode; 27use function implode; 28use function is_string; 29use function parse_url; 30use function rawurlencode; 31use function session_name; 32use function session_regenerate_id; 33use function session_register_shutdown; 34use function session_set_cookie_params; 35use function session_set_save_handler; 36use function session_start; 37use function session_status; 38use function session_write_close; 39 40use const PHP_SESSION_ACTIVE; 41use const PHP_URL_HOST; 42use const PHP_URL_PATH; 43use const PHP_URL_SCHEME; 44 45/** 46 * Session handling 47 */ 48class Session 49{ 50 // Use the secure prefix with HTTPS. 51 private const SESSION_NAME = 'WT2_SESSION'; 52 private const SECURE_SESSION_NAME = '__Secure-WT-ID'; 53 54 /** 55 * Start a session 56 * 57 * @param ServerRequestInterface $request 58 * 59 * @return void 60 */ 61 public static function start(ServerRequestInterface $request): void 62 { 63 // Store sessions in the database 64 session_set_save_handler(new SessionDatabaseHandler($request)); 65 66 $url = Validator::attributes($request)->string('base_url'); 67 $secure = parse_url($url, PHP_URL_SCHEME) === 'https'; 68 $domain = (string) parse_url($url, PHP_URL_HOST); 69 $path = (string) parse_url($url, PHP_URL_PATH); 70 71 // Paths containing UTF-8 characters need special handling. 72 $path = implode('/', array_map(static fn (string $x): string => rawurlencode($x), explode('/', $path))); 73 74 session_name($secure ? self::SECURE_SESSION_NAME : self::SESSION_NAME); 75 session_register_shutdown(); 76 session_set_cookie_params([ 77 'lifetime' => 0, 78 'path' => $path . '/', 79 'domain' => $domain, 80 'secure' => $secure, 81 'httponly' => true, 82 'samesite' => 'Lax', 83 ]); 84 session_start(); 85 86 // A new session? Prevent session fixation attacks by choosing a new session ID. 87 if (self::get('initiated') !== true) { 88 self::regenerate(true); 89 self::put('initiated', true); 90 } 91 } 92 93 /** 94 * Save/close the session. This releases the session lock. 95 * Closing early can help concurrent connections. 96 */ 97 public static function save(): void 98 { 99 if (session_status() === PHP_SESSION_ACTIVE) { 100 session_write_close(); 101 } 102 } 103 104 /** 105 * Read a value from the session 106 * 107 * @param string $name 108 * @param mixed $default 109 * 110 * @return mixed 111 */ 112 public static function get(string $name, $default = null) 113 { 114 return $_SESSION[$name] ?? $default; 115 } 116 117 /** 118 * Read a value from the session and remove it. 119 * 120 * @param string $name 121 * 122 * @return mixed 123 */ 124 public static function pull(string $name) 125 { 126 $value = self::get($name); 127 self::forget($name); 128 129 return $value; 130 } 131 132 /** 133 * After any change in authentication level, we should use a new session ID. 134 * 135 * @param bool $destroy 136 * 137 * @return void 138 */ 139 public static function regenerate(bool $destroy = false): void 140 { 141 if ($destroy) { 142 self::clear(); 143 } 144 145 if (session_status() === PHP_SESSION_ACTIVE) { 146 session_regenerate_id($destroy); 147 } 148 } 149 150 /** 151 * Remove all stored data from the session. 152 * 153 * @return void 154 */ 155 public static function clear(): void 156 { 157 $_SESSION = []; 158 } 159 160 /** 161 * Write a value to the session 162 * 163 * @param string $name 164 * @param mixed $value 165 * 166 * @return void 167 */ 168 public static function put(string $name, $value): void 169 { 170 $_SESSION[$name] = $value; 171 } 172 173 /** 174 * Remove a value from the session 175 * 176 * @param string $name 177 * 178 * @return void 179 */ 180 public static function forget(string $name): void 181 { 182 unset($_SESSION[$name]); 183 } 184 185 /** 186 * Cross-Site Request Forgery tokens - ensure that the user is submitting 187 * a form that was generated by the current session. 188 * 189 * @return string 190 */ 191 public static function getCsrfToken(): string 192 { 193 $csrf_token = self::get('CSRF_TOKEN'); 194 195 if (is_string($csrf_token)) { 196 return $csrf_token; 197 } 198 199 $csrf_token = Str::random(32); 200 201 self::put('CSRF_TOKEN', $csrf_token); 202 203 return $csrf_token; 204 } 205 206 /** 207 * Does a session variable exist? 208 * 209 * @param string $name 210 * 211 * @return bool 212 */ 213 public static function has(string $name): bool 214 { 215 return isset($_SESSION[$name]); 216 } 217} 218