PageRenderTime 40ms CodeModel.GetById 9ms RepoModel.GetById 1ms app.codeStats 0ms

/libs/Nette/Http/Session.php

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