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