1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Http;
9:
10: use Nette;
11:
12:
13: 14: 15:
16: class Session extends Nette\Object
17: {
18:
19: const DEFAULT_FILE_LIFETIME = 10800;
20:
21:
22: private $regenerated;
23:
24:
25: private static $started;
26:
27:
28: private $options = array(
29:
30: 'referer_check' => '',
31: 'use_cookies' => 1,
32: 'use_only_cookies' => 1,
33: 'use_trans_sid' => 0,
34:
35:
36: 'cookie_lifetime' => 0,
37: 'cookie_path' => '/',
38: 'cookie_domain' => '',
39: 'cookie_secure' => FALSE,
40: 'cookie_httponly' => TRUE,
41:
42:
43: 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
44: 'cache_limiter' => NULL,
45: 'cache_expire' => NULL,
46: 'hash_function' => NULL,
47: 'hash_bits_per_character' => NULL,
48: );
49:
50:
51: private $request;
52:
53:
54: private $response;
55:
56:
57: private $handler;
58:
59:
60: public function __construct(IRequest $request, IResponse $response)
61: {
62: $this->request = $request;
63: $this->response = $response;
64: }
65:
66:
67: 68: 69: 70: 71:
72: public function start()
73: {
74: if (self::$started) {
75: return;
76: }
77:
78: $this->configure($this->options);
79:
80: $id = $this->request->getCookie(session_name());
81: if (is_string($id) && preg_match('#^[0-9a-zA-Z,-]{22,128}\z#i', $id)) {
82: session_id($id);
83: } else {
84: unset($_COOKIE[session_name()]);
85: }
86:
87: try {
88:
89: Nette\Utils\Callback::invokeSafe('session_start', array(), function ($message) use (& $e) {
90: $e = new Nette\InvalidStateException($message);
91: });
92: } catch (\Exception $e) {
93: }
94:
95: Helpers::removeDuplicateCookies();
96: if ($e) {
97: @session_write_close();
98: throw $e;
99: }
100:
101: self::$started = TRUE;
102:
103: 104: 105: 106: 107:
108: $nf = & $_SESSION['__NF'];
109:
110:
111: if (empty($nf['Time'])) {
112: $nf['Time'] = time();
113: $this->regenerated = TRUE;
114: }
115:
116:
117: $browserKey = $this->request->getCookie('nette-browser');
118: if (!is_string($browserKey) || !preg_match('#^[0-9a-z]{10}\z#', $browserKey)) {
119: $browserKey = Nette\Utils\Random::generate();
120: }
121: $browserClosed = !isset($nf['B']) || $nf['B'] !== $browserKey;
122: $nf['B'] = $browserKey;
123:
124:
125: $this->sendCookie();
126:
127:
128: if (isset($nf['META'])) {
129: $now = time();
130:
131: foreach ($nf['META'] as $section => $metadata) {
132: if (is_array($metadata)) {
133: foreach ($metadata as $variable => $value) {
134: if ((!empty($value['B']) && $browserClosed) || (!empty($value['T']) && $now > $value['T'])) {
135: if ($variable === '') {
136: unset($nf['META'][$section], $nf['DATA'][$section]);
137: continue 2;
138: }
139: unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]);
140: }
141: }
142: }
143: }
144: }
145:
146: if ($this->regenerated) {
147: $this->regenerated = FALSE;
148: $this->regenerateId();
149: }
150:
151: register_shutdown_function(array($this, 'clean'));
152: }
153:
154:
155: 156: 157: 158:
159: public function isStarted()
160: {
161: return (bool) self::$started;
162: }
163:
164:
165: 166: 167: 168:
169: public function close()
170: {
171: if (self::$started) {
172: $this->clean();
173: session_write_close();
174: self::$started = FALSE;
175: }
176: }
177:
178:
179: 180: 181: 182:
183: public function destroy()
184: {
185: if (!self::$started) {
186: throw new Nette\InvalidStateException('Session is not started.');
187: }
188:
189: session_destroy();
190: $_SESSION = NULL;
191: self::$started = FALSE;
192: if (!$this->response->isSent()) {
193: $params = session_get_cookie_params();
194: $this->response->deleteCookie(session_name(), $params['path'], $params['domain'], $params['secure']);
195: }
196: }
197:
198:
199: 200: 201: 202:
203: public function exists()
204: {
205: return self::$started || $this->request->getCookie($this->getName()) !== NULL;
206: }
207:
208:
209: 210: 211: 212: 213:
214: public function regenerateId()
215: {
216: if (self::$started && !$this->regenerated) {
217: if (headers_sent($file, $line)) {
218: throw new Nette\InvalidStateException('Cannot regenerate session ID after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.'));
219: }
220: if (session_id() !== '') {
221: session_regenerate_id(TRUE);
222: }
223: session_write_close();
224: $backup = $_SESSION;
225: session_start();
226: $_SESSION = $backup;
227: Helpers::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: $normalized = array();
364: foreach ($options as $key => $value) {
365: if (!strncmp($key, 'session.', 8)) {
366: $key = substr($key, 8);
367: }
368: $key = strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $key));
369: $normalized[$key] = $value;
370: }
371: if (self::$started) {
372: $this->configure($normalized);
373: }
374: $this->options = $normalized + $this->options;
375: if (!empty($normalized['auto_start'])) {
376: $this->start();
377: }
378: return $this;
379: }
380:
381:
382: 383: 384: 385:
386: public function getOptions()
387: {
388: return $this->options;
389: }
390:
391:
392: 393: 394: 395: 396:
397: private function configure(array $config)
398: {
399: $special = array('cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1);
400:
401: foreach ($config as $key => $value) {
402: if ($value === NULL || ini_get("session.$key") == $value) {
403: continue;
404:
405: } elseif (strncmp($key, 'cookie_', 7) === 0) {
406: if (!isset($cookie)) {
407: $cookie = session_get_cookie_params();
408: }
409: $cookie[substr($key, 7)] = $value;
410:
411: } else {
412: if (defined('SID')) {
413: 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().'));
414: }
415: if (isset($special[$key])) {
416: $key = "session_$key";
417: $key($value);
418:
419: } elseif (function_exists('ini_set')) {
420: ini_set("session.$key", $value);
421:
422: } elseif (ini_get("session.$key") != $value) {
423: throw new Nette\NotSupportedException("Unable to set 'session.$key' to '$value' because function ini_set() is disabled.");
424: }
425: }
426: }
427:
428: if (isset($cookie)) {
429: session_set_cookie_params(
430: $cookie['lifetime'], $cookie['path'], $cookie['domain'],
431: $cookie['secure'], $cookie['httponly']
432: );
433: if (self::$started) {
434: $this->sendCookie();
435: }
436: }
437:
438: if ($this->handler) {
439: session_set_save_handler($this->handler);
440: }
441: }
442:
443:
444: 445: 446: 447: 448:
449: public function setExpiration($time)
450: {
451: if (empty($time)) {
452: return $this->setOptions(array(
453: 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
454: 'cookie_lifetime' => 0,
455: ));
456:
457: } else {
458: $time = Nette\Utils\DateTime::from($time)->format('U') - time();
459: return $this->setOptions(array(
460: 'gc_maxlifetime' => $time,
461: 'cookie_lifetime' => $time,
462: ));
463: }
464: }
465:
466:
467: 468: 469: 470: 471: 472: 473:
474: public function setCookieParameters($path, $domain = NULL, $secure = NULL)
475: {
476: return $this->setOptions(array(
477: 'cookie_path' => $path,
478: 'cookie_domain' => $domain,
479: 'cookie_secure' => $secure,
480: ));
481: }
482:
483:
484: 485: 486: 487:
488: public function getCookieParameters()
489: {
490: return session_get_cookie_params();
491: }
492:
493:
494: 495: 496: 497:
498: public function setSavePath($path)
499: {
500: return $this->setOptions(array(
501: 'save_path' => $path,
502: ));
503: }
504:
505:
506: 507: 508: 509:
510: public function setStorage(ISessionStorage $storage)
511: {
512: if (self::$started) {
513: throw new Nette\InvalidStateException('Unable to set storage when session has been started.');
514: }
515: session_set_save_handler(
516: array($storage, 'open'), array($storage, 'close'), array($storage, 'read'),
517: array($storage, 'write'), array($storage, 'remove'), array($storage, 'clean')
518: );
519: return $this;
520: }
521:
522:
523: 524: 525: 526:
527: public function setHandler(\SessionHandlerInterface $handler)
528: {
529: if (self::$started) {
530: throw new Nette\InvalidStateException('Unable to set handler when session has been started.');
531: }
532: $this->handler = $handler;
533: return $this;
534: }
535:
536:
537: 538: 539: 540:
541: private function sendCookie()
542: {
543: $cookie = $this->getCookieParameters();
544: $this->response->setCookie(
545: session_name(), session_id(),
546: $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0,
547: $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly']
548: );
549: $this->response->setCookie(
550: 'nette-browser', $_SESSION['__NF']['B'],
551: Response::BROWSER, $cookie['path'], $cookie['domain']
552: );
553: }
554:
555: }
556: