PluginProbe ʕ •ᴥ•ʔ
FAPI Member / 2.2.31
FAPI Member v2.2.31
2.2.33 2.2.32 trunk 1.9.47 2.1.18 2.2.24 2.2.25 2.2.26 2.2.28 2.2.29 2.2.30 2.2.31
fapi-member / libs / nette / http / src / Http / Session.php
fapi-member / libs / nette / http / src / Http Last commit date
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