Namespaces

  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
      • Config
        • Adapters
      • Diagnostics
      • Extensions
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
      • Diagnostics
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • PhpGenerator
    • Reflection
    • Security
      • Diagnostics
    • Templating
    • Utils
  • NetteModule
  • none

Classes

  • Context
  • FileUpload
  • Helpers
  • Request
  • RequestFactory
  • Response
  • Session
  • SessionSection
  • Url
  • UrlScript
  • UserStorage

Interfaces

  • IRequest
  • IResponse
  • ISessionStorage
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Other releases
  • Nette homepage
  1: <?php
  2: 
  3: /**
  4:  * This file is part of the Nette Framework (https://nette.org)
  5:  * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
  6:  */
  7: 
  8: namespace Nette\Http;
  9: 
 10: use Nette;
 11: 
 12: 
 13: /**
 14:  * Provides access to session sections as well as session settings and management methods.
 15:  *
 16:  * @author     David Grudl
 17:  *
 18:  * @property-read bool $started
 19:  * @property-read string $id
 20:  * @property   string $name
 21:  * @property-read \ArrayIterator $iterator
 22:  * @property   array $options
 23:  * @property-write $savePath
 24:  * @property-write ISessionStorage $storage
 25:  */
 26: class Session extends Nette\Object
 27: {
 28:     /** Default file lifetime is 3 hours */
 29:     const DEFAULT_FILE_LIFETIME = 10800;
 30: 
 31:     /** @var bool  has been session ID regenerated? */
 32:     private $regenerated;
 33: 
 34:     /** @var bool  has been session started? */
 35:     private static $started;
 36: 
 37:     /** @var array default configuration */
 38:     private $options = array(
 39:         // security
 40:         'referer_check' => '',    // must be disabled because PHP implementation is invalid
 41:         'use_cookies' => 1,       // must be enabled to prevent Session Hijacking and Fixation
 42:         'use_only_cookies' => 1,  // must be enabled to prevent Session Fixation
 43:         'use_trans_sid' => 0,     // must be disabled to prevent Session Hijacking and Fixation
 44: 
 45:         // cookies
 46:         'cookie_lifetime' => 0,   // until the browser is closed
 47:         'cookie_path' => '/',     // cookie is available within the entire domain
 48:         'cookie_domain' => '',    // cookie is available on current subdomain only
 49:         'cookie_secure' => FALSE, // cookie is available on HTTP & HTTPS
 50:         'cookie_httponly' => TRUE,// must be enabled to prevent Session Hijacking
 51: 
 52:         // other
 53:         'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,// 3 hours
 54:         'cache_limiter' => NULL,  // (default "nocache", special value "\0")
 55:         'cache_expire' => NULL,   // (default "180")
 56:         'hash_function' => NULL,  // (default "0", means MD5)
 57:         'hash_bits_per_character' => NULL, // (default "4")
 58:     );
 59: 
 60:     /** @var IRequest */
 61:     private $request;
 62: 
 63:     /** @var IResponse */
 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:      * Starts and initializes session data.
 76:      * @throws Nette\InvalidStateException
 77:      * @return void
 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:         // session_start returns FALSE on failure only sometimes
 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(); // this is needed
100:             throw new Nette\InvalidStateException($error);
101:         }
102: 
103:         self::$started = TRUE;
104: 
105:         /* structure:
106:             __NF: BrowserKey, Data, Meta, Time
107:                 DATA: section->variable = data
108:                 META: section->variable = Timestamp, Browser, Version
109:         */
110:         $nf = & $_SESSION['__NF'];
111: 
112:         // regenerate empty session
113:         if (empty($nf['Time'])) {
114:             $nf['Time'] = time();
115:             $this->regenerated = TRUE;
116:         }
117: 
118:         // browser closing detection
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:         // resend cookie
127:         $this->sendCookie();
128: 
129:         // process meta metadata
130:         if (isset($nf['META'])) {
131:             $now = time();
132:             // expire section variables
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']) // whenBrowserIsClosed || Time
137:                             || (isset($nf['DATA'][$section][$variable]) && is_object($nf['DATA'][$section][$variable]) && (isset($value['V']) ? $value['V'] : NULL) // Version
138:                                 != Nette\Reflection\ClassType::from($nf['DATA'][$section][$variable])->getAnnotation('serializationVersion')) // intentionally !=
139:                         ) {
140:                             if ($variable === '') { // expire whole section
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:      * Has been session started?
162:      * @return bool
163:      */
164:     public function isStarted()
165:     {
166:         return (bool) self::$started;
167:     }
168: 
169: 
170:     /**
171:      * Ends the current session and store session data.
172:      * @return void
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:      * Destroys all data registered to a session.
186:      * @return void
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:      * Does session exists for the current request?
206:      * @return bool
207:      */
208:     public function exists()
209:     {
210:         return self::$started || $this->request->getCookie($this->getName()) !== NULL;
211:     }
212: 
213: 
214:     /**
215:      * Regenerates the session ID.
216:      * @throws Nette\InvalidStateException
217:      * @return void
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:      * Returns the current session ID. Don't make dependencies, can be changed for each request.
238:      * @return string
239:      */
240:     public function getId()
241:     {
242:         return session_id();
243:     }
244: 
245: 
246:     /**
247:      * Sets the session name to a specified one.
248:      * @param  string
249:      * @return self
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:      * Gets the session name.
266:      * @return string
267:      */
268:     public function getName()
269:     {
270:         return isset($this->options['name']) ? $this->options['name'] : session_name();
271:     }
272: 
273: 
274:     /********************* sections management ****************d*g**/
275: 
276: 
277:     /**
278:      * Returns specified session section.
279:      * @param  string
280:      * @param  string
281:      * @return SessionSection
282:      * @throws Nette\InvalidArgumentException
283:      */
284:     public function getSection($section, $class = 'Nette\Http\SessionSection')
285:     {
286:         return new $class($this, $section);
287:     }
288: 
289: 
290:     /**
291:      * Checks if a session section exist and is not empty.
292:      * @param  string
293:      * @return bool
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:      * Iteration over all sections.
307:      * @return \ArrayIterator
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:      * Cleans and minimizes meta structures. This method is called automatically on shutdown, do not call it directly.
326:      * @internal
327:      * @return void
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:     /********************* configuration ****************d*g**/
355: 
356: 
357:     /**
358:      * Sets session options.
359:      * @param  array
360:      * @return self
361:      * @throws Nette\NotSupportedException
362:      * @throws Nette\InvalidStateException
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:      * Returns all session options.
379:      * @return array
380:      */
381:     public function getOptions()
382:     {
383:         return $this->options;
384:     }
385: 
386: 
387:     /**
388:      * Configures session environment.
389:      * @param  array
390:      * @return void
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)) { // back compatibility
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) { // intentionally ==
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) { // intentionally ==
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:      * Sets the amount of time allowed between requests before the session will be terminated.
442:      * @param  string|int|\DateTime  time, value 0 means "until the browser is closed"
443:      * @return self
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:      * Sets the session cookie parameters.
465:      * @param  string  path
466:      * @param  string  domain
467:      * @param  bool    secure
468:      * @return self
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:      * Returns the session cookie parameters.
482:      * @return array  containing items: lifetime, path, domain, secure, httponly
483:      */
484:     public function getCookieParameters()
485:     {
486:         return session_get_cookie_params();
487:     }
488: 
489: 
490:     /**
491:      * Sets path of the directory used to save session data.
492:      * @return self
493:      */
494:     public function setSavePath($path)
495:     {
496:         return $this->setOptions(array(
497:             'save_path' => $path,
498:         ));
499:     }
500: 
501: 
502:     /**
503:      * Sets user session storage for PHP < 5.4. For PHP >= 5.4, use setHandler().
504:      * @return self
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:      * Sets user session handler.
521:      * @return self
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:      * Sends the session cookies.
535:      * @return void
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: 
Nette 2.1 API documentation generated by ApiGen 2.8.0