Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationLatte
      • ApplicationTracy
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsLatte
      • Framework
      • HttpTracy
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Drivers
      • Reflection
      • Table
    • DI
      • Config
        • Adapters
      • Extensions
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Latte
    • Loaders
    • Localization
    • Mail
    • Neon
    • PhpGenerator
    • Reflection
    • Security
    • Templating
    • Utils
  • NetteModule
  • none
  • Tracy

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: class Session extends Nette\Object
 19: {
 20:     /** Default file lifetime is 3 hours */
 21:     const DEFAULT_FILE_LIFETIME = 10800;
 22: 
 23:     /** @var bool  has been session ID regenerated? */
 24:     private $regenerated;
 25: 
 26:     /** @var bool  has been session started? */
 27:     private static $started;
 28: 
 29:     /** @var array default configuration */
 30:     private $options = array(
 31:         // security
 32:         'referer_check' => '',    // must be disabled because PHP implementation is invalid
 33:         'use_cookies' => 1,       // must be enabled to prevent Session Hijacking and Fixation
 34:         'use_only_cookies' => 1,  // must be enabled to prevent Session Fixation
 35:         'use_trans_sid' => 0,     // must be disabled to prevent Session Hijacking and Fixation
 36: 
 37:         // cookies
 38:         'cookie_lifetime' => 0,   // until the browser is closed
 39:         'cookie_path' => '/',     // cookie is available within the entire domain
 40:         'cookie_domain' => '',    // cookie is available on current subdomain only
 41:         'cookie_secure' => FALSE, // cookie is available on HTTP & HTTPS
 42:         'cookie_httponly' => TRUE,// must be enabled to prevent Session Hijacking
 43: 
 44:         // other
 45:         'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,// 3 hours
 46:         'cache_limiter' => NULL,  // (default "nocache", special value "\0")
 47:         'cache_expire' => NULL,   // (default "180")
 48:         'hash_function' => NULL,  // (default "0", means MD5)
 49:         'hash_bits_per_character' => NULL, // (default "4")
 50:     );
 51: 
 52:     /** @var IRequest */
 53:     private $request;
 54: 
 55:     /** @var IResponse */
 56:     private $response;
 57: 
 58:     /** @var \SessionHandlerInterface */
 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:      * Starts and initializes session data.
 71:      * @throws Nette\InvalidStateException
 72:      * @return void
 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:         // session_start returns FALSE on failure only sometimes
 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(); // this is needed
100:             throw $e;
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
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 (!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:         // 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:                             if ($variable === '') { // expire whole section
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:      * Has been session started?
159:      * @return bool
160:      */
161:     public function isStarted()
162:     {
163:         return (bool) self::$started;
164:     }
165: 
166: 
167:     /**
168:      * Ends the current session and store session data.
169:      * @return void
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:      * Destroys all data registered to a session.
183:      * @return void
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:      * Does session exists for the current request?
203:      * @return bool
204:      */
205:     public function exists()
206:     {
207:         return self::$started || $this->request->getCookie($this->getName()) !== NULL;
208:     }
209: 
210: 
211:     /**
212:      * Regenerates the session ID.
213:      * @throws Nette\InvalidStateException
214:      * @return void
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:      * Returns the current session ID. Don't make dependencies, can be changed for each request.
237:      * @return string
238:      */
239:     public function getId()
240:     {
241:         return session_id();
242:     }
243: 
244: 
245:     /**
246:      * Sets the session name to a specified one.
247:      * @param  string
248:      * @return self
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:      * Gets the session name.
265:      * @return string
266:      */
267:     public function getName()
268:     {
269:         return isset($this->options['name']) ? $this->options['name'] : session_name();
270:     }
271: 
272: 
273:     /********************* sections management ****************d*g**/
274: 
275: 
276:     /**
277:      * Returns specified session section.
278:      * @param  string
279:      * @param  string
280:      * @return SessionSection
281:      * @throws Nette\InvalidArgumentException
282:      */
283:     public function getSection($section, $class = 'Nette\Http\SessionSection')
284:     {
285:         return new $class($this, $section);
286:     }
287: 
288: 
289:     /**
290:      * Checks if a session section exist and is not empty.
291:      * @param  string
292:      * @return bool
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:      * Iteration over all sections.
306:      * @return \ArrayIterator
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:      * Cleans and minimizes meta structures. This method is called automatically on shutdown, do not call it directly.
325:      * @internal
326:      * @return void
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:     /********************* configuration ****************d*g**/
354: 
355: 
356:     /**
357:      * Sets session options.
358:      * @param  array
359:      * @return self
360:      * @throws Nette\NotSupportedException
361:      * @throws Nette\InvalidStateException
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:      * Returns all session options.
378:      * @return array
379:      */
380:     public function getOptions()
381:     {
382:         return $this->options;
383:     }
384: 
385: 
386:     /**
387:      * Configures session environment.
388:      * @param  array
389:      * @return void
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)) { // back compatibility
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) { // intentionally ==
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) { // intentionally ==
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:      * Sets the amount of time allowed between requests before the session will be terminated.
445:      * @param  string|int|\DateTime  time, value 0 means "until the browser is closed"
446:      * @return self
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:      * Sets the session cookie parameters.
468:      * @param  string  path
469:      * @param  string  domain
470:      * @param  bool    secure
471:      * @return self
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:      * Returns the session cookie parameters.
485:      * @return array  containing items: lifetime, path, domain, secure, httponly
486:      */
487:     public function getCookieParameters()
488:     {
489:         return session_get_cookie_params();
490:     }
491: 
492: 
493:     /**
494:      * Sets path of the directory used to save session data.
495:      * @return self
496:      */
497:     public function setSavePath($path)
498:     {
499:         return $this->setOptions(array(
500:             'save_path' => $path,
501:         ));
502:     }
503: 
504: 
505:     /**
506:      * Sets user session storage for PHP < 5.4. For PHP >= 5.4, use setHandler().
507:      * @return self
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:      * Sets user session handler.
524:      * @return self
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:      * Sends the session cookies.
538:      * @return void
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: 
Nette 2.2 API documentation generated by ApiGen 2.8.0