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