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