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