Source for file Session.php

Documentation is available at Session.php

  1. 1: <?php
  2. 2:  
  3. 3: /**
  4. 4:  * Nette Framework
  5. 5:  *
  6. 6:  * Copyright (c) 2004, 2009 David Grudl (http://davidgrudl.com)
  7. 7:  *
  8. 8:  * This source file is subject to the "Nette license" that is bundled
  9. 9:  * with this package in the file license.txt.
  10. 10:  *
  11. 11:  * For more information please see https://nette.org
  12. 12:  *
  13. 13:  * @copyright  Copyright (c) 2004, 2009 David Grudl
  14. 14:  * @license    https://nette.org/license  Nette license
  15. 15:  * @link       https://nette.org
  16. 16:  * @category   Nette
  17. 17:  * @package    Nette\Web
  18. 18:  * @version    $Id$
  19. 19:  */
  20. 20:  
  21. 21:  
  22. 22:  
  23. 23: require_once dirname(__FILE__'/../Object.php';
  24. 24:  
  25. 25:  
  26. 26:  
  27. 27: /**
  28. 28:  * Provides access to session namespaces as well as session settings and management methods.
  29. 29:  *
  30. 30:  * @author     David Grudl
  31. 31:  * @copyright  Copyright (c) 2004, 2009 David Grudl
  32. 32:  * @package    Nette\Web
  33. 33:  */
  34. 34: class Session extends Object
  35. 35: {
  36. 36:     /** Default file lifetime is 3 hours */
  37. 37:     const DEFAULT_FILE_LIFETIME = 10800;
  38. 38:  
  39. 39:     /** @var callback  Validation key generator */
  40. 41:  
  41. 42:     /** @var bool  is required session ID regeneration? */
  42. 43:     private $regenerationNeeded;
  43. 44:  
  44. 45:     /** @var bool  has been session started? */
  45. 46:     private static $started;
  46. 47:  
  47. 48:     /** @var array default configuration */
  48. 49:     private static $defaultConfig array(
  49. 50:         // security
  50. 51:         'session.referer_check' => '',    // must be disabled because PHP implementation is invalid
  51. 52:         'session.use_cookies' => 1,       // must be enabled to prevent Session Hijacking and Fixation
  52. 53:         'session.use_only_cookies' => 1,  // must be enabled to prevent Session Fixation
  53. 54:         'session.use_trans_sid' => 0,     // must be disabled to prevent Session Hijacking and Fixation
  54. 55:  
  55. 56:         // cookies
  56. 57:         'session.cookie_lifetime' => 0,   // until the browser is closed
  57. 58:         'session.cookie_path' => '/',    // cookie is available within the entire domain
  58. 59:         'session.cookie_domain' => '',    // cookie is available on current subdomain only
  59. 60:         'session.cookie_secure' => FALSE// cookie is available on HTTP & HTTPS
  60. 61:         'session.cookie_httponly' => TRUE,// must be enabled to prevent Session Fixation
  61. 62:  
  62. 63:         // other
  63. 64:         'session.gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,// 3 hours
  64. 65:         'session.cache_limiter' => NULL,  // (default "nocache", special value "\0")
  65. 66:         'session.cache_expire' => NULL,   // (default "180")
  66. 67:         'session.hash_function' => NULL,  // (default "0", means MD5)
  67. 68:         'session.hash_bits_per_character' => NULL// (default "4")
  68. 69:     );
  69. 70:  
  70. 71:  
  71. 72:  
  72. 73:     public function __construct()
  73. 74:     {
  74. 75:         $this->verificationKeyGenerator = array($this'generateVerificationKey');
  75. 76:     }
  76. 77:  
  77. 78:  
  78. 79:  
  79. 80:     /**
  80. 81:      * Starts and initializes session data.
  81. 82:      * @throws InvalidStateException
  82. 83:      * @return void 
  83. 84:      */
  84. 85:     public function start()
  85. 86:     {
  86. 87:         if (self::$started{
  87. 88:             throw new InvalidStateException('Session has already been started.');
  88. 89:  
  89. 90:         elseif (self::$started === NULL && defined('SID')) {
  90. 91:             throw new InvalidStateException('A session had already been started by session.auto-start or session_start().');
  91. 92:         }
  92. 93:  
  93. 94:  
  94. 95:         // additional protection against Session Hijacking & Fixation
  95. 96:         if ($this->verificationKeyGenerator{
  96. 97:             fixCallback($this->verificationKeyGenerator);
  97. 98:             if (!is_callable($this->verificationKeyGenerator)) {
  98. 99:                 $able is_callable($this->verificationKeyGeneratorTRUE$textual);
  99. 100:                 throw new InvalidStateException("Verification key generator '$textual' is not ($able 'callable.' 'valid PHP callback.'));
  100. 101:             }
  101. 102:         }
  102. 103:  
  103. 104:  
  104. 105:         // start session
  105. 106:         $this->configure(self::$defaultConfigFALSE);
  106. 107:  
  107. 108:         Tools::tryError();
  108. 109:         session_start();
  109. 110:         if (Tools::catchError($msg)) {
  110. 111:             @session_write_close()// this is needed
  111. 112:             throw new InvalidStateException($msg);
  112. 113:         }
  113. 114:  
  114. 115:         self::$started TRUE;
  115. 116:         if ($this->regenerationNeeded{
  116. 117:             session_regenerate_id(TRUE);
  117. 118:             $this->regenerationNeeded FALSE;
  118. 119:         }
  119. 120:  
  120. 121:         /* structure:
  121. 122:             nette: __NT
  122. 123:             data:  __NS->namespace->variable = data
  123. 124:             meta:  __NM->namespace->EXP->variable = timestamp
  124. 125:         */
  125. 126:  
  126. 127:         // initialize structures
  127. 128:         $verKey $this->verificationKeyGenerator ? (string) call_user_func($this->verificationKeyGenerator'';
  128. 129:         if (!isset($_SESSION['__NT']['V'])) // new session
  129. 130:             $_SESSION['__NT'array();
  130. 131:             $_SESSION['__NT']['C'0;
  131. 132:             $_SESSION['__NT']['V'$verKey;
  132. 133:  
  133. 134:         else {
  134. 135:             $saved $_SESSION['__NT']['V'];
  135. 136:             if ($saved === $verKey// verified
  136. 137:                 $_SESSION['__NT']['C']++;
  137. 138:  
  138. 139:             else // session attack?
  139. 140:                 session_regenerate_id(TRUE);
  140. 141:                 $_SESSION array();
  141. 142:                 $_SESSION['__NT']['C'0;
  142. 143:                 $_SESSION['__NT']['V'$verKey;
  143. 144:             }
  144. 145:         }
  145. 146:  
  146. 147:         // browser closing detection
  147. 148:         $browserKey $this->getHttpRequest()->getCookie('nette-browser');
  148. 149:         if (!$browserKey{
  149. 150:             $browserKey = (string) lcg_value();
  150. 151:         }
  151. 152:         $browserClosed !isset($_SESSION['__NT']['B']|| $_SESSION['__NT']['B'!== $browserKey;
  152. 153:         $_SESSION['__NT']['B'$browserKey;
  153. 154:  
  154. 155:         // resend cookie
  155. 156:         $this->sendCookie();
  156. 157:  
  157. 158:         // process meta metadata
  158. 159:         if (isset($_SESSION['__NM'])) {
  159. 160:             $now time();
  160. 161:  
  161. 162:             // expire namespace variables
  162. 163:             foreach ($_SESSION['__NM'as $namespace => $metadata{
  163. 164:                 if (isset($metadata['EXP'])) {
  164. 165:                     foreach ($metadata['EXP'as $variable => $time{
  165. 166:                         if ((!$time && $browserClosed|| ($time && $now $time)) {
  166. 167:                             if ($variable === ''// expire whole namespace
  167. 168:                                 unset($_SESSION['__NM'][$namespace]$_SESSION['__NS'][$namespace]);
  168. 169:                                 continue 2;
  169. 170:                             }
  170. 171:                             unset($_SESSION['__NS'][$namespace][$variable],
  171. 172:                                 $_SESSION['__NM'][$namespace]['EXP'][$variable]);
  172. 173:                         }
  173. 174:                     }
  174. 175:                 }
  175. 176:             }
  176. 177:         }
  177. 178:  
  178. 179:         register_shutdown_function(array($this'clean'));
  179. 180:     }
  180. 181:  
  181. 182:  
  182. 183:  
  183. 184:     /**
  184. 185:      * Has been session started?
  185. 186:      * @return bool 
  186. 187:      */
  187. 188:     public function isStarted()
  188. 189:     {
  189. 190:         return (bool) self::$started;
  190. 191:     }
  191. 192:  
  192. 193:  
  193. 194:  
  194. 195:     /**
  195. 196:      * Ends the current session and store session data.
  196. 197:      * @return void 
  197. 198:      */
  198. 199:     public function close()
  199. 200:     {
  200. 201:         if (self::$started{
  201. 202:             session_write_close();
  202. 203:             self::$started FALSE;
  203. 204:         }
  204. 205:     }
  205. 206:  
  206. 207:  
  207. 208:  
  208. 209:     /**
  209. 210:      * Destroys all data registered to a session.
  210. 211:      * @return void 
  211. 212:      */
  212. 213:     public function destroy()
  213. 214:     {
  214. 215:         if (!self::$started{
  215. 216:             throw new InvalidStateException('Session is not started.');
  216. 217:         }
  217. 218:  
  218. 219:         session_destroy();
  219. 220:         $_SESSION NULL;
  220. 221:         self::$started FALSE;
  221. 222:         if (!headers_sent()) {
  222. 223:             $params session_get_cookie_params();
  223. 224:             $this->getHttpResponse()->deleteCookie(session_name()$params['path']$params['domain']$params['secure']);
  224. 225:         }
  225. 226:     }
  226. 227:  
  227. 228:  
  228. 229:  
  229. 230:     /**
  230. 231:      * Does session exists for the current request?
  231. 232:      * @return bool 
  232. 233:      */
  233. 234:     public function exists()
  234. 235:     {
  235. 236:         return self::$started || $this->getHttpRequest()->getCookie(session_name()) !== NULL;
  236. 237:     }
  237. 238:  
  238. 239:  
  239. 240:  
  240. 241:     /**
  241. 242:      * Regenerates the session ID.
  242. 243:      * @throws InvalidStateException
  243. 244:      * @return void 
  244. 245:      */
  245. 246:     public function regenerateId()
  246. 247:     {
  247. 248:         if (self::$started{
  248. 249:             if (headers_sent($file$line)) {
  249. 250:                 throw new InvalidStateException("Cannot regenerate session ID after HTTP headers have been sent" ($file " (output started at $file:$line)."."));
  250. 251:             }
  251. 252:             $_SESSION['__NT']['V'$this->verificationKeyGenerator ? (string) call_user_func($this->verificationKeyGenerator'';
  252. 253:             session_regenerate_id(TRUE);
  253. 254:  
  254. 255:         else {
  255. 256:             $this->regenerationNeeded TRUE;
  256. 257:         }
  257. 258:     }
  258. 259:  
  259. 260:  
  260. 261:  
  261. 262:     /**
  262. 263:      * Sets the session ID to a specified one.
  263. 264:      * @deprecated
  264. 265:      */
  265. 266:     public function setId($id)
  266. 267:     {
  267. 268:         throw new DeprecatedException('Method '.__METHOD__.'() is deprecated.');
  268. 269:     }
  269. 270:  
  270. 271:  
  271. 272:  
  272. 273:     /**
  273. 274:      * Returns the current session ID. Don't make dependencies, can be changed for each request.
  274. 275:      * @return string 
  275. 276:      */
  276. 277:     public function getId()
  277. 278:     {
  278. 279:         return session_id();
  279. 280:     }
  280. 281:  
  281. 282:  
  282. 283:  
  283. 284:     /**
  284. 285:      * Sets the session name to a specified one.
  285. 286:      * @param  string 
  286. 287:      * @return void 
  287. 288:      */
  288. 289:     public function setName($name)
  289. 290:     {
  290. 291:         if (!is_string($name|| !preg_match('#[^0-9.][^.]*$#A'$name)) {
  291. 292:             throw new InvalidArgumentException('Session name must be a string and cannot contain dot.');
  292. 293:         }
  293. 294:  
  294. 295:         $this->configure(array(
  295. 296:             'session.name' => $name,
  296. 297:         ));
  297. 298:     }
  298. 299:  
  299. 300:  
  300. 301:  
  301. 302:     /**
  302. 303:      * Gets the session name.
  303. 304:      * @return string 
  304. 305:      */
  305. 306:     public function getName()
  306. 307:     {
  307. 308:         return session_name();
  308. 309:     }
  309. 310:  
  310. 311:  
  311. 312:  
  312. 313:     /**
  313. 314:      * Generates key as protection against Session Hijacking & Fixation.
  314. 315:      * @return string 
  315. 316:      */
  316. 317:     public function generateVerificationKey()
  317. 318:     {
  318. 319:         $list array('Accept-Charset''Accept-Encoding''Accept-Language''User-Agent');
  319. 320:         $key array();
  320. 321:         $httpRequest $this->getHttpRequest();
  321. 322:         foreach ($list as $header{
  322. 323:             $key[$httpRequest->getHeader($header);
  323. 324:         }
  324. 325:         return md5(implode("\0"$key));
  325. 326:     }
  326. 327:  
  327. 328:  
  328. 329:  
  329. 330:     /********************* namespaces management ****************d*g**/
  330. 331:  
  331. 332:  
  332. 333:  
  333. 334:     /**
  334. 335:      * Returns specified session namespace.
  335. 336:      * @param  string 
  336. 337:      * @param  string 
  337. 338:      * @return SessionNamespace 
  338. 339:      * @throws InvalidArgumentException
  339. 340:      */
  340. 341:     public function getNamespace($namespace$class 'SessionNamespace')
  341. 342:     {
  342. 343:         if (!is_string($namespace|| $namespace === ''{
  343. 344:             throw new InvalidArgumentException('Session namespace must be a non-empty string.');
  344. 345:         }
  345. 346:  
  346. 347:         if (!self::$started{
  347. 348:             $this->start();
  348. 349:         }
  349. 350:  
  350. 351:         return new $class($_SESSION['__NS'][$namespace]$_SESSION['__NM'][$namespace]);
  351. 352:     }
  352. 353:  
  353. 354:  
  354. 355:  
  355. 356:     /**
  356. 357:      * Checks if a session namespace exist and is not empty.
  357. 358:      * @param  string 
  358. 359:      * @return bool 
  359. 360:      */
  360. 361:     public function hasNamespace($namespace)
  361. 362:     {
  362. 363:         if ($this->exists(&& !self::$started{
  363. 364:             $this->start();
  364. 365:         }
  365. 366:  
  366. 367:         return !empty($_SESSION['__NS'][$namespace]);
  367. 368:     }
  368. 369:  
  369. 370:  
  370. 371:  
  371. 372:     /**
  372. 373:      * Iteration over all namespaces.
  373. 374:      * @return ArrayIterator 
  374. 375:      */
  375. 376:     public function getIterator()
  376. 377:     {
  377. 378:         if ($this->exists(&& !self::$started{
  378. 379:             $this->start();
  379. 380:         }
  380. 381:  
  381. 382:         if (isset($_SESSION['__NS'])) {
  382. 383:             return new ArrayIterator(array_keys($_SESSION['__NS']));
  383. 384:  
  384. 385:         else {
  385. 386:             return new ArrayIterator;
  386. 387:         }
  387. 388:     }
  388. 389:  
  389. 390:  
  390. 391:  
  391. 392:     /**
  392. 393:      * Cleans and minimizes meta structures.
  393. 394:      * @return void 
  394. 395:      */
  395. 396:     public function clean()
  396. 397:     {
  397. 398:         if (!self::$started || empty($_SESSION)) {
  398. 399:             return;
  399. 400:         }
  400. 401:  
  401. 402:         if (isset($_SESSION['__NM']&& is_array($_SESSION['__NM'])) {
  402. 403:             foreach ($_SESSION['__NM'as $name => $foo{
  403. 404:                 if (empty($_SESSION['__NM'][$name]['EXP'])) {
  404. 405:                     unset($_SESSION['__NM'][$name]['EXP']);
  405. 406:                 }
  406. 407:  
  407. 408:                 if (empty($_SESSION['__NM'][$name])) {
  408. 409:                     unset($_SESSION['__NM'][$name]);
  409. 410:                 }
  410. 411:             }
  411. 412:         }
  412. 413:  
  413. 414:         if (empty($_SESSION['__NM'])) {
  414. 415:             unset($_SESSION['__NM']);
  415. 416:         }
  416. 417:  
  417. 418:         if (empty($_SESSION['__NS'])) {
  418. 419:             unset($_SESSION['__NS']);
  419. 420:         }
  420. 421:  
  421. 422:         if (empty($_SESSION)) {
  422. 423:             //$this->destroy(); only when shutting down
  423. 424:         }
  424. 425:     }
  425. 426:  
  426. 427:  
  427. 428:  
  428. 429:     /********************* configuration ****************d*g**/
  429. 430:  
  430. 431:  
  431. 432:  
  432. 433:     /**
  433. 434:      * Configurates session environment.
  434. 435:      * @param  array 
  435. 436:      * @param  bool   throw exception?
  436. 437:      * @return void 
  437. 438:      * @throws NotSupportedException
  438. 439:      * @throws InvalidStateException
  439. 440:      */
  440. 441:     public function configure(array $config$throwException TRUE)
  441. 442:     {
  442. 443:         $special array('session.cache_expire' => 1'session.cache_limiter' => 1,
  443. 444:             'session.save_path' => 1'session.name' => 1);
  444. 445:  
  445. 446:         foreach ($config as $key => $value{
  446. 447:             unset(self::$defaultConfig[$key])// prevents overwriting
  447. 448:  
  448. 449:             if ($value === NULL{
  449. 450:                 continue;
  450. 451:  
  451. 452:             elseif (isset($special[$key])) {
  452. 453:                 if (self::$started{
  453. 454:                     throw new InvalidStateException('Session has already been started.');
  454. 455:                 }
  455. 456:                 $key strtr($key'.''_');
  456. 457:                 $key($value);
  457. 458:  
  458. 459:             elseif (strncmp($key'session.cookie_'15=== 0{
  459. 460:                 if (!isset($cookie)) {
  460. 461:                     $cookie session_get_cookie_params();
  461. 462:                 }
  462. 463:                 $cookie[substr($key15)$value;
  463. 464:  
  464. 465:             elseif (!function_exists('ini_set')) {
  465. 466:                 if ($throwException && ini_get($key!= $value// intentionally ==
  466. 467:                     throw new NotSupportedException('Required function ini_set() is disabled.');
  467. 468:                 }
  468. 469:  
  469. 470:             else {
  470. 471:                 if (self::$started{
  471. 472:                     throw new InvalidStateException('Session has already been started.');
  472. 473:                 }
  473. 474:                 ini_set($key$value);
  474. 475:             }
  475. 476:         }
  476. 477:  
  477. 478:         if (isset($cookie)) {
  478. 479:             session_set_cookie_params($cookie['lifetime']$cookie['path']$cookie['domain']$cookie['secure']$cookie['httponly']);
  479. 480:             if (self::$started{
  480. 481:                 $this->sendCookie();
  481. 482:             }
  482. 483:         }
  483. 484:     }
  484. 485:  
  485. 486:  
  486. 487:  
  487. 488:     /**
  488. 489:      * Sets the amount of time allowed between requests before the session will be terminated.
  489. 490:      * @param  int  number of seconds, value 0 means "until the browser is closed"
  490. 491:      * @return void 
  491. 492:      */
  492. 493:     public function setExpiration($seconds)
  493. 494:     {
  494. 495:         if ($seconds <= 0{
  495. 496:             $this->configure(array(
  496. 497:                 'session.gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
  497. 498:                 'session.cookie_lifetime' => 0,
  498. 499:             ));
  499. 500:  
  500. 501:         else {
  501. 502:             if ($seconds Tools::YEAR{
  502. 503:                 $seconds -= time();
  503. 504:             }
  504. 505:             $this->configure(array(
  505. 506:                 'session.gc_maxlifetime' => $seconds,
  506. 507:                 'session.cookie_lifetime' => $seconds,
  507. 508:             ));
  508. 509:         }
  509. 510:     }
  510. 511:  
  511. 512:  
  512. 513:  
  513. 514:     /**
  514. 515:      * Sets the session cookie parameters.
  515. 516:      * @param  string  path
  516. 517:      * @param  string  domain
  517. 518:      * @param  bool    secure
  518. 519:      * @return void 
  519. 520:      */
  520. 521:     public function setCookieParams($path$domain NULL$secure NULL)
  521. 522:     {
  522. 523:         $this->configure(array(
  523. 524:             'session.cookie_path' => $path,
  524. 525:             'session.cookie_domain' => $domain,
  525. 526:             'session.cookie_secure' => $secure
  526. 527:         ));
  527. 528:     }
  528. 529:  
  529. 530:  
  530. 531:  
  531. 532:     /**
  532. 533:      * Returns the session cookie parameters.
  533. 534:      * @return array  containing items: lifetime, path, domain, secure, httponly
  534. 535:      */
  535. 536:     public function getCookieParams()
  536. 537:     {
  537. 538:         return session_get_cookie_params();
  538. 539:     }
  539. 540:  
  540. 541:  
  541. 542:  
  542. 543:     /**
  543. 544:      * Sets path of the directory used to save session data.
  544. 545:      * @return void 
  545. 546:      */
  546. 547:     public function setSavePath($path)
  547. 548:     {
  548. 549:         $this->configure(array(
  549. 550:             'session.save_path' => $path,
  550. 551:         ));
  551. 552:     }
  552. 553:  
  553. 554:  
  554. 555:  
  555. 556:     /**
  556. 557:      * Sends the session cookies.
  557. 558:      * @return void 
  558. 559:      */
  559. 560:     private function sendCookie()
  560. 561:     {
  561. 562:         $cookie $this->getCookieParams();
  562. 563:         $this->getHttpResponse()->setCookie(session_name()session_id()$cookie['lifetime']$cookie['path']$cookie['domain']$cookie['secure']$cookie['httponly']);
  563. 564:         $this->getHttpResponse()->setCookie('nette-browser'$_SESSION['__NT']['B']HttpResponse::BROWSER$cookie['path']$cookie['domain']$cookie['secure']$cookie['httponly']);
  564. 565:     }
  565. 566:  
  566. 567:  
  567. 568:  
  568. 569:     /********************* backend ****************d*g**/
  569. 570:  
  570. 571:  
  571. 572:  
  572. 573:     /**
  573. 574:      * @return IHttpRequest 
  574. 575:      */
  575. 576:     protected function getHttpRequest()
  576. 577:     {
  577. 578:         return Environment::getHttpRequest();
  578. 579:     }
  579. 580:  
  580. 581:  
  581. 582:  
  582. 583:     /**
  583. 584:      * @return IHttpResponse 
  584. 585:      */
  585. 586:     protected function getHttpResponse()
  586. 587:     {
  587. 588:         return Environment::getHttpResponse();
  588. 589:     }
  589. 590: