1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use Illuminate\Support\Str; 23use Psr\Http\Message\ServerRequestInterface; 24 25use function array_map; 26use function explode; 27use function implode; 28use function parse_url; 29use function session_name; 30use function session_regenerate_id; 31use function session_register_shutdown; 32use function session_set_cookie_params; 33use function session_set_save_handler; 34use function session_start; 35use function session_status; 36use function session_write_close; 37 38use const PHP_SESSION_ACTIVE; 39use const PHP_URL_HOST; 40use const PHP_URL_PATH; 41use const PHP_URL_SCHEME; 42 43/** 44 * Session handling 45 */ 46class Session 47{ 48 // Use the secure prefix with HTTPS. 49 private const SESSION_NAME = 'WT2_SESSION'; 50 private const SECURE_SESSION_NAME = '__Secure-WT-ID'; 51 52 /** 53 * Start a session 54 * 55 * @param ServerRequestInterface $request 56 * 57 * @return void 58 */ 59 public static function start(ServerRequestInterface $request): void 60 { 61 // Store sessions in the database 62 session_set_save_handler(new SessionDatabaseHandler($request)); 63 64 $url = $request->getAttribute('base_url'); 65 $secure = parse_url($url, PHP_URL_SCHEME) === 'https'; 66 $domain = (string) parse_url($url, PHP_URL_HOST); 67 $path = (string) parse_url($url, PHP_URL_PATH); 68 69 // Paths containing UTF-8 characters need special handling. 70 $path = implode('/', array_map('rawurlencode', explode('/', $path))); 71 72 session_name($secure ? self::SECURE_SESSION_NAME : self::SESSION_NAME); 73 session_register_shutdown(); 74 session_set_cookie_params([ 75 'lifetime' => 0, 76 'path' => $path . '/', 77 'domain' => $domain, 78 'secure' => $secure, 79 'httponly' => true, 80 'samesite' => 'Lax', 81 ]); 82 session_start(); 83 84 // A new session? Prevent session fixation attacks by choosing a new session ID. 85 if (self::get('initiated') !== true) { 86 self::regenerate(true); 87 self::put('initiated', true); 88 } 89 } 90 91 /** 92 * Save/close the session. This releases the session lock. 93 * Closing early can help concurrent connections. 94 */ 95 public static function save(): void 96 { 97 if (session_status() === PHP_SESSION_ACTIVE) { 98 session_write_close(); 99 } 100 } 101 102 /** 103 * Read a value from the session 104 * 105 * @param string $name 106 * @param mixed $default 107 * 108 * @return mixed 109 */ 110 public static function get(string $name, $default = null) 111 { 112 return $_SESSION[$name] ?? $default; 113 } 114 115 /** 116 * Read a value from the session and remove it. 117 * 118 * @param string $name 119 * @param mixed $default 120 * 121 * @return mixed 122 */ 123 public static function pull(string $name, $default = null) 124 { 125 $value = self::get($name, $default); 126 self::forget($name); 127 128 return $value; 129 } 130 131 /** 132 * After any change in authentication level, we should use a new session ID. 133 * 134 * @param bool $destroy 135 * 136 * @return void 137 */ 138 public static function regenerate(bool $destroy = false): void 139 { 140 if ($destroy) { 141 self::clear(); 142 } 143 144 if (session_status() === PHP_SESSION_ACTIVE) { 145 session_regenerate_id($destroy); 146 } 147 } 148 149 /** 150 * Remove all stored data from the session. 151 * 152 * @return void 153 */ 154 public static function clear(): void 155 { 156 $_SESSION = []; 157 } 158 159 /** 160 * Write a value to the session 161 * 162 * @param string $name 163 * @param mixed $value 164 * 165 * @return void 166 */ 167 public static function put(string $name, $value): void 168 { 169 $_SESSION[$name] = $value; 170 } 171 172 /** 173 * Remove a value from the session 174 * 175 * @param string $name 176 * 177 * @return void 178 */ 179 public static function forget(string $name): void 180 { 181 unset($_SESSION[$name]); 182 } 183 184 /** 185 * Cross-Site Request Forgery tokens - ensure that the user is submitting 186 * a form that was generated by the current session. 187 * 188 * @return string 189 */ 190 public static function getCsrfToken(): string 191 { 192 if (!self::has('CSRF_TOKEN')) { 193 self::put('CSRF_TOKEN', Str::random(32)); 194 } 195 196 return self::get('CSRF_TOKEN'); 197 } 198 199 /** 200 * Does a session variable exist? 201 * 202 * @param string $name 203 * 204 * @return bool 205 */ 206 public static function has(string $name): bool 207 { 208 return isset($_SESSION[$name]); 209 } 210} 211