1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 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 <http://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; 42use const PHP_VERSION_ID; 43 44/** 45 * Session handling 46 */ 47class Session 48{ 49 private const SESSION_NAME = 'WT2_SESSION'; 50 51 /** 52 * Start a session 53 * 54 * @param ServerRequestInterface $request 55 * 56 * @return void 57 */ 58 public static function start(ServerRequestInterface $request): void 59 { 60 // Store sessions in the database 61 session_set_save_handler(new SessionDatabaseHandler($request)); 62 63 $url = $request->getAttribute('base_url'); 64 $secure = parse_url($url, PHP_URL_SCHEME) === 'https'; 65 $domain = parse_url($url, PHP_URL_HOST); 66 $path = parse_url($url, PHP_URL_PATH) ?? ''; 67 68 // Paths containing UTF-8 characters need special handling. 69 $path = implode('/', array_map('rawurlencode', explode('/', $path))); 70 71 session_name(self::SESSION_NAME); 72 session_register_shutdown(); 73 // Since PHP 7.3, we can set "SameSite: Lax" to help protect against CSRF attacks. 74 if (PHP_VERSION_ID > 70300) { 75 session_set_cookie_params([ 76 'lifetime' => 0, 77 'path' => $path . '/', 78 'domain' => $domain, 79 'secure' => $secure, 80 'httponly' => true, 81 'samesite' => 'Lax', 82 ]); 83 } else { 84 session_set_cookie_params(0, $path . '/', $domain, $secure, true); 85 } 86 session_start(); 87 88 // A new session? Prevent session fixation attacks by choosing a new session ID. 89 if (self::get('initiated') !== true) { 90 self::regenerate(true); 91 self::put('initiated', true); 92 } 93 } 94 95 /** 96 * Save/close the session. This releases the session lock. 97 * Closing early can help concurrent connections. 98 */ 99 public static function save(): void 100 { 101 if (session_status() === PHP_SESSION_ACTIVE) { 102 session_write_close(); 103 } 104 } 105 106 /** 107 * Read a value from the session 108 * 109 * @param string $name 110 * @param mixed $default 111 * 112 * @return mixed 113 */ 114 public static function get(string $name, $default = null) 115 { 116 return $_SESSION[$name] ?? $default; 117 } 118 119 /** 120 * Read a value from the session and remove it. 121 * 122 * @param string $name 123 * @param mixed $default 124 * 125 * @return mixed 126 */ 127 public static function pull(string $name, $default = null) 128 { 129 $value = self::get($name, $default); 130 self::forget($name); 131 132 return $value; 133 } 134 135 /** 136 * After any change in authentication level, we should use a new session ID. 137 * 138 * @param bool $destroy 139 * 140 * @return void 141 */ 142 public static function regenerate(bool $destroy = false): void 143 { 144 if ($destroy) { 145 self::clear(); 146 } 147 148 if (session_status() === PHP_SESSION_ACTIVE) { 149 session_regenerate_id($destroy); 150 } 151 } 152 153 /** 154 * Remove all stored data from the session. 155 * 156 * @return void 157 */ 158 public static function clear(): void 159 { 160 $_SESSION = []; 161 } 162 163 /** 164 * Write a value to the session 165 * 166 * @param string $name 167 * @param mixed $value 168 * 169 * @return void 170 */ 171 public static function put(string $name, $value): void 172 { 173 $_SESSION[$name] = $value; 174 } 175 176 /** 177 * Remove a value from the session 178 * 179 * @param string $name 180 * 181 * @return void 182 */ 183 public static function forget(string $name): void 184 { 185 unset($_SESSION[$name]); 186 } 187 188 /** 189 * Cross-Site Request Forgery tokens - ensure that the user is submitting 190 * a form that was generated by the current session. 191 * 192 * @return string 193 */ 194 public static function getCsrfToken(): string 195 { 196 if (!self::has('CSRF_TOKEN')) { 197 self::put('CSRF_TOKEN', Str::random(32)); 198 } 199 200 return self::get('CSRF_TOKEN'); 201 } 202 203 /** 204 * Does a session variable exist? 205 * 206 * @param string $name 207 * 208 * @return bool 209 */ 210 public static function has(string $name): bool 211 { 212 return isset($_SESSION[$name]); 213 } 214} 215