1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 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 parse_url; 29use function session_name; 30use function session_regenerate_id; 31use function session_register_shutdown; 32use function session_set_cookie_params; 33use function session_set_save_handler; 34use function session_start; 35use function session_status; 36use function session_write_close; 37 38use const PHP_SESSION_ACTIVE; 39use const PHP_URL_HOST; 40use const PHP_URL_PATH; 41use const PHP_URL_SCHEME; 42use const PHP_VERSION_ID; 43 44/** 45 * Session handling 46 */ 47class Session 48{ 49 // Use the secure prefix with HTTPS. 50 private const SESSION_NAME = 'WT2_SESSION'; 51 private const SECURE_SESSION_NAME = '__Secure-WT-ID'; 52 53 /** 54 * Start a session 55 * 56 * @param ServerRequestInterface $request 57 * 58 * @return void 59 */ 60 public static function start(ServerRequestInterface $request): void 61 { 62 // Store sessions in the database 63 session_set_save_handler(new SessionDatabaseHandler($request)); 64 65 $url = $request->getAttribute('base_url'); 66 $secure = parse_url($url, PHP_URL_SCHEME) === 'https'; 67 $domain = (string) parse_url($url, PHP_URL_HOST); 68 $path = (string) parse_url($url, PHP_URL_PATH); 69 70 // Paths containing UTF-8 characters need special handling. 71 $path = implode('/', array_map('rawurlencode', explode('/', $path))); 72 73 session_name($secure ? self::SECURE_SESSION_NAME : self::SESSION_NAME); 74 session_register_shutdown(); 75 // Since PHP 7.3, we can set "SameSite: Lax" to help protect against CSRF attacks. 76 if (PHP_VERSION_ID > 70300) { 77 session_set_cookie_params([ 78 'lifetime' => 0, 79 'path' => $path . '/', 80 'domain' => $domain, 81 'secure' => $secure, 82 'httponly' => true, 83 'samesite' => 'Lax', 84 ]); 85 } else { 86 session_set_cookie_params(0, $path . '/', $domain, $secure, true); 87 } 88 session_start(); 89 90 // A new session? Prevent session fixation attacks by choosing a new session ID. 91 if (self::get('initiated') !== true) { 92 self::regenerate(true); 93 self::put('initiated', true); 94 } 95 } 96 97 /** 98 * Save/close the session. This releases the session lock. 99 * Closing early can help concurrent connections. 100 */ 101 public static function save(): void 102 { 103 if (session_status() === PHP_SESSION_ACTIVE) { 104 session_write_close(); 105 } 106 } 107 108 /** 109 * Read a value from the session 110 * 111 * @param string $name 112 * @param mixed $default 113 * 114 * @return mixed 115 */ 116 public static function get(string $name, $default = null) 117 { 118 return $_SESSION[$name] ?? $default; 119 } 120 121 /** 122 * Read a value from the session and remove it. 123 * 124 * @param string $name 125 * @param mixed $default 126 * 127 * @return mixed 128 */ 129 public static function pull(string $name, $default = null) 130 { 131 $value = self::get($name, $default); 132 self::forget($name); 133 134 return $value; 135 } 136 137 /** 138 * After any change in authentication level, we should use a new session ID. 139 * 140 * @param bool $destroy 141 * 142 * @return void 143 */ 144 public static function regenerate(bool $destroy = false): void 145 { 146 if ($destroy) { 147 self::clear(); 148 } 149 150 if (session_status() === PHP_SESSION_ACTIVE) { 151 session_regenerate_id($destroy); 152 } 153 } 154 155 /** 156 * Remove all stored data from the session. 157 * 158 * @return void 159 */ 160 public static function clear(): void 161 { 162 $_SESSION = []; 163 } 164 165 /** 166 * Write a value to the session 167 * 168 * @param string $name 169 * @param mixed $value 170 * 171 * @return void 172 */ 173 public static function put(string $name, $value): void 174 { 175 $_SESSION[$name] = $value; 176 } 177 178 /** 179 * Remove a value from the session 180 * 181 * @param string $name 182 * 183 * @return void 184 */ 185 public static function forget(string $name): void 186 { 187 unset($_SESSION[$name]); 188 } 189 190 /** 191 * Cross-Site Request Forgery tokens - ensure that the user is submitting 192 * a form that was generated by the current session. 193 * 194 * @return string 195 */ 196 public static function getCsrfToken(): string 197 { 198 if (!self::has('CSRF_TOKEN')) { 199 self::put('CSRF_TOKEN', Str::random(32)); 200 } 201 202 return self::get('CSRF_TOKEN'); 203 } 204 205 /** 206 * Does a session variable exist? 207 * 208 * @param string $name 209 * 210 * @return bool 211 */ 212 public static function has(string $name): bool 213 { 214 return isset($_SESSION[$name]); 215 } 216} 217