PageRenderTime 32ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/libs/Nette/Web/Session.php

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