Context.php
4 weeks ago
FileUpload.php
4 weeks ago
Helpers.php
4 weeks ago
IRequest.php
4 weeks ago
IResponse.php
4 weeks ago
Request.php
4 weeks ago
RequestFactory.php
4 weeks ago
Response.php
4 weeks ago
Session.php
4 weeks ago
SessionSection.php
4 weeks ago
Url.php
4 weeks ago
UrlImmutable.php
4 weeks ago
UrlScript.php
4 weeks ago
UserStorage.php
4 weeks ago
Session.php
458 lines
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * This file is part of the Nette Framework (https://nette.org) |
| 5 | * Copyright (c) 2004 David Grudl (https://davidgrudl.com) |
| 6 | */ |
| 7 | declare (strict_types=1); |
| 8 | namespace FapiMember\Library\Nette\Http; |
| 9 | |
| 10 | use FapiMember\Library\Nette; |
| 11 | /** |
| 12 | * Provides access to session sections as well as session settings and management methods. |
| 13 | */ |
| 14 | class Session |
| 15 | { |
| 16 | use Nette\SmartObject; |
| 17 | /** Default file lifetime */ |
| 18 | private const DefaultFileLifetime = 3 * Nette\Utils\DateTime::HOUR; |
| 19 | private const SecurityOptions = [ |
| 20 | 'referer_check' => '', |
| 21 | // must be disabled because PHP implementation is invalid |
| 22 | 'use_cookies' => 1, |
| 23 | // must be enabled to prevent Session Hijacking and Fixation |
| 24 | 'use_only_cookies' => 1, |
| 25 | // must be enabled to prevent Session Fixation |
| 26 | 'use_trans_sid' => 0, |
| 27 | // must be disabled to prevent Session Hijacking and Fixation |
| 28 | 'use_strict_mode' => 1, |
| 29 | // must be enabled to prevent Session Fixation |
| 30 | 'cookie_httponly' => \true, |
| 31 | ]; |
| 32 | /** @var array<callable(self): void> Occurs when the session is started */ |
| 33 | public $onStart = []; |
| 34 | /** @var array<callable(self): void> Occurs before the session is written to disk */ |
| 35 | public $onBeforeWrite = []; |
| 36 | /** @var bool has been session ID regenerated? */ |
| 37 | private $regenerated = \false; |
| 38 | /** @var bool has been session started by Nette? */ |
| 39 | private $started = \false; |
| 40 | /** @var array default configuration */ |
| 41 | private $options = [ |
| 42 | 'cookie_samesite' => IResponse::SameSiteLax, |
| 43 | 'cookie_lifetime' => 0, |
| 44 | // for a maximum of 3 hours or until the browser is closed |
| 45 | 'gc_maxlifetime' => self::DefaultFileLifetime, |
| 46 | ]; |
| 47 | /** @var IRequest */ |
| 48 | private $request; |
| 49 | /** @var IResponse */ |
| 50 | private $response; |
| 51 | /** @var \SessionHandlerInterface */ |
| 52 | private $handler; |
| 53 | /** @var bool */ |
| 54 | private $readAndClose = \false; |
| 55 | /** @var bool */ |
| 56 | private $fileExists = \true; |
| 57 | /** @var bool */ |
| 58 | private $autoStart = \true; |
| 59 | public function __construct(IRequest $request, IResponse $response) |
| 60 | { |
| 61 | $this->request = $request; |
| 62 | $this->response = $response; |
| 63 | $this->options['cookie_path'] =& $this->response->cookiePath; |
| 64 | $this->options['cookie_domain'] =& $this->response->cookieDomain; |
| 65 | $this->options['cookie_secure'] =& $this->response->cookieSecure; |
| 66 | } |
| 67 | /** |
| 68 | * Starts and initializes session data. |
| 69 | * @throws Nette\InvalidStateException |
| 70 | */ |
| 71 | public function start(): void |
| 72 | { |
| 73 | $this->doStart(); |
| 74 | } |
| 75 | private function doStart($mustExists = \false): void |
| 76 | { |
| 77 | if (session_status() === \PHP_SESSION_ACTIVE) { |
| 78 | // adapt an existing session |
| 79 | if (!$this->started) { |
| 80 | $this->configure(self::SecurityOptions); |
| 81 | $this->initialize(); |
| 82 | } |
| 83 | return; |
| 84 | } |
| 85 | $this->configure(self::SecurityOptions + $this->options); |
| 86 | if (!session_id()) { |
| 87 | // session is started for first time |
| 88 | $id = $this->request->getCookie(session_name()); |
| 89 | $id = (is_string($id) && preg_match('#^[0-9a-zA-Z,-]{22,256}$#Di', $id)) ? $id : session_create_id(); |
| 90 | session_id($id); |
| 91 | // causes resend of a cookie to make sure it has the right parameters |
| 92 | } |
| 93 | try { |
| 94 | // session_start returns false on failure only sometimes (even in PHP >= 7.1) |
| 95 | Nette\Utils\Callback::invokeSafe('session_start', [['read_and_close' => $this->readAndClose]], function (string $message) use (&$e): void { |
| 96 | $e = new Nette\InvalidStateException($message); |
| 97 | }); |
| 98 | } catch (\Throwable $e) { |
| 99 | } |
| 100 | if ($e) { |
| 101 | @session_write_close(); |
| 102 | // this is needed |
| 103 | throw $e; |
| 104 | } |
| 105 | if ($mustExists && $this->request->getCookie(session_name()) !== session_id()) { |
| 106 | // PHP regenerated the ID which means that the session did not exist and cookie was invalid |
| 107 | $this->destroy(); |
| 108 | return; |
| 109 | } |
| 110 | $this->initialize(); |
| 111 | Nette\Utils\Arrays::invoke($this->onStart, $this); |
| 112 | } |
| 113 | /** @internal */ |
| 114 | public function autoStart(bool $forWrite): void |
| 115 | { |
| 116 | if ($this->started || !$forWrite && !$this->exists()) { |
| 117 | return; |
| 118 | } elseif (!$this->autoStart) { |
| 119 | trigger_error('Cannot auto-start session because autostarting is disabled', \E_USER_WARNING); |
| 120 | return; |
| 121 | } |
| 122 | $this->doStart(!$forWrite); |
| 123 | } |
| 124 | private function initialize(): void |
| 125 | { |
| 126 | $this->started = \true; |
| 127 | $this->fileExists = \true; |
| 128 | /* structure: |
| 129 | __NF: Data, Meta, Time |
| 130 | DATA: section->variable = data |
| 131 | META: section->variable = Timestamp |
| 132 | */ |
| 133 | $nf =& $_SESSION['__NF']; |
| 134 | if (!is_array($nf)) { |
| 135 | $nf = []; |
| 136 | } |
| 137 | // regenerate empty session |
| 138 | if (empty($nf['Time']) && !$this->readAndClose) { |
| 139 | $nf['Time'] = time(); |
| 140 | if ($this->request->getCookie(session_name()) === session_id()) { |
| 141 | // ensures that the session was created with use_strict_mode (ie by Nette) |
| 142 | $this->regenerateId(); |
| 143 | } |
| 144 | } |
| 145 | // expire section variables |
| 146 | $now = time(); |
| 147 | foreach ($nf['META'] ?? [] as $section => $metadata) { |
| 148 | foreach ($metadata ?? [] as $variable => $value) { |
| 149 | if (!empty($value['T']) && $now > $value['T']) { |
| 150 | if ($variable === '') { |
| 151 | // expire whole section |
| 152 | unset($nf['META'][$section], $nf['DATA'][$section]); |
| 153 | continue 2; |
| 154 | } |
| 155 | unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]); |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | public function __destruct() |
| 161 | { |
| 162 | $this->clean(); |
| 163 | } |
| 164 | /** |
| 165 | * Has been session started? |
| 166 | */ |
| 167 | public function isStarted(): bool |
| 168 | { |
| 169 | return $this->started && session_status() === \PHP_SESSION_ACTIVE; |
| 170 | } |
| 171 | /** |
| 172 | * Ends the current session and store session data. |
| 173 | */ |
| 174 | public function close(): void |
| 175 | { |
| 176 | if (session_status() === \PHP_SESSION_ACTIVE) { |
| 177 | $this->clean(); |
| 178 | session_write_close(); |
| 179 | $this->started = \false; |
| 180 | } |
| 181 | } |
| 182 | /** |
| 183 | * Destroys all data registered to a session. |
| 184 | */ |
| 185 | public function destroy(): void |
| 186 | { |
| 187 | if (session_status() !== \PHP_SESSION_ACTIVE) { |
| 188 | throw new Nette\InvalidStateException('Session is not started.'); |
| 189 | } |
| 190 | session_destroy(); |
| 191 | $_SESSION = null; |
| 192 | $this->started = \false; |
| 193 | $this->fileExists = \false; |
| 194 | if (!$this->response->isSent()) { |
| 195 | $params = session_get_cookie_params(); |
| 196 | $this->response->deleteCookie(session_name(), $params['path'], $params['domain'], $params['secure']); |
| 197 | } |
| 198 | } |
| 199 | /** |
| 200 | * Does session exist for the current request? |
| 201 | */ |
| 202 | public function exists(): bool |
| 203 | { |
| 204 | return session_status() === \PHP_SESSION_ACTIVE || $this->fileExists && $this->request->getCookie($this->getName()); |
| 205 | } |
| 206 | /** |
| 207 | * Regenerates the session ID. |
| 208 | * @throws Nette\InvalidStateException |
| 209 | */ |
| 210 | public function regenerateId(): void |
| 211 | { |
| 212 | if ($this->regenerated) { |
| 213 | return; |
| 214 | } |
| 215 | if (session_status() === \PHP_SESSION_ACTIVE) { |
| 216 | if (headers_sent($file, $line)) { |
| 217 | throw new Nette\InvalidStateException('Cannot regenerate session ID after HTTP headers have been sent' . ($file ? " (output started at {$file}:{$line})." : '.')); |
| 218 | } |
| 219 | session_regenerate_id(\true); |
| 220 | } else { |
| 221 | session_id(session_create_id()); |
| 222 | } |
| 223 | $this->regenerated = \true; |
| 224 | } |
| 225 | /** |
| 226 | * Returns the current session ID. Don't make dependencies, can be changed for each request. |
| 227 | */ |
| 228 | public function getId(): string |
| 229 | { |
| 230 | return session_id(); |
| 231 | } |
| 232 | /** |
| 233 | * Sets the session name to a specified one. |
| 234 | * @return static |
| 235 | */ |
| 236 | public function setName(string $name) |
| 237 | { |
| 238 | if (!preg_match('#[^0-9.][^.]*$#DA', $name)) { |
| 239 | throw new Nette\InvalidArgumentException('Session name cannot contain dot.'); |
| 240 | } |
| 241 | session_name($name); |
| 242 | return $this->setOptions(['name' => $name]); |
| 243 | } |
| 244 | /** |
| 245 | * Gets the session name. |
| 246 | */ |
| 247 | public function getName(): string |
| 248 | { |
| 249 | return $this->options['name'] ?? session_name(); |
| 250 | } |
| 251 | /********************* sections management ****************d*g**/ |
| 252 | /** |
| 253 | * Returns specified session section. |
| 254 | * @throws Nette\InvalidArgumentException |
| 255 | */ |
| 256 | public function getSection(string $section, string $class = SessionSection::class): SessionSection |
| 257 | { |
| 258 | return new $class($this, $section); |
| 259 | } |
| 260 | /** |
| 261 | * Checks if a session section exist and is not empty. |
| 262 | */ |
| 263 | public function hasSection(string $section): bool |
| 264 | { |
| 265 | if ($this->exists() && !$this->started) { |
| 266 | $this->autoStart(\false); |
| 267 | } |
| 268 | return !empty($_SESSION['__NF']['DATA'][$section]); |
| 269 | } |
| 270 | /** @return string[] */ |
| 271 | public function getSectionNames(): array |
| 272 | { |
| 273 | if ($this->exists() && !$this->started) { |
| 274 | $this->autoStart(\false); |
| 275 | } |
| 276 | return array_keys($_SESSION['__NF']['DATA'] ?? []); |
| 277 | } |
| 278 | /** @deprecated use getSectionNames() */ |
| 279 | public function getIterator(): \Iterator |
| 280 | { |
| 281 | trigger_error(__METHOD__ . '() is deprecated', \E_USER_DEPRECATED); |
| 282 | return new \ArrayIterator($this->getSectionNames()); |
| 283 | } |
| 284 | /** |
| 285 | * Cleans and minimizes meta structures. |
| 286 | */ |
| 287 | private function clean(): void |
| 288 | { |
| 289 | if (!$this->isStarted()) { |
| 290 | return; |
| 291 | } |
| 292 | Nette\Utils\Arrays::invoke($this->onBeforeWrite, $this); |
| 293 | $nf =& $_SESSION['__NF']; |
| 294 | foreach ($nf['META'] ?? [] as $name => $foo) { |
| 295 | if (empty($nf['META'][$name])) { |
| 296 | unset($nf['META'][$name]); |
| 297 | } |
| 298 | } |
| 299 | } |
| 300 | /********************* configuration ****************d*g**/ |
| 301 | /** |
| 302 | * Sets session options. |
| 303 | * @return static |
| 304 | * @throws Nette\NotSupportedException |
| 305 | * @throws Nette\InvalidStateException |
| 306 | */ |
| 307 | public function setOptions(array $options) |
| 308 | { |
| 309 | $normalized = []; |
| 310 | $allowed = ini_get_all('session', \false) + ['session.read_and_close' => 1, 'session.cookie_samesite' => 1]; |
| 311 | // for PHP < 7.3 |
| 312 | foreach ($options as $key => $value) { |
| 313 | if (!strncmp($key, 'session.', 8)) { |
| 314 | // back compatibility |
| 315 | $key = substr($key, 8); |
| 316 | } |
| 317 | $normKey = strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $key)); |
| 318 | // camelCase -> snake_case |
| 319 | if (!isset($allowed["session.{$normKey}"])) { |
| 320 | $hint = substr((string) Nette\Utils\Helpers::getSuggestion(array_keys($allowed), "session.{$normKey}"), 8); |
| 321 | if ($key !== $normKey) { |
| 322 | $hint = preg_replace_callback('#_(.)#', function ($m) { |
| 323 | return strtoupper($m[1]); |
| 324 | }, $hint); |
| 325 | // snake_case -> camelCase |
| 326 | } |
| 327 | throw new Nette\InvalidStateException("Invalid session configuration option '{$key}'" . ($hint ? ", did you mean '{$hint}'?" : '.')); |
| 328 | } |
| 329 | $normalized[$normKey] = $value; |
| 330 | } |
| 331 | if (isset($normalized['read_and_close'])) { |
| 332 | if (session_status() === \PHP_SESSION_ACTIVE) { |
| 333 | throw new Nette\InvalidStateException('Cannot configure "read_and_close" for already started session.'); |
| 334 | } |
| 335 | $this->readAndClose = (bool) $normalized['read_and_close']; |
| 336 | unset($normalized['read_and_close']); |
| 337 | } |
| 338 | $this->autoStart = $normalized['auto_start'] ?? \true; |
| 339 | unset($normalized['auto_start']); |
| 340 | if (session_status() === \PHP_SESSION_ACTIVE) { |
| 341 | $this->configure($normalized); |
| 342 | } |
| 343 | $this->options = $normalized + $this->options; |
| 344 | return $this; |
| 345 | } |
| 346 | /** |
| 347 | * Returns all session options. |
| 348 | */ |
| 349 | public function getOptions(): array |
| 350 | { |
| 351 | return $this->options; |
| 352 | } |
| 353 | /** |
| 354 | * Configures session environment. |
| 355 | */ |
| 356 | private function configure(array $config): void |
| 357 | { |
| 358 | $special = ['cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1]; |
| 359 | $cookie = $origCookie = session_get_cookie_params(); |
| 360 | foreach ($config as $key => $value) { |
| 361 | if ($value === null || ini_get("session.{$key}") == $value) { |
| 362 | // intentionally == |
| 363 | continue; |
| 364 | } elseif (strncmp($key, 'cookie_', 7) === 0) { |
| 365 | $cookie[substr($key, 7)] = $value; |
| 366 | } else { |
| 367 | if (session_status() === \PHP_SESSION_ACTIVE) { |
| 368 | throw new Nette\InvalidStateException("Unable to set 'session.{$key}' to value '{$value}' when session has been started" . ($this->started ? '.' : ' by session.auto_start or session_start().')); |
| 369 | } |
| 370 | if (isset($special[$key])) { |
| 371 | ("session_{$key}")($value); |
| 372 | } elseif (function_exists('ini_set')) { |
| 373 | ini_set("session.{$key}", (string) $value); |
| 374 | } else { |
| 375 | throw new Nette\NotSupportedException("Unable to set 'session.{$key}' to '{$value}' because function ini_set() is disabled."); |
| 376 | } |
| 377 | } |
| 378 | } |
| 379 | if ($cookie !== $origCookie) { |
| 380 | if (\PHP_VERSION_ID >= 70300) { |
| 381 | @session_set_cookie_params($cookie); |
| 382 | // @ may trigger warning when session is active since PHP 7.2 |
| 383 | } else { |
| 384 | @session_set_cookie_params( |
| 385 | // @ may trigger warning when session is active since PHP 7.2 |
| 386 | $cookie['lifetime'], |
| 387 | $cookie['path'] . (isset($cookie['samesite']) ? '; SameSite=' . $cookie['samesite'] : ''), |
| 388 | $cookie['domain'], |
| 389 | $cookie['secure'], |
| 390 | $cookie['httponly'] |
| 391 | ); |
| 392 | } |
| 393 | if (session_status() === \PHP_SESSION_ACTIVE) { |
| 394 | $this->sendCookie(); |
| 395 | } |
| 396 | } |
| 397 | if ($this->handler) { |
| 398 | session_set_save_handler($this->handler); |
| 399 | } |
| 400 | } |
| 401 | /** |
| 402 | * Sets the amount of time (like '20 minutes') allowed between requests before the session will be terminated, |
| 403 | * null means "for a maximum of 3 hours or until the browser is closed". |
| 404 | * @return static |
| 405 | */ |
| 406 | public function setExpiration(?string $expire) |
| 407 | { |
| 408 | if ($expire === null) { |
| 409 | return $this->setOptions(['gc_maxlifetime' => self::DefaultFileLifetime, 'cookie_lifetime' => 0]); |
| 410 | } else { |
| 411 | $expire = Nette\Utils\DateTime::from($expire)->format('U') - time(); |
| 412 | return $this->setOptions(['gc_maxlifetime' => $expire, 'cookie_lifetime' => $expire]); |
| 413 | } |
| 414 | } |
| 415 | /** |
| 416 | * Sets the session cookie parameters. |
| 417 | * @return static |
| 418 | */ |
| 419 | public function setCookieParameters(string $path, ?string $domain = null, ?bool $secure = null, ?string $sameSite = null) |
| 420 | { |
| 421 | return $this->setOptions(['cookie_path' => $path, 'cookie_domain' => $domain, 'cookie_secure' => $secure, 'cookie_samesite' => $sameSite]); |
| 422 | } |
| 423 | /** @deprecated */ |
| 424 | public function getCookieParameters(): array |
| 425 | { |
| 426 | trigger_error(__METHOD__ . '() is deprecated.', \E_USER_DEPRECATED); |
| 427 | return session_get_cookie_params(); |
| 428 | } |
| 429 | /** |
| 430 | * Sets path of the directory used to save session data. |
| 431 | * @return static |
| 432 | */ |
| 433 | public function setSavePath(string $path) |
| 434 | { |
| 435 | return $this->setOptions(['save_path' => $path]); |
| 436 | } |
| 437 | /** |
| 438 | * Sets user session handler. |
| 439 | * @return static |
| 440 | */ |
| 441 | public function setHandler(\SessionHandlerInterface $handler) |
| 442 | { |
| 443 | if ($this->started) { |
| 444 | throw new Nette\InvalidStateException('Unable to set handler when session has been started.'); |
| 445 | } |
| 446 | $this->handler = $handler; |
| 447 | return $this; |
| 448 | } |
| 449 | /** |
| 450 | * Sends the session cookies. |
| 451 | */ |
| 452 | private function sendCookie(): void |
| 453 | { |
| 454 | $cookie = session_get_cookie_params(); |
| 455 | $this->response->setCookie(session_name(), session_id(), $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0, $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly'], $cookie['samesite'] ?? null); |
| 456 | } |
| 457 | } |
| 458 |