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