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