PageRenderTime 62ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/fSession.php

https://bitbucket.org/ZilIsiltk/flourish
PHP | 668 lines | 357 code | 97 blank | 214 comment | 50 complexity | 4a9ac7971359c1cd03de45eb7a58f27e MD5 | raw file
  1. <?php
  2. /**
  3. * Wraps the session control functions and the `$_SESSION` superglobal for a more consistent and safer API
  4. *
  5. * A `Cannot send session cache limiter` warning will be triggered if ::open(),
  6. * ::add(), ::clear(), ::delete(), ::get() or ::set() is called after output has
  7. * been sent to the browser. To prevent such a warning, explicitly call ::open()
  8. * before generating any output.
  9. *
  10. * @copyright Copyright (c) 2007-2010 Will Bond, others
  11. * @author Will Bond [wb] <will@flourishlib.com>
  12. * @author Alex Leeds [al] <alex@kingleeds.com>
  13. * @license http://flourishlib.com/license
  14. *
  15. * @package Flourish
  16. * @link http://flourishlib.com/fSession
  17. *
  18. * @version 1.0.0b16
  19. * @changes 1.0.0b16 Changed ::delete() to return the value of the key being deleted [wb, 2010-09-19]
  20. * @changes 1.0.0b15 Added documentation about `[sub-key]` syntax [wb, 2010-09-12]
  21. * @changes 1.0.0b14 Backwards Compatibility Break - ::add(), ::delete(), ::get() and ::set() now interpret `[` and `]` as array shorthand and thus they can not be used in keys - added `$beginning` parameter to ::add(), added ::remove() method [wb, 2010-09-12]
  22. * @changes 1.0.0b13 Fixed a bug that prevented working with existing sessions since they did not have the `fSession::expires` key [wb, 2010-08-24]
  23. * @changes 1.0.0b12 Changed ::enablePersistence() to always regenerate the session ID, which ensures the function works even if the ID has already been regenerated by fAuthorizaton [wb, 2010-08-21]
  24. * @changes 1.0.0b11 Updated the class to make sure ::enablePersistence() is called after ::ignoreSubdomain(), ::setLength() and ::setPath() [wb, 2010-05-29]
  25. * @changes 1.0.0b10 Fixed some documentation bugs [wb, 2010-03-03]
  26. * @changes 1.0.0b9 Fixed a bug in ::destroy() where sessions weren't always being properly destroyed [wb, 2009-12-08]
  27. * @changes 1.0.0b8 Fixed a bug that made the unit tests fail on PHP 5.1 [wb, 2009-10-27]
  28. * @changes 1.0.0b7 Backwards Compatibility Break - Removed the `$prefix` parameter from the methods ::delete(), ::get() and ::set() - added the methods ::add(), ::enablePersistence(), ::regenerateID() [wb+al, 2009-10-23]
  29. * @changes 1.0.0b6 Backwards Compatibility Break - the first parameter of ::clear() was removed, use ::delete() instead [wb, 2009-05-08]
  30. * @changes 1.0.0b5 Added documentation about session cache limiter warnings [wb, 2009-05-04]
  31. * @changes 1.0.0b4 The class now works with existing sessions [wb, 2009-05-04]
  32. * @changes 1.0.0b3 Fixed ::clear() to properly handle when `$key` is `NULL` [wb, 2009-02-05]
  33. * @changes 1.0.0b2 Made ::open() public, fixed some consistency issues with setting session options through the class [wb, 2009-01-06]
  34. * @changes 1.0.0b The initial implementation [wb, 2007-06-14]
  35. */
  36. class fSession
  37. {
  38. // The following constants allow for nice looking callbacks to static methods
  39. const add = 'fSession::add';
  40. const clear = 'fSession::clear';
  41. const close = 'fSession::close';
  42. const delete = 'fSession::delete';
  43. const destroy = 'fSession::destroy';
  44. const enablePersistence = 'fSession::enablePersistence';
  45. const get = 'fSession::get';
  46. const ignoreSubdomain = 'fSession::ignoreSubdomain';
  47. const open = 'fSession::open';
  48. const regenerateID = 'fSession::regenerateID';
  49. const reset = 'fSession::reset';
  50. const set = 'fSession::set';
  51. const setLength = 'fSession::setLength';
  52. const setPath = 'fSession::setPath';
  53. /**
  54. * The length for a normal session
  55. *
  56. * @var integer
  57. */
  58. static private $normal_timespan = NULL;
  59. /**
  60. * If the session is open
  61. *
  62. * @var boolean
  63. */
  64. static private $open = FALSE;
  65. /**
  66. * The length for a persistent session cookie - one that survives browser restarts
  67. *
  68. * @var integer
  69. */
  70. static private $persistent_timespan = NULL;
  71. /**
  72. * If the session ID was regenerated during this script
  73. *
  74. * @var boolean
  75. */
  76. static private $regenerated = FALSE;
  77. /**
  78. * Adds a value to an already-existing array value, or to a new array value
  79. *
  80. * @param string $key The name to access the array under - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in key names
  81. * @param mixed $value The value to add to the array
  82. * @param boolean $beginning If the value should be added to the beginning
  83. * @return void
  84. */
  85. static public function add($key, $value, $beginning=FALSE)
  86. {
  87. self::open();
  88. $tip =& $_SESSION;
  89. if ($bracket_pos = strpos($key, '[')) {
  90. $original_key = $key;
  91. $array_dereference = substr($key, $bracket_pos);
  92. $key = substr($key, 0, $bracket_pos);
  93. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  94. $array_keys = array_map('current', $array_keys);
  95. array_unshift($array_keys, $key);
  96. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  97. if (!isset($tip[$array_key])) {
  98. $tip[$array_key] = array();
  99. } elseif (!is_array($tip[$array_key])) {
  100. throw new fProgrammerException(
  101. '%1$s was called for the key, %2$s, which is not an array',
  102. __CLASS__ . '::add()',
  103. $original_key
  104. );
  105. }
  106. $tip =& $tip[$array_key];
  107. }
  108. $key = end($array_keys);
  109. }
  110. if (!isset($tip[$key])) {
  111. $tip[$key] = array();
  112. } elseif (!is_array($tip[$key])) {
  113. throw new fProgrammerException(
  114. '%1$s was called for the key, %2$s, which is not an array',
  115. __CLASS__ . '::add()',
  116. $key
  117. );
  118. }
  119. if ($beginning) {
  120. array_unshift($tip[$key], $value);
  121. } else {
  122. $tip[$key][] = $value;
  123. }
  124. }
  125. /**
  126. * Removes all session values with the provided prefix
  127. *
  128. * This method will not remove session variables used by this class, which
  129. * are prefixed with `fSession::`.
  130. *
  131. * @param string $prefix The prefix to clear all session values for
  132. * @return void
  133. */
  134. static public function clear($prefix=NULL)
  135. {
  136. self::open();
  137. $session_type = $_SESSION['fSession::type'];
  138. $session_expires = $_SESSION['fSession::expires'];
  139. if ($prefix) {
  140. foreach ($_SESSION as $key => $value) {
  141. if (strpos($key, $prefix) === 0) {
  142. unset($_SESSION[$key]);
  143. }
  144. }
  145. } else {
  146. $_SESSION = array();
  147. }
  148. $_SESSION['fSession::type'] = $session_type;
  149. $_SESSION['fSession::expires'] = $session_expires;
  150. }
  151. /**
  152. * Closes the session for writing, allowing other pages to open the session
  153. *
  154. * @return void
  155. */
  156. static public function close()
  157. {
  158. if (!self::$open) { return; }
  159. session_write_close();
  160. unset($_SESSION);
  161. self::$open = FALSE;
  162. }
  163. /**
  164. * Deletes a value from the session
  165. *
  166. * @param string $key The key of the value to delete - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in key names
  167. * @param mixed $default_value The value to return if the `$key` is not set
  168. * @return mixed The value of the `$key` that was deleted
  169. */
  170. static public function delete($key, $default_value=NULL)
  171. {
  172. self::open();
  173. $value = $default_value;
  174. if ($bracket_pos = strpos($key, '[')) {
  175. $original_key = $key;
  176. $array_dereference = substr($key, $bracket_pos);
  177. $key = substr($key, 0, $bracket_pos);
  178. if (!isset($_SESSION[$key])) {
  179. return $value;
  180. }
  181. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  182. $array_keys = array_map('current', $array_keys);
  183. $tip =& $_SESSION[$key];
  184. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  185. if (!isset($tip[$array_key])) {
  186. return $value;
  187. } elseif (!is_array($tip[$array_key])) {
  188. throw new fProgrammerException(
  189. '%1$s was called for an element, %2$s, which is not an array',
  190. __CLASS__ . '::delete()',
  191. $original_key
  192. );
  193. }
  194. $tip =& $tip[$array_key];
  195. }
  196. $key = end($array_keys);
  197. } else {
  198. $tip =& $_SESSION;
  199. }
  200. if (isset($tip[$key])) {
  201. $value = $tip[$key];
  202. unset($tip[$key]);
  203. }
  204. return $value;
  205. }
  206. /**
  207. * Destroys the session, removing all values
  208. *
  209. * @return void
  210. */
  211. static public function destroy()
  212. {
  213. self::open();
  214. $_SESSION = array();
  215. if (isset($_COOKIE[session_name()])) {
  216. $params = session_get_cookie_params();
  217. setcookie(session_name(), '', time()-43200, $params['path'], $params['domain'], $params['secure']);
  218. }
  219. session_destroy();
  220. self::regenerateID();
  221. }
  222. /**
  223. * Changed the session to use a time-based cookie instead of a session-based cookie
  224. *
  225. * The length of the time-based cookie is controlled by ::setLength(). When
  226. * this method is called, a time-based cookie is used to store the session
  227. * ID. This means the session can persist browser restarts. Normally, a
  228. * session-based cookie is used, which is wiped when a browser restart
  229. * occurs.
  230. *
  231. * This method should be called during the login process and will normally
  232. * be controlled by a checkbox or similar where the user can indicate if
  233. * they want to stay logged in for an extended period of time.
  234. *
  235. * @return void
  236. */
  237. static public function enablePersistence()
  238. {
  239. if (self::$persistent_timespan === NULL) {
  240. throw new fProgrammerException(
  241. 'The method %1$s must be called with the %2$s parameter before calling %3$s',
  242. __CLASS__ . '::setLength()',
  243. '$persistent_timespan',
  244. __CLASS__ . '::enablePersistence()'
  245. );
  246. }
  247. $current_params = session_get_cookie_params();
  248. $params = array(
  249. self::$persistent_timespan,
  250. $current_params['path'],
  251. $current_params['domain'],
  252. $current_params['secure']
  253. );
  254. call_user_func_array('session_set_cookie_params', $params);
  255. self::open();
  256. $_SESSION['fSession::type'] = 'persistent';
  257. session_regenerate_id();
  258. self::$regenerated = TRUE;
  259. }
  260. /**
  261. * Gets data from the `$_SESSION` superglobal
  262. *
  263. * @param string $key The name to get the value for - array elements can be accessed via `[sub-key]` syntax, and thus `[` and `]` can not be used in key names
  264. * @param mixed $default_value The default value to use if the requested key is not set
  265. * @return mixed The data element requested
  266. */
  267. static public function get($key, $default_value=NULL)
  268. {
  269. self::open();
  270. $array_dereference = NULL;
  271. if ($bracket_pos = strpos($key, '[')) {
  272. $array_dereference = substr($key, $bracket_pos);
  273. $key = substr($key, 0, $bracket_pos);
  274. }
  275. if (!isset($_SESSION[$key])) {
  276. return $default_value;
  277. }
  278. $value = $_SESSION[$key];
  279. if ($array_dereference) {
  280. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  281. $array_keys = array_map('current', $array_keys);
  282. foreach ($array_keys as $array_key) {
  283. if (!is_array($value) || !isset($value[$array_key])) {
  284. $value = $default_value;
  285. break;
  286. }
  287. $value = $value[$array_key];
  288. }
  289. }
  290. return $value;
  291. }
  292. /**
  293. * Sets the session to run on the main domain, not just the specific subdomain currently being accessed
  294. *
  295. * This method should be called after any calls to
  296. * [http://php.net/session_set_cookie_params `session_set_cookie_params()`].
  297. *
  298. * @return void
  299. */
  300. static public function ignoreSubdomain()
  301. {
  302. if (self::$open || isset($_SESSION)) {
  303. throw new fProgrammerException(
  304. '%1$s must be called before any of %2$s, %3$s, %4$s, %5$s, %6$s, %7$s or %8$s',
  305. __CLASS__ . '::ignoreSubdomain()',
  306. __CLASS__ . '::add()',
  307. __CLASS__ . '::clear()',
  308. __CLASS__ . '::enablePersistence()',
  309. __CLASS__ . '::get()',
  310. __CLASS__ . '::open()',
  311. __CLASS__ . '::set()',
  312. 'session_start()'
  313. );
  314. }
  315. $current_params = session_get_cookie_params();
  316. $params = array(
  317. $current_params['lifetime'],
  318. $current_params['path'],
  319. preg_replace('#.*?([a-z0-9\\-]+\.[a-z]+)$#iD', '.\1', $_SERVER['SERVER_NAME']),
  320. $current_params['secure']
  321. );
  322. call_user_func_array('session_set_cookie_params', $params);
  323. }
  324. /**
  325. * Opens the session for writing, is automatically called by ::clear(), ::get() and ::set()
  326. *
  327. * A `Cannot send session cache limiter` warning will be triggered if this,
  328. * ::add(), ::clear(), ::delete(), ::get() or ::set() is called after output
  329. * has been sent to the browser. To prevent such a warning, explicitly call
  330. * this method before generating any output.
  331. *
  332. * @param boolean $cookie_only_session_id If the session id should only be allowed via cookie - this is a security issue and should only be set to `FALSE` when absolutely necessary
  333. * @return void
  334. */
  335. static public function open($cookie_only_session_id=TRUE)
  336. {
  337. if (self::$open) { return; }
  338. self::$open = TRUE;
  339. if (self::$normal_timespan === NULL) {
  340. self::$normal_timespan = ini_get('session.gc_maxlifetime');
  341. }
  342. // If the session is already open, we just piggy-back without setting options
  343. if (!isset($_SESSION)) {
  344. if ($cookie_only_session_id) {
  345. ini_set('session.use_cookies', 1);
  346. ini_set('session.use_only_cookies', 1);
  347. }
  348. session_start();
  349. }
  350. // If the session has existed for too long, reset it
  351. if (isset($_SESSION['fSession::expires']) && $_SESSION['fSession::expires'] < $_SERVER['REQUEST_TIME']) {
  352. $_SESSION = array();
  353. self::regenerateID();
  354. }
  355. if (!isset($_SESSION['fSession::type'])) {
  356. $_SESSION['fSession::type'] = 'normal';
  357. }
  358. // We store the expiration time for a session to allow for both normal and persistent sessions
  359. if ($_SESSION['fSession::type'] == 'persistent' && self::$persistent_timespan) {
  360. $_SESSION['fSession::expires'] = $_SERVER['REQUEST_TIME'] + self::$persistent_timespan;
  361. } else {
  362. $_SESSION['fSession::expires'] = $_SERVER['REQUEST_TIME'] + self::$normal_timespan;
  363. }
  364. }
  365. /**
  366. * Regenerates the session ID, but only once per script execution
  367. *
  368. * @internal
  369. *
  370. * @return void
  371. */
  372. static public function regenerateID()
  373. {
  374. if (!self::$regenerated){
  375. session_regenerate_id();
  376. self::$regenerated = TRUE;
  377. }
  378. }
  379. /**
  380. * Removes and returns the value from the end of an array value
  381. *
  382. * @param string $key The name of the element to remove the value from - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in key names
  383. * @param boolean $beginning If the value should be removed to the beginning
  384. * @return mixed The value that was removed
  385. */
  386. static public function remove($key, $beginning=FALSE)
  387. {
  388. self::open();
  389. $tip =& $_SESSION;
  390. if ($bracket_pos = strpos($key, '[')) {
  391. $original_key = $key;
  392. $array_dereference = substr($key, $bracket_pos);
  393. $key = substr($key, 0, $bracket_pos);
  394. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  395. $array_keys = array_map('current', $array_keys);
  396. array_unshift($array_keys, $key);
  397. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  398. if (!isset($tip[$array_key])) {
  399. return NULL;
  400. } elseif (!is_array($tip[$array_key])) {
  401. throw new fProgrammerException(
  402. '%1$s was called for the key, %2$s, which is not an array',
  403. __CLASS__ . '::remove()',
  404. $original_key
  405. );
  406. }
  407. $tip =& $tip[$array_key];
  408. }
  409. $key = end($array_keys);
  410. }
  411. if (!isset($tip[$key])) {
  412. return NULL;
  413. } elseif (!is_array($tip[$key])) {
  414. throw new fProgrammerException(
  415. '%1$s was called for the key, %2$s, which is not an array',
  416. __CLASS__ . '::remove()',
  417. $key
  418. );
  419. }
  420. if ($beginning) {
  421. return array_shift($tip[$key]);
  422. }
  423. return array_pop($tip[$key]);
  424. }
  425. /**
  426. * Resets the configuration of the class
  427. *
  428. * @internal
  429. *
  430. * @return void
  431. */
  432. static public function reset()
  433. {
  434. self::$normal_timespan = NULL;
  435. self::$persistent_timespan = NULL;
  436. self::$regenerated = FALSE;
  437. self::destroy();
  438. self::close();
  439. }
  440. /**
  441. * Sets data to the `$_SESSION` superglobal
  442. *
  443. * @param string $key The name to save the value under - array elements can be modified via `[sub-key]` syntax, and thus `[` and `]` can not be used in key names
  444. * @param mixed $value The value to store
  445. * @return void
  446. */
  447. static public function set($key, $value)
  448. {
  449. self::open();
  450. $tip =& $_SESSION;
  451. if ($bracket_pos = strpos($key, '[')) {
  452. $array_dereference = substr($key, $bracket_pos);
  453. $key = substr($key, 0, $bracket_pos);
  454. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  455. $array_keys = array_map('current', $array_keys);
  456. array_unshift($array_keys, $key);
  457. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  458. if (!isset($tip[$array_key]) || !is_array($tip[$array_key])) {
  459. $tip[$array_key] = array();
  460. }
  461. $tip =& $tip[$array_key];
  462. }
  463. $tip[end($array_keys)] = $value;
  464. } else {
  465. $tip[$key] = $value;
  466. }
  467. }
  468. /**
  469. * Sets the minimum length of a session - PHP might not clean up the session data right away once this timespan has elapsed
  470. *
  471. * Please be sure to set a custom session path via ::setPath() to ensure
  472. * another site on the server does not garbage collect the session files
  473. * from this site!
  474. *
  475. * Both of the timespan can accept either a integer timespan in seconds,
  476. * or an english description of a timespan (e.g. `'30 minutes'`, `'1 hour'`,
  477. * `'1 day 2 hours'`).
  478. *
  479. * @param string|integer $normal_timespan The normal, session-based cookie, length for the session
  480. * @param string|integer $persistent_timespan The persistent, timed-based cookie, length for the session - this is enabled by calling ::enabledPersistence() during login
  481. * @return void
  482. */
  483. static public function setLength($normal_timespan, $persistent_timespan=NULL)
  484. {
  485. if (self::$open || isset($_SESSION)) {
  486. throw new fProgrammerException(
  487. '%1$s must be called before any of %2$s, %3$s, %4$s, %5$s, %6$s, %7$s or %8$s',
  488. __CLASS__ . '::setLength()',
  489. __CLASS__ . '::add()',
  490. __CLASS__ . '::clear()',
  491. __CLASS__ . '::enablePersistence()',
  492. __CLASS__ . '::get()',
  493. __CLASS__ . '::open()',
  494. __CLASS__ . '::set()',
  495. 'session_start()'
  496. );
  497. }
  498. $seconds = (!is_numeric($normal_timespan)) ? strtotime($normal_timespan) - time() : $normal_timespan;
  499. self::$normal_timespan = $seconds;
  500. if ($persistent_timespan) {
  501. $seconds = (!is_numeric($persistent_timespan)) ? strtotime($persistent_timespan) - time() : $persistent_timespan;
  502. self::$persistent_timespan = $seconds;
  503. }
  504. ini_set('session.gc_maxlifetime', $seconds);
  505. }
  506. /**
  507. * Sets the path to store session files in
  508. *
  509. * This method should always be called with a non-standard directory
  510. * whenever ::setLength() is called to ensure that another site on the
  511. * server does not garbage collect the session files for this site.
  512. *
  513. * Standard session directories usually include `/tmp` and `/var/tmp`.
  514. *
  515. * @param string|fDirectory $directory The directory to store session files in
  516. * @return void
  517. */
  518. static public function setPath($directory)
  519. {
  520. if (self::$open || isset($_SESSION)) {
  521. throw new fProgrammerException(
  522. '%1$s must be called before any of %2$s, %3$s, %4$s, %5$s, %6$s, %7$s or %8$s',
  523. __CLASS__ . '::setPath()',
  524. __CLASS__ . '::add()',
  525. __CLASS__ . '::clear()',
  526. __CLASS__ . '::enablePersistence()',
  527. __CLASS__ . '::get()',
  528. __CLASS__ . '::open()',
  529. __CLASS__ . '::set()',
  530. 'session_start()'
  531. );
  532. }
  533. if (!$directory instanceof fDirectory) {
  534. $directory = new fDirectory($directory);
  535. }
  536. if (!$directory->isWritable()) {
  537. throw new fEnvironmentException(
  538. 'The directory specified, %s, is not writable',
  539. $directory->getPath()
  540. );
  541. }
  542. session_save_path($directory->getPath());
  543. }
  544. /**
  545. * Forces use as a static class
  546. *
  547. * @return fSession
  548. */
  549. private function __construct() { }
  550. }
  551. /**
  552. * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>, others
  553. *
  554. * Permission is hereby granted, free of charge, to any person obtaining a copy
  555. * of this software and associated documentation files (the "Software"), to deal
  556. * in the Software without restriction, including without limitation the rights
  557. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  558. * copies of the Software, and to permit persons to whom the Software is
  559. * furnished to do so, subject to the following conditions:
  560. *
  561. * The above copyright notice and this permission notice shall be included in
  562. * all copies or substantial portions of the Software.
  563. *
  564. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  565. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  566. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  567. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  568. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  569. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  570. * THE SOFTWARE.
  571. */