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