1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees; 19 20use function session_status; 21use Symfony\Component\HttpFoundation\Request; 22 23/** 24 * Session handling 25 */ 26class Session 27{ 28 /** 29 * Start a session 30 * 31 * @return void 32 */ 33 public static function start() 34 { 35 $domain = ''; 36 $path = parse_url(WT_BASE_URL, PHP_URL_PATH); 37 $secure = parse_url(WT_BASE_URL, PHP_URL_SCHEME) === 'https'; 38 $httponly = true; 39 40 // Paths containing UTF-8 characters need special handling. 41 $path = implode('/', array_map('rawurlencode', explode('/', $path))); 42 43 self::setSaveHandler(); 44 45 session_name('WT_SESSION'); 46 session_register_shutdown(); 47 session_set_cookie_params(0, $path, $domain, $secure, $httponly); 48 session_start(); 49 50 // A new session? Prevent session fixation attacks by choosing a new session ID. 51 if (!self::get('initiated')) { 52 self::regenerate(true); 53 self::put('initiated', true); 54 } 55 } 56 57 /** 58 * Read a value from the session 59 * 60 * @param string $name 61 * @param mixed $default 62 * 63 * @return mixed 64 */ 65 public static function get(string $name, $default = null) 66 { 67 return $_SESSION[$name] ?? $default; 68 } 69 70 /** 71 * Write a value to the session 72 * 73 * @param string $name 74 * @param mixed $value 75 * 76 * @return void 77 */ 78 public static function put(string $name, $value) 79 { 80 $_SESSION[$name] = $value; 81 } 82 83 /** 84 * Remove a value from the session 85 * 86 * @param string $name 87 * 88 * @return void 89 */ 90 public static function forget(string $name) 91 { 92 unset($_SESSION[$name]); 93 } 94 95 /** 96 * Does a session variable exist? 97 * 98 * @param string $name 99 * 100 * @return bool 101 */ 102 public static function has(string $name): bool 103 { 104 return isset($_SESSION[$name]); 105 } 106 107 /** 108 * Remove all stored data from the session. 109 * 110 * @return void 111 */ 112 public static function clear() 113 { 114 $_SESSION = []; 115 } 116 117 /** 118 * After any change in authentication level, we should use a new session ID. 119 * 120 * @param bool $destroy 121 * 122 * @return void 123 */ 124 public static function regenerate(bool $destroy = false) 125 { 126 if ($destroy) { 127 self::clear(); 128 } 129 130 if (session_status() === PHP_SESSION_ACTIVE) { 131 session_regenerate_id($destroy); 132 } 133 } 134 135 /** 136 * Set an explicit session ID. Typically used for search robots. 137 * 138 * @param string $id 139 * 140 * @return void 141 */ 142 public static function setId(string $id) 143 { 144 session_id($id); 145 } 146 147 /** 148 * Initialise our session save handler 149 * 150 * @return void 151 */ 152 private static function setSaveHandler() 153 { 154 session_set_save_handler( 155 function (): bool { 156 return Session::open(); 157 }, 158 function (): bool { 159 return Session::close(); 160 }, 161 function (string $id): string { 162 return Session::read($id); 163 }, 164 function (string $id, string $data): bool { 165 return Session::write($id, $data); 166 }, 167 function (string $id): bool { 168 return Session::destroy($id); 169 }, 170 function (int $maxlifetime): bool { 171 return Session::gc($maxlifetime); 172 } 173 ); 174 } 175 176 /** 177 * For session_set_save_handler() 178 * 179 * @return bool 180 */ 181 private static function close(): bool 182 { 183 return true; 184 } 185 186 /** 187 * For session_set_save_handler() 188 * 189 * @param string $id 190 * 191 * @return bool 192 */ 193 private static function destroy(string $id): bool 194 { 195 Database::prepare( 196 "DELETE FROM `##session` WHERE session_id = :session_id" 197 )->execute([ 198 'session_id' => $id, 199 ]); 200 201 return true; 202 } 203 204 /** 205 * For session_set_save_handler() 206 * 207 * @param int $maxlifetime 208 * 209 * @return bool 210 */ 211 private static function gc(int $maxlifetime): bool 212 { 213 Database::prepare( 214 "DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)" 215 )->execute([ 216 'maxlifetime' => $maxlifetime, 217 ]); 218 219 return true; 220 } 221 222 /** 223 * For session_set_save_handler() 224 * 225 * @return bool 226 */ 227 private static function open(): bool 228 { 229 return true; 230 } 231 232 /** 233 * For session_set_save_handler() 234 * 235 * @param string $id 236 * 237 * @return string 238 */ 239 private static function read(string $id): string 240 { 241 return (string) Database::prepare( 242 "SELECT session_data FROM `##session` WHERE session_id = :session_id" 243 )->execute([ 244 'session_id' => $id, 245 ])->fetchOne(); 246 } 247 248 /** 249 * For session_set_save_handler() 250 * 251 * @param string $id 252 * @param string $data 253 * 254 * @return bool 255 */ 256 private static function write(string $id, string $data): bool 257 { 258 $request = Request::createFromGlobals(); 259 260 // Only update the session table once per minute, unless the session data has actually changed. 261 Database::prepare( 262 "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" . 263 " VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" . 264 " ON DUPLICATE KEY UPDATE" . 265 " user_id = VALUES(user_id)," . 266 " ip_address = VALUES(ip_address)," . 267 " session_data = VALUES(session_data)," . 268 " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)" 269 )->execute([ 270 'session_id' => $id, 271 'user_id' => (int) Auth::id(), 272 'ip_address' => $request->getClientIp(), 273 'data' => $data, 274 ]); 275 276 return true; 277 } 278 279 280 /** 281 * Cross-Site Request Forgery tokens - ensure that the user is submitting 282 * a form that was generated by the current session. 283 * 284 * @return string 285 */ 286 public static function getCsrfToken(): string 287 { 288 if (!Session::has('CSRF_TOKEN')) { 289 $charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcedfghijklmnopqrstuvwxyz0123456789'; 290 $csrf_token = ''; 291 for ($n = 0; $n < 32; ++$n) { 292 $csrf_token .= substr($charset, random_int(0, 61), 1); 293 } 294 Session::put('CSRF_TOKEN', $csrf_token); 295 } 296 297 return Session::get('CSRF_TOKEN'); 298 } 299} 300