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