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