PluginProbe
Packeta / trunk
Packeta vtrunk
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / http / src / Http / Session.php

Session.php in Packeta trunk, at deps/nette/http/src/Http/Session.php

458 lines 16.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 Packetery\Nette\Http;
9
10 use Packetery\Nette;
11 /**
12 * Provides access to session sections as well as session settings and management methods.
13 */
14 class Session
15 {
16 use \Packetery\Nette\SmartObject;
17 /** Default file lifetime */
18 private const DefaultFileLifetime = 3 * \Packetery\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 \Packetery\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 \Packetery\Nette\Utils\Callback::invokeSafe('session_start', [['read_and_close' => $this->readAndClose]], function (string $message) use(&$e) : void {
96 $e = new \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\Nette\NotSupportedException
305 * @throws \Packetery\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) \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\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 \Packetery\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 = \Packetery\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 \Packetery\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