1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Http;
9:
10: use Nette;
11:
12:
13: 14: 15:
16: class Session
17: {
18: use Nette\SmartObject;
19:
20:
21: const DEFAULT_FILE_LIFETIME = 3 * Nette\Utils\DateTime::HOUR;
22:
23:
24: private $regenerated = false;
25:
26:
27: private static $started = false;
28:
29:
30: private $options = [
31:
32: 'referer_check' => '',
33: 'use_cookies' => 1,
34: 'use_only_cookies' => 1,
35: 'use_trans_sid' => 0,
36:
37:
38: 'cookie_lifetime' => 0,
39: 'cookie_path' => '/',
40: 'cookie_domain' => '',
41: 'cookie_secure' => false,
42: 'cookie_httponly' => true,
43:
44:
45: 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
46: ];
47:
48:
49: private $request;
50:
51:
52: private $response;
53:
54:
55: private $handler;
56:
57:
58: public function __construct(IRequest $request, IResponse $response)
59: {
60: $this->request = $request;
61: $this->response = $response;
62: }
63:
64:
65: 66: 67: 68: 69:
70: public function start()
71: {
72: if (self::$started) {
73: return;
74: }
75:
76: $this->configure($this->options);
77:
78: if (!session_id()) {
79: $id = $this->request->getCookie(session_name());
80: if (is_string($id) && preg_match('#^[0-9a-zA-Z,-]{22,256}\z#i', $id)) {
81: session_id($id);
82: } else {
83: unset($_COOKIE[session_name()]);
84: }
85: }
86:
87: try {
88:
89: Nette\Utils\Callback::invokeSafe('session_start', [], function ($message) use (&$e) {
90: $e = new Nette\InvalidStateException($message);
91: });
92: } catch (\Exception $e) {
93: }
94:
95: if ($e) {
96: @session_write_close();
97: throw $e;
98: }
99:
100: self::$started = true;
101:
102: 103: 104: 105: 106:
107: $nf = &$_SESSION['__NF'];
108:
109: if (!is_array($nf)) {
110: $nf = [];
111: }
112:
113:
114: if (empty($nf['Time'])) {
115: $nf['Time'] = time();
116: if (!empty($id)) {
117: $this->regenerateId();
118: }
119: }
120:
121:
122: if (isset($nf['META'])) {
123: $now = time();
124:
125: foreach ($nf['META'] as $section => $metadata) {
126: if (is_array($metadata)) {
127: foreach ($metadata as $variable => $value) {
128: if (!empty($value['T']) && $now > $value['T']) {
129: if ($variable === '') {
130: unset($nf['META'][$section], $nf['DATA'][$section]);
131: continue 2;
132: }
133: unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]);
134: }
135: }
136: }
137: }
138: }
139:
140: register_shutdown_function([$this, 'clean']);
141: }
142:
143:
144: 145: 146: 147:
148: public function isStarted()
149: {
150: return (bool) self::$started;
151: }
152:
153:
154: 155: 156: 157:
158: public function close()
159: {
160: if (self::$started) {
161: $this->clean();
162: session_write_close();
163: self::$started = false;
164: }
165: }
166:
167:
168: 169: 170: 171:
172: public function destroy()
173: {
174: if (!self::$started) {
175: throw new Nette\InvalidStateException('Session is not started.');
176: }
177:
178: session_destroy();
179: $_SESSION = null;
180: self::$started = false;
181: if (!$this->response->isSent()) {
182: $params = session_get_cookie_params();
183: $this->response->deleteCookie(session_name(), $params['path'], $params['domain'], $params['secure']);
184: }
185: }
186:
187:
188: 189: 190: 191:
192: public function exists()
193: {
194: return self::$started || $this->request->getCookie($this->getName()) !== null;
195: }
196:
197:
198: 199: 200: 201: 202:
203: public function regenerateId()
204: {
205: if (self::$started && !$this->regenerated) {
206: if (headers_sent($file, $line)) {
207: throw new Nette\InvalidStateException('Cannot regenerate session ID after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.'));
208: }
209: if (session_status() === PHP_SESSION_ACTIVE) {
210: session_regenerate_id(true);
211: session_write_close();
212: }
213: $backup = $_SESSION;
214: session_start();
215: $_SESSION = $backup;
216: }
217: $this->regenerated = true;
218: }
219:
220:
221: 222: 223: 224:
225: public function getId()
226: {
227: return session_id();
228: }
229:
230:
231: 232: 233: 234: 235:
236: public function setName($name)
237: {
238: if (!is_string($name) || !preg_match('#[^0-9.][^.]*\z#A', $name)) {
239: throw new Nette\InvalidArgumentException('Session name must be a string and cannot contain dot.');
240: }
241:
242: session_name($name);
243: return $this->setOptions([
244: 'name' => $name,
245: ]);
246: }
247:
248:
249: 250: 251: 252:
253: public function getName()
254: {
255: return isset($this->options['name']) ? $this->options['name'] : session_name();
256: }
257:
258:
259:
260:
261:
262: 263: 264: 265: 266: 267: 268:
269: public function getSection($section, $class = SessionSection::class)
270: {
271: return new $class($this, $section);
272: }
273:
274:
275: 276: 277: 278: 279:
280: public function hasSection($section)
281: {
282: if ($this->exists() && !self::$started) {
283: $this->start();
284: }
285:
286: return !empty($_SESSION['__NF']['DATA'][$section]);
287: }
288:
289:
290: 291: 292: 293:
294: public function getIterator()
295: {
296: if ($this->exists() && !self::$started) {
297: $this->start();
298: }
299:
300: if (isset($_SESSION['__NF']['DATA'])) {
301: return new \ArrayIterator(array_keys($_SESSION['__NF']['DATA']));
302:
303: } else {
304: return new \ArrayIterator;
305: }
306: }
307:
308:
309: 310: 311: 312: 313:
314: public function clean()
315: {
316: if (!self::$started || empty($_SESSION)) {
317: return;
318: }
319:
320: $nf = &$_SESSION['__NF'];
321: if (isset($nf['META']) && is_array($nf['META'])) {
322: foreach ($nf['META'] as $name => $foo) {
323: if (empty($nf['META'][$name])) {
324: unset($nf['META'][$name]);
325: }
326: }
327: }
328:
329: if (empty($nf['META'])) {
330: unset($nf['META']);
331: }
332:
333: if (empty($nf['DATA'])) {
334: unset($nf['DATA']);
335: }
336: }
337:
338:
339:
340:
341:
342: 343: 344: 345: 346: 347: 348:
349: public function setOptions(array $options)
350: {
351: $normalized = [];
352: foreach ($options as $key => $value) {
353: if (!strncmp($key, 'session.', 8)) {
354: $key = substr($key, 8);
355: }
356: $key = strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $key));
357: $normalized[$key] = $value;
358: }
359: if (self::$started) {
360: $this->configure($normalized);
361: }
362: $this->options = $normalized + $this->options;
363: if (!empty($normalized['auto_start'])) {
364: $this->start();
365: }
366: return $this;
367: }
368:
369:
370: 371: 372: 373:
374: public function getOptions()
375: {
376: return $this->options;
377: }
378:
379:
380: 381: 382: 383: 384:
385: private function configure(array $config)
386: {
387: $special = ['cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1];
388: $cookie = $origCookie = session_get_cookie_params();
389:
390: foreach ($config as $key => $value) {
391: if ($value === null || ini_get("session.$key") == $value) {
392: continue;
393:
394: } elseif (strncmp($key, 'cookie_', 7) === 0) {
395: $cookie[substr($key, 7)] = $value;
396:
397: } else {
398: if (session_status() === PHP_SESSION_ACTIVE) {
399: throw new Nette\InvalidStateException("Unable to set 'session.$key' to value '$value' when session has been started" . (self::$started ? '.' : ' by session.auto_start or session_start().'));
400: }
401: if (isset($special[$key])) {
402: $key = "session_$key";
403: $key($value);
404:
405: } elseif (function_exists('ini_set')) {
406: ini_set("session.$key", (string) $value);
407:
408: } elseif (ini_get("session.$key") != $value) {
409: throw new Nette\NotSupportedException("Unable to set 'session.$key' to '$value' because function ini_set() is disabled.");
410: }
411: }
412: }
413:
414: if ($cookie !== $origCookie) {
415: if (PHP_VERSION_ID >= 70300) {
416: session_set_cookie_params($cookie);
417: } else {
418: session_set_cookie_params(
419: $cookie['lifetime'],
420: $cookie['path'] . (isset($cookie['samesite']) ? '; SameSite=' . $cookie['samesite'] : ''),
421: $cookie['domain'],
422: $cookie['secure'],
423: $cookie['httponly']
424: );
425: }
426: if (self::$started) {
427: $this->sendCookie();
428: }
429: }
430:
431: if ($this->handler) {
432: session_set_save_handler($this->handler);
433: }
434: }
435:
436:
437: 438: 439: 440: 441:
442: public function setExpiration($time)
443: {
444: if (empty($time)) {
445: return $this->setOptions([
446: 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
447: 'cookie_lifetime' => 0,
448: ]);
449:
450: } else {
451: $time = Nette\Utils\DateTime::from($time)->format('U') - time();
452: return $this->setOptions([
453: 'gc_maxlifetime' => $time,
454: 'cookie_lifetime' => $time,
455: ]);
456: }
457: }
458:
459:
460: 461: 462: 463: 464: 465: 466: 467:
468: public function setCookieParameters($path, $domain = null, $secure = null, $samesite = null)
469: {
470: return $this->setOptions([
471: 'cookie_path' => $path,
472: 'cookie_domain' => $domain,
473: 'cookie_secure' => $secure,
474: 'cookie_samesite' => $samesite,
475: ]);
476: }
477:
478:
479: 480: 481: 482:
483: public function getCookieParameters()
484: {
485: return session_get_cookie_params();
486: }
487:
488:
489: 490: 491: 492:
493: public function setSavePath($path)
494: {
495: return $this->setOptions([
496: 'save_path' => $path,
497: ]);
498: }
499:
500:
501: 502: 503: 504:
505: public function setStorage(ISessionStorage $storage)
506: {
507: if (self::$started) {
508: throw new Nette\InvalidStateException('Unable to set storage when session has been started.');
509: }
510: session_set_save_handler(
511: [$storage, 'open'], [$storage, 'close'], [$storage, 'read'],
512: [$storage, 'write'], [$storage, 'remove'], [$storage, 'clean']
513: );
514: return $this;
515: }
516:
517:
518: 519: 520: 521:
522: public function setHandler(\SessionHandlerInterface $handler)
523: {
524: if (self::$started) {
525: throw new Nette\InvalidStateException('Unable to set handler when session has been started.');
526: }
527: $this->handler = $handler;
528: return $this;
529: }
530:
531:
532: 533: 534: 535:
536: private function sendCookie()
537: {
538: $cookie = $this->getCookieParameters();
539: $this->response->setCookie(
540: session_name(), session_id(),
541: $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0,
542: $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly'],
543: isset($cookie['samesite']) ? $cookie['samesite'] : null
544: );
545: }
546: }
547: