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