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    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    Web
  32. 32:  */
  33. 33: class NSession extends NObject
  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 NSession Hijacking and Fixation
  51. 52:         'use_only_cookies' => 1,  // must be enabled to prevent NSession Fixation
  52. 53:         'use_trans_sid' => 0,     // must be disabled to prevent NSession 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 NSession 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('NSession 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 NSession 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:         NTools::tryError();
  111. 112:         session_start();
  112. 113:         if (NTools::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('NSession 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 NSession  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('NSession name must be a string and cannot contain dot.');
  286. 287:         }
  287. 288:  
  288. 289:         return $this->setOptions(array(
  289. 290:             'name' => $name,
  290. 291:         ));
  291. 292:     }
  292. 293:  
  293. 294:  
  294. 295:  
  295. 296:     /**
  296. 297:      * Gets the session name.
  297. 298:      * @return string 
  298. 299:      */
  299. 300:     public function getName()
  300. 301:     {
  301. 302:         return session_name();
  302. 303:     }
  303. 304:  
  304. 305:  
  305. 306:  
  306. 307:     /**
  307. 308:      * Generates key as protection against NSession Hijacking & Fixation.
  308. 309:      * @return string 
  309. 310:      */
  310. 311:     public function generateVerificationKey()
  311. 312:     {
  312. 313:         $httpRequest $this->getHttpRequest();
  313. 314:         $key[$httpRequest->getHeader('Accept-Charset');
  314. 315:         $key[$httpRequest->getHeader('Accept-Encoding');
  315. 316:         $key[$httpRequest->getHeader('Accept-Language');
  316. 317:         $key[$httpRequest->getHeader('User-Agent');
  317. 318:         if (strpos($key[3]'MSIE 8.0')) // IE 8 AJAX bug
  318. 319:             $key[2substr($key[2]02);
  319. 320:         }
  320. 321:         return md5(implode("\0"$key));
  321. 322:     }
  322. 323:  
  323. 324:  
  324. 325:  
  325. 326:     /********************* namespaces management ****************d*g**/
  326. 327:  
  327. 328:  
  328. 329:  
  329. 330:     /**
  330. 331:      * Returns specified session namespace.
  331. 332:      * @param  string 
  332. 333:      * @param  string 
  333. 334:      * @return NSessionNamespace 
  334. 335:      * @throws InvalidArgumentException
  335. 336:      */
  336. 337:     public function getNamespace($namespace$class 'NSessionNamespace')
  337. 338:     {
  338. 339:         if (!is_string($namespace|| $namespace === ''{
  339. 340:             throw new InvalidArgumentException('NSession namespace must be a non-empty string.');
  340. 341:         }
  341. 342:  
  342. 343:         if (!self::$started{
  343. 344:             $this->start();
  344. 345:         }
  345. 346:  
  346. 347:         return new $class($_SESSION['__NS'][$namespace]$_SESSION['__NM'][$namespace]);
  347. 348:     }
  348. 349:  
  349. 350:  
  350. 351:  
  351. 352:     /**
  352. 353:      * Checks if a session namespace exist and is not empty.
  353. 354:      * @param  string 
  354. 355:      * @return bool 
  355. 356:      */
  356. 357:     public function hasNamespace($namespace)
  357. 358:     {
  358. 359:         if ($this->exists(&& !self::$started{
  359. 360:             $this->start();
  360. 361:         }
  361. 362:  
  362. 363:         return !empty($_SESSION['__NS'][$namespace]);
  363. 364:     }
  364. 365:  
  365. 366:  
  366. 367:  
  367. 368:     /**
  368. 369:      * Iteration over all namespaces.
  369. 370:      * @return ArrayIterator 
  370. 371:      */
  371. 372:     public function getIterator()
  372. 373:     {
  373. 374:         if ($this->exists(&& !self::$started{
  374. 375:             $this->start();
  375. 376:         }
  376. 377:  
  377. 378:         if (isset($_SESSION['__NS'])) {
  378. 379:             return new ArrayIterator(array_keys($_SESSION['__NS']));
  379. 380:  
  380. 381:         else {
  381. 382:             return new ArrayIterator;
  382. 383:         }
  383. 384:     }
  384. 385:  
  385. 386:  
  386. 387:  
  387. 388:     /**
  388. 389:      * Cleans and minimizes meta structures.
  389. 390:      * @return void 
  390. 391:      */
  391. 392:     public function clean()
  392. 393:     {
  393. 394:         if (!self::$started || empty($_SESSION)) {
  394. 395:             return;
  395. 396:         }
  396. 397:  
  397. 398:         if (isset($_SESSION['__NM']&& is_array($_SESSION['__NM'])) {
  398. 399:             foreach ($_SESSION['__NM'as $name => $foo{
  399. 400:                 if (empty($_SESSION['__NM'][$name]['EXP'])) {
  400. 401:                     unset($_SESSION['__NM'][$name]['EXP']);
  401. 402:                 }
  402. 403:  
  403. 404:                 if (empty($_SESSION['__NM'][$name])) {
  404. 405:                     unset($_SESSION['__NM'][$name]);
  405. 406:                 }
  406. 407:             }
  407. 408:         }
  408. 409:  
  409. 410:         if (empty($_SESSION['__NM'])) {
  410. 411:             unset($_SESSION['__NM']);
  411. 412:         }
  412. 413:  
  413. 414:         if (empty($_SESSION['__NS'])) {
  414. 415:             unset($_SESSION['__NS']);
  415. 416:         }
  416. 417:  
  417. 418:         if (empty($_SESSION)) {
  418. 419:             //$this->destroy(); only when shutting down
  419. 420:         }
  420. 421:     }
  421. 422:  
  422. 423:  
  423. 424:  
  424. 425:     /********************* configuration ****************d*g**/
  425. 426:  
  426. 427:  
  427. 428:  
  428. 429:     /**
  429. 430:      * Sets session options.
  430. 431:      * @param  array 
  431. 432:      * @return NSession  provides a fluent interface
  432. 433:      * @throws NotSupportedException
  433. 434:      * @throws InvalidStateException
  434. 435:      */
  435. 436:     public function setOptions(array $options)
  436. 437:     {
  437. 438:         if (self::$started{
  438. 439:             $this->configure($options);
  439. 440:         }
  440. 441:         $this->options $options $this->options;
  441. 442:         return $this;
  442. 443:     }
  443. 444:  
  444. 445:  
  445. 446:  
  446. 447:     /**
  447. 448:      * Returns all session options.
  448. 449:      * @return array 
  449. 450:      */
  450. 451:     public function getOptions()
  451. 452:     {
  452. 453:         return $this->options;
  453. 454:     }
  454. 455:  
  455. 456:  
  456. 457:  
  457. 458:     /**
  458. 459:      * Configurates session environment.
  459. 460:      * @param  array 
  460. 461:      * @return void 
  461. 462:      */
  462. 463:     private function configure(array $config)
  463. 464:     {
  464. 465:         $special array('cache_expire' => 1'cache_limiter' => 1'save_path' => 1'name' => 1);
  465. 466:  
  466. 467:         foreach ($config as $key => $value{
  467. 468:             if (!strncmp($key'session.'8)) // back compatibility
  468. 469:                 $key substr($key8);
  469. 470:             }
  470. 471:  
  471. 472:             if ($value === NULL{
  472. 473:                 continue;
  473. 474:  
  474. 475:             elseif (isset($special[$key])) {
  475. 476:                 if (self::$started{
  476. 477:                     throw new InvalidStateException("Unable to set '$key' when session has been started.");
  477. 478:                 }
  478. 479:                 $key "session_$key";
  479. 480:                 $key($value);
  480. 481:  
  481. 482:             elseif (strncmp($key'cookie_'7=== 0{
  482. 483:                 if (!isset($cookie)) {
  483. 484:                     $cookie session_get_cookie_params();
  484. 485:                 }
  485. 486:                 $cookie[substr($key7)$value;
  486. 487:  
  487. 488:             elseif (!function_exists('ini_set')) {
  488. 489:                 if (ini_get($key!= $value// intentionally ==
  489. 490:                     throw new NotSupportedException('Required function ini_set() is disabled.');
  490. 491:                 }
  491. 492:  
  492. 493:             else {
  493. 494:                 if (self::$started{
  494. 495:                     throw new InvalidStateException("Unable to set '$key' when session has been started.");
  495. 496:                 }
  496. 497:                 ini_set("session.$key"$value);
  497. 498:             }
  498. 499:         }
  499. 500:  
  500. 501:         if (isset($cookie)) {
  501. 502:             session_set_cookie_params($cookie['lifetime']$cookie['path']$cookie['domain']$cookie['secure']$cookie['httponly']);
  502. 503:             if (self::$started{
  503. 504:                 $this->sendCookie();
  504. 505:             }
  505. 506:         }
  506. 507:     }
  507. 508:  
  508. 509:  
  509. 510:  
  510. 511:     /**
  511. 512:      * Sets the amount of time allowed between requests before the session will be terminated.
  512. 513:      * @param  mixed  number of seconds, value 0 means "until the browser is closed"
  513. 514:      * @return NSession  provides a fluent interface
  514. 515:      */
  515. 516:     public function setExpiration($seconds)
  516. 517:     {
  517. 518:         if (is_string($seconds&& !is_numeric($seconds)) {
  518. 519:             $seconds strtotime($seconds);
  519. 520:         }
  520. 521:  
  521. 522:         if ($seconds <= 0{
  522. 523:             return $this->setOptions(array(
  523. 524:                 'gc_maxlifetime' => self::DEFAULT_FILE_LIFETIME,
  524. 525:                 'cookie_lifetime' => 0,
  525. 526:             ));
  526. 527:  
  527. 528:         else {
  528. 529:             if ($seconds NTools::YEAR{
  529. 530:                 $seconds -= time();
  530. 531:             }
  531. 532:             return $this->setOptions(array(
  532. 533:                 'gc_maxlifetime' => $seconds,
  533. 534:                 'cookie_lifetime' => $seconds,
  534. 535:             ));
  535. 536:         }
  536. 537:     }
  537. 538:  
  538. 539:  
  539. 540:  
  540. 541:     /**
  541. 542:      * Sets the session cookie parameters.
  542. 543:      * @param  string  path
  543. 544:      * @param  string  domain
  544. 545:      * @param  bool    secure
  545. 546:      * @return NSession  provides a fluent interface
  546. 547:      */
  547. 548:     public function setCookieParams($path$domain NULL$secure NULL)
  548. 549:     {
  549. 550:         return $this->setOptions(array(
  550. 551:             'cookie_path' => $path,
  551. 552:             'cookie_domain' => $domain,
  552. 553:             'cookie_secure' => $secure
  553. 554:         ));
  554. 555:     }
  555. 556:  
  556. 557:  
  557. 558:  
  558. 559:     /**
  559. 560:      * Returns the session cookie parameters.
  560. 561:      * @return array  containing items: lifetime, path, domain, secure, httponly
  561. 562:      */
  562. 563:     public function getCookieParams()
  563. 564:     {
  564. 565:         return session_get_cookie_params();
  565. 566:     }
  566. 567:  
  567. 568:  
  568. 569:  
  569. 570:     /**
  570. 571:      * Sets path of the directory used to save session data.
  571. 572:      * @return NSession  provides a fluent interface
  572. 573:      */
  573. 574:     public function setSavePath($path)
  574. 575:     {
  575. 576:         return $this->setOptions(array(
  576. 577:             'save_path' => $path,
  577. 578:         ));
  578. 579:     }
  579. 580:  
  580. 581:  
  581. 582:  
  582. 583:     /**
  583. 584:      * Sends the session cookies.
  584. 585:      * @return void 
  585. 586:      */
  586. 587:     private function sendCookie()
  587. 588:     {
  588. 589:         $cookie $this->getCookieParams();
  589. 590:         $this->getHttpResponse()->setCookie(session_name()session_id()$cookie['lifetime']$cookie['path']$cookie['domain']$cookie['secure']$cookie['httponly']);
  590. 591:         $this->getHttpResponse()->setCookie('nette-browser'$_SESSION['__NT']['B']NHttpResponse::BROWSER$cookie['path']$cookie['domain']$cookie['secure']$cookie['httponly']);
  591. 592:     }
  592. 593:  
  593. 594:  
  594. 595:  
  595. 596:     /********************* backend ****************d*g**/
  596. 597:  
  597. 598:  
  598. 599:  
  599. 600:     /**
  600. 601:      * @return IHttpRequest 
  601. 602:      */
  602. 603:     protected function getHttpRequest()
  603. 604:     {
  604. 605:         return NEnvironment::getHttpRequest();
  605. 606:     }
  606. 607:  
  607. 608:  
  608. 609:  
  609. 610:     /**
  610. 611:      * @return IHttpResponse 
  611. 612:      */
  612. 613:     protected function getHttpResponse()
  613. 614:     {
  614. 615:         return NEnvironment::getHttpResponse();
  615. 616:     }
  616. 617: