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