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