Request.php 10 KB

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