PageRenderTime 44ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/Nette/Web/Session.php

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