Request.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. <?php declare(strict_types=1);
  2. /**
  3. * PrivateBin
  4. *
  5. * a zero-knowledge paste bin
  6. *
  7. * @link https://github.com/PrivateBin/PrivateBin
  8. * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
  9. * @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
  10. */
  11. namespace PrivateBin;
  12. use JsonException;
  13. use PrivateBin\Model\Paste;
  14. /**
  15. * Request
  16. *
  17. * parses request parameters and provides helper functions for routing
  18. */
  19. class Request
  20. {
  21. /**
  22. * MIME type for JSON
  23. *
  24. * @const string
  25. */
  26. const MIME_JSON = 'application/json';
  27. /**
  28. * MIME type for HTML
  29. *
  30. * @const string
  31. */
  32. const MIME_HTML = 'text/html';
  33. /**
  34. * MIME type for XHTML
  35. *
  36. * @const string
  37. */
  38. const MIME_XHTML = 'application/xhtml+xml';
  39. /**
  40. * Input stream to use for PUT parameter parsing
  41. *
  42. * @access private
  43. * @var string
  44. */
  45. private static $_inputStream = 'php://input';
  46. /**
  47. * Operation to perform
  48. *
  49. * @access private
  50. * @var string
  51. */
  52. private $_operation = 'view';
  53. /**
  54. * Request parameters
  55. *
  56. * @access private
  57. * @var array
  58. */
  59. private $_params = [];
  60. /**
  61. * If we are in a JSON API context
  62. *
  63. * @access private
  64. * @var bool
  65. */
  66. private $_isJsonApi = false;
  67. /**
  68. * Return the paste ID of the current document.
  69. *
  70. * @access private
  71. * @return string
  72. */
  73. private function getPasteId()
  74. {
  75. foreach ($_GET as $key => $value) {
  76. // only return if value is empty and key is 16 hex chars
  77. $key = (string) $key;
  78. if (empty($value) && Paste::isValidId($key)) {
  79. return $key;
  80. }
  81. }
  82. return 'invalid id';
  83. }
  84. /**
  85. * Constructor
  86. *
  87. * @access public
  88. */
  89. public function __construct()
  90. {
  91. // decide if we are in JSON API or HTML context
  92. $this->_isJsonApi = $this->_detectJsonRequest();
  93. // parse parameters, depending on request type
  94. switch ($_SERVER['REQUEST_METHOD'] ?? 'GET') {
  95. case 'DELETE':
  96. case 'PUT':
  97. case 'POST':
  98. // it might be a creation or a deletion, the latter is detected below
  99. $this->_operation = 'create';
  100. try {
  101. $data = file_get_contents(self::$_inputStream);
  102. $this->_params = Json::decode($data);
  103. // a valid JSON scalar (number, bool or string) decodes
  104. // without error, but is not a usable set of parameters
  105. if (!is_array($this->_params)) {
  106. $this->_params = [];
  107. }
  108. } catch (JsonException $e) {
  109. // ignore error, $this->_params will remain empty
  110. }
  111. break;
  112. default:
  113. $this->_params = filter_var_array($_GET, [
  114. 'deletetoken' => FILTER_SANITIZE_SPECIAL_CHARS,
  115. 'jsonld' => FILTER_SANITIZE_SPECIAL_CHARS,
  116. 'link' => FILTER_SANITIZE_URL,
  117. 'pasteid' => FILTER_SANITIZE_SPECIAL_CHARS,
  118. 'shortenviayourls' => FILTER_SANITIZE_SPECIAL_CHARS,
  119. 'shortenviashlink' => FILTER_SANITIZE_SPECIAL_CHARS,
  120. 'shortenviachhoto' => FILTER_SANITIZE_SPECIAL_CHARS,
  121. ], false);
  122. }
  123. if (
  124. !array_key_exists('pasteid', $this->_params) &&
  125. !array_key_exists('jsonld', $this->_params) &&
  126. !array_key_exists('link', $this->_params) &&
  127. array_key_exists('QUERY_STRING', $_SERVER) &&
  128. !empty($_SERVER['QUERY_STRING'])
  129. ) {
  130. $this->_params['pasteid'] = $this->getPasteId();
  131. }
  132. // prepare operation, depending on current parameters
  133. if (
  134. array_key_exists('pasteid', $this->_params) &&
  135. is_string($this->_params['pasteid']) &&
  136. !empty($this->_params['pasteid'])
  137. ) {
  138. if (
  139. array_key_exists('deletetoken', $this->_params) &&
  140. is_string($this->_params['deletetoken']) &&
  141. !empty($this->_params['deletetoken'])
  142. ) {
  143. $this->_operation = 'delete';
  144. } elseif ($this->_operation !== 'create') {
  145. $this->_operation = 'read';
  146. }
  147. } elseif (
  148. array_key_exists('jsonld', $this->_params) &&
  149. is_string($this->_params['jsonld']) &&
  150. !empty($this->_params['jsonld'])
  151. ) {
  152. $this->_operation = 'jsonld';
  153. } elseif (
  154. array_key_exists('link', $this->_params) &&
  155. is_string($this->_params['link']) &&
  156. !empty($this->_params['link'])
  157. ) {
  158. if (str_contains($this->getRequestUri(), '/shortenviayourls') || array_key_exists('shortenviayourls', $this->_params)) {
  159. $this->_operation = 'yourlsproxy';
  160. }
  161. if (str_contains($this->getRequestUri(), '/shortenviashlink') || array_key_exists('shortenviashlink', $this->_params)) {
  162. $this->_operation = 'shlinkproxy';
  163. }
  164. if (str_contains($this->getRequestUri(), '/shortenviachhoto') || array_key_exists('shortenviachhoto', $this->_params)) {
  165. $this->_operation = 'chhotoproxy';
  166. }
  167. }
  168. }
  169. /**
  170. * Get current operation
  171. *
  172. * @access public
  173. * @return string
  174. */
  175. public function getOperation()
  176. {
  177. return $this->_operation;
  178. }
  179. /**
  180. * Get data of paste or comment
  181. *
  182. * @access public
  183. * @return array
  184. */
  185. public function getData()
  186. {
  187. $data = [
  188. 'adata' => $this->getParam('adata'),
  189. ];
  190. $required_keys = ['v', 'ct'];
  191. $meta = $this->getParam('meta');
  192. if (empty($meta)) {
  193. $required_keys[] = 'pasteid';
  194. $required_keys[] = 'parentid';
  195. } else {
  196. $data['meta'] = $meta;
  197. }
  198. foreach ($required_keys as $key) {
  199. $data[$key] = $this->getParam($key, $key === 'v' ? 1 : '');
  200. }
  201. return $data;
  202. }
  203. /**
  204. * Get a request parameter
  205. *
  206. * @access public
  207. * @param string $param
  208. * @param string $default
  209. * @return string
  210. */
  211. public function getParam($param, $default = '')
  212. {
  213. return $this->_params[$param] ?? $default;
  214. }
  215. /**
  216. * Get host as requested by the client
  217. *
  218. * @access public
  219. * @return string
  220. */
  221. public function getHost()
  222. {
  223. $host = array_key_exists('HTTP_HOST', $_SERVER) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : '';
  224. return empty($host) ? 'localhost' : $host;
  225. }
  226. /**
  227. * Get request URI path without GET parameters
  228. *
  229. * @access public
  230. * @return string
  231. */
  232. public function getRequestUri()
  233. {
  234. $uri = array_key_exists('REQUEST_URI', $_SERVER) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : '';
  235. $path = empty($uri) ? '' : parse_url($uri, PHP_URL_PATH);
  236. return is_string($path) && $path !== '' ? $path : '/';
  237. }
  238. /**
  239. * If we are in a JSON API context
  240. *
  241. * @access public
  242. * @return bool
  243. */
  244. public function isJsonApiCall()
  245. {
  246. return $this->_isJsonApi;
  247. }
  248. /**
  249. * Override the default input stream source, used for unit testing
  250. *
  251. * @param string $input
  252. */
  253. public static function setInputStream($input)
  254. {
  255. self::$_inputStream = $input;
  256. }
  257. /**
  258. * Detect the clients supported media type and decide if its a JSON API call or not
  259. *
  260. * Adapted from: https://stackoverflow.com/questions/3770513/detect-browser-language-in-php#3771447
  261. *
  262. * @access private
  263. * @return bool
  264. */
  265. private function _detectJsonRequest()
  266. {
  267. $acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
  268. // simple cases
  269. if (
  270. ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') === 'JSONHttpRequest' ||
  271. (
  272. str_contains($acceptHeader, self::MIME_JSON) &&
  273. !str_contains($acceptHeader, self::MIME_HTML) &&
  274. !str_contains($acceptHeader, self::MIME_XHTML)
  275. )
  276. ) {
  277. return true;
  278. }
  279. // advanced case: media type negotiation
  280. if (!empty($acceptHeader)) {
  281. $mediaTypes = [];
  282. foreach (explode(',', trim($acceptHeader)) as $mediaTypeRange) {
  283. if (preg_match(
  284. '#(\*/\*|[a-z\-]+/[a-z\-+*]+(?:\s*;\s*[^q]\S*)*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?#',
  285. trim($mediaTypeRange), $match
  286. )) {
  287. if (!isset($match[2])) {
  288. $match[2] = '1.0';
  289. } else {
  290. $match[2] = (string) floatval($match[2]);
  291. if ($match[2] === '0.0') {
  292. continue;
  293. }
  294. }
  295. if (!isset($mediaTypes[$match[2]])) {
  296. $mediaTypes[$match[2]] = [];
  297. }
  298. $mediaTypes[$match[2]][] = strtolower($match[1]);
  299. }
  300. }
  301. krsort($mediaTypes);
  302. foreach ($mediaTypes as $acceptedQuality => $acceptedValues) {
  303. foreach ($acceptedValues as $acceptedValue) {
  304. if (
  305. str_starts_with($acceptedValue, self::MIME_HTML) ||
  306. str_starts_with($acceptedValue, self::MIME_XHTML)
  307. ) {
  308. return false;
  309. } elseif (str_starts_with($acceptedValue, self::MIME_JSON)) {
  310. return true;
  311. }
  312. }
  313. }
  314. }
  315. return false;
  316. }
  317. }