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