1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2017 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 if (isset($_SESSION[$name])) { 57 return $_SESSION[$name]; 58 } else { 59 return $default; 60 } 61 } 62 63 /** 64 * Write a value to the session 65 * 66 * @param string $name 67 * @param mixed $value 68 */ 69 public static function put($name, $value) { 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 unset($_SESSION[$name]); 80 } 81 82 /** 83 * Does a session variable exist? 84 * 85 * @param string $name 86 * 87 * @return bool 88 */ 89 public static function has($name) { 90 return isset($_SESSION[$name]); 91 } 92 93 /** 94 * Remove all stored data from the session. 95 */ 96 public static function clear() { 97 $_SESSION = []; 98 } 99 100 /** 101 * After any change in authentication level, we should use a new session ID. 102 * 103 * @param bool $destroy 104 */ 105 public static function regenerate($destroy = false) { 106 if ($destroy) { 107 self::clear(); 108 } 109 session_regenerate_id($destroy); 110 } 111 112 /** 113 * Set an explicit session ID. Typically used for search robots. 114 * 115 * @param string $id 116 */ 117 public static function setId($id) { 118 session_id($id); 119 } 120 121 /** 122 * Initialise our session save handler 123 */ 124 public static function setSaveHandler() { 125 session_set_save_handler( 126 function (): bool { 127 return Session::open(); 128 }, 129 function ():bool { 130 return Session::close(); 131 }, 132 function (string $id): string { 133 return Session::read($id); 134 }, 135 function (string $id, string $data): bool { 136 return Session::write($id, $data); 137 }, 138 function (string $id): bool { 139 return Session::destroy($id); 140 }, 141 function (int $maxlifetime):bool { 142 return Session::gc($maxlifetime); 143 } 144 ); 145 } 146 147 /** 148 * For session_set_save_handler() 149 * 150 * @return bool 151 */ 152 private static function close() { 153 return true; 154 } 155 156 /** 157 * For session_set_save_handler() 158 * 159 * @param string $id 160 * 161 * @return bool 162 */ 163 private static function destroy(string $id) { 164 Database::prepare( 165 "DELETE FROM `##session` WHERE session_id = :session_id" 166 )->execute([ 167 'session_id' => $id 168 ]); 169 170 return true; 171 } 172 173 /** 174 * For session_set_save_handler() 175 * 176 * @param int $maxlifetime 177 * 178 * @return bool 179 */ 180 private static function gc(int $maxlifetime) { 181 Database::prepare( 182 "DELETE FROM `##session` WHERE session_time < DATE_SUB(NOW(), INTERVAL :maxlifetime SECOND)" 183 )->execute([ 184 'maxlifetime' => $maxlifetime 185 ]); 186 187 return true; 188 } 189 190 /** 191 * For session_set_save_handler() 192 * 193 * @return bool 194 */ 195 private static function open() { 196 return true; 197 } 198 199 /** 200 * For session_set_save_handler() 201 * 202 * @param string $id 203 * 204 * @return string 205 */ 206 private static function read(string $id): string { 207 return (string) Database::prepare( 208 "SELECT session_data FROM `##session` WHERE session_id = :session_id" 209 )->execute([ 210 'session_id' => $id 211 ])->fetchOne(); 212 } 213 214 /** 215 * For session_set_save_handler() 216 * 217 * @param string $id 218 * @param string $data 219 * 220 * @return bool 221 */ 222 private static function write(string $id, string $data): bool { 223 $request = Request::createFromGlobals(); 224 225 // Only update the session table once per minute, unless the session data has actually changed. 226 Database::prepare( 227 "INSERT INTO `##session` (session_id, user_id, ip_address, session_data, session_time)" . 228 " VALUES (:session_id, :user_id, :ip_address, :data, CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP))" . 229 " ON DUPLICATE KEY UPDATE" . 230 " user_id = VALUES(user_id)," . 231 " ip_address = VALUES(ip_address)," . 232 " session_data = VALUES(session_data)," . 233 " session_time = CURRENT_TIMESTAMP - SECOND(CURRENT_TIMESTAMP)" 234 )->execute([ 235 'session_id' => $id, 236 'user_id' => (int) Auth::id(), 237 'ip_address' => $request->getClientIp(), 238 'data' => $data, 239 ]); 240 241 return true; 242 } 243} 244