Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / Core.php
blob5ec72e9d01415d958f8322c3b3f710672669ab0c
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use PhpMyAdmin\Error\ErrorHandler;
8 use PhpMyAdmin\Exceptions\MissingExtensionException;
9 use PhpMyAdmin\Http\ServerRequest;
11 use function __;
12 use function array_pop;
13 use function array_walk_recursive;
14 use function chr;
15 use function count;
16 use function explode;
17 use function filter_var;
18 use function function_exists;
19 use function getenv;
20 use function gmdate;
21 use function hash_equals;
22 use function hash_hmac;
23 use function header;
24 use function header_remove;
25 use function htmlspecialchars;
26 use function http_build_query;
27 use function in_array;
28 use function is_array;
29 use function is_scalar;
30 use function is_string;
31 use function json_decode;
32 use function mb_strpos;
33 use function mb_substr;
34 use function parse_str;
35 use function parse_url;
36 use function preg_match;
37 use function preg_replace;
38 use function sprintf;
39 use function str_replace;
40 use function stripos;
41 use function strlen;
42 use function strpos;
43 use function substr;
44 use function unserialize;
45 use function urldecode;
47 use const DATE_RFC1123;
48 use const E_USER_WARNING;
49 use const FILTER_VALIDATE_IP;
51 /**
52 * Core functions used all over the scripts.
54 class Core
56 /**
57 * Removes insecure parts in a path; used before include() or
58 * require() when a part of the path comes from an insecure source
59 * like a cookie or form.
61 * @param string $path The path to check
63 public static function securePath(string $path): string
65 // change .. to .
66 return (string) preg_replace('@\.\.*@', '.', $path);
69 /**
70 * Returns a link to the PHP documentation
72 * @param string $target anchor in documentation
74 * @return string the URL
76 public static function getPHPDocLink(string $target): string
78 /* List of PHP documentation translations */
79 $phpDocLanguages = ['pt_BR', 'zh_CN', 'fr', 'de', 'ja', 'ru', 'es', 'tr'];
81 $lang = 'en';
82 if (isset($GLOBALS['lang']) && in_array($GLOBALS['lang'], $phpDocLanguages, true)) {
83 $lang = $GLOBALS['lang'] === 'zh_CN' ? 'zh' : $GLOBALS['lang'];
86 return self::linkURL('https://www.php.net/manual/' . $lang . '/' . $target);
89 /**
90 * Warn or fail on missing extension.
92 * @param string $extension Extension name
93 * @param bool $fatal Whether the error is fatal.
94 * @param string $extra Extra string to append to message.
96 public static function warnMissingExtension(
97 string $extension,
98 bool $fatal = false,
99 string $extra = '',
100 ): void {
101 $message = 'The %s extension is missing. Please check your PHP configuration.';
103 /* Gettext does not have to be loaded yet here */
104 if (function_exists('__')) {
105 $message = __('The %s extension is missing. Please check your PHP configuration.');
108 $doclink = self::getPHPDocLink('book.' . $extension . '.php');
109 $message = sprintf($message, '[a@' . $doclink . '@Documentation][em]' . $extension . '[/em][/a]');
110 if ($extra !== '') {
111 $message .= ' ' . $extra;
114 if ($fatal) {
115 throw new MissingExtensionException(Sanitize::convertBBCode($message));
118 ErrorHandler::getInstance()->addError($message, E_USER_WARNING, '', 0, false);
122 * Converts numbers like 10M into bytes
123 * Used with permission from Moodle (https://moodle.org) by Martin Dougiamas
124 * (renamed with PMA prefix to avoid double definition when embedded
125 * in Moodle)
127 * @param string|int $size size (Default = 0)
129 public static function getRealSize(string|int $size = 0): int
131 if (! $size) {
132 return 0;
135 $binaryprefixes = [
136 'T' => 1099511627776,
137 't' => 1099511627776,
138 'G' => 1073741824,
139 'g' => 1073741824,
140 'M' => 1048576,
141 'm' => 1048576,
142 'K' => 1024,
143 'k' => 1024,
146 if (preg_match('/^([0-9]+)([KMGT])/i', (string) $size, $matches)) {
147 return (int) ($matches[1] * $binaryprefixes[$matches[2]]);
150 return (int) $size;
154 * Checks if the given $page is index.php and returns true if valid.
155 * It ignores query parameters in $page (script.php?ignored)
157 public static function checkPageValidity(string $page): bool
159 if ($page === '') {
160 return false;
163 if ($page === 'index.php') {
164 return true;
167 $newPage = mb_substr(
168 $page,
170 (int) mb_strpos($page . '?', '?'),
172 if ($newPage === 'index.php') {
173 return true;
176 $newPage = urldecode($page);
177 $newPage = mb_substr(
178 $newPage,
180 (int) mb_strpos($newPage . '?', '?'),
183 return $newPage === 'index.php';
187 * Tries to find the value for the given environment variable name
189 * Searches in $_SERVER, $_ENV then tries getenv() and apache_getenv() in this order.
191 * @psalm-param non-empty-string $variableName
193 public static function getEnv(string $variableName): string
195 $value = $_SERVER[$variableName] ?? $_ENV[$variableName] ?? getenv($variableName);
196 if (is_scalar($value) && (string) $value !== '') {
197 return (string) $value;
200 if (function_exists('apache_getenv')) {
201 return (string) apache_getenv($variableName, true); // @codeCoverageIgnore
204 return '';
208 * Returns application/json headers. This includes no caching.
210 * @return array<string, string>
212 public static function headerJSON(): array
214 // No caching
215 $headers = self::getNoCacheHeaders();
217 // Media type
218 $headers['Content-Type'] = 'application/json; charset=UTF-8';
221 * Disable content sniffing in browser.
222 * This is needed in case we include HTML in JSON, browser might assume it's html to display.
224 $headers['X-Content-Type-Options'] = 'nosniff';
226 return $headers;
229 /** @return array<string, string> */
230 public static function getNoCacheHeaders(): array
232 $headers = [];
233 $date = gmdate(DATE_RFC1123);
235 // rfc2616 - Section 14.21
236 $headers['Expires'] = $date;
238 // HTTP/1.1
239 $headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0';
241 // HTTP/1.0
242 $headers['Pragma'] = 'no-cache';
244 // test case: exporting a database into a .gz file with Safari
245 // would produce files not having the current time
246 // (added this header for Safari but should not harm other browsers)
247 $headers['Last-Modified'] = $date;
249 return $headers;
253 * Sends header indicating file download.
255 * @param string $filename Filename to include in headers if empty,
256 * none Content-Disposition header will be sent.
257 * @param string $mimetype MIME type to include in headers.
258 * @param int $length Length of content (optional)
259 * @param bool $noCache Whether to include no-caching headers.
261 public static function downloadHeader(
262 string $filename,
263 string $mimetype,
264 int $length = 0,
265 bool $noCache = true,
266 ): void {
267 $headers = [];
269 if ($noCache) {
270 $headers = self::getNoCacheHeaders();
273 /* Replace all possibly dangerous chars in filename */
274 $filename = Sanitize::sanitizeFilename($filename);
275 if ($filename !== '') {
276 $headers['Content-Description'] = 'File Transfer';
277 $headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
280 $headers['Content-Type'] = $mimetype;
282 // The default output in PMA uses gzip,
283 // so if we want to output uncompressed file, we should reset the encoding.
284 // See PHP bug https://github.com/php/php-src/issues/8218
285 header_remove('Content-Encoding');
287 $headers['Content-Transfer-Encoding'] = 'binary';
289 if ($length > 0) {
290 $headers['Content-Length'] = (string) $length;
293 foreach ($headers as $name => $value) {
294 header(sprintf('%s: %s', $name, $value));
299 * Returns value of an element in $array given by $path.
300 * $path is a string describing position of an element in an associative array,
301 * eg. Servers/1/host refers to $array[Servers][1][host]
303 * @param string $path path in the array
304 * @param mixed[] $array the array
305 * @param mixed $default default value
307 * @return mixed[]|mixed|null array element or $default
309 public static function arrayRead(string $path, array $array, mixed $default = null): mixed
311 $keys = explode('/', $path);
312 $value =& $array;
313 foreach ($keys as $key) {
314 if (! isset($value[$key])) {
315 return $default;
318 $value =& $value[$key];
321 return $value;
325 * Stores value in an array
327 * @param string $path path in the array
328 * @param mixed[] $array the array
329 * @param mixed $value value to store
331 public static function arrayWrite(string $path, array &$array, mixed $value): void
333 $keys = explode('/', $path);
334 $lastKey = array_pop($keys);
335 $a =& $array;
336 foreach ($keys as $key) {
337 if (! isset($a[$key])) {
338 $a[$key] = [];
341 $a =& $a[$key];
344 $a[$lastKey] = $value;
348 * Removes value from an array
350 * @param string $path path in the array
351 * @param mixed[] $array the array
353 public static function arrayRemove(string $path, array &$array): void
355 $keys = explode('/', $path);
356 $keysLast = array_pop($keys);
357 $path = [];
358 $depth = 0;
360 $path[0] =& $array;
361 $found = true;
362 // go as deep as required or possible
363 foreach ($keys as $key) {
364 if (! isset($path[$depth][$key])) {
365 $found = false;
366 break;
369 $depth++;
370 $path[$depth] =& $path[$depth - 1][$key];
373 // if element found, remove it
374 if ($found) {
375 unset($path[$depth][$keysLast]);
376 $depth--;
379 // remove empty nested arrays
380 /** @infection-ignore-all */
381 for (; $depth >= 0; $depth--) {
382 if (isset($path[$depth + 1]) && count($path[$depth + 1]) !== 0) {
383 break;
386 unset($path[$depth][$keys[$depth]]);
391 * Returns link to (possibly) external site using defined redirector.
393 * @param string $url URL where to go.
395 * @return string URL for a link.
397 public static function linkURL(string $url): string
399 if (! preg_match('#^https?://#', $url)) {
400 return $url;
403 $params = [];
404 $params['url'] = $url;
406 $url = Url::getCommon($params);
407 //strip off token and such sensitive information. Just keep url.
408 $arr = parse_url($url);
410 if (! is_array($arr)) {
411 $arr = [];
414 parse_str($arr['query'] ?? '', $vars);
415 $query = http_build_query(['url' => $vars['url']]);
417 if (Config::getInstance()->get('is_setup')) {
418 return '../index.php?route=/url&' . $query;
421 return 'index.php?route=/url&' . $query;
425 * Checks whether domain of URL is an allowed domain or not.
426 * Use only for URLs of external sites.
428 * @param string $url URL of external site.
430 public static function isAllowedDomain(string $url): bool
432 $parsedUrl = parse_url($url);
433 if (
434 ! is_array($parsedUrl)
435 || ! isset($parsedUrl['host'])
436 || isset($parsedUrl['user'])
437 || isset($parsedUrl['pass'])
438 || isset($parsedUrl['port'])
440 return false;
443 $domainAllowList = [
444 /* Include current domain */
445 $_SERVER['SERVER_NAME'],
446 /* phpMyAdmin domains */
447 'wiki.phpmyadmin.net',
448 'www.phpmyadmin.net',
449 'phpmyadmin.net',
450 'demo.phpmyadmin.net',
451 'docs.phpmyadmin.net',
452 /* mysql.com domains */
453 'dev.mysql.com',
454 'bugs.mysql.com',
455 /* mariadb domains */
456 'mariadb.org',
457 'mariadb.com',
458 /* php.net domains */
459 'php.net',
460 'www.php.net',
461 /* Github domains*/
462 'github.com',
463 'www.github.com',
464 /* Percona domains */
465 'www.percona.com',
466 /* CVE domain */
467 'www.cve.org',
468 /* Following are doubtful ones. */
469 'mysqldatabaseadministration.blogspot.com',
472 return in_array($parsedUrl['host'], $domainAllowList, true);
476 * Replace some html-unfriendly stuff
478 * @param string $buffer String to process
480 * @return string Escaped and cleaned up text suitable for html
482 public static function mimeDefaultFunction(string $buffer): string
484 $buffer = htmlspecialchars($buffer);
485 $buffer = str_replace(' ', ' &nbsp;', $buffer);
487 return (string) preg_replace("@((\015\012)|(\015)|(\012))@", '<br>' . "\n", $buffer);
491 * Displays SQL query before executing.
493 * @param mixed[]|string $queryData Array containing queries or query itself
495 public static function previewSQL(array|string $queryData): void
497 $retval = '<div class="preview_sql">';
498 if ($queryData === '' || $queryData === []) {
499 $retval .= __('No change');
500 } elseif (is_array($queryData)) {
501 foreach ($queryData as $query) {
502 $retval .= Html\Generator::formatSql($query);
504 } else {
505 $retval .= Html\Generator::formatSql($queryData);
508 $retval .= '</div>';
509 $response = ResponseRenderer::getInstance();
510 $response->addJSON('sql_data', $retval);
514 * recursively check if variable is empty
516 * @param mixed $value the variable
518 public static function emptyRecursive(mixed $value): bool
520 if (is_array($value)) {
521 $empty = true;
522 array_walk_recursive(
523 $value,
524 /** @param mixed $item */
525 static function ($item) use (&$empty): void {
526 $empty = $empty && empty($item);
530 return $empty;
533 return empty($value);
537 * Gets the "true" IP address of the current user
539 * @return string|bool the ip of the user
541 public static function getIp(): string|bool
543 /* Get the address of user */
544 if (empty($_SERVER['REMOTE_ADDR'])) {
545 /* We do not know remote IP */
546 return false;
549 $directIp = $_SERVER['REMOTE_ADDR'];
551 /* Do we trust this IP as a proxy? If yes we will use it's header. */
552 $config = Config::getInstance();
553 if (! isset($config->settings['TrustedProxies'][$directIp])) {
554 /* Return true IP */
555 return $directIp;
559 * Parse header in form:
560 * X-Forwarded-For: client, proxy1, proxy2
562 // Get header content
563 $value = self::getEnv($config->settings['TrustedProxies'][$directIp]);
564 // Grab first element what is client adddress
565 $value = explode(',', $value)[0];
566 // checks that the header contains only one IP address,
567 $isIp = filter_var($value, FILTER_VALIDATE_IP);
569 if ($isIp !== false) {
570 // True IP behind a proxy
571 return $value;
574 // We could not parse header
575 return false;
579 * Sanitizes MySQL hostname
581 * * strips p: prefix(es)
583 * @param string $name User given hostname
585 public static function sanitizeMySQLHost(string $name): string
587 while (stripos($name, 'p:') === 0) {
588 /** @infection-ignore-all */
589 $name = substr($name, 2);
592 return $name;
596 * Sanitizes MySQL username
598 * * strips part behind null byte
600 * @param string $name User given username
602 public static function sanitizeMySQLUser(string $name): string
604 $position = strpos($name, chr(0));
605 if ($position !== false) {
606 return substr($name, 0, $position);
609 return $name;
613 * Safe unserializer wrapper
615 * It does not unserialize data containing objects
617 * @param string $data Data to unserialize
619 public static function safeUnserialize(string $data): mixed
621 /* validate serialized data */
622 $length = strlen($data);
623 $depth = 0;
624 for ($i = 0; $i < $length; $i++) {
625 $value = $data[$i];
627 switch ($value) {
628 case '}':
629 /* end of array */
630 if ($depth <= 0) {
631 return null;
634 $depth--;
635 break;
636 case 's':
637 /* string */
638 // parse sting length
639 $strlen = (int) substr($data, $i + 2);
640 // string start
641 $i = strpos($data, ':', $i + 2);
642 if ($i === false) {
643 return null;
646 // skip string, quotes and ;
647 $i += 2 + $strlen + 1;
648 if ($data[$i] !== ';') {
649 return null;
652 break;
654 case 'b':
655 case 'i':
656 case 'd':
657 /* bool, integer or double */
658 // skip value to separator
659 $i = strpos($data, ';', $i);
660 if ($i === false) {
661 return null;
664 break;
665 case 'a':
666 /* array */
667 // find array start
668 $i = strpos($data, '{', $i);
669 if ($i === false) {
670 return null;
673 // remember nesting
674 $depth++;
675 break;
676 case 'N':
677 /* null */
678 // skip to end
679 $i = strpos($data, ';', $i);
680 if ($i === false) {
681 return null;
684 break;
685 default:
686 /* any other elements are not wanted */
687 return null;
691 // check unterminated arrays
692 if ($depth > 0) {
693 return null;
696 return unserialize($data);
700 * Sign the sql query using hmac using the session token
702 * @param string $sqlQuery The sql query
704 public static function signSqlQuery(string $sqlQuery): string
706 $secret = $_SESSION[' HMAC_secret '] ?? '';
708 return hash_hmac('sha256', $sqlQuery, $secret . Config::getInstance()->settings['blowfish_secret']);
712 * Check that the sql query has a valid hmac signature
714 * @param string $sqlQuery The sql query
715 * @param string $signature The Signature to check
717 public static function checkSqlQuerySignature(string $sqlQuery, string $signature): bool
719 $secret = $_SESSION[' HMAC_secret '] ?? '';
720 $hmac = hash_hmac('sha256', $sqlQuery, $secret . Config::getInstance()->settings['blowfish_secret']);
722 return hash_equals($hmac, $signature);
725 public static function populateRequestWithEncryptedQueryParams(ServerRequest $request): ServerRequest
727 $queryParams = $request->getQueryParams();
728 $parsedBody = $request->getParsedBody();
730 unset($_GET['eq'], $_POST['eq'], $_REQUEST['eq']);
732 if (! isset($queryParams['eq']) && (! is_array($parsedBody) || ! isset($parsedBody['eq']))) {
733 return $request;
736 $encryptedQuery = '';
737 if (
738 is_array($parsedBody)
739 && isset($parsedBody['eq'])
740 && is_string($parsedBody['eq'])
741 && $parsedBody['eq'] !== ''
743 $encryptedQuery = $parsedBody['eq'];
744 unset($parsedBody['eq'], $queryParams['eq']);
745 } elseif (isset($queryParams['eq']) && is_string($queryParams['eq']) && $queryParams['eq'] !== '') {
746 $encryptedQuery = $queryParams['eq'];
747 unset($queryParams['eq']);
750 $decryptedQuery = null;
751 if ($encryptedQuery !== '') {
752 $decryptedQuery = Url::decryptQuery($encryptedQuery);
755 if ($decryptedQuery === null) {
756 $request = $request->withQueryParams($queryParams);
757 if (is_array($parsedBody)) {
758 return $request->withParsedBody($parsedBody);
761 return $request;
764 $urlQueryParams = (array) json_decode($decryptedQuery);
765 foreach ($urlQueryParams as $urlQueryParamKey => $urlQueryParamValue) {
766 if (is_array($parsedBody)) {
767 $parsedBody[$urlQueryParamKey] = $urlQueryParamValue;
768 $_POST[$urlQueryParamKey] = $urlQueryParamValue;
769 } else {
770 $queryParams[$urlQueryParamKey] = $urlQueryParamValue;
771 $_GET[$urlQueryParamKey] = $urlQueryParamValue;
774 $_REQUEST[$urlQueryParamKey] = $urlQueryParamValue;
777 $request = $request->withQueryParams($queryParams);
778 if (is_array($parsedBody)) {
779 return $request->withParsedBody($parsedBody);
782 return $request;