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