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