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