Core.php 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\Http\ServerRequest;
  5. use Symfony\Component\Config\FileLocator;
  6. use Symfony\Component\DependencyInjection\ContainerBuilder;
  7. use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
  8. use function __;
  9. use function array_keys;
  10. use function array_pop;
  11. use function array_walk_recursive;
  12. use function chr;
  13. use function count;
  14. use function defined;
  15. use function explode;
  16. use function filter_var;
  17. use function function_exists;
  18. use function getenv;
  19. use function gmdate;
  20. use function hash_equals;
  21. use function hash_hmac;
  22. use function header;
  23. use function header_remove;
  24. use function htmlspecialchars;
  25. use function http_build_query;
  26. use function in_array;
  27. use function intval;
  28. use function is_array;
  29. use function is_string;
  30. use function json_decode;
  31. use function json_encode;
  32. use function mb_strlen;
  33. use function mb_strpos;
  34. use function mb_substr;
  35. use function parse_str;
  36. use function parse_url;
  37. use function preg_match;
  38. use function preg_replace;
  39. use function session_write_close;
  40. use function sprintf;
  41. use function str_replace;
  42. use function strlen;
  43. use function strpos;
  44. use function strtolower;
  45. use function strtr;
  46. use function substr;
  47. use function trigger_error;
  48. use function unserialize;
  49. use function urldecode;
  50. use function vsprintf;
  51. use const DATE_RFC1123;
  52. use const E_USER_ERROR;
  53. use const E_USER_WARNING;
  54. use const FILTER_VALIDATE_IP;
  55. /**
  56. * Core functions used all over the scripts.
  57. */
  58. class Core
  59. {
  60. /**
  61. * Removes insecure parts in a path; used before include() or
  62. * require() when a part of the path comes from an insecure source
  63. * like a cookie or form.
  64. *
  65. * @param string $path The path to check
  66. */
  67. public static function securePath(string $path): string
  68. {
  69. // change .. to .
  70. return (string) preg_replace('@\.\.*@', '.', $path);
  71. }
  72. /**
  73. * displays the given error message on phpMyAdmin error page in foreign language,
  74. * ends script execution and closes session
  75. *
  76. * loads language file if not loaded already
  77. *
  78. * @param string $error_message the error message or named error message
  79. * @param string|array $message_args arguments applied to $error_message
  80. */
  81. public static function fatalError(
  82. string $error_message,
  83. $message_args = null
  84. ): void {
  85. global $dbi;
  86. /* Use format string if applicable */
  87. if (is_string($message_args)) {
  88. $error_message = sprintf($error_message, $message_args);
  89. } elseif (is_array($message_args)) {
  90. $error_message = vsprintf($error_message, $message_args);
  91. }
  92. /*
  93. * Avoid using Response class as config does not have to be loaded yet
  94. * (this can happen on early fatal error)
  95. */
  96. if (
  97. isset($dbi, $GLOBALS['config'])
  98. && $GLOBALS['config']->get('is_setup') === false
  99. && ResponseRenderer::getInstance()->isAjax()
  100. ) {
  101. $response = ResponseRenderer::getInstance();
  102. $response->setRequestStatus(false);
  103. $response->addJSON('message', Message::error($error_message));
  104. if (! defined('TESTSUITE')) {
  105. exit;
  106. }
  107. return;
  108. }
  109. if (! empty($_REQUEST['ajax_request'])) {
  110. // Generate JSON manually
  111. self::headerJSON();
  112. echo json_encode(
  113. [
  114. 'success' => false,
  115. 'message' => Message::error($error_message)->getDisplay(),
  116. ]
  117. );
  118. if (! defined('TESTSUITE')) {
  119. exit;
  120. }
  121. return;
  122. }
  123. $error_message = strtr($error_message, ['<br>' => '[br]']);
  124. $template = new Template();
  125. echo $template->render('error/generic', [
  126. 'lang' => $GLOBALS['lang'] ?? 'en',
  127. 'dir' => $GLOBALS['text_dir'] ?? 'ltr',
  128. 'error_message' => Sanitize::sanitizeMessage($error_message),
  129. ]);
  130. if (! defined('TESTSUITE')) {
  131. exit;
  132. }
  133. }
  134. /**
  135. * Returns a link to the PHP documentation
  136. *
  137. * @param string $target anchor in documentation
  138. *
  139. * @return string the URL
  140. */
  141. public static function getPHPDocLink(string $target): string
  142. {
  143. /* List of PHP documentation translations */
  144. $php_doc_languages = [
  145. 'pt_BR',
  146. 'zh_CN',
  147. 'fr',
  148. 'de',
  149. 'ja',
  150. 'ru',
  151. 'es',
  152. 'tr',
  153. ];
  154. $lang = 'en';
  155. if (isset($GLOBALS['lang']) && in_array($GLOBALS['lang'], $php_doc_languages)) {
  156. if ($GLOBALS['lang'] === 'zh_CN') {
  157. $lang = 'zh';
  158. } else {
  159. $lang = $GLOBALS['lang'];
  160. }
  161. }
  162. return self::linkURL('https://www.php.net/manual/' . $lang . '/' . $target);
  163. }
  164. /**
  165. * Warn or fail on missing extension.
  166. *
  167. * @param string $extension Extension name
  168. * @param bool $fatal Whether the error is fatal.
  169. * @param string $extra Extra string to append to message.
  170. */
  171. public static function warnMissingExtension(
  172. string $extension,
  173. bool $fatal = false,
  174. string $extra = ''
  175. ): void {
  176. global $errorHandler;
  177. $message = 'The %s extension is missing. Please check your PHP configuration.';
  178. /* Gettext does not have to be loaded yet here */
  179. if (function_exists('__')) {
  180. $message = __('The %s extension is missing. Please check your PHP configuration.');
  181. }
  182. $doclink = self::getPHPDocLink('book.' . $extension . '.php');
  183. $message = sprintf($message, '[a@' . $doclink . '@Documentation][em]' . $extension . '[/em][/a]');
  184. if ($extra != '') {
  185. $message .= ' ' . $extra;
  186. }
  187. if ($fatal) {
  188. self::fatalError($message);
  189. return;
  190. }
  191. $errorHandler->addError($message, E_USER_WARNING, '', 0, false);
  192. }
  193. /**
  194. * returns count of tables in given db
  195. *
  196. * @param string $db database to count tables for
  197. *
  198. * @return int count of tables in $db
  199. */
  200. public static function getTableCount(string $db): int
  201. {
  202. global $dbi;
  203. $tables = $dbi->tryQuery('SHOW TABLES FROM ' . Util::backquote($db) . ';');
  204. if ($tables) {
  205. return $tables->numRows();
  206. }
  207. return 0;
  208. }
  209. /**
  210. * Converts numbers like 10M into bytes
  211. * Used with permission from Moodle (https://moodle.org) by Martin Dougiamas
  212. * (renamed with PMA prefix to avoid double definition when embedded
  213. * in Moodle)
  214. *
  215. * @param string|int $size size (Default = 0)
  216. */
  217. public static function getRealSize($size = 0): int
  218. {
  219. if (! $size) {
  220. return 0;
  221. }
  222. $binaryprefixes = [
  223. 'T' => 1099511627776,
  224. 't' => 1099511627776,
  225. 'G' => 1073741824,
  226. 'g' => 1073741824,
  227. 'M' => 1048576,
  228. 'm' => 1048576,
  229. 'K' => 1024,
  230. 'k' => 1024,
  231. ];
  232. if (preg_match('/^([0-9]+)([KMGT])/i', (string) $size, $matches)) {
  233. return (int) ($matches[1] * $binaryprefixes[$matches[2]]);
  234. }
  235. return (int) $size;
  236. }
  237. /**
  238. * Checks given $page against given $allowList and returns true if valid
  239. * it optionally ignores query parameters in $page (script.php?ignored)
  240. *
  241. * @param string $page page to check
  242. * @param array $allowList allow list to check page against
  243. * @param bool $include whether the page is going to be included
  244. */
  245. public static function checkPageValidity(&$page, array $allowList = [], $include = false): bool
  246. {
  247. if (empty($allowList)) {
  248. $allowList = ['index.php'];
  249. }
  250. if (empty($page)) {
  251. return false;
  252. }
  253. if (in_array($page, $allowList)) {
  254. return true;
  255. }
  256. if ($include) {
  257. return false;
  258. }
  259. $_page = mb_substr(
  260. $page,
  261. 0,
  262. (int) mb_strpos($page . '?', '?')
  263. );
  264. if (in_array($_page, $allowList)) {
  265. return true;
  266. }
  267. $_page = urldecode($page);
  268. $_page = mb_substr(
  269. $_page,
  270. 0,
  271. (int) mb_strpos($_page . '?', '?')
  272. );
  273. return in_array($_page, $allowList);
  274. }
  275. /**
  276. * tries to find the value for the given environment variable name
  277. *
  278. * searches in $_SERVER, $_ENV then tries getenv() and apache_getenv()
  279. * in this order
  280. *
  281. * @param string $var_name variable name
  282. *
  283. * @return string value of $var or empty string
  284. */
  285. public static function getenv(string $var_name): string
  286. {
  287. if (isset($_SERVER[$var_name])) {
  288. return (string) $_SERVER[$var_name];
  289. }
  290. if (isset($_ENV[$var_name])) {
  291. return (string) $_ENV[$var_name];
  292. }
  293. if (getenv($var_name)) {
  294. return (string) getenv($var_name);
  295. }
  296. if (function_exists('apache_getenv') && apache_getenv($var_name, true)) {
  297. return (string) apache_getenv($var_name, true);
  298. }
  299. return '';
  300. }
  301. /**
  302. * Send HTTP header, taking IIS limits into account (600 seems ok)
  303. *
  304. * @param string $uri the header to send
  305. * @param bool $use_refresh whether to use Refresh: header when running on IIS
  306. */
  307. public static function sendHeaderLocation(string $uri, bool $use_refresh = false): void
  308. {
  309. if ($GLOBALS['config']->get('PMA_IS_IIS') && mb_strlen($uri) > 600) {
  310. ResponseRenderer::getInstance()->disable();
  311. $template = new Template();
  312. echo $template->render('header_location', ['uri' => $uri]);
  313. return;
  314. }
  315. /*
  316. * Avoid relative path redirect problems in case user entered URL
  317. * like /phpmyadmin/index.php/ which some web servers happily accept.
  318. */
  319. if ($uri[0] === '.') {
  320. $uri = $GLOBALS['config']->getRootPath() . substr($uri, 2);
  321. }
  322. $response = ResponseRenderer::getInstance();
  323. session_write_close();
  324. if ($response->headersSent()) {
  325. trigger_error('Core::sendHeaderLocation called when headers are already sent!', E_USER_ERROR);
  326. }
  327. // bug #1523784: IE6 does not like 'Refresh: 0', it
  328. // results in a blank page
  329. // but we need it when coming from the cookie login panel)
  330. if ($GLOBALS['config']->get('PMA_IS_IIS') && $use_refresh) {
  331. $response->header('Refresh: 0; ' . $uri);
  332. return;
  333. }
  334. $response->header('Location: ' . $uri);
  335. }
  336. /**
  337. * Outputs application/json headers. This includes no caching.
  338. */
  339. public static function headerJSON(): void
  340. {
  341. if (defined('TESTSUITE')) {
  342. return;
  343. }
  344. // No caching
  345. $headers = self::getNoCacheHeaders();
  346. // Media type
  347. $headers['Content-Type'] = 'application/json; charset=UTF-8';
  348. /**
  349. * Disable content sniffing in browser.
  350. * This is needed in case we include HTML in JSON, browser might assume it's html to display.
  351. */
  352. $headers['X-Content-Type-Options'] = 'nosniff';
  353. foreach ($headers as $name => $value) {
  354. header(sprintf('%s: %s', $name, $value));
  355. }
  356. }
  357. /**
  358. * Outputs headers to prevent caching in browser (and on the way).
  359. */
  360. public static function noCacheHeader(): void
  361. {
  362. if (defined('TESTSUITE')) {
  363. return;
  364. }
  365. $headers = self::getNoCacheHeaders();
  366. foreach ($headers as $name => $value) {
  367. header(sprintf('%s: %s', $name, $value));
  368. }
  369. }
  370. /**
  371. * @return array<string, string>
  372. */
  373. public static function getNoCacheHeaders(): array
  374. {
  375. $headers = [];
  376. $date = (string) gmdate(DATE_RFC1123);
  377. // rfc2616 - Section 14.21
  378. $headers['Expires'] = $date;
  379. // HTTP/1.1
  380. $headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0';
  381. // HTTP/1.0
  382. $headers['Pragma'] = 'no-cache';
  383. // test case: exporting a database into a .gz file with Safari
  384. // would produce files not having the current time
  385. // (added this header for Safari but should not harm other browsers)
  386. $headers['Last-Modified'] = $date;
  387. return $headers;
  388. }
  389. /**
  390. * Sends header indicating file download.
  391. *
  392. * @param string $filename Filename to include in headers if empty,
  393. * none Content-Disposition header will be sent.
  394. * @param string $mimetype MIME type to include in headers.
  395. * @param int $length Length of content (optional)
  396. * @param bool $no_cache Whether to include no-caching headers.
  397. */
  398. public static function downloadHeader(
  399. string $filename,
  400. string $mimetype,
  401. int $length = 0,
  402. bool $no_cache = true
  403. ): void {
  404. $headers = [];
  405. if ($no_cache) {
  406. $headers = self::getNoCacheHeaders();
  407. }
  408. /* Replace all possibly dangerous chars in filename */
  409. $filename = Sanitize::sanitizeFilename($filename);
  410. if (! empty($filename)) {
  411. $headers['Content-Description'] = 'File Transfer';
  412. $headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
  413. }
  414. $headers['Content-Type'] = $mimetype;
  415. // The default output in PMA uses gzip,
  416. // so if we want to output uncompressed file, we should reset the encoding.
  417. // See PHP bug https://github.com/php/php-src/issues/8218
  418. header_remove('Content-Encoding');
  419. $headers['Content-Transfer-Encoding'] = 'binary';
  420. if ($length > 0) {
  421. $headers['Content-Length'] = (string) $length;
  422. }
  423. foreach ($headers as $name => $value) {
  424. header(sprintf('%s: %s', $name, $value));
  425. }
  426. }
  427. /**
  428. * Returns value of an element in $array given by $path.
  429. * $path is a string describing position of an element in an associative array,
  430. * eg. Servers/1/host refers to $array[Servers][1][host]
  431. *
  432. * @param string $path path in the array
  433. * @param array $array the array
  434. * @param mixed $default default value
  435. *
  436. * @return array|mixed|null array element or $default
  437. */
  438. public static function arrayRead(string $path, array $array, $default = null)
  439. {
  440. $keys = explode('/', $path);
  441. $value =& $array;
  442. foreach ($keys as $key) {
  443. if (! isset($value[$key])) {
  444. return $default;
  445. }
  446. $value =& $value[$key];
  447. }
  448. return $value;
  449. }
  450. /**
  451. * Stores value in an array
  452. *
  453. * @param string $path path in the array
  454. * @param array $array the array
  455. * @param mixed $value value to store
  456. */
  457. public static function arrayWrite(string $path, array &$array, $value): void
  458. {
  459. $keys = explode('/', $path);
  460. $last_key = array_pop($keys);
  461. $a =& $array;
  462. foreach ($keys as $key) {
  463. if (! isset($a[$key])) {
  464. $a[$key] = [];
  465. }
  466. $a =& $a[$key];
  467. }
  468. $a[$last_key] = $value;
  469. }
  470. /**
  471. * Removes value from an array
  472. *
  473. * @param string $path path in the array
  474. * @param array $array the array
  475. */
  476. public static function arrayRemove(string $path, array &$array): void
  477. {
  478. $keys = explode('/', $path);
  479. $keys_last = array_pop($keys);
  480. $path = [];
  481. $depth = 0;
  482. $path[0] =& $array;
  483. $found = true;
  484. // go as deep as required or possible
  485. foreach ($keys as $key) {
  486. if (! isset($path[$depth][$key])) {
  487. $found = false;
  488. break;
  489. }
  490. $depth++;
  491. $path[$depth] =& $path[$depth - 1][$key];
  492. }
  493. // if element found, remove it
  494. if ($found) {
  495. unset($path[$depth][$keys_last]);
  496. $depth--;
  497. }
  498. // remove empty nested arrays
  499. for (; $depth >= 0; $depth--) {
  500. if (isset($path[$depth + 1]) && count($path[$depth + 1]) !== 0) {
  501. break;
  502. }
  503. unset($path[$depth][$keys[$depth]]);
  504. }
  505. }
  506. /**
  507. * Returns link to (possibly) external site using defined redirector.
  508. *
  509. * @param string $url URL where to go.
  510. *
  511. * @return string URL for a link.
  512. */
  513. public static function linkURL(string $url): string
  514. {
  515. if (! preg_match('#^https?://#', $url)) {
  516. return $url;
  517. }
  518. $params = [];
  519. $params['url'] = $url;
  520. $url = Url::getCommon($params);
  521. //strip off token and such sensitive information. Just keep url.
  522. $arr = parse_url($url);
  523. if (! is_array($arr)) {
  524. $arr = [];
  525. }
  526. parse_str($arr['query'] ?? '', $vars);
  527. $query = http_build_query(['url' => $vars['url']]);
  528. if ($GLOBALS['config'] !== null && $GLOBALS['config']->get('is_setup')) {
  529. return '../url.php?' . $query;
  530. }
  531. return './url.php?' . $query;
  532. }
  533. /**
  534. * Checks whether domain of URL is an allowed domain or not.
  535. * Use only for URLs of external sites.
  536. *
  537. * @param string $url URL of external site.
  538. */
  539. public static function isAllowedDomain(string $url): bool
  540. {
  541. $arr = parse_url($url);
  542. if (! is_array($arr)) {
  543. $arr = [];
  544. }
  545. // We need host to be set
  546. if (! isset($arr['host']) || strlen($arr['host']) == 0) {
  547. return false;
  548. }
  549. // We do not want these to be present
  550. $blocked = [
  551. 'user',
  552. 'pass',
  553. 'port',
  554. ];
  555. foreach ($blocked as $part) {
  556. if (isset($arr[$part]) && strlen((string) $arr[$part]) != 0) {
  557. return false;
  558. }
  559. }
  560. $domain = $arr['host'];
  561. $domainAllowList = [
  562. /* Include current domain */
  563. $_SERVER['SERVER_NAME'],
  564. /* phpMyAdmin domains */
  565. 'wiki.phpmyadmin.net',
  566. 'www.phpmyadmin.net',
  567. 'phpmyadmin.net',
  568. 'demo.phpmyadmin.net',
  569. 'docs.phpmyadmin.net',
  570. /* mysql.com domains */
  571. 'dev.mysql.com',
  572. 'bugs.mysql.com',
  573. /* mariadb domains */
  574. 'mariadb.org',
  575. 'mariadb.com',
  576. /* php.net domains */
  577. 'php.net',
  578. 'www.php.net',
  579. /* Github domains*/
  580. 'github.com',
  581. 'www.github.com',
  582. /* Percona domains */
  583. 'www.percona.com',
  584. /* CVE domain */
  585. 'www.cve.org',
  586. /* Following are doubtful ones. */
  587. 'mysqldatabaseadministration.blogspot.com',
  588. ];
  589. return in_array($domain, $domainAllowList);
  590. }
  591. /**
  592. * Replace some html-unfriendly stuff
  593. *
  594. * @param string $buffer String to process
  595. *
  596. * @return string Escaped and cleaned up text suitable for html
  597. */
  598. public static function mimeDefaultFunction(string $buffer): string
  599. {
  600. $buffer = htmlspecialchars($buffer);
  601. $buffer = str_replace(' ', ' &nbsp;', $buffer);
  602. return (string) preg_replace("@((\015\012)|(\015)|(\012))@", '<br>' . "\n", $buffer);
  603. }
  604. /**
  605. * Displays SQL query before executing.
  606. *
  607. * @param array|string $query_data Array containing queries or query itself
  608. */
  609. public static function previewSQL($query_data): void
  610. {
  611. $retval = '<div class="preview_sql">';
  612. if (empty($query_data)) {
  613. $retval .= __('No change');
  614. } elseif (is_array($query_data)) {
  615. foreach ($query_data as $query) {
  616. $retval .= Html\Generator::formatSql($query);
  617. }
  618. } else {
  619. $retval .= Html\Generator::formatSql($query_data);
  620. }
  621. $retval .= '</div>';
  622. $response = ResponseRenderer::getInstance();
  623. $response->addJSON('sql_data', $retval);
  624. }
  625. /**
  626. * recursively check if variable is empty
  627. *
  628. * @param mixed $value the variable
  629. */
  630. public static function emptyRecursive($value): bool
  631. {
  632. if (is_array($value)) {
  633. $empty = true;
  634. array_walk_recursive(
  635. $value,
  636. /**
  637. * @param mixed $item
  638. */
  639. static function ($item) use (&$empty): void {
  640. $empty = $empty && empty($item);
  641. }
  642. );
  643. return $empty;
  644. }
  645. return empty($value);
  646. }
  647. /**
  648. * Creates some globals from $_POST variables matching a pattern
  649. *
  650. * @param array $post_patterns The patterns to search for
  651. */
  652. public static function setPostAsGlobal(array $post_patterns): void
  653. {
  654. global $containerBuilder;
  655. foreach (array_keys($_POST) as $post_key) {
  656. foreach ($post_patterns as $one_post_pattern) {
  657. if (! preg_match($one_post_pattern, $post_key)) {
  658. continue;
  659. }
  660. $GLOBALS[$post_key] = $_POST[$post_key];
  661. $containerBuilder->setParameter($post_key, $GLOBALS[$post_key]);
  662. }
  663. }
  664. }
  665. /**
  666. * Gets the "true" IP address of the current user
  667. *
  668. * @return string|bool the ip of the user
  669. */
  670. public static function getIp()
  671. {
  672. /* Get the address of user */
  673. if (empty($_SERVER['REMOTE_ADDR'])) {
  674. /* We do not know remote IP */
  675. return false;
  676. }
  677. $direct_ip = $_SERVER['REMOTE_ADDR'];
  678. /* Do we trust this IP as a proxy? If yes we will use it's header. */
  679. if (! isset($GLOBALS['cfg']['TrustedProxies'][$direct_ip])) {
  680. /* Return true IP */
  681. return $direct_ip;
  682. }
  683. /**
  684. * Parse header in form:
  685. * X-Forwarded-For: client, proxy1, proxy2
  686. */
  687. // Get header content
  688. $value = self::getenv($GLOBALS['cfg']['TrustedProxies'][$direct_ip]);
  689. // Grab first element what is client adddress
  690. $value = explode(',', $value)[0];
  691. // checks that the header contains only one IP address,
  692. $is_ip = filter_var($value, FILTER_VALIDATE_IP);
  693. if ($is_ip !== false) {
  694. // True IP behind a proxy
  695. return $value;
  696. }
  697. // We could not parse header
  698. return false;
  699. }
  700. /**
  701. * Sanitizes MySQL hostname
  702. *
  703. * * strips p: prefix(es)
  704. *
  705. * @param string $name User given hostname
  706. */
  707. public static function sanitizeMySQLHost(string $name): string
  708. {
  709. while (strtolower(substr($name, 0, 2)) === 'p:') {
  710. $name = substr($name, 2);
  711. }
  712. return $name;
  713. }
  714. /**
  715. * Sanitizes MySQL username
  716. *
  717. * * strips part behind null byte
  718. *
  719. * @param string $name User given username
  720. */
  721. public static function sanitizeMySQLUser(string $name): string
  722. {
  723. $position = strpos($name, chr(0));
  724. if ($position !== false) {
  725. return substr($name, 0, $position);
  726. }
  727. return $name;
  728. }
  729. /**
  730. * Safe unserializer wrapper
  731. *
  732. * It does not unserialize data containing objects
  733. *
  734. * @param string $data Data to unserialize
  735. *
  736. * @return mixed|null
  737. */
  738. public static function safeUnserialize(string $data)
  739. {
  740. /* validate serialized data */
  741. $length = strlen($data);
  742. $depth = 0;
  743. for ($i = 0; $i < $length; $i++) {
  744. $value = $data[$i];
  745. switch ($value) {
  746. case '}':
  747. /* end of array */
  748. if ($depth <= 0) {
  749. return null;
  750. }
  751. $depth--;
  752. break;
  753. case 's':
  754. /* string */
  755. // parse sting length
  756. $strlen = intval(substr($data, $i + 2));
  757. // string start
  758. $i = strpos($data, ':', $i + 2);
  759. if ($i === false) {
  760. return null;
  761. }
  762. // skip string, quotes and ;
  763. $i += 2 + $strlen + 1;
  764. if ($data[$i] !== ';') {
  765. return null;
  766. }
  767. break;
  768. case 'b':
  769. case 'i':
  770. case 'd':
  771. /* bool, integer or double */
  772. // skip value to separator
  773. $i = strpos($data, ';', $i);
  774. if ($i === false) {
  775. return null;
  776. }
  777. break;
  778. case 'a':
  779. /* array */
  780. // find array start
  781. $i = strpos($data, '{', $i);
  782. if ($i === false) {
  783. return null;
  784. }
  785. // remember nesting
  786. $depth++;
  787. break;
  788. case 'N':
  789. /* null */
  790. // skip to end
  791. $i = strpos($data, ';', $i);
  792. if ($i === false) {
  793. return null;
  794. }
  795. break;
  796. default:
  797. /* any other elements are not wanted */
  798. return null;
  799. }
  800. }
  801. // check unterminated arrays
  802. if ($depth > 0) {
  803. return null;
  804. }
  805. return unserialize($data);
  806. }
  807. /**
  808. * Sign the sql query using hmac using the session token
  809. *
  810. * @param string $sqlQuery The sql query
  811. *
  812. * @return string
  813. */
  814. public static function signSqlQuery($sqlQuery)
  815. {
  816. global $cfg;
  817. $secret = $_SESSION[' HMAC_secret '] ?? '';
  818. return hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']);
  819. }
  820. /**
  821. * Check that the sql query has a valid hmac signature
  822. *
  823. * @param string $sqlQuery The sql query
  824. * @param string $signature The Signature to check
  825. */
  826. public static function checkSqlQuerySignature($sqlQuery, $signature): bool
  827. {
  828. global $cfg;
  829. $secret = $_SESSION[' HMAC_secret '] ?? '';
  830. $hmac = hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']);
  831. return hash_equals($hmac, $signature);
  832. }
  833. /**
  834. * Get the container builder
  835. */
  836. public static function getContainerBuilder(): ContainerBuilder
  837. {
  838. $containerBuilder = new ContainerBuilder();
  839. $loader = new PhpFileLoader($containerBuilder, new FileLocator(ROOT_PATH . 'libraries'));
  840. $loader->load('services_loader.php');
  841. return $containerBuilder;
  842. }
  843. public static function populateRequestWithEncryptedQueryParams(ServerRequest $request): ServerRequest
  844. {
  845. $queryParams = $request->getQueryParams();
  846. $parsedBody = $request->getParsedBody();
  847. unset($_GET['eq'], $_POST['eq'], $_REQUEST['eq']);
  848. if (! isset($queryParams['eq']) && (! is_array($parsedBody) || ! isset($parsedBody['eq']))) {
  849. return $request;
  850. }
  851. $encryptedQuery = '';
  852. if (
  853. is_array($parsedBody)
  854. && isset($parsedBody['eq'])
  855. && is_string($parsedBody['eq'])
  856. && $parsedBody['eq'] !== ''
  857. ) {
  858. $encryptedQuery = $parsedBody['eq'];
  859. unset($parsedBody['eq'], $queryParams['eq']);
  860. } elseif (isset($queryParams['eq']) && is_string($queryParams['eq']) && $queryParams['eq'] !== '') {
  861. $encryptedQuery = $queryParams['eq'];
  862. unset($queryParams['eq']);
  863. }
  864. $decryptedQuery = null;
  865. if ($encryptedQuery !== '') {
  866. $decryptedQuery = Url::decryptQuery($encryptedQuery);
  867. }
  868. if ($decryptedQuery === null) {
  869. $request = $request->withQueryParams($queryParams);
  870. if (is_array($parsedBody)) {
  871. $request = $request->withParsedBody($parsedBody);
  872. }
  873. return $request;
  874. }
  875. $urlQueryParams = (array) json_decode($decryptedQuery);
  876. foreach ($urlQueryParams as $urlQueryParamKey => $urlQueryParamValue) {
  877. if (is_array($parsedBody)) {
  878. $parsedBody[$urlQueryParamKey] = $urlQueryParamValue;
  879. $_POST[$urlQueryParamKey] = $urlQueryParamValue;
  880. } else {
  881. $queryParams[$urlQueryParamKey] = $urlQueryParamValue;
  882. $_GET[$urlQueryParamKey] = $urlQueryParamValue;
  883. }
  884. $_REQUEST[$urlQueryParamKey] = $urlQueryParamValue;
  885. }
  886. $request = $request->withQueryParams($queryParams);
  887. if (is_array($parsedBody)) {
  888. $request = $request->withParsedBody($parsedBody);
  889. }
  890. return $request;
  891. }
  892. }