1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Http;
9:
10: use Nette;
11:
12:
13: 14: 15: 16: 17:
18: class Session extends Nette\Object
19: {
20:
21: const DEFAULT_FILE_LIFETIME = 10800;
22:
23:
24: private $regenerated;
25:
26:
27: private static $started;
28:
29:
30: private $options = array(
31:
32: 'referer_check' => '',
33: 'use_cookies' => 1,
34: 'use_only_cookies' => 1,
35: 'use_trans_sid' => 0,
36:
37:
38: 'cookie_lifetime' => 0,
39: 'cookie_path' => '/',
40: 'cookie_domain' => '',
41: 'cookie_secure' => FALSE,
42: 'cookie_httponly' => TRUE,
43:
44:
45: 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
46: 'cache_limiter' => NULL,
47: 'cache_expire' => NULL,
48: 'hash_function' => NULL,
49: 'hash_bits_per_character' => NULL,
50: );
51:
52:
53: private $request;
54:
55:
56: private $response;
57:
58:
59: private $handler;
60:
61:
62: public function __construct(IRequest $request, IResponse $response)
63: {
64: $this->request = $request;
65: $this->response = $response;
66: }
67:
68:
69: 70: 71: 72: 73:
74: public function start()
75: {
76: if (self::$started) {
77: return;
78: }
79:
80: $this->configure($this->options);
81:
82: $id = $this->request->getCookie(session_name());
83: if (is_string($id) && preg_match('#^[0-9a-zA-Z,-]{22,128}\z#i', $id)) {
84: session_id($id);
85: } else {
86: unset($_COOKIE[session_name()]);
87: }
88:
89: try {
90:
91: Nette\Utils\Callback::invokeSafe('session_start', array(), function ($message) use (& $e) {
92: $e = new Nette\InvalidStateException($message);
93: });
94: } catch (\Exception $e) {
95: }
96:
97: $this->response->removeDuplicateCookies();
98: if ($e) {
99: @session_write_close();
100: throw $e;
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: if (session_id() !== '') {
223: session_regenerate_id(TRUE);
224: }
225: session_write_close();
226: $backup = $_SESSION;
227: session_start();
228: $_SESSION = $backup;
229: $this->response->removeDuplicateCookies();
230: }
231: $this->regenerated = TRUE;
232: }
233:
234:
235: 236: 237: 238:
239: public function getId()
240: {
241: return session_id();
242: }
243:
244:
245: 246: 247: 248: 249:
250: public function setName($name)
251: {
252: if (!is_string($name) || !preg_match('#[^0-9.][^.]*\z#A', $name)) {
253: throw new Nette\InvalidArgumentException('Session name must be a string and cannot contain dot.');
254: }
255:
256: session_name($name);
257: return $this->setOptions(array(
258: 'name' => $name,
259: ));
260: }
261:
262:
263: 264: 265: 266:
267: public function getName()
268: {
269: return isset($this->options['name']) ? $this->options['name'] : session_name();
270: }
271:
272:
273:
274:
275:
276: 277: 278: 279: 280: 281: 282:
283: public function getSection($section, $class = 'Nette\Http\SessionSection')
284: {
285: return new $class($this, $section);
286: }
287:
288:
289: 290: 291: 292: 293:
294: public function hasSection($section)
295: {
296: if ($this->exists() && !self::$started) {
297: $this->start();
298: }
299:
300: return !empty($_SESSION['__NF']['DATA'][$section]);
301: }
302:
303:
304: 305: 306: 307:
308: public function getIterator()
309: {
310: if ($this->exists() && !self::$started) {
311: $this->start();
312: }
313:
314: if (isset($_SESSION['__NF']['DATA'])) {
315: return new \ArrayIterator(array_keys($_SESSION['__NF']['DATA']));
316:
317: } else {
318: return new \ArrayIterator;
319: }
320: }
321:
322:
323: 324: 325: 326: 327:
328: public function clean()
329: {
330: if (!self::$started || empty($_SESSION)) {
331: return;
332: }
333:
334: $nf = & $_SESSION['__NF'];
335: if (isset($nf['META']) && is_array($nf['META'])) {
336: foreach ($nf['META'] as $name => $foo) {
337: if (empty($nf['META'][$name])) {
338: unset($nf['META'][$name]);
339: }
340: }
341: }
342:
343: if (empty($nf['META'])) {
344: unset($nf['META']);
345: }
346:
347: if (empty($nf['DATA'])) {
348: unset($nf['DATA']);
349: }
350: }
351:
352:
353:
354:
355:
356: 357: 358: 359: 360: 361: 362:
363: public function setOptions(array $options)
364: {
365: if (self::$started) {
366: $this->configure($options);
367: }
368: $this->options = $options + $this->options;
369: if (!empty($options['auto_start'])) {
370: $this->start();
371: }
372: return $this;
373: }
374:
375:
376: 377: 378: 379:
380: public function getOptions()
381: {
382: return $this->options;
383: }
384:
385:
386: 387: 388: 389: 390:
391: private function configure(array $config)
392: {
393: $special = array('cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1);
394:
395: foreach ($config as $key => $value) {
396: if (!strncmp($key, 'session.', 8)) {
397: $key = substr($key, 8);
398: }
399: $key = strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $key));
400:
401: if ($value === NULL || ini_get("session.$key") == $value) {
402: continue;
403:
404: } elseif (strncmp($key, 'cookie_', 7) === 0) {
405: if (!isset($cookie)) {
406: $cookie = session_get_cookie_params();
407: }
408: $cookie[substr($key, 7)] = $value;
409:
410: } else {
411: if (defined('SID')) {
412: 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().'));
413: }
414: if (isset($special[$key])) {
415: $key = "session_$key";
416: $key($value);
417:
418: } elseif (function_exists('ini_set')) {
419: ini_set("session.$key", $value);
420:
421: } elseif (ini_get("session.$key") != $value) {
422: throw new Nette\NotSupportedException("Unable to set 'session.$key' to '$value' because function ini_set() is disabled.");
423: }
424: }
425: }
426:
427: if (isset($cookie)) {
428: session_set_cookie_params(
429: $cookie['lifetime'], $cookie['path'], $cookie['domain'],
430: $cookie['secure'], $cookie['httponly']
431: );
432: if (self::$started) {
433: $this->sendCookie();
434: }
435: }
436:
437: if ($this->handler) {
438: session_set_save_handler($this->handler);
439: }
440: }
441:
442:
443: 444: 445: 446: 447:
448: public function setExpiration($time)
449: {
450: if (empty($time)) {
451: return $this->setOptions(array(
452: 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
453: 'cookie_lifetime' => 0,
454: ));
455:
456: } else {
457: $time = Nette\Utils\DateTime::from($time)->format('U') - time();
458: return $this->setOptions(array(
459: 'gc_maxlifetime' => $time,
460: 'cookie_lifetime' => $time,
461: ));
462: }
463: }
464:
465:
466: 467: 468: 469: 470: 471: 472:
473: public function setCookieParameters($path, $domain = NULL, $secure = NULL)
474: {
475: return $this->setOptions(array(
476: 'cookie_path' => $path,
477: 'cookie_domain' => $domain,
478: 'cookie_secure' => $secure,
479: ));
480: }
481:
482:
483: 484: 485: 486:
487: public function getCookieParameters()
488: {
489: return session_get_cookie_params();
490: }
491:
492:
493: 494: 495: 496:
497: public function setSavePath($path)
498: {
499: return $this->setOptions(array(
500: 'save_path' => $path,
501: ));
502: }
503:
504:
505: 506: 507: 508:
509: public function setStorage(ISessionStorage $storage)
510: {
511: if (self::$started) {
512: throw new Nette\InvalidStateException('Unable to set storage when session has been started.');
513: }
514: session_set_save_handler(
515: array($storage, 'open'), array($storage, 'close'), array($storage, 'read'),
516: array($storage, 'write'), array($storage, 'remove'), array($storage, 'clean')
517: );
518: return $this;
519: }
520:
521:
522: 523: 524: 525:
526: public function setHandler(\SessionHandlerInterface $handler)
527: {
528: if (self::$started) {
529: throw new Nette\InvalidStateException('Unable to set handler when session has been started.');
530: }
531: $this->handler = $handler;
532: return $this;
533: }
534:
535:
536: 537: 538: 539:
540: private function sendCookie()
541: {
542: if (!headers_sent() && ob_get_level() && ob_get_length()) {
543: 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);
544: }
545:
546: $cookie = $this->getCookieParameters();
547: $this->response->setCookie(
548: session_name(), session_id(),
549: $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0,
550: $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly']
551: );
552: $this->response->setCookie(
553: 'nette-browser', $_SESSION['__NF']['B'],
554: Response::BROWSER, $cookie['path'], $cookie['domain']
555: );
556: }
557:
558: }
559: