PageRenderTime 43ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fRequest.php

https://bitbucket.org/wbond/flourish/
PHP | 957 lines | 470 code | 136 blank | 351 comment | 142 complexity | 7c5b7d8172c0cda17af62ff3a720d849 MD5 | raw file
  1. <?php
  2. /**
  3. * Provides request-related methods
  4. *
  5. * This class is implemented to use the UTF-8 character encoding. Please see
  6. * http://flourishlib.com/docs/UTF-8 for more information.
  7. *
  8. * Please also note that using this class in a PUT or DELETE request will
  9. * cause the php://input stream to be consumed, and thus no longer available.
  10. *
  11. * @copyright Copyright (c) 2007-2011 Will Bond, others
  12. * @author Will Bond [wb] <will@flourishlib.com>
  13. * @author Alex Leeds [al] <alex@kingleeds.com>
  14. * @license http://flourishlib.com/license
  15. *
  16. * @package Flourish
  17. * @link http://flourishlib.com/fRequest
  18. *
  19. * @version 1.0.0b19
  20. * @changes 1.0.0b19 Added the `$use_default_for_blank` parameter to ::get() [wb, 2011-06-03]
  21. * @changes 1.0.0b18 Backwards Compatibility Break - ::getBestAcceptType() and ::getBestAcceptLanguage() now return either `NULL`, `FALSE` or a string instead of `NULL` or a string, both methods are more robust in handling edge cases [wb, 2011-02-06]
  22. * @changes 1.0.0b17 Fixed support for 3+ dimensional input arrays, added a fixed for the PHP DoS float bug #53632, added support for type-casted arrays in ::get() [wb, 2011-01-09]
  23. * @changes 1.0.0b16 Backwards Compatibility Break - changed ::get() to remove binary characters when casting to a `string`, changed `int` and `integer` to cast to a real integer when possible, added new types of `binary` and `integer!` [wb, 2010-11-30]
  24. * @changes 1.0.0b15 Added documentation about `[sub-key]` syntax, added `[sub-key]` support to ::check() [wb, 2010-09-12]
  25. * @changes 1.0.0b14 Rewrote ::set() to not require recursion for array syntax [wb, 2010-09-12]
  26. * @changes 1.0.0b13 Fixed ::set() to work with `PUT` requests [wb, 2010-06-30]
  27. * @changes 1.0.0b12 Fixed a bug with ::getBestAcceptLanguage() returning the second-best language [wb, 2010-05-27]
  28. * @changes 1.0.0b11 Added ::isAjax() [al, 2010-03-15]
  29. * @changes 1.0.0b10 Fixed ::get() to not truncate integers to the 32bit integer limit [wb, 2010-03-05]
  30. * @changes 1.0.0b9 Updated class to use new fSession API [wb, 2009-10-23]
  31. * @changes 1.0.0b8 Casting to an integer or string in ::get() now properly casts when the `$key` isn't present in the request, added support for date, time, timestamp and `?` casts [wb, 2009-08-25]
  32. * @changes 1.0.0b7 Fixed a bug with ::filter() not properly creating new `$_FILES` entries [wb, 2009-07-02]
  33. * @changes 1.0.0b6 ::filter() now works with empty prefixes and filtering the `$_FILES` superglobal has been fixed [wb, 2009-07-02]
  34. * @changes 1.0.0b5 Changed ::filter() so that it can be called multiple times for multi-level filtering [wb, 2009-06-02]
  35. * @changes 1.0.0b4 Added the HTML escaping functions ::encode() and ::prepare() [wb, 2009-05-27]
  36. * @changes 1.0.0b3 Updated class to use new fSession API [wb, 2009-05-08]
  37. * @changes 1.0.0b2 Added ::generateCSRFToken() from fCRUD::generateRequestToken() and ::validateCSRFToken() from fCRUD::validateRequestToken() [wb, 2009-05-08]
  38. * @changes 1.0.0b The initial implementation [wb, 2007-06-14]
  39. */
  40. class fRequest
  41. {
  42. // The following constants allow for nice looking callbacks to static methods
  43. const check = 'fRequest::check';
  44. const encode = 'fRequest::encode';
  45. const filter = 'fRequest::filter';
  46. const generateCSRFToken = 'fRequest::generateCSRFToken';
  47. const get = 'fRequest::get';
  48. const getAcceptLanguages = 'fRequest::getAcceptLanguages';
  49. const getAcceptTypes = 'fRequest::getAcceptTypes';
  50. const getBestAcceptLanguage = 'fRequest::getBestAcceptLanguage';
  51. const getBestAcceptType = 'fRequest::getBestAcceptType';
  52. const getValid = 'fRequest::getValid';
  53. const isAjax = 'fRequest::isAjax';
  54. const isDelete = 'fRequest::isDelete';
  55. const isGet = 'fRequest::isGet';
  56. const isPost = 'fRequest::isPost';
  57. const isPut = 'fRequest::isPut';
  58. const overrideAction = 'fRequest::overrideAction';
  59. const prepare = 'fRequest::prepare';
  60. const reset = 'fRequest::reset';
  61. const set = 'fRequest::set';
  62. const unfilter = 'fRequest::unfilter';
  63. const validateCSRFToken = 'fRequest::validateCSRFToken';
  64. /**
  65. * A backup copy of `$_FILES` for ::unfilter()
  66. *
  67. * @var array
  68. */
  69. static private $backup_files = array();
  70. /**
  71. * A backup copy of `$_GET` for ::unfilter()
  72. *
  73. * @var array
  74. */
  75. static private $backup_get = array();
  76. /**
  77. * A backup copy of `$_POST` for unfilter()
  78. *
  79. * @var array
  80. */
  81. static private $backup_post = array();
  82. /**
  83. * A backup copy of the local `PUT`/`DELETE` post data for ::unfilter()
  84. *
  85. * @var array
  86. */
  87. static private $backup_put_delete = array();
  88. /**
  89. * The key/value pairs from the post data of a `PUT`/`DELETE` request
  90. *
  91. * @var array
  92. */
  93. static private $put_delete = NULL;
  94. /**
  95. * Recursively handles casting values
  96. *
  97. * @param string|array $value The value to be casted
  98. * @param string $cast_to The data type to cast to
  99. * @param integer $level The nesting level of the call
  100. * @return mixed The casted `$value`
  101. */
  102. static private function cast($value, $cast_to, $level=0)
  103. {
  104. $level++;
  105. $strict_array = substr($cast_to, -2) == '[]';
  106. $array_type = $cast_to == 'array' || $strict_array;
  107. if ($level == 1 && $array_type) {
  108. if (is_string($value) && strpos($value, ',') !== FALSE) {
  109. $value = explode(',', $value);
  110. } elseif ($value === NULL || $value === '') {
  111. $value = array();
  112. } else {
  113. settype($value, 'array');
  114. }
  115. }
  116. // Iterate through array values and cast them individually
  117. if (is_array($value) && ($cast_to == 'array' || $cast_to === NULL || ($strict_array && $level == 1))) {
  118. if ($value === array()) {
  119. return $value;
  120. }
  121. foreach ($value as $key => $sub_value) {
  122. $value[$key] = self::cast($sub_value, $cast_to, $level);
  123. }
  124. return $value;
  125. }
  126. if ($array_type) {
  127. $cast_to = preg_replace('#\[\]$#D', '', $cast_to);
  128. }
  129. if ($cast_to == 'array' && $level > 1) {
  130. $cast_to = 'string';
  131. }
  132. if (get_magic_quotes_gpc() && (self::isPost() || self::isGet())) {
  133. $value = self::stripSlashes($value);
  134. }
  135. // This normalizes an empty element to NULL
  136. if ($cast_to === NULL && $value === '') {
  137. $value = NULL;
  138. } elseif ($cast_to == 'date') {
  139. try {
  140. $value = new fDate($value);
  141. } catch (fValidationException $e) {
  142. $value = new fDate();
  143. }
  144. } elseif ($cast_to == 'time') {
  145. try {
  146. $value = new fTime($value);
  147. } catch (fValidationException $e) {
  148. $value = new fTime();
  149. }
  150. } elseif ($cast_to == 'timestamp') {
  151. try {
  152. $value = new fTimestamp($value);
  153. } catch (fValidationException $e) {
  154. $value = new fTimestamp();
  155. }
  156. } elseif ($cast_to == 'bool' || $cast_to == 'boolean') {
  157. if (strtolower($value) == 'f' || strtolower($value) == 'false' || strtolower($value) == 'no' || !$value) {
  158. $value = FALSE;
  159. } else {
  160. $value = TRUE;
  161. }
  162. } elseif (($cast_to == 'int' || $cast_to == 'integer') && is_string($value) && preg_match('#^-?\d+$#D', $value)) {
  163. // Only explicitly cast integers than can be represented by a real
  164. // PHP integer to prevent truncation due to 32 bit integer limits
  165. if (strval(intval($value)) == $value) {
  166. $value = (int) $value;
  167. }
  168. // This patches PHP bug #53632 for vulnerable versions of PHP - http://bugs.php.net/bug.php?id=53632
  169. } elseif ($cast_to == 'float' && $value === "2.2250738585072011e-308") {
  170. static $vulnerable_to_53632 = NULL;
  171. if ($vulnerable_to_53632 === NULL) {
  172. $running_version = preg_replace(
  173. '#^(\d+\.\d+\.\d+).*$#D',
  174. '\1',
  175. PHP_VERSION
  176. );
  177. $vulnerable_to_53632 = version_compare($running_version, '5.2.17', '<') || (version_compare($running_version, '5.3.5', '<') && version_compare($running_version, '5.3.0', '>='));
  178. }
  179. if ($vulnerable_to_53632) {
  180. $value = "2.2250738585072012e-308";
  181. }
  182. settype($value, 'float');
  183. } elseif ($cast_to != 'binary' && $cast_to !== NULL) {
  184. $cast_to = str_replace('integer!', 'integer', $cast_to);
  185. settype($value, $cast_to);
  186. }
  187. // Clean values coming in to ensure we don't have invalid UTF-8
  188. if (($cast_to === NULL || $cast_to == 'string' || $cast_to == 'array') && $value !== NULL) {
  189. $value = self::stripLowOrderBytes($value);
  190. $value = fUTF8::clean($value);
  191. }
  192. return $value;
  193. }
  194. /**
  195. * Indicated if the parameter specified is set in the `$_GET` or `$_POST` superglobals or in the post data of a `PUT` or `DELETE` request
  196. *
  197. * @param string $key The key to check - array elements can be checked via `[sub-key]` syntax
  198. * @return boolean If the parameter is set
  199. */
  200. static public function check($key)
  201. {
  202. self::initPutDelete();
  203. $array_dereference = NULL;
  204. if (strpos($key, '[')) {
  205. $bracket_pos = strpos($key, '[');
  206. $array_dereference = substr($key, $bracket_pos);
  207. $key = substr($key, 0, $bracket_pos);
  208. }
  209. if (!isset($_GET[$key]) && !isset($_POST[$key]) && !isset(self::$put_delete[$key])) {
  210. return FALSE;
  211. }
  212. $values = array($_GET, $_POST, self::$put_delete);
  213. if ($array_dereference) {
  214. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  215. $array_keys = array_map('current', $array_keys);
  216. array_unshift($array_keys, $key);
  217. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  218. foreach ($values as &$value) {
  219. if (!is_array($value) || !isset($value[$array_key])) {
  220. $value = NULL;
  221. } else {
  222. $value = $value[$array_key];
  223. }
  224. }
  225. }
  226. $key = end($array_keys);
  227. }
  228. return isset($values[0][$key]) || isset($values[1][$key]) || isset($values[2][$key]);
  229. }
  230. /**
  231. * Gets a value from ::get() and passes it through fHTML::encode()
  232. *
  233. * @param string $key The key to get the value of - array elements can be accessed via `[sub-key]` syntax
  234. * @param string $cast_to Cast the value to this data type
  235. * @param mixed $default_value If the parameter is not set in the `DELETE`/`PUT` post data, `$_POST` or `$_GET`, use this value instead
  236. * @return string The encoded value
  237. */
  238. static public function encode($key, $cast_to=NULL, $default_value=NULL)
  239. {
  240. return fHTML::encode(self::get($key, $cast_to, $default_value));
  241. }
  242. /**
  243. * Parses through `$_FILES`, `$_GET`, `$_POST` and the `PUT`/`DELETE` post data and filters out everything that doesn't match the prefix and key, also removes the prefix from the field name
  244. *
  245. * @internal
  246. *
  247. * @param string $prefix The prefix to filter by
  248. * @param mixed $key If the field is an array, get the value corresponding to this key
  249. * @return void
  250. */
  251. static public function filter($prefix, $key)
  252. {
  253. self::initPutDelete();
  254. $regex = '#^' . preg_quote($prefix, '#') . '#';
  255. $current_backup = sizeof(self::$backup_files);
  256. self::$backup_files[] = $_FILES;
  257. $_FILES = array();
  258. foreach (self::$backup_files[$current_backup] as $field => $value) {
  259. $matches_prefix = !$prefix || ($prefix && strpos($field, $prefix) === 0);
  260. if ($matches_prefix && is_array($value) && isset($value['name'][$key])) {
  261. $new_field = preg_replace($regex, '', $field);
  262. $_FILES[$new_field] = array();
  263. $_FILES[$new_field]['name'] = $value['name'][$key];
  264. $_FILES[$new_field]['type'] = $value['type'][$key];
  265. $_FILES[$new_field]['tmp_name'] = $value['tmp_name'][$key];
  266. $_FILES[$new_field]['error'] = $value['error'][$key];
  267. $_FILES[$new_field]['size'] = $value['size'][$key];
  268. }
  269. }
  270. $globals = array(
  271. 'get' => array('array' => &$_GET, 'backup' => &self::$backup_get),
  272. 'post' => array('array' => &$_POST, 'backup' => &self::$backup_post),
  273. 'put/delete' => array('array' => &self::$put_delete, 'backup' => &self::$backup_put_delete)
  274. );
  275. foreach ($globals as $refs) {
  276. $current_backup = sizeof($refs['backup']);
  277. $refs['backup'][] = $refs['array'];
  278. $refs['array'] = array();
  279. foreach ($refs['backup'][$current_backup] as $field => $value) {
  280. $matches_prefix = !$prefix || ($prefix && strpos($field, $prefix) === 0);
  281. if ($matches_prefix && is_array($value) && isset($value[$key])) {
  282. $new_field = preg_replace($regex, '', $field);
  283. $refs['array'][$new_field] = $value[$key];
  284. }
  285. }
  286. }
  287. }
  288. /**
  289. * Returns a request token that should be placed in each HTML form to prevent [http://en.wikipedia.org/wiki/Cross-site_request_forgery cross-site request forgery]
  290. *
  291. * This method will return a random 15 character string that should be
  292. * placed in a hidden `input` element on every HTML form. When the form
  293. * contents are being processed, the token should be retrieved and passed
  294. * into ::validateCSRFToken().
  295. *
  296. * The value returned by this method is stored in the session and then
  297. * checked by the validate method, which helps prevent cross site request
  298. * forgeries and (naive) automated form submissions.
  299. *
  300. * Tokens generated by this method are single use, so a user must request
  301. * the page that generates the token at least once per submission.
  302. *
  303. * @param string $url The URL to generate a token for, default to the current page
  304. * @return string The token to be submitted with the form
  305. */
  306. static public function generateCSRFToken($url=NULL)
  307. {
  308. if ($url === NULL) {
  309. $url = fURL::get();
  310. }
  311. $token = fCryptography::randomString(16);
  312. fSession::add(__CLASS__ . '::' . $url . '::csrf_tokens', $token);
  313. return $token;
  314. }
  315. /**
  316. * Gets a value from the `DELETE`/`PUT` post data, `$_POST` or `$_GET` superglobals (in that order)
  317. *
  318. * A value that exactly equals `''` and is not cast to a specific type will
  319. * become `NULL`.
  320. *
  321. * Valid `$cast_to` types include:
  322. * - `'string'`
  323. * - `'binary'`
  324. * - `'int'`
  325. * - `'integer'`
  326. * - `'bool'`
  327. * - `'boolean'`
  328. * - `'array'`
  329. * - `'date'`
  330. * - `'time'`
  331. * - `'timestamp'`
  332. *
  333. * It is possible to append a `?` to a data type to return `NULL`
  334. * whenever the `$key` was not specified in the request, or if the value
  335. * was a blank string.
  336. *
  337. * The `array` and unspecified `$cast_to` types allow for multi-dimensional
  338. * arrays of string data. It is possible to cast an input value as a
  339. * single-dimensional array of a specific type by appending `[]` to the
  340. * `$cast_to`.
  341. *
  342. * All `string`, `array` or unspecified `$cast_to` will result in the value(s)
  343. * being interpreted as UTF-8 string and appropriately cleaned of invalid
  344. * byte sequences. Also, all low-byte, non-printable characters will be
  345. * stripped from the value. This includes all bytes less than the value of
  346. * 32 (Space) other than Tab (`\t`), Newline (`\n`) and Cariage Return
  347. * (`\r`).
  348. *
  349. * To preserve low-byte, non-printable characters, or get the raw value
  350. * without cleaning invalid UTF-8 byte sequences, plase use the value of
  351. * `binary` for the `$cast_to` parameter.
  352. *
  353. * Any integers that are beyond the range of 32bit storage will be returned
  354. * as a string. The returned value can be forced to always be a real
  355. * integer, which may cause truncation of the value, by passing `integer!`
  356. * as the `$cast_to`.
  357. *
  358. * @param string $key The key to get the value of - array elements can be accessed via `[sub-key]` syntax
  359. * @param string $cast_to Cast the value to this data type - see method description for details
  360. * @param mixed $default_value If the parameter is not set in the `DELETE`/`PUT` post data, `$_POST` or `$_GET`, use this value instead. This value will get cast if a `$cast_to` is specified.
  361. * @param boolean $use_default_for_blank If the request value is a blank string and `$default_value` is specified, this flag will cause the `$default_value` to be returned
  362. * @return mixed The value
  363. */
  364. static public function get($key, $cast_to=NULL, $default_value=NULL, $use_default_for_blank=FALSE)
  365. {
  366. self::initPutDelete();
  367. $value = $default_value;
  368. $array_dereference = NULL;
  369. if (strpos($key, '[')) {
  370. $bracket_pos = strpos($key, '[');
  371. $array_dereference = substr($key, $bracket_pos);
  372. $key = substr($key, 0, $bracket_pos);
  373. }
  374. if (isset(self::$put_delete[$key])) {
  375. $value = self::$put_delete[$key];
  376. } elseif (isset($_POST[$key])) {
  377. $value = $_POST[$key];
  378. } elseif (isset($_GET[$key])) {
  379. $value = $_GET[$key];
  380. }
  381. if ($value === '' && $use_default_for_blank && $default_value !== NULL) {
  382. $value = $default_value;
  383. }
  384. if ($array_dereference) {
  385. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  386. $array_keys = array_map('current', $array_keys);
  387. foreach ($array_keys as $array_key) {
  388. if (!is_array($value) || !isset($value[$array_key])) {
  389. $value = $default_value;
  390. break;
  391. }
  392. $value = $value[$array_key];
  393. }
  394. }
  395. // This allows for data_type? casts to allow NULL through
  396. if ($cast_to !== NULL && substr($cast_to, -1) == '?') {
  397. if ($value === NULL || $value === '') {
  398. return NULL;
  399. }
  400. $cast_to = substr($cast_to, 0, -1);
  401. }
  402. return self::cast($value, $cast_to);
  403. }
  404. /**
  405. * Returns the HTTP `Accept-Language`s sorted by their `q` values (from high to low)
  406. *
  407. * @return array An associative array of `{language} => {q value}` sorted (in a stable-fashion) from highest to lowest `q` - if no header was sent, an empty array will be returned
  408. */
  409. static public function getAcceptLanguages()
  410. {
  411. return self::processAcceptHeader('HTTP_ACCEPT_LANGUAGE');
  412. }
  413. /**
  414. * Returns the HTTP `Accept` types sorted by their `q` values (from high to low)
  415. *
  416. * @return array An associative array of `{type} => {q value}` sorted (in a stable-fashion) from highest to lowest `q` - if no header was sent, an empty array will be returned
  417. */
  418. static public function getAcceptTypes()
  419. {
  420. return self::processAcceptHeader('HTTP_ACCEPT');
  421. }
  422. /**
  423. * Returns the best HTTP `Accept-Language` (based on `q` value) - can be filtered to only allow certain languages
  424. *
  425. * Special conditions affecting the return value:
  426. * - If no `$filter` is provided and the client does not send the `Accept-Language` header, `NULL` will be returned
  427. * - If no `$filter` is provided and the client specifies `*` with the highest `q`, `NULL` will be returned
  428. * - If `$filter` contains one or more values, but the `Accept-Language` header does not match any, `FALSE` will be returned
  429. * - If `$filter` contains one or more values, but the client does not send the `Accept-Language` header, the first entry from `$filter` will be returned
  430. * - If `$filter` contains two or more values, and two of the values have the same `q` value, the one listed first in `$filter` will be returned
  431. *
  432. * @param array $filter An array of languages that are valid to return - these should be in the form `{language}-{territory}`, e.g. `en-us`
  433. * @param string |$language A language that is valid to return
  434. * @param string |...
  435. * @return string|NULL|FALSE The best language listed in the `Accept-Language` header - see method description for edge cases
  436. */
  437. static public function getBestAcceptLanguage($filter=array())
  438. {
  439. if (!is_array($filter)) {
  440. $filter = func_get_args();
  441. }
  442. return self::pickBestAcceptItem('HTTP_ACCEPT_LANGUAGE', $filter);
  443. }
  444. /**
  445. * Returns the best HTTP `Accept` type (based on `q` value) - can be filtered to only allow certain types
  446. *
  447. * Special conditions affecting the return value:
  448. * - If no `$filter` is provided and the client does not send the `Accept` header, `NULL` will be returned
  449. * - If no `$filter` is provided and the client specifies `{@*}*` with the highest `q`, `NULL` will be returned
  450. * - If `$filter` contains one or more values, but the `Accept` header does not match any, `FALSE` will be returned
  451. * - If `$filter` contains one or more values, but the client does not send the `Accept` header, the first entry from `$filter` will be returned
  452. * - If `$filter` contains two or more values, and two of the values have the same `q` value, the one listed first in `$filter` will be returned
  453. *
  454. * @param array $filter An array of types that are valid to return
  455. * @param string |$type A type that is valid to return
  456. * @param string |...
  457. * @return string|NULL|FALSE The best type listed in the `Accept` header - see method description for edge cases
  458. */
  459. static public function getBestAcceptType($filter=array())
  460. {
  461. if (!is_array($filter)) {
  462. $filter = func_get_args();
  463. }
  464. return self::pickBestAcceptItem('HTTP_ACCEPT', $filter);
  465. }
  466. /**
  467. * Gets a value from the `DELETE`/`PUT` post data, `$_POST` or `$_GET` superglobals (in that order), restricting to a specific set of values
  468. *
  469. * @param string $key The key to get the value of - array elements can be accessed via `[sub-key]` syntax
  470. * @param array $valid_values The array of values that are permissible, if one is not selected, picks first
  471. * @return mixed The value
  472. */
  473. static public function getValid($key, $valid_values)
  474. {
  475. settype($valid_values, 'array');
  476. $valid_values = array_merge(array_unique($valid_values));
  477. $value = self::get($key);
  478. if (!in_array($value, $valid_values)) {
  479. return $valid_values[0];
  480. }
  481. return $value;
  482. }
  483. /**
  484. * Parses post data for `PUT` and `DELETE` HTTP methods
  485. *
  486. * @return void
  487. */
  488. static private function initPutDelete()
  489. {
  490. if (is_array(self::$put_delete)) {
  491. return;
  492. }
  493. if (self::isPut() || self::isDelete()) {
  494. parse_str(file_get_contents('php://input'), self::$put_delete);
  495. } else {
  496. self::$put_delete = array();
  497. }
  498. }
  499. /**
  500. * Indicates if the URL was accessed via an XMLHttpRequest
  501. *
  502. * @return boolean If the URL was accessed via an XMLHttpRequest
  503. */
  504. static public function isAjax()
  505. {
  506. return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
  507. }
  508. /**
  509. * Indicates if the URL was accessed via the `DELETE` HTTP method
  510. *
  511. * @return boolean If the URL was accessed via the `DELETE` HTTP method
  512. */
  513. static public function isDelete()
  514. {
  515. return strtolower($_SERVER['REQUEST_METHOD']) == 'delete';
  516. }
  517. /**
  518. * Indicates if the URL was accessed via the `GET` HTTP method
  519. *
  520. * @return boolean If the URL was accessed via the `GET` HTTP method
  521. */
  522. static public function isGet()
  523. {
  524. return strtolower($_SERVER['REQUEST_METHOD']) == 'get';
  525. }
  526. /**
  527. * Indicates if the URL was accessed via the `POST` HTTP method
  528. *
  529. * @return boolean If the URL was accessed via the `POST` HTTP method
  530. */
  531. static public function isPost()
  532. {
  533. return strtolower($_SERVER['REQUEST_METHOD']) == 'post';
  534. }
  535. /**
  536. * Indicates if the URL was accessed via the `PUT` HTTP method
  537. *
  538. * @return boolean If the URL was accessed via the `PUT` HTTP method
  539. */
  540. static public function isPut()
  541. {
  542. return strtolower($_SERVER['REQUEST_METHOD']) == 'put';
  543. }
  544. /**
  545. * Overrides the value of `'action'` in the `DELETE`/`PUT` post data, `$_POST` or `$_GET` superglobals based on the `'action::{action_name}'` value
  546. *
  547. * This method is primarily intended to be used for hanlding multiple
  548. * submit buttons.
  549. *
  550. * @param string $redirect The url to redirect to if the action is overriden. `%action%` will be replaced with the overridden action.
  551. * @return void
  552. */
  553. static public function overrideAction($redirect=NULL)
  554. {
  555. self::initPutDelete();
  556. $found = FALSE;
  557. $globals = array(&$_GET, &$_POST, &self::$put_delete);
  558. foreach ($globals as &$global) {
  559. foreach ($global as $key => $value) {
  560. if (substr($key, 0, 8) == 'action::') {
  561. $found = (boolean) $global['action'] = substr($key, 8);
  562. unset($global[$key]);
  563. }
  564. }
  565. }
  566. if ($redirect && $found) {
  567. fURL::redirect(str_replace('%action%', $found, $redirect));
  568. }
  569. }
  570. /**
  571. * Returns the best HTTP `Accept-*` header item match (based on `q` value), optionally filtered by an array of options
  572. *
  573. * @param string $header_name The key in `$_SERVER` that contains the `Accept-*` header to pick the best item from
  574. * @param array $options A list of supported options to pick the best from
  575. * @return string The best accept item, `FALSE` if an options array is specified and none are valid, `NULL` if anything is accepted
  576. */
  577. static private function pickBestAcceptItem($header_name, $options)
  578. {
  579. settype($options, 'array');
  580. if (!isset($_SERVER[$header_name]) || !strlen($_SERVER[$header_name])) {
  581. if (empty($options)) {
  582. return NULL;
  583. }
  584. return reset($options);
  585. }
  586. $items = self::processAcceptHeader($header_name);
  587. reset($items);
  588. if (!$options) {
  589. $result = key($items);
  590. if ($result == '*/*' || $result == '*') {
  591. $result = NULL;
  592. }
  593. return $result;
  594. }
  595. $top_q = 0;
  596. $top_item = FALSE;
  597. foreach ($options as $option) {
  598. foreach ($items as $item => $q) {
  599. if ($q < $top_q) {
  600. continue;
  601. }
  602. // Type matches have /s
  603. if (strpos($item, '/') !== FALSE) {
  604. $regex = '#^' . str_replace('*', '.*', $item) . '$#iD';
  605. // Language matches that don't have a - are a wildcard
  606. } elseif (strpos($item, '-') === FALSE) {
  607. $regex = '#^' . str_replace('*', '.*', $item) . '(-.*)?$#iD';
  608. // Non-wildcard languages are straight-up matches
  609. } else {
  610. $regex = '#^' . str_replace('*', '.*', $item) . '$#iD';
  611. }
  612. if (preg_match($regex, $option) && $top_q < $q) {
  613. $top_q = $q;
  614. $top_item = $option;
  615. continue;
  616. }
  617. }
  618. }
  619. return $top_item;
  620. }
  621. /**
  622. * Gets a value from ::get() and passes it through fHTML::prepare()
  623. *
  624. * @param string $key The key to get the value of - array elements can be accessed via `[sub-key]` syntax
  625. * @param string $cast_to Cast the value to this data type
  626. * @param mixed $default_value If the parameter is not set in the `DELETE`/`PUT` post data, `$_POST` or `$_GET`, use this value instead
  627. * @return string The prepared value
  628. */
  629. static public function prepare($key, $cast_to=NULL, $default_value=NULL)
  630. {
  631. return fHTML::prepare(self::get($key, $cast_to, $default_value));
  632. }
  633. /**
  634. * Returns an array of values from one of the HTTP `Accept-*` headers
  635. *
  636. * @param string $header_name The key in `$_SERVER` to get the header value from
  637. * @return array An associative array of `{value} => {quality}` sorted (in a stable-fashion) from highest to lowest `q` - an empty array is returned if the header is empty
  638. */
  639. static private function processAcceptHeader($header_name)
  640. {
  641. if (!isset($_SERVER[$header_name]) || !strlen($_SERVER[$header_name])) {
  642. return array();
  643. }
  644. $types = explode(',', $_SERVER[$header_name]);
  645. $output = array();
  646. // We use this suffix to force stable sorting with the built-in sort function
  647. $suffix = sizeof($types);
  648. foreach ($types as $type) {
  649. $parts = explode(';', $type);
  650. if (!empty($parts[1]) && preg_match('#^q=(\d(?:\.\d)?)#', $parts[1], $match)) {
  651. $q = number_format((float)$match[1], 5);
  652. } else {
  653. $q = number_format(1.0, 5);
  654. }
  655. $q .= $suffix--;
  656. $output[$parts[0]] = $q;
  657. }
  658. arsort($output, SORT_NUMERIC);
  659. foreach ($output as $type => $q) {
  660. $output[$type] = (float) substr($q, 0, -1);
  661. }
  662. return $output;
  663. }
  664. /**
  665. * Resets the configuration and data of the class
  666. *
  667. * @internal
  668. *
  669. * @return void
  670. */
  671. static public function reset()
  672. {
  673. fSession::clear(__CLASS__ . '::');
  674. self::$backup_files = NULL;
  675. self::$backup_get = NULL;
  676. self::$backup_post = NULL;
  677. self::$backup_put_delete = NULL;
  678. self::$put_delete = NULL;
  679. }
  680. /**
  681. * Sets a value into the appropriate `$_GET` or `$_POST` superglobal, or the local `PUT`/`DELETE` post data based on what HTTP method was used for the request
  682. *
  683. * @param string $key The key to set the value to - array elements can be modified via `[sub-key]` syntax
  684. * @param mixed $value The value to set
  685. * @return void
  686. */
  687. static public function set($key, $value)
  688. {
  689. if (self::isPost()) {
  690. $tip =& $_POST;
  691. } elseif (self::isGet()) {
  692. $tip =& $_GET;
  693. } elseif (self::isDelete() || self::isPut()) {
  694. self::initPutDelete();
  695. $tip =& self::$put_delete;
  696. }
  697. if ($bracket_pos = strpos($key, '[')) {
  698. $array_dereference = substr($key, $bracket_pos);
  699. $key = substr($key, 0, $bracket_pos);
  700. preg_match_all('#(?<=\[)[^\[\]]+(?=\])#', $array_dereference, $array_keys, PREG_SET_ORDER);
  701. $array_keys = array_map('current', $array_keys);
  702. array_unshift($array_keys, $key);
  703. foreach (array_slice($array_keys, 0, -1) as $array_key) {
  704. if (!isset($tip[$array_key]) || !is_array($tip[$array_key])) {
  705. $tip[$array_key] = array();
  706. }
  707. $tip =& $tip[$array_key];
  708. }
  709. $tip[end($array_keys)] = $value;
  710. } else {
  711. $tip[$key] = $value;
  712. }
  713. }
  714. /**
  715. * Removes low-order bytes from a value
  716. *
  717. * @param string|array $value The value to strip
  718. * @return string|array The `$value` with low-order bytes stripped
  719. */
  720. static private function stripLowOrderBytes($value)
  721. {
  722. if (is_array($value)) {
  723. foreach ($value as $key => $sub_value) {
  724. $value[$key] = self::stripLowOrderBytes($sub_value);
  725. }
  726. return $value;
  727. }
  728. return preg_replace('#[\x00-\x08\x0B\x0C\x0E-\x1F]#', '', $value);
  729. }
  730. /**
  731. * Removes slashes from a value
  732. *
  733. * @param string|array $value The value to strip
  734. * @return string|array The `$value` with slashes stripped
  735. */
  736. static private function stripSlashes($value)
  737. {
  738. if (is_array($value)) {
  739. foreach ($value as $key => $sub_value) {
  740. $value[$key] = self::stripSlashes($sub_value);
  741. }
  742. return $value;
  743. }
  744. return stripslashes($value);
  745. }
  746. /**
  747. * Returns `$_GET`, `$_POST` and `$_FILES` and the `PUT`/`DELTE` post data to the state they were at before ::filter() was called
  748. *
  749. * @internal
  750. *
  751. * @return void
  752. */
  753. static public function unfilter()
  754. {
  755. if (self::$backup_get === array()) {
  756. throw new fProgrammerException(
  757. '%1$s can only be called after %2$s',
  758. __CLASS__ . '::unfilter()',
  759. __CLASS__ . '::filter()'
  760. );
  761. }
  762. $_FILES = array_pop(self::$backup_files);
  763. $_GET = array_pop(self::$backup_get);
  764. $_POST = array_pop(self::$backup_post);
  765. self::$put_delete = array_pop(self::$backup_put_delete);
  766. }
  767. /**
  768. * Validates a request token generated by ::generateCSRFToken()
  769. *
  770. * This method takes a request token and ensures it is valid, otherwise
  771. * it will throw an fValidationException.
  772. *
  773. * @throws fValidationException When the CSRF token specified is invalid
  774. *
  775. * @param string $token The request token to validate
  776. * @param string $url The URL to validate the token for, default to the current page
  777. * @return void
  778. */
  779. static public function validateCSRFToken($token, $url=NULL)
  780. {
  781. if ($url === NULL) {
  782. $url = fURL::get();
  783. }
  784. $key = __CLASS__ . '::' . $url . '::csrf_tokens';
  785. $tokens = fSession::get($key, array());
  786. if (!in_array($token, $tokens)) {
  787. throw new fValidationException(
  788. 'The form submitted could not be validated as authentic, please try submitting it again'
  789. );
  790. }
  791. $tokens = array_diff($tokens, array($token));;
  792. fSession::set($key, $tokens);
  793. }
  794. /**
  795. * Forces use as a static class
  796. *
  797. * @return fRequest
  798. */
  799. private function __construct() { }
  800. }
  801. /**
  802. * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
  803. *
  804. * Permission is hereby granted, free of charge, to any person obtaining a copy
  805. * of this software and associated documentation files (the "Software"), to deal
  806. * in the Software without restriction, including without limitation the rights
  807. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  808. * copies of the Software, and to permit persons to whom the Software is
  809. * furnished to do so, subject to the following conditions:
  810. *
  811. * The above copyright notice and this permission notice shall be included in
  812. * all copies or substantial portions of the Software.
  813. *
  814. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  815. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  816. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  817. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  818. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  819. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  820. * THE SOFTWARE.
  821. */