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