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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  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. . Tools::geoip($_SERVER['REMOTE_ADDR'])
  177. . ")")
  178. . " accessing https://"
  179. . SITE_DOMAIN
  180. . ""
  181. . $_SERVER['REQUEST_URI']
  182. . (!empty($_SERVER['HTTP_REFERER']) ? " from "
  183. . $_SERVER['HTTP_REFERER'] : '')
  184. );
  185. }
  186. /**
  187. * Advanced error handling
  188. *
  189. * Displays an HTTP status code with description and triggers an error.
  190. * If you use your own string for $Error, it becomes the error description.
  191. *
  192. * @param int|string $Error Error type
  193. * The available HTTP status codes are
  194. * - Client: [ 400, 403, 404, 405, 408, 413, 429 ]
  195. * - Server: [ 500, 502, 504 ]
  196. * - Gazelle: [ -1, 0, !! ]
  197. *
  198. * @param boolean $NoHTML If true, the header/footer won't be shown, just the error.
  199. * @param string $Log If true, the user is given a link to search $Log in the site log.
  200. * @param boolean $Debug If true, print bug reporting instructions and a stack trace.
  201. * @param boolean $JSON If true, print the error as a JSON response.
  202. */
  203. function error($Error = 1, $NoHTML = false, $Log = false, $Debug = true) # , $JSON = false)
  204. {
  205. $ENV = ENV::go();
  206. # Error out on erroneous $Error
  207. (!$Error || $Error === null)
  208. ?? trigger_error('No $Error.', E_USER_ERROR);
  209. (!is_int($Error) || !is_string($Error))
  210. ?? trigger_error('$Error must be int or string.', E_USER_ERROR);
  211. # Formerly in sections/error/index.php
  212. if (!empty($_GET['e']) && is_int($_GET['e'])) {
  213. # Request error, i.e., /nonexistent_page.php
  214. $Error = $_GET['e'];
  215. }
  216. # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
  217. switch ($Error) {
  218. /**
  219. * Client errors
  220. */
  221. case 400:
  222. case 1: # Probably the user's fault
  223. $Title = '400 Bad Request';
  224. $Message = 'The server cannot or will not process the request due to an apparent client error
  225. (e.g., malformed request syntax, size too large, invalid request message framing, or deceptive request routing).';
  226. break;
  227. case 403:
  228. $Title = '403 Forbidden';
  229. $Message = 'The request contained valid data and was understood by the server, but the server is refusing action.
  230. 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
  231. (e.g., creating a duplicate record where only one is allowed).
  232. The request should not be repeated.';
  233. if (substr($_SERVER['REQUEST_URI'], 0, 9) !== '/static/') {
  234. notify($ENV->DEBUG_CHAN, $Title);
  235. }
  236. break;
  237. case 404:
  238. $Title = '404 Not Found';
  239. $Message = 'The requested resource could not be found but may be available in the future.
  240. Subsequent requests by the client are permissible.';
  241. // Hide alerts for missing images and static requests
  242. if (!preg_match(
  243. "/\.(ico|jpg|jpeg|gif|png)$/",
  244. $_SERVER['REQUEST_URI']
  245. ) && substr($_SERVER['REQUEST_URI'], 0, 9) !== '/static/') {
  246. notify($ENV->DEBUG_CHAN, $Title);
  247. }
  248. break;
  249. case 405:
  250. $Title = '405 Method Not Allowed';
  251. $Message = 'A request method is not supported for the requested resource;
  252. for example, a GET request on a form that requires data to be presented via POST,
  253. or a PUT request on a read-only resource.';
  254. notify($ENV->DEBUG_CHAN, $Title);
  255. break;
  256. case 408:
  257. $Title = '408 Request Timeout';
  258. $Message = 'The server timed out waiting for the request.
  259. According to HTTP specifications:
  260. "The client did not produce a request within the time that the server was prepared to wait.
  261. The client MAY repeat the request without modifications at any later time."';
  262. break;
  263. case 413:
  264. $Title = '413 Payload Too Large';
  265. $Message = 'The request is larger than the server is willing or able to process.';
  266. notify($ENV->DEBUG_CHAN, $Title);
  267. break;
  268. case 429:
  269. $Title = '429 Too Many Requests';
  270. $Message = 'The user has sent too many requests in a given amount of time.';
  271. notify($ENV->DEBUG_CHAN, $Title);
  272. break;
  273. /**
  274. * Server errors
  275. */
  276. case 500:
  277. $Title = '500 Internal Server Error';
  278. $Message = 'A generic error message,
  279. given when an unexpected condition was encountered and no more specific message is suitable.';
  280. break;
  281. case 502:
  282. $Title = '502 Bad Gateway';
  283. $Message = 'The server was acting as a gateway or proxy and received an invalid response from the upstream server.';
  284. notify($ENV->DEBUG_CHAN, $Title);
  285. break;
  286. case 504:
  287. $Title = '504 Gateway Timeout';
  288. $Message = 'The server was acting as a gateway or proxy and did not receive a timely response from the upstream server.';
  289. notify($ENV->DEBUG_CHAN, $Title);
  290. break;
  291. /**
  292. * Gazelle errors
  293. */
  294. case -1:
  295. #case 0: # Matches custom error strings
  296. $Title = 'Invalid Input';
  297. $Message = 'Something was wrong with the input provided with your request, and the server is refusing to fulfill it.';
  298. notify($ENV->DEBUG_CHAN, 'PHP-0');
  299. break;
  300. case '!!':
  301. $Title = 'Unexpected Error';
  302. $Message = 'You have encountered an unexpected error.';
  303. notify($ENV->DEBUG_CHAN, 'unexpected');
  304. break;
  305. default:
  306. $Title = 'Other Error';
  307. $Message = "A function supplied its own error message: $Error";
  308. notify($ENV->DEBUG_CHAN, $Message);
  309. }
  310. # Normalize whitespace before adding features
  311. $Message = preg_replace('/\s{2,}/', ' ', $Message);
  312. /**
  313. * JSON error output
  314. */
  315. /*
  316. if ($JSON) {
  317. print
  318. json_encode(
  319. array(
  320. 'status' => 'error',
  321. 'response' => $Message
  322. )
  323. );
  324. }
  325. */
  326. /**
  327. * Append $Log
  328. * Formerly in sections/error/index.php
  329. */
  330. if ($Log ?? false) {
  331. $Message .= " <a href='log.php?search=$Title'>Search Log</a>";
  332. }
  333. /**
  334. * Append $Debug
  335. */
  336. if ($Debug ?? false) {
  337. $DateTime = strftime('%c', $_SERVER['REQUEST_TIME']);
  338. $BackTrace = debug_string_backtrace();
  339. $Message .= ($NoHTML)
  340. ? $BackTrace
  341. : <<<HTML
  342. <br /><br />
  343. Please include the server response below,
  344. as in a <a href="/staff.php">Staff PM</a>,
  345. to help with debugging.
  346. <pre>
  347. ```
  348. $DateTime
  349. {$_SERVER['SERVER_PROTOCOL']} {$_SERVER['REQUEST_METHOD']} $Title
  350. {$_SERVER['SCRIPT_FILENAME']}
  351. {$_SERVER['REQUEST_URI']}
  352. $BackTrace
  353. ```
  354. </pre>
  355. HTML;
  356. }
  357. /**
  358. * Display HTML
  359. * Formerly in sections/error/index.php
  360. */
  361. if (empty($NoHTML)) {
  362. View::show_header($Title);
  363. echo $HTML = <<<HTML
  364. <div>
  365. <h2 class="header">$Title</h2>
  366. <div class="box pad">
  367. <p>$Message</p>
  368. </div>
  369. </div>
  370. HTML;
  371. View::show_footer();
  372. }
  373. # Trigger the error
  374. global $Debug;
  375. $Debug->profile();
  376. trigger_error("$Title - $Message", E_USER_ERROR);
  377. throw new Exception("$Title - $Message");
  378. }
  379. /**
  380. * debug_string_backtrace()
  381. * https://stackoverflow.com/a/7039409
  382. */
  383. function debug_string_backtrace()
  384. {
  385. $e = new Exception;
  386. return $e->getTraceAsString();
  387. }
  388. /**
  389. * Convenience function. See doc in permissions.class.php
  390. */
  391. function check_perms($PermissionName, $MinClass = 0)
  392. {
  393. return Permissions::check_perms($PermissionName, $MinClass);
  394. }
  395. /**
  396. * get_permissions_for_user()
  397. */
  398. function get_permissions_for_user($UserID, $CustomPermissions = false)
  399. {
  400. return Permissions::get_permissions_for_user($UserID, $CustomPermissions = false);
  401. }
  402. /**
  403. * Print the site's URL including the appropriate URI scheme, including the trailing slash
  404. */
  405. function site_url()
  406. {
  407. return 'https://' . SITE_DOMAIN . '/';
  408. }
  409. # End OT/Bio Gazelle util.php
  410. /**
  411. * OPS JSON functions
  412. * @see https://github.com/OPSnet/Gazelle/blob/master/classes/util.php
  413. */
  414. /**
  415. * Print JSON status result with an optional message and die.
  416. */
  417. function json_die($Status, $Message = 'bad parameters')
  418. {
  419. json_print($Status, $Message);
  420. die();
  421. }
  422. /**
  423. * Print JSON status result with an optional message.
  424. */
  425. function json_print($Status, $Message)
  426. {
  427. if ($Status === 'success' && $Message) {
  428. $response = ['status' => $Status, 'response' => $Message];
  429. } elseif ($Message) {
  430. $response = ['status' => $Status, 'error' => $Message];
  431. } else {
  432. $response = ['status' => $Status, 'response' => []];
  433. }
  434. print(json_encode(add_json_info($response)));
  435. }
  436. /**
  437. * json_error
  438. */
  439. function json_error($Code)
  440. {
  441. echo json_encode(add_json_info(['status' => 'failure', 'error' => $Code, 'response' => []]));
  442. die();
  443. }
  444. /**
  445. * json_or_error
  446. */
  447. function json_or_error($JsonError, $Error = null, $NoHTML = false)
  448. {
  449. if (defined('AJAX')) {
  450. json_error($JsonError);
  451. } else {
  452. error($Error ?? $JsonError, $NoHTML);
  453. }
  454. }
  455. /**
  456. * add_json_info
  457. */
  458. function add_json_info($Json)
  459. {
  460. $ENV = ENV::go();
  461. if (!isset($Json['info'])) {
  462. $Json = array_merge($Json, [
  463. 'info' => [
  464. 'source' => $ENV->SITE_NAME,
  465. 'version' => 1,
  466. ],
  467. ]);
  468. }
  469. if (!isset($Json['debug']) && check_perms('site_debug')) {
  470. /** @var DEBUG $Debug */
  471. global $Debug;
  472. $Json = array_merge($Json, [
  473. 'debug' => [
  474. 'queries' => $Debug->get_queries(),
  475. 'searches' => $Debug->get_sphinxql_queries()
  476. ],
  477. ]);
  478. }
  479. return $Json;
  480. }
  481. # End OPS JSON functions
  482. # Start OPS misc functions
  483. /**
  484. * Hydrate an array from a query string (everything that follow '?')
  485. * This reimplements parse_str() and side-steps the issue of max_input_vars limits.
  486. *
  487. * Example:
  488. * in: li[]=14&li[]=31&li[]=58&li[]=68&li[]=69&li[]=54&li[]=5, param=li[]
  489. * parsed: ['li[]' => ['14', '31, '58', '68', '69', '5']]
  490. * out: ['14', '31, '58', '68', '69', '5']
  491. *
  492. * @param string query string from url
  493. * @param string url param to extract
  494. * @return array hydrated equivalent
  495. */
  496. function parseUrlArgs(string $urlArgs, string $param): array
  497. {
  498. $list = [];
  499. $pairs = explode('&', $urlArgs);
  500. foreach ($pairs as $p) {
  501. [$name, $value] = explode('=', $p, 2);
  502. if (!isset($list[$name])) {
  503. $list[$name] = $value;
  504. } else {
  505. if (!is_array($list[$name])) {
  506. $list[$name] = [$list[$name]];
  507. }
  508. $list[$name][] = $value;
  509. }
  510. }
  511. return array_key_exists($param, $list) ? $list[$param] : [];
  512. }
  513. /**
  514. * base64UrlEncode
  515. * base64UrlDecode
  516. * @see https://github.com/OPSnet/Gazelle/blob/master/app/Util/Text.php
  517. */
  518. function base64UrlEncode($data)
  519. {
  520. return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
  521. }
  522. function base64UrlDecode($data)
  523. {
  524. return base64_decode(str_pad(
  525. strtr($data, '-_', '+/'),
  526. strlen($data) % 4,
  527. '=',
  528. STR_PAD_RIGHT
  529. ));
  530. }
  531. /**
  532. * log_token_attempt
  533. * @see https://github.com/OPSnet/Gazelle/blob/master/classes/util.php
  534. */
  535. // TODO: reconcile this with log_attempt in login/index.php
  536. function log_token_attempt(DB_MYSQL $db, int $userId = 0): void
  537. {
  538. $watch = new LoginWatch;
  539. $ipStr = $_SERVER['REMOTE_ADDR'];
  540. [$attemptId, $attempts, $bans] = $db->row(
  541. '
  542. SELECT ID, Attempts, Bans
  543. FROM login_attempts
  544. WHERE IP = ?
  545. ',
  546. $_SERVER['REMOTE_ADDR']
  547. );
  548. if (!$attemptId) {
  549. $watch->create($ipStr, null, $userId);
  550. return;
  551. }
  552. $attempts++;
  553. $watch->setWatch($attemptId);
  554. if ($attempts < 6) {
  555. $watch->increment($userId, $ipStr, null);
  556. return;
  557. }
  558. $watch->ban($attempts, null, $userId);
  559. if ($bans > 9) {
  560. (new IPv4())->createBan(0, $ipStr, $ipStr, 'Automated ban per failed token usage');
  561. }
  562. }