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