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