3 declare(strict_types
=1);
7 use PhpMyAdmin\Exceptions\MissingExtensionException
;
8 use PhpMyAdmin\Http\ServerRequest
;
9 use Symfony\Component\Config\FileLocator
;
10 use Symfony\Component\DependencyInjection\ContainerBuilder
;
11 use Symfony\Component\DependencyInjection\Loader\PhpFileLoader
;
14 use function array_keys
;
15 use function array_pop
;
16 use function array_walk_recursive
;
20 use function filter_var
;
21 use function function_exists
;
24 use function hash_equals
;
25 use function hash_hmac
;
27 use function header_remove
;
28 use function htmlspecialchars
;
29 use function http_build_query
;
30 use function in_array
;
32 use function is_array
;
33 use function is_scalar
;
34 use function is_string
;
35 use function json_decode
;
36 use function mb_strpos
;
37 use function mb_substr
;
38 use function parse_str
;
39 use function parse_url
;
40 use function preg_match
;
41 use function preg_replace
;
43 use function str_replace
;
46 use function strtolower
;
48 use function unserialize
;
49 use function urldecode
;
51 use const DATE_RFC1123
;
52 use const E_USER_WARNING
;
53 use const FILTER_VALIDATE_IP
;
56 * Core functions used all over the scripts.
60 public static ContainerBuilder|
null $containerBuilder = null;
63 * Removes insecure parts in a path; used before include() or
64 * require() when a part of the path comes from an insecure source
65 * like a cookie or form.
67 * @param string $path The path to check
69 public static function securePath(string $path): string
72 return (string) preg_replace('@\.\.*@', '.', $path);
76 * Returns a link to the PHP documentation
78 * @param string $target anchor in documentation
80 * @return string the URL
82 public static function getPHPDocLink(string $target): string
84 /* List of PHP documentation translations */
85 $phpDocLanguages = ['pt_BR', 'zh_CN', 'fr', 'de', 'ja', 'ru', 'es', 'tr'];
88 if (isset($GLOBALS['lang']) && in_array($GLOBALS['lang'], $phpDocLanguages)) {
89 $lang = $GLOBALS['lang'] === 'zh_CN' ?
'zh' : $GLOBALS['lang'];
92 return self
::linkURL('https://www.php.net/manual/' . $lang . '/' . $target);
96 * Warn or fail on missing extension.
98 * @param string $extension Extension name
99 * @param bool $fatal Whether the error is fatal.
100 * @param string $extra Extra string to append to message.
102 public static function warnMissingExtension(
107 $message = 'The %s extension is missing. Please check your PHP configuration.';
109 /* Gettext does not have to be loaded yet here */
110 if (function_exists('__')) {
111 $message = __('The %s extension is missing. Please check your PHP configuration.');
114 $doclink = self
::getPHPDocLink('book.' . $extension . '.php');
115 $message = sprintf($message, '[a@' . $doclink . '@Documentation][em]' . $extension . '[/em][/a]');
117 $message .= ' ' . $extra;
121 throw new MissingExtensionException(Sanitize
::sanitizeMessage($message));
124 ErrorHandler
::getInstance()->addError($message, E_USER_WARNING
, '', 0, false);
128 * Converts numbers like 10M into bytes
129 * Used with permission from Moodle (https://moodle.org) by Martin Dougiamas
130 * (renamed with PMA prefix to avoid double definition when embedded
133 * @param string|int $size size (Default = 0)
135 public static function getRealSize(string|
int $size = 0): int
142 'T' => 1099511627776,
143 't' => 1099511627776,
152 if (preg_match('/^([0-9]+)([KMGT])/i', (string) $size, $matches)) {
153 return (int) ($matches[1] * $binaryprefixes[$matches[2]]);
160 * Checks given $page against given $allowList and returns true if valid
161 * it optionally ignores query parameters in $page (script.php?ignored)
163 * @param string $page page to check
164 * @param mixed[] $allowList allow list to check page against
165 * @param bool $include whether the page is going to be included
167 public static function checkPageValidity(string $page, array $allowList = [], bool $include = false): bool
169 if ($allowList === []) {
170 $allowList = ['index.php'];
177 if (in_array($page, $allowList)) {
185 $newPage = mb_substr(
188 (int) mb_strpos($page . '?', '?'),
190 if (in_array($newPage, $allowList)) {
194 $newPage = urldecode($page);
195 $newPage = mb_substr(
198 (int) mb_strpos($newPage . '?', '?'),
201 return in_array($newPage, $allowList);
205 * Tries to find the value for the given environment variable name
207 * Searches in $_SERVER, $_ENV then tries getenv() and apache_getenv() in this order.
209 * @psalm-param non-empty-string $variableName
211 public static function getEnv(string $variableName): string
213 $value = $_SERVER[$variableName] ??
$_ENV[$variableName] ??
getenv($variableName);
214 if (is_scalar($value) && (string) $value !== '') {
215 return (string) $value;
218 if (function_exists('apache_getenv')) {
219 return (string) apache_getenv($variableName, true); // @codeCoverageIgnore
226 * Returns application/json headers. This includes no caching.
228 * @return array<string, string>
230 public static function headerJSON(): array
233 $headers = self
::getNoCacheHeaders();
236 $headers['Content-Type'] = 'application/json; charset=UTF-8';
239 * Disable content sniffing in browser.
240 * This is needed in case we include HTML in JSON, browser might assume it's html to display.
242 $headers['X-Content-Type-Options'] = 'nosniff';
247 /** @return array<string, string> */
248 public static function getNoCacheHeaders(): array
251 $date = gmdate(DATE_RFC1123
);
253 // rfc2616 - Section 14.21
254 $headers['Expires'] = $date;
257 $headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0';
260 $headers['Pragma'] = 'no-cache';
262 // test case: exporting a database into a .gz file with Safari
263 // would produce files not having the current time
264 // (added this header for Safari but should not harm other browsers)
265 $headers['Last-Modified'] = $date;
271 * Sends header indicating file download.
273 * @param string $filename Filename to include in headers if empty,
274 * none Content-Disposition header will be sent.
275 * @param string $mimetype MIME type to include in headers.
276 * @param int $length Length of content (optional)
277 * @param bool $noCache Whether to include no-caching headers.
279 public static function downloadHeader(
283 bool $noCache = true,
288 $headers = self
::getNoCacheHeaders();
291 /* Replace all possibly dangerous chars in filename */
292 $filename = Sanitize
::sanitizeFilename($filename);
293 if ($filename !== '') {
294 $headers['Content-Description'] = 'File Transfer';
295 $headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
298 $headers['Content-Type'] = $mimetype;
300 // The default output in PMA uses gzip,
301 // so if we want to output uncompressed file, we should reset the encoding.
302 // See PHP bug https://github.com/php/php-src/issues/8218
303 header_remove('Content-Encoding');
305 $headers['Content-Transfer-Encoding'] = 'binary';
308 $headers['Content-Length'] = (string) $length;
311 foreach ($headers as $name => $value) {
312 header(sprintf('%s: %s', $name, $value));
317 * Returns value of an element in $array given by $path.
318 * $path is a string describing position of an element in an associative array,
319 * eg. Servers/1/host refers to $array[Servers][1][host]
321 * @param string $path path in the array
322 * @param mixed[] $array the array
323 * @param mixed $default default value
325 * @return mixed[]|mixed|null array element or $default
327 public static function arrayRead(string $path, array $array, mixed $default = null): mixed
329 $keys = explode('/', $path);
331 foreach ($keys as $key) {
332 if (! isset($value[$key])) {
336 $value =& $value[$key];
343 * Stores value in an array
345 * @param string $path path in the array
346 * @param mixed[] $array the array
347 * @param mixed $value value to store
349 public static function arrayWrite(string $path, array &$array, mixed $value): void
351 $keys = explode('/', $path);
352 $lastKey = array_pop($keys);
354 foreach ($keys as $key) {
355 if (! isset($a[$key])) {
362 $a[$lastKey] = $value;
366 * Removes value from an array
368 * @param string $path path in the array
369 * @param mixed[] $array the array
371 public static function arrayRemove(string $path, array &$array): void
373 $keys = explode('/', $path);
374 $keysLast = array_pop($keys);
380 // go as deep as required or possible
381 foreach ($keys as $key) {
382 if (! isset($path[$depth][$key])) {
388 $path[$depth] =& $path[$depth - 1][$key];
391 // if element found, remove it
393 unset($path[$depth][$keysLast]);
397 // remove empty nested arrays
398 /** @infection-ignore-all */
399 for (; $depth >= 0; $depth--) {
400 if (isset($path[$depth +
1]) && count($path[$depth +
1]) !== 0) {
404 unset($path[$depth][$keys[$depth]]);
409 * Returns link to (possibly) external site using defined redirector.
411 * @param string $url URL where to go.
413 * @return string URL for a link.
415 public static function linkURL(string $url): string
417 if (! preg_match('#^https?://#', $url)) {
422 $params['url'] = $url;
424 $url = Url
::getCommon($params);
425 //strip off token and such sensitive information. Just keep url.
426 $arr = parse_url($url);
428 if (! is_array($arr)) {
432 parse_str($arr['query'] ??
'', $vars);
433 $query = http_build_query(['url' => $vars['url']]);
435 if (Config
::getInstance()->get('is_setup')) {
436 return '../index.php?route=/url&' . $query;
439 return 'index.php?route=/url&' . $query;
443 * Checks whether domain of URL is an allowed domain or not.
444 * Use only for URLs of external sites.
446 * @param string $url URL of external site.
448 public static function isAllowedDomain(string $url): bool
450 $parsedUrl = parse_url($url);
452 ! is_array($parsedUrl)
453 ||
! isset($parsedUrl['host'])
454 ||
isset($parsedUrl['user'])
455 ||
isset($parsedUrl['pass'])
456 ||
isset($parsedUrl['port'])
462 /* Include current domain */
463 $_SERVER['SERVER_NAME'],
464 /* phpMyAdmin domains */
465 'wiki.phpmyadmin.net',
466 'www.phpmyadmin.net',
468 'demo.phpmyadmin.net',
469 'docs.phpmyadmin.net',
470 /* mysql.com domains */
473 /* mariadb domains */
476 /* php.net domains */
482 /* Percona domains */
486 /* Following are doubtful ones. */
487 'mysqldatabaseadministration.blogspot.com',
490 return in_array($parsedUrl['host'], $domainAllowList, true);
494 * Replace some html-unfriendly stuff
496 * @param string $buffer String to process
498 * @return string Escaped and cleaned up text suitable for html
500 public static function mimeDefaultFunction(string $buffer): string
502 $buffer = htmlspecialchars($buffer);
503 $buffer = str_replace(' ', ' ', $buffer);
505 return (string) preg_replace("@((\015\012)|(\015)|(\012))@", '<br>' . "\n", $buffer);
509 * Displays SQL query before executing.
511 * @param mixed[]|string $queryData Array containing queries or query itself
513 public static function previewSQL(array|
string $queryData): void
515 $retval = '<div class="preview_sql">';
516 if ($queryData === '' ||
$queryData === []) {
517 $retval .= __('No change');
518 } elseif (is_array($queryData)) {
519 foreach ($queryData as $query) {
520 $retval .= Html\Generator
::formatSql($query);
523 $retval .= Html\Generator
::formatSql($queryData);
527 $response = ResponseRenderer
::getInstance();
528 $response->addJSON('sql_data', $retval);
532 * recursively check if variable is empty
534 * @param mixed $value the variable
536 public static function emptyRecursive(mixed $value): bool
538 if (is_array($value)) {
540 array_walk_recursive(
542 /** @param mixed $item */
543 static function ($item) use (&$empty): void
{
544 $empty = $empty && empty($item);
551 return empty($value);
555 * Creates some globals from $_POST variables matching a pattern
557 * @param mixed[] $postPatterns The patterns to search for
559 public static function setPostAsGlobal(array $postPatterns): void
561 foreach (array_keys($_POST) as $postKey) {
562 foreach ($postPatterns as $onePostPattern) {
563 if (! preg_match($onePostPattern, $postKey)) {
567 $GLOBALS[$postKey] = $_POST[$postKey];
573 * Gets the "true" IP address of the current user
575 * @return string|bool the ip of the user
577 public static function getIp(): string|
bool
579 /* Get the address of user */
580 if (empty($_SERVER['REMOTE_ADDR'])) {
581 /* We do not know remote IP */
585 $directIp = $_SERVER['REMOTE_ADDR'];
587 /* Do we trust this IP as a proxy? If yes we will use it's header. */
588 $config = Config
::getInstance();
589 if (! isset($config->settings
['TrustedProxies'][$directIp])) {
595 * Parse header in form:
596 * X-Forwarded-For: client, proxy1, proxy2
598 // Get header content
599 $value = self
::getEnv($config->settings
['TrustedProxies'][$directIp]);
600 // Grab first element what is client adddress
601 $value = explode(',', $value)[0];
602 // checks that the header contains only one IP address,
603 $isIp = filter_var($value, FILTER_VALIDATE_IP
);
605 if ($isIp !== false) {
606 // True IP behind a proxy
610 // We could not parse header
615 * Sanitizes MySQL hostname
617 * * strips p: prefix(es)
619 * @param string $name User given hostname
621 public static function sanitizeMySQLHost(string $name): string
623 while (strtolower(substr($name, 0, 2)) === 'p:') {
624 /** @infection-ignore-all */
625 $name = substr($name, 2);
632 * Sanitizes MySQL username
634 * * strips part behind null byte
636 * @param string $name User given username
638 public static function sanitizeMySQLUser(string $name): string
640 $position = strpos($name, chr(0));
641 if ($position !== false) {
642 return substr($name, 0, $position);
649 * Safe unserializer wrapper
651 * It does not unserialize data containing objects
653 * @param string $data Data to unserialize
655 public static function safeUnserialize(string $data): mixed
657 /* validate serialized data */
658 $length = strlen($data);
660 for ($i = 0; $i < $length; $i++
) {
674 // parse sting length
675 $strlen = intval(substr($data, $i +
2));
677 $i = strpos($data, ':', $i +
2);
682 // skip string, quotes and ;
683 $i +
= 2 +
$strlen +
1;
684 if ($data[$i] !== ';') {
693 /* bool, integer or double */
694 // skip value to separator
695 $i = strpos($data, ';', $i);
704 $i = strpos($data, '{', $i);
715 $i = strpos($data, ';', $i);
722 /* any other elements are not wanted */
727 // check unterminated arrays
732 return unserialize($data);
736 * Sign the sql query using hmac using the session token
738 * @param string $sqlQuery The sql query
740 public static function signSqlQuery(string $sqlQuery): string
742 $secret = $_SESSION[' HMAC_secret '] ??
'';
744 return hash_hmac('sha256', $sqlQuery, $secret . Config
::getInstance()->settings
['blowfish_secret']);
748 * Check that the sql query has a valid hmac signature
750 * @param string $sqlQuery The sql query
751 * @param string $signature The Signature to check
753 public static function checkSqlQuerySignature(string $sqlQuery, string $signature): bool
755 $secret = $_SESSION[' HMAC_secret '] ??
'';
756 $hmac = hash_hmac('sha256', $sqlQuery, $secret . Config
::getInstance()->settings
['blowfish_secret']);
758 return hash_equals($hmac, $signature);
761 public static function getContainerBuilder(): ContainerBuilder
763 if (self
::$containerBuilder !== null) {
764 return self
::$containerBuilder;
767 self
::$containerBuilder = new ContainerBuilder();
768 $loader = new PhpFileLoader(self
::$containerBuilder, new FileLocator(ROOT_PATH
. 'app'));
769 $loader->load('services_loader.php');
771 return self
::$containerBuilder;
774 public static function populateRequestWithEncryptedQueryParams(ServerRequest
$request): ServerRequest
776 $queryParams = $request->getQueryParams();
777 $parsedBody = $request->getParsedBody();
779 unset($_GET['eq'], $_POST['eq'], $_REQUEST['eq']);
781 if (! isset($queryParams['eq']) && (! is_array($parsedBody) ||
! isset($parsedBody['eq']))) {
785 $encryptedQuery = '';
787 is_array($parsedBody)
788 && isset($parsedBody['eq'])
789 && is_string($parsedBody['eq'])
790 && $parsedBody['eq'] !== ''
792 $encryptedQuery = $parsedBody['eq'];
793 unset($parsedBody['eq'], $queryParams['eq']);
794 } elseif (isset($queryParams['eq']) && is_string($queryParams['eq']) && $queryParams['eq'] !== '') {
795 $encryptedQuery = $queryParams['eq'];
796 unset($queryParams['eq']);
799 $decryptedQuery = null;
800 if ($encryptedQuery !== '') {
801 $decryptedQuery = Url
::decryptQuery($encryptedQuery);
804 if ($decryptedQuery === null) {
805 $request = $request->withQueryParams($queryParams);
806 if (is_array($parsedBody)) {
807 return $request->withParsedBody($parsedBody);
813 $urlQueryParams = (array) json_decode($decryptedQuery);
814 foreach ($urlQueryParams as $urlQueryParamKey => $urlQueryParamValue) {
815 if (is_array($parsedBody)) {
816 $parsedBody[$urlQueryParamKey] = $urlQueryParamValue;
817 $_POST[$urlQueryParamKey] = $urlQueryParamValue;
819 $queryParams[$urlQueryParamKey] = $urlQueryParamValue;
820 $_GET[$urlQueryParamKey] = $urlQueryParamValue;
823 $_REQUEST[$urlQueryParamKey] = $urlQueryParamValue;
826 $request = $request->withQueryParams($queryParams);
827 if (is_array($parsedBody)) {
828 return $request->withParsedBody($parsedBody);