Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationDI
      • ApplicationLatte
      • ApplicationTracy
      • CacheDI
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsDI
      • FormsLatte
      • Framework
      • HttpDI
      • HttpTracy
      • MailDI
      • ReflectionDI
      • SecurityDI
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Conventions
      • Drivers
      • Table
    • DI
      • Config
        • Adapters
      • Extensions
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Loaders
    • Localization
    • Mail
    • Neon
    • PhpGenerator
      • Traits
    • Reflection
    • Security
    • Tokenizer
    • Utils
  • Tracy
    • Bridges
      • Nette
  • none

Classes

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

Interfaces

  • IRequest
  • IResponse
  • ISessionStorage
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  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: class Session
 17: {
 18:     use Nette\SmartObject;
 19: 
 20:     /** Default file lifetime */
 21:     const DEFAULT_FILE_LIFETIME = 3 * Nette\Utils\DateTime::HOUR;
 22: 
 23:     /** @var bool  has been session ID regenerated? */
 24:     private $regenerated = false;
 25: 
 26:     /** @var bool  has been session started? */
 27:     private static $started = false;
 28: 
 29:     /** @var array default configuration */
 30:     private $options = [
 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:     ];
 47: 
 48:     /** @var IRequest */
 49:     private $request;
 50: 
 51:     /** @var IResponse */
 52:     private $response;
 53: 
 54:     /** @var \SessionHandlerInterface */
 55:     private $handler;
 56: 
 57: 
 58:     public function __construct(IRequest $request, IResponse $response)
 59:     {
 60:         $this->request = $request;
 61:         $this->response = $response;
 62:     }
 63: 
 64: 
 65:     /**
 66:      * Starts and initializes session data.
 67:      * @throws Nette\InvalidStateException
 68:      * @return void
 69:      */
 70:     public function start()
 71:     {
 72:         if (self::$started) {
 73:             return;
 74:         }
 75: 
 76:         $this->configure($this->options);
 77: 
 78:         if (!session_id()) { // session is started for first time
 79:             $id = $this->request->getCookie(session_name());
 80:             if (is_string($id) && preg_match('#^[0-9a-zA-Z,-]{22,256}\z#i', $id)) {
 81:                 session_id($id);
 82:             } else {
 83:                 unset($_COOKIE[session_name()]);
 84:             }
 85:         }
 86: 
 87:         try {
 88:             // session_start returns false on failure only sometimes
 89:             Nette\Utils\Callback::invokeSafe('session_start', [], function ($message) use (&$e) {
 90:                 $e = new Nette\InvalidStateException($message);
 91:             });
 92:         } catch (\Exception $e) {
 93:         }
 94: 
 95:         if ($e) {
 96:             @session_write_close(); // this is needed
 97:             throw $e;
 98:         }
 99: 
100:         self::$started = true;
101: 
102:         /* structure:
103:             __NF: Data, Meta, Time
104:                 DATA: section->variable = data
105:                 META: section->variable = Timestamp
106:         */
107:         $nf = &$_SESSION['__NF'];
108: 
109:         if (!is_array($nf)) {
110:             $nf = [];
111:         }
112: 
113:         // regenerate empty session
114:         if (empty($nf['Time'])) {
115:             $nf['Time'] = time();
116:             if (!empty($id)) { // ensures that the session was created in strict mode (see use_strict_mode)
117:                 $this->regenerateId();
118:             }
119:         }
120: 
121:         // process meta metadata
122:         if (isset($nf['META'])) {
123:             $now = time();
124:             // expire section variables
125:             foreach ($nf['META'] as $section => $metadata) {
126:                 if (is_array($metadata)) {
127:                     foreach ($metadata as $variable => $value) {
128:                         if (!empty($value['T']) && $now > $value['T']) {
129:                             if ($variable === '') { // expire whole section
130:                                 unset($nf['META'][$section], $nf['DATA'][$section]);
131:                                 continue 2;
132:                             }
133:                             unset($nf['META'][$section][$variable], $nf['DATA'][$section][$variable]);
134:                         }
135:                     }
136:                 }
137:             }
138:         }
139: 
140:         register_shutdown_function([$this, 'clean']);
141:     }
142: 
143: 
144:     /**
145:      * Has been session started?
146:      * @return bool
147:      */
148:     public function isStarted()
149:     {
150:         return (bool) self::$started;
151:     }
152: 
153: 
154:     /**
155:      * Ends the current session and store session data.
156:      * @return void
157:      */
158:     public function close()
159:     {
160:         if (self::$started) {
161:             $this->clean();
162:             session_write_close();
163:             self::$started = false;
164:         }
165:     }
166: 
167: 
168:     /**
169:      * Destroys all data registered to a session.
170:      * @return void
171:      */
172:     public function destroy()
173:     {
174:         if (!self::$started) {
175:             throw new Nette\InvalidStateException('Session is not started.');
176:         }
177: 
178:         session_destroy();
179:         $_SESSION = null;
180:         self::$started = false;
181:         if (!$this->response->isSent()) {
182:             $params = session_get_cookie_params();
183:             $this->response->deleteCookie(session_name(), $params['path'], $params['domain'], $params['secure']);
184:         }
185:     }
186: 
187: 
188:     /**
189:      * Does session exists for the current request?
190:      * @return bool
191:      */
192:     public function exists()
193:     {
194:         return self::$started || $this->request->getCookie($this->getName()) !== null;
195:     }
196: 
197: 
198:     /**
199:      * Regenerates the session ID.
200:      * @throws Nette\InvalidStateException
201:      * @return void
202:      */
203:     public function regenerateId()
204:     {
205:         if (self::$started && !$this->regenerated) {
206:             if (headers_sent($file, $line)) {
207:                 throw new Nette\InvalidStateException('Cannot regenerate session ID after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.'));
208:             }
209:             if (session_status() === PHP_SESSION_ACTIVE) {
210:                 session_regenerate_id(true);
211:                 session_write_close();
212:             }
213:             $backup = $_SESSION;
214:             session_start();
215:             $_SESSION = $backup;
216:         }
217:         $this->regenerated = true;
218:     }
219: 
220: 
221:     /**
222:      * Returns the current session ID. Don't make dependencies, can be changed for each request.
223:      * @return string
224:      */
225:     public function getId()
226:     {
227:         return session_id();
228:     }
229: 
230: 
231:     /**
232:      * Sets the session name to a specified one.
233:      * @param  string
234:      * @return static
235:      */
236:     public function setName($name)
237:     {
238:         if (!is_string($name) || !preg_match('#[^0-9.][^.]*\z#A', $name)) {
239:             throw new Nette\InvalidArgumentException('Session name must be a string and cannot contain dot.');
240:         }
241: 
242:         session_name($name);
243:         return $this->setOptions([
244:             'name' => $name,
245:         ]);
246:     }
247: 
248: 
249:     /**
250:      * Gets the session name.
251:      * @return string
252:      */
253:     public function getName()
254:     {
255:         return isset($this->options['name']) ? $this->options['name'] : session_name();
256:     }
257: 
258: 
259:     /********************* sections management ****************d*g**/
260: 
261: 
262:     /**
263:      * Returns specified session section.
264:      * @param  string
265:      * @param  string
266:      * @return SessionSection
267:      * @throws Nette\InvalidArgumentException
268:      */
269:     public function getSection($section, $class = SessionSection::class)
270:     {
271:         return new $class($this, $section);
272:     }
273: 
274: 
275:     /**
276:      * Checks if a session section exist and is not empty.
277:      * @param  string
278:      * @return bool
279:      */
280:     public function hasSection($section)
281:     {
282:         if ($this->exists() && !self::$started) {
283:             $this->start();
284:         }
285: 
286:         return !empty($_SESSION['__NF']['DATA'][$section]);
287:     }
288: 
289: 
290:     /**
291:      * Iteration over all sections.
292:      * @return \Iterator
293:      */
294:     public function getIterator()
295:     {
296:         if ($this->exists() && !self::$started) {
297:             $this->start();
298:         }
299: 
300:         if (isset($_SESSION['__NF']['DATA'])) {
301:             return new \ArrayIterator(array_keys($_SESSION['__NF']['DATA']));
302: 
303:         } else {
304:             return new \ArrayIterator;
305:         }
306:     }
307: 
308: 
309:     /**
310:      * Cleans and minimizes meta structures. This method is called automatically on shutdown, do not call it directly.
311:      * @internal
312:      * @return void
313:      */
314:     public function clean()
315:     {
316:         if (!self::$started || empty($_SESSION)) {
317:             return;
318:         }
319: 
320:         $nf = &$_SESSION['__NF'];
321:         if (isset($nf['META']) && is_array($nf['META'])) {
322:             foreach ($nf['META'] as $name => $foo) {
323:                 if (empty($nf['META'][$name])) {
324:                     unset($nf['META'][$name]);
325:                 }
326:             }
327:         }
328: 
329:         if (empty($nf['META'])) {
330:             unset($nf['META']);
331:         }
332: 
333:         if (empty($nf['DATA'])) {
334:             unset($nf['DATA']);
335:         }
336:     }
337: 
338: 
339:     /********************* configuration ****************d*g**/
340: 
341: 
342:     /**
343:      * Sets session options.
344:      * @param  array
345:      * @return static
346:      * @throws Nette\NotSupportedException
347:      * @throws Nette\InvalidStateException
348:      */
349:     public function setOptions(array $options)
350:     {
351:         $normalized = [];
352:         foreach ($options as $key => $value) {
353:             if (!strncmp($key, 'session.', 8)) { // back compatibility
354:                 $key = substr($key, 8);
355:             }
356:             $key = strtolower(preg_replace('#(.)(?=[A-Z])#', '$1_', $key)); // camelCase -> snake_case
357:             $normalized[$key] = $value;
358:         }
359:         if (self::$started) {
360:             $this->configure($normalized);
361:         }
362:         $this->options = $normalized + $this->options;
363:         if (!empty($normalized['auto_start'])) {
364:             $this->start();
365:         }
366:         return $this;
367:     }
368: 
369: 
370:     /**
371:      * Returns all session options.
372:      * @return array
373:      */
374:     public function getOptions()
375:     {
376:         return $this->options;
377:     }
378: 
379: 
380:     /**
381:      * Configures session environment.
382:      * @param  array
383:      * @return void
384:      */
385:     private function configure(array $config)
386:     {
387:         $special = ['cache_expire' => 1, 'cache_limiter' => 1, 'save_path' => 1, 'name' => 1];
388:         $cookie = $origCookie = session_get_cookie_params();
389: 
390:         foreach ($config as $key => $value) {
391:             if ($value === null || ini_get("session.$key") == $value) { // intentionally ==
392:                 continue;
393: 
394:             } elseif (strncmp($key, 'cookie_', 7) === 0) {
395:                 $cookie[substr($key, 7)] = $value;
396: 
397:             } else {
398:                 if (session_status() === PHP_SESSION_ACTIVE) {
399:                     throw new Nette\InvalidStateException("Unable to set 'session.$key' to value '$value' when session has been started" . (self::$started ? '.' : ' by session.auto_start or session_start().'));
400:                 }
401:                 if (isset($special[$key])) {
402:                     $key = "session_$key";
403:                     $key($value);
404: 
405:                 } elseif (function_exists('ini_set')) {
406:                     ini_set("session.$key", (string) $value);
407: 
408:                 } elseif (ini_get("session.$key") != $value) { // intentionally !=
409:                     throw new Nette\NotSupportedException("Unable to set 'session.$key' to '$value' because function ini_set() is disabled.");
410:                 }
411:             }
412:         }
413: 
414:         if ($cookie !== $origCookie) {
415:             if (PHP_VERSION_ID >= 70300) {
416:                 session_set_cookie_params($cookie);
417:             } else {
418:                 session_set_cookie_params(
419:                     $cookie['lifetime'],
420:                     $cookie['path'] . (isset($cookie['samesite']) ? '; SameSite=' . $cookie['samesite'] : ''),
421:                     $cookie['domain'],
422:                     $cookie['secure'],
423:                     $cookie['httponly']
424:                 );
425:             }
426:             if (self::$started) {
427:                 $this->sendCookie();
428:             }
429:         }
430: 
431:         if ($this->handler) {
432:             session_set_save_handler($this->handler);
433:         }
434:     }
435: 
436: 
437:     /**
438:      * Sets the amount of time allowed between requests before the session will be terminated.
439:      * @param  string|int|\DateTimeInterface  time, value 0 means "until the browser is closed"
440:      * @return static
441:      */
442:     public function setExpiration($time)
443:     {
444:         if (empty($time)) {
445:             return $this->setOptions([
446:                 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
447:                 'cookie_lifetime' => 0,
448:             ]);
449: 
450:         } else {
451:             $time = Nette\Utils\DateTime::from($time)->format('U') - time();
452:             return $this->setOptions([
453:                 'gc_maxlifetime' => $time,
454:                 'cookie_lifetime' => $time,
455:             ]);
456:         }
457:     }
458: 
459: 
460:     /**
461:      * Sets the session cookie parameters.
462:      * @param  string  path
463:      * @param  string  domain
464:      * @param  bool    secure
465:      * @param  string  samesite
466:      * @return static
467:      */
468:     public function setCookieParameters($path, $domain = null, $secure = null, $samesite = null)
469:     {
470:         return $this->setOptions([
471:             'cookie_path' => $path,
472:             'cookie_domain' => $domain,
473:             'cookie_secure' => $secure,
474:             'cookie_samesite' => $samesite,
475:         ]);
476:     }
477: 
478: 
479:     /**
480:      * Returns the session cookie parameters.
481:      * @return array  containing items: lifetime, path, domain, secure, httponly
482:      */
483:     public function getCookieParameters()
484:     {
485:         return session_get_cookie_params();
486:     }
487: 
488: 
489:     /**
490:      * Sets path of the directory used to save session data.
491:      * @return static
492:      */
493:     public function setSavePath($path)
494:     {
495:         return $this->setOptions([
496:             'save_path' => $path,
497:         ]);
498:     }
499: 
500: 
501:     /**
502:      * @deprecated  use setHandler().
503:      * @return static
504:      */
505:     public function setStorage(ISessionStorage $storage)
506:     {
507:         if (self::$started) {
508:             throw new Nette\InvalidStateException('Unable to set storage when session has been started.');
509:         }
510:         session_set_save_handler(
511:             [$storage, 'open'], [$storage, 'close'], [$storage, 'read'],
512:             [$storage, 'write'], [$storage, 'remove'], [$storage, 'clean']
513:         );
514:         return $this;
515:     }
516: 
517: 
518:     /**
519:      * Sets user session handler.
520:      * @return static
521:      */
522:     public function setHandler(\SessionHandlerInterface $handler)
523:     {
524:         if (self::$started) {
525:             throw new Nette\InvalidStateException('Unable to set handler when session has been started.');
526:         }
527:         $this->handler = $handler;
528:         return $this;
529:     }
530: 
531: 
532:     /**
533:      * Sends the session cookies.
534:      * @return void
535:      */
536:     private function sendCookie()
537:     {
538:         $cookie = $this->getCookieParameters();
539:         $this->response->setCookie(
540:             session_name(), session_id(),
541:             $cookie['lifetime'] ? $cookie['lifetime'] + time() : 0,
542:             $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly'],
543:             isset($cookie['samesite']) ? $cookie['samesite'] : null
544:         );
545:     }
546: }
547: 
Nette 2.4-20191120 API API documentation generated by ApiGen 2.8.0