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