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