BioTorrents.de’s version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

util.php 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. <?php
  2. #declare(strict_types = 1);
  3. // This is a file of miscellaneous functions that are called so damn often
  4. // that it'd just be annoying to stick them in namespaces.
  5. /**
  6. * Return true if the given string is numeric.
  7. *
  8. * @param mixed $Str
  9. * @return bool
  10. */
  11. function is_number($Str)
  12. {
  13. # todo: Strict equality breaks everything
  14. return $Str == strval(intval($Str));
  15. }
  16. /**
  17. * is_date()
  18. */
  19. function is_date($Date)
  20. {
  21. list($Y, $M, $D) = explode('-', $Date);
  22. if (checkdate($M, $D, $Y)) {
  23. return true;
  24. }
  25. return false;
  26. }
  27. /**
  28. * Check that some given variables (usually in _GET or _POST) are numbers
  29. *
  30. * @param array $Base array that's supposed to contain all keys to check
  31. * @param array $Keys list of keys to check
  32. * @param mixed $Error error code or string to pass to the error() function if a key isn't numeric
  33. */
  34. function assert_numbers(&$Base, $Keys, $Error = 0)
  35. {
  36. // Make sure both arguments are arrays
  37. if (!is_array($Base) || !is_array($Keys)) {
  38. return;
  39. }
  40. foreach ($Keys as $Key) {
  41. if (!isset($Base[$Key]) || !is_number($Base[$Key])) {
  42. error($Error);
  43. }
  44. }
  45. }
  46. /**
  47. * Return true, false or null, depending on the input value's "truthiness" or "non-truthiness"
  48. *
  49. * @param $Value the input value to check for truthiness
  50. * @return true if $Value is "truthy", false if it is "non-truthy" or null if $Value was not
  51. * a bool-like value
  52. */
  53. function is_bool_value($Value)
  54. {
  55. if (is_bool($Value)) {
  56. return $Value;
  57. }
  58. if (is_string($Value)) {
  59. switch (strtolower($Value)) {
  60. case 'true':
  61. case 'yes':
  62. case 'on':
  63. case '1':
  64. return true;
  65. case 'false':
  66. case 'no':
  67. case 'off':
  68. case '0':
  69. return false;
  70. }
  71. }
  72. if (is_numeric($Value)) {
  73. if ($Value === 1) {
  74. return true;
  75. } elseif ($Value === 0) {
  76. return false;
  77. }
  78. }
  79. return;
  80. }
  81. /**
  82. * HTML-escape a string for output.
  83. * This is preferable to htmlspecialchars because it doesn't screw up upon a double escape.
  84. *
  85. * @param string $Str
  86. * @return string escaped string.
  87. */
  88. function display_str($Str)
  89. {
  90. if ($Str === null || $Str === false || is_array($Str)) {
  91. return '';
  92. }
  93. if ($Str !== '' && !is_number($Str)) {
  94. $Str = Format::make_utf8($Str);
  95. $Str = mb_convert_encoding($Str, 'HTML-ENTITIES', 'UTF-8');
  96. $Str = preg_replace("/&(?![A-Za-z]{0,4}\w{2,3};|#[0-9]{2,6};)/m", '&amp;', $Str);
  97. $Replace = array(
  98. "'",'"',"<",">",
  99. '&#128;','&#130;','&#131;','&#132;','&#133;','&#134;','&#135;','&#136;',
  100. '&#137;','&#138;','&#139;','&#140;','&#142;','&#145;','&#146;','&#147;',
  101. '&#148;','&#149;','&#150;','&#151;','&#152;','&#153;','&#154;','&#155;',
  102. '&#156;','&#158;','&#159;'
  103. );
  104. $With = array(
  105. '&#39;','&quot;','&lt;','&gt;',
  106. '&#8364;','&#8218;','&#402;','&#8222;','&#8230;','&#8224;','&#8225;','&#710;',
  107. '&#8240;','&#352;','&#8249;','&#338;','&#381;','&#8216;','&#8217;','&#8220;',
  108. '&#8221;','&#8226;','&#8211;','&#8212;','&#732;','&#8482;','&#353;','&#8250;',
  109. '&#339;','&#382;','&#376;'
  110. );
  111. $Str = str_replace($Replace, $With, $Str);
  112. }
  113. return $Str;
  114. }
  115. /**
  116. * Send a message to an IRC bot listening on SOCKET_LISTEN_PORT
  117. *
  118. * @param string $Raw An IRC protocol snippet to send.
  119. */
  120. function send_irc($Channels = null, $Message = '')
  121. {
  122. $ENV = ENV::go();
  123. // Check if IRC is enabled
  124. if (!$ENV->FEATURE_IRC || !$Channels) {
  125. return false;
  126. }
  127. # The fn takes an array or string
  128. $Dest = [];
  129. # Quick missed connection fix
  130. if (is_string($Channels)) {
  131. $Channels = explode(' ', $Channels);
  132. }
  133. # Strip leading #channel hash
  134. foreach ($Channels as $c) {
  135. array_push($Dest, preg_replace('/^#/', '', $c));
  136. }
  137. # Specific to AB's kana bot
  138. # https://github.com/anniemaybytes/kana
  139. $Command =
  140. implode('-', $Dest)
  141. . '|%|'
  142. . html_entity_decode(
  143. display_str($Message),
  144. ENT_QUOTES
  145. );
  146. # Original input sanitization
  147. $Command = str_replace(array("\n", "\r"), '', $Command);
  148. # Send the raw echo
  149. $IRCSocket = fsockopen(SOCKET_LISTEN_ADDRESS, SOCKET_LISTEN_PORT);
  150. fwrite($IRCSocket, $Command);
  151. fclose($IRCSocket);
  152. }
  153. /**
  154. * notify()
  155. * Formerly in sections/error/index.php
  156. */
  157. function notify($Channel, $Message)
  158. {
  159. $ENV = ENV::go();
  160. global $LoggedUser;
  161. # Redirect dev messages to debug channel
  162. if ($ENV->DEV) {
  163. $Channel = $ENV->DEBUG_CHAN;
  164. }
  165. #
  166. send_irc(
  167. $Channel,
  168. $Message
  169. . " error by "
  170. . (!empty($LoggedUser['ID']) ? site_url()
  171. . "user.php?id=".$LoggedUser['ID']
  172. . " ("
  173. . $LoggedUser['Username']
  174. . ")" : $_SERVER['REMOTE_ADDR']
  175. . " ("
  176. . ")")
  177. . " accessing https://"
  178. . SITE_DOMAIN
  179. . ""
  180. . $_SERVER['REQUEST_URI']
  181. . (!empty($_SERVER['HTTP_REFERER']) ? " from "
  182. . $_SERVER['HTTP_REFERER'] : '')
  183. );
  184. }
  185. /**
  186. * Advanced error handling
  187. *
  188. * Displays an HTTP status code with description and triggers an error.
  189. * If you use your own string for $Error, it becomes the error description.
  190. *
  191. * @param int|string $Error Error type
  192. * The available HTTP status codes are
  193. * - Client: [ 400, 403, 404, 405, 408, 413, 429 ]
  194. * - Server: [ 500, 502, 504 ]
  195. * - Gazelle: [ -1, 0, !! ]
  196. *
  197. * @param boolean $NoHTML If true, the header/footer won't be shown, just the error.
  198. * @param string $Log If true, the user is given a link to search $Log in the site log.
  199. * @param boolean $Debug If true, print bug reporting instructions and a stack trace.
  200. * @param boolean $JSON If true, print the error as a JSON response.
  201. */
  202. function error($Error = 1, $NoHTML = false, $Log = false, $Debug = true) # , $JSON = false)
  203. {
  204. $ENV = ENV::go();
  205. # Error out on erroneous $Error
  206. (!$Error || $Error === null)
  207. ?? trigger_error('No $Error.', E_USER_ERROR);
  208. (!is_int($Error) || !is_string($Error))
  209. ?? trigger_error('$Error must be int or string.', E_USER_ERROR);
  210. # Formerly in sections/error/index.php
  211. if (!empty($_GET['e']) && is_int($_GET['e'])) {
  212. # Request error, i.e., /nonexistent_page.php
  213. $Error = $_GET['e'];
  214. }
  215. # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
  216. switch ($Error) {
  217. /**
  218. * Client errors
  219. */
  220. case 400:
  221. case 1: # Probably the user's fault
  222. $Title = '400 Bad Request';
  223. $Message = 'The server cannot or will not process the request due to an apparent client error
  224. (e.g., malformed request syntax, size too large, invalid request message framing, or deceptive request routing).';
  225. break;
  226. case 403:
  227. $Title = '403 Forbidden';
  228. $Message = 'The request contained valid data and was understood by the server, but the server is refusing action.
  229. This may be due to the user not having the necessary permissions for a resource or needing an account of some sort, or attempting a prohibited action
  230. (e.g., creating a duplicate record where only one is allowed).
  231. The request should not be repeated.';
  232. if (substr($_SERVER['REQUEST_URI'], 0, 9) !== '/static/') {
  233. notify($ENV->DEBUG_CHAN, $Title);
  234. }
  235. break;
  236. case 404:
  237. $Title = '404 Not Found';
  238. $Message = 'The requested resource could not be found but may be available in the future.
  239. Subsequent requests by the client are permissible.';
  240. // Hide alerts for missing images and static requests
  241. if (!preg_match(
  242. "/\.(ico|jpg|jpeg|gif|png)$/",
  243. $_SERVER['REQUEST_URI']
  244. ) && substr($_SERVER['REQUEST_URI'], 0, 9) !== '/static/') {
  245. notify($ENV->DEBUG_CHAN, $Title);
  246. }
  247. break;
  248. case 405:
  249. $Title = '405 Method Not Allowed';
  250. $Message = 'A request method is not supported for the requested resource;
  251. for example, a GET request on a form that requires data to be presented via POST,
  252. or a PUT request on a read-only resource.';
  253. notify($ENV->DEBUG_CHAN, $Title);
  254. break;
  255. case 408:
  256. $Title = '408 Request Timeout';
  257. $Message = 'The server timed out waiting for the request.
  258. According to HTTP specifications:
  259. "The client did not produce a request within the time that the server was prepared to wait.
  260. The client MAY repeat the request without modifications at any later time."';
  261. break;
  262. case 413:
  263. $Title = '413 Payload Too Large';
  264. $Message = 'The request is larger than the server is willing or able to process.';
  265. notify($ENV->DEBUG_CHAN, $Title);
  266. break;
  267. case 429:
  268. $Title = '429 Too Many Requests';
  269. $Message = 'The user has sent too many requests in a given amount of time.';
  270. notify($ENV->DEBUG_CHAN, $Title);
  271. break;
  272. /**
  273. * Server errors
  274. */
  275. case 500:
  276. $Title = '500 Internal Server Error';
  277. $Message = 'A generic error message,
  278. given when an unexpected condition was encountered and no more specific message is suitable.';
  279. break;
  280. case 502:
  281. $Title = '502 Bad Gateway';
  282. $Message = 'The server was acting as a gateway or proxy and received an invalid response from the upstream server.';
  283. notify($ENV->DEBUG_CHAN, $Title);
  284. break;
  285. case 504:
  286. $Title = '504 Gateway Timeout';
  287. $Message = 'The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.';
  288. notify($ENV->DEBUG_CHAN, $Title);
  289. break;
  290. /**
  291. * Gazelle errors
  292. */
  293. case -1:
  294. #case 0: # Matches custom error strings
  295. $Title = 'Invalid Input';
  296. $Message = 'Something was wrong with the input provided with your request, and the server is refusing to fulfill it.';
  297. notify($ENV->DEBUG_CHAN, 'PHP-0');
  298. break;
  299. case '!!':
  300. $Title = 'Unexpected Error';
  301. $Message = 'You have encountered an unexpected error.';
  302. notify($ENV->DEBUG_CHAN, 'unexpected');
  303. break;
  304. default:
  305. $Title = 'Other Error';
  306. $Message = "A function supplied its own error message: $Error";
  307. notify($ENV->DEBUG_CHAN, $Message);
  308. }
  309. # Normalize whitespace before adding features
  310. $Message = preg_replace('/\s{2,}/', ' ', $Message);
  311. /**
  312. * JSON error output
  313. */
  314. /*
  315. if ($JSON) {
  316. print
  317. json_encode(
  318. array(
  319. 'status' => 'error',
  320. 'response' => $Message
  321. )
  322. );
  323. }
  324. */
  325. /**
  326. * Append $Log
  327. * Formerly in sections/error/index.php
  328. */
  329. if ($Log ?? false) {
  330. $Message .= " <a href='log.php?search=$Title'>Search Log</a>";
  331. }
  332. /**
  333. * Append $Debug
  334. */
  335. if ($Debug ?? false) {
  336. $DateTime = strftime('%c', $_SERVER['REQUEST_TIME']);
  337. $BackTrace = debug_string_backtrace();
  338. $Message .= ($NoHTML)
  339. ? $BackTrace
  340. : <<<HTML
  341. <br /><br />
  342. Please include the server response below,
  343. as in a <a href="/staff.php">Staff PM</a>,
  344. to help with debugging.
  345. <pre>
  346. ```
  347. $DateTime
  348. {$_SERVER['SERVER_PROTOCOL']} {$_SERVER['REQUEST_METHOD']} $Title
  349. {$_SERVER['SCRIPT_FILENAME']}
  350. {$_SERVER['REQUEST_URI']}
  351. $BackTrace
  352. ```
  353. </pre>
  354. HTML;
  355. }
  356. /**
  357. * Display HTML
  358. * Formerly in sections/error/index.php
  359. */
  360. if (empty($NoHTML)) {
  361. View::show_header($Title);
  362. echo $HTML = <<<HTML
  363. <div>
  364. <h2 class="header">$Title</h2>
  365. <div class="box pad">
  366. <p>$Message</p>
  367. </div>
  368. </div>
  369. HTML;
  370. View::show_footer();
  371. }
  372. # Trigger the error
  373. global $Debug;
  374. $Debug->profile();
  375. trigger_error("$Title - $Message", E_USER_ERROR);
  376. throw new Exception("$Title - $Message");
  377. }
  378. /**
  379. * debug_string_backtrace()
  380. * https://stackoverflow.com/a/7039409
  381. */
  382. function debug_string_backtrace()
  383. {
  384. $e = new Exception;
  385. return $e->getTraceAsString();
  386. }
  387. /**
  388. * Convenience function. See doc in permissions.class.php
  389. */
  390. function check_perms($PermissionName, $MinClass = 0)
  391. {
  392. return Permissions::check_perms($PermissionName, $MinClass);
  393. }
  394. /**
  395. * get_permissions_for_user()
  396. */
  397. function get_permissions_for_user($UserID, $CustomPermissions = false)
  398. {
  399. return Permissions::get_permissions_for_user($UserID, $CustomPermissions = false);
  400. }
  401. /**
  402. * Print the site's URL including the appropriate URI scheme, including the trailing slash
  403. */
  404. function site_url()
  405. {
  406. return 'https://' . SITE_DOMAIN . '/';
  407. }
  408. # End OT/Bio Gazelle util.php
  409. /**
  410. * OPS JSON functions
  411. * @see https://github.com/OPSnet/Gazelle/blob/master/classes/util.php
  412. */
  413. /**
  414. * Print JSON status result with an optional message and die.
  415. */
  416. function json_die($Status, $Message = 'bad parameters')
  417. {
  418. json_print($Status, $Message);
  419. die();
  420. }
  421. /**
  422. * Print JSON status result with an optional message.
  423. */
  424. function json_print($Status, $Message)
  425. {
  426. if ($Status === 'success' && $Message) {
  427. $response = ['status' => $Status, 'response' => $Message];
  428. } elseif ($Message) {
  429. $response = ['status' => $Status, 'error' => $Message];
  430. } else {
  431. $response = ['status' => $Status, 'response' => []];
  432. }
  433. print(json_encode(add_json_info($response)));
  434. }
  435. /**
  436. * json_error
  437. */
  438. function json_error($Code)
  439. {
  440. echo json_encode(
  441. add_json_info(
  442. [
  443. 'status' => 'failure',
  444. 'error' => $Code,
  445. 'response' => []
  446. ]
  447. )
  448. );
  449. die();
  450. }
  451. /**
  452. * json_or_error
  453. */
  454. function json_or_error($JsonError, $Error = null, $NoHTML = false)
  455. {
  456. if (defined('AJAX')) {
  457. json_error($JsonError);
  458. } else {
  459. error($Error ?? $JsonError, $NoHTML);
  460. }
  461. }
  462. /**
  463. * add_json_info
  464. */
  465. function add_json_info($Json)
  466. {
  467. $ENV = ENV::go();
  468. if (!isset($Json['info'])) {
  469. $Json = array_merge($Json, [
  470. 'info' => [
  471. 'source' => $ENV->SITE_NAME,
  472. 'version' => 1,
  473. ],
  474. ]);
  475. }
  476. if (!isset($Json['debug']) && check_perms('site_debug')) {
  477. /** @var DEBUG $Debug */
  478. global $Debug;
  479. $Json = array_merge($Json, [
  480. 'debug' => [
  481. 'queries' => $Debug->get_queries(),
  482. 'searches' => $Debug->get_sphinxql_queries()
  483. ],
  484. ]);
  485. }
  486. return $Json;
  487. }
  488. # End OPS JSON functions
  489. # Start OPS misc functions
  490. /**
  491. * Hydrate an array from a query string (everything that follow '?')
  492. * This reimplements parse_str() and side-steps the issue of max_input_vars limits.
  493. *
  494. * Example:
  495. * in: li[]=14&li[]=31&li[]=58&li[]=68&li[]=69&li[]=54&li[]=5, param=li[]
  496. * parsed: ['li[]' => ['14', '31, '58', '68', '69', '5']]
  497. * out: ['14', '31, '58', '68', '69', '5']
  498. *
  499. * @param string query string from url
  500. * @param string url param to extract
  501. * @return array hydrated equivalent
  502. */
  503. function parseUrlArgs(string $urlArgs, string $param): array
  504. {
  505. $list = [];
  506. $pairs = explode('&', $urlArgs);
  507. foreach ($pairs as $p) {
  508. [$name, $value] = explode('=', $p, 2);
  509. if (!isset($list[$name])) {
  510. $list[$name] = $value;
  511. } else {
  512. if (!is_array($list[$name])) {
  513. $list[$name] = [$list[$name]];
  514. }
  515. $list[$name][] = $value;
  516. }
  517. }
  518. return array_key_exists($param, $list) ? $list[$param] : [];
  519. }
  520. /**
  521. * base64UrlEncode
  522. * base64UrlDecode
  523. * @see https://github.com/OPSnet/Gazelle/blob/master/app/Util/Text.php
  524. */
  525. function base64UrlEncode($data)
  526. {
  527. return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
  528. }
  529. function base64UrlDecode($data)
  530. {
  531. return base64_decode(str_pad(
  532. strtr($data, '-_', '+/'),
  533. strlen($data) % 4,
  534. '=',
  535. STR_PAD_RIGHT
  536. ));
  537. }
  538. /**
  539. * log_token_attempt
  540. * @see https://github.com/OPSnet/Gazelle/blob/master/classes/util.php
  541. */
  542. // TODO: reconcile this with log_attempt in login/index.php
  543. function log_token_attempt(DB_MYSQL $db, int $userId = 0): void
  544. {
  545. $watch = new LoginWatch;
  546. $ipStr = $_SERVER['REMOTE_ADDR'];
  547. [$attemptId, $attempts, $bans] = $db->row(
  548. '
  549. SELECT ID, Attempts, Bans
  550. FROM login_attempts
  551. WHERE IP = ?
  552. ',
  553. $_SERVER['REMOTE_ADDR']
  554. );
  555. if (!$attemptId) {
  556. $watch->create($ipStr, null, $userId);
  557. return;
  558. }
  559. $attempts++;
  560. $watch->setWatch($attemptId);
  561. if ($attempts < 6) {
  562. $watch->increment($userId, $ipStr, null);
  563. return;
  564. }
  565. $watch->ban($attempts, null, $userId);
  566. if ($bans > 9) {
  567. (new IPv4())->createBan(0, $ipStr, $ipStr, 'Automated ban per failed token usage');
  568. }
  569. }