Core.php 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429
  1. <?php
  2. /**
  3. * Core functions used all over the scripts.
  4. * This script is distinct from libraries/common.inc.php because this
  5. * script is called from /test.
  6. */
  7. declare(strict_types=1);
  8. namespace PhpMyAdmin;
  9. use PhpMyAdmin\Plugins\AuthenticationPlugin;
  10. use Symfony\Component\Config\FileLocator;
  11. use Symfony\Component\DependencyInjection\ContainerBuilder;
  12. use Symfony\Component\DependencyInjection\ContainerInterface;
  13. use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
  14. use const DATE_RFC1123;
  15. use const E_USER_ERROR;
  16. use const E_USER_WARNING;
  17. use const FILTER_VALIDATE_IP;
  18. use function array_keys;
  19. use function array_pop;
  20. use function array_walk_recursive;
  21. use function chr;
  22. use function count;
  23. use function date_default_timezone_get;
  24. use function date_default_timezone_set;
  25. use function defined;
  26. use function explode;
  27. use function extension_loaded;
  28. use function filter_var;
  29. use function function_exists;
  30. use function getenv;
  31. use function gettype;
  32. use function gmdate;
  33. use function hash_equals;
  34. use function hash_hmac;
  35. use function header;
  36. use function htmlspecialchars;
  37. use function http_build_query;
  38. use function implode;
  39. use function in_array;
  40. use function ini_get;
  41. use function ini_set;
  42. use function intval;
  43. use function is_array;
  44. use function is_numeric;
  45. use function is_scalar;
  46. use function is_string;
  47. use function json_encode;
  48. use function mb_internal_encoding;
  49. use function mb_strlen;
  50. use function mb_strpos;
  51. use function mb_strrpos;
  52. use function mb_substr;
  53. use function parse_str;
  54. use function parse_url;
  55. use function preg_match;
  56. use function preg_replace;
  57. use function session_id;
  58. use function session_write_close;
  59. use function sprintf;
  60. use function str_replace;
  61. use function strlen;
  62. use function strpos;
  63. use function strtolower;
  64. use function strtr;
  65. use function substr;
  66. use function trigger_error;
  67. use function unserialize;
  68. use function urldecode;
  69. use function vsprintf;
  70. /**
  71. * Core class
  72. */
  73. class Core
  74. {
  75. /**
  76. * checks given $var and returns it if valid, or $default of not valid
  77. * given $var is also checked for type being 'similar' as $default
  78. * or against any other type if $type is provided
  79. *
  80. * <code>
  81. * // $_REQUEST['db'] not set
  82. * echo Core::ifSetOr($_REQUEST['db'], ''); // ''
  83. * // $_POST['sql_query'] not set
  84. * echo Core::ifSetOr($_POST['sql_query']); // null
  85. * // $cfg['EnableFoo'] not set
  86. * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // false
  87. * echo Core::ifSetOr($cfg['EnableFoo']); // null
  88. * // $cfg['EnableFoo'] set to 1
  89. * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // false
  90. * echo Core::ifSetOr($cfg['EnableFoo'], false, 'similar'); // 1
  91. * echo Core::ifSetOr($cfg['EnableFoo'], false); // 1
  92. * // $cfg['EnableFoo'] set to true
  93. * echo Core::ifSetOr($cfg['EnableFoo'], false, 'boolean'); // true
  94. * </code>
  95. *
  96. * @see self::isValid()
  97. *
  98. * @param mixed $var param to check
  99. * @param mixed $default default value
  100. * @param mixed $type var type or array of values to check against $var
  101. *
  102. * @return mixed $var or $default
  103. */
  104. public static function ifSetOr(&$var, $default = null, $type = 'similar')
  105. {
  106. if (! self::isValid($var, $type, $default)) {
  107. return $default;
  108. }
  109. return $var;
  110. }
  111. /**
  112. * checks given $var against $type or $compare
  113. *
  114. * $type can be:
  115. * - false : no type checking
  116. * - 'scalar' : whether type of $var is integer, float, string or boolean
  117. * - 'numeric' : whether type of $var is any number representation
  118. * - 'length' : whether type of $var is scalar with a string length > 0
  119. * - 'similar' : whether type of $var is similar to type of $compare
  120. * - 'equal' : whether type of $var is identical to type of $compare
  121. * - 'identical' : whether $var is identical to $compare, not only the type!
  122. * - or any other valid PHP variable type
  123. *
  124. * <code>
  125. * // $_REQUEST['doit'] = true;
  126. * Core::isValid($_REQUEST['doit'], 'identical', 'true'); // false
  127. * // $_REQUEST['doit'] = 'true';
  128. * Core::isValid($_REQUEST['doit'], 'identical', 'true'); // true
  129. * </code>
  130. *
  131. * NOTE: call-by-reference is used to not get NOTICE on undefined vars,
  132. * but the var is not altered inside this function, also after checking a var
  133. * this var exists nut is not set, example:
  134. * <code>
  135. * // $var is not set
  136. * isset($var); // false
  137. * functionCallByReference($var); // false
  138. * isset($var); // true
  139. * functionCallByReference($var); // true
  140. * </code>
  141. *
  142. * to avoid this we set this var to null if not isset
  143. *
  144. * @see https://www.php.net/gettype
  145. *
  146. * @param mixed $var variable to check
  147. * @param mixed $type var type or array of valid values to check against $var
  148. * @param mixed $compare var to compare with $var
  149. *
  150. * @return bool whether valid or not
  151. *
  152. * @todo add some more var types like hex, bin, ...?
  153. */
  154. public static function isValid(&$var, $type = 'length', $compare = null): bool
  155. {
  156. if (! isset($var)) {
  157. // var is not even set
  158. return false;
  159. }
  160. if ($type === false) {
  161. // no vartype requested
  162. return true;
  163. }
  164. if (is_array($type)) {
  165. return in_array($var, $type);
  166. }
  167. // allow some aliases of var types
  168. $type = strtolower($type);
  169. switch ($type) {
  170. case 'identic':
  171. $type = 'identical';
  172. break;
  173. case 'len':
  174. $type = 'length';
  175. break;
  176. case 'bool':
  177. $type = 'boolean';
  178. break;
  179. case 'float':
  180. $type = 'double';
  181. break;
  182. case 'int':
  183. $type = 'integer';
  184. break;
  185. case 'null':
  186. $type = 'NULL';
  187. break;
  188. }
  189. if ($type === 'identical') {
  190. return $var === $compare;
  191. }
  192. // whether we should check against given $compare
  193. if ($type === 'similar') {
  194. switch (gettype($compare)) {
  195. case 'string':
  196. case 'boolean':
  197. $type = 'scalar';
  198. break;
  199. case 'integer':
  200. case 'double':
  201. $type = 'numeric';
  202. break;
  203. default:
  204. $type = gettype($compare);
  205. }
  206. } elseif ($type === 'equal') {
  207. $type = gettype($compare);
  208. }
  209. // do the check
  210. if ($type === 'length' || $type === 'scalar') {
  211. $is_scalar = is_scalar($var);
  212. if ($is_scalar && $type === 'length') {
  213. return strlen((string) $var) > 0;
  214. }
  215. return $is_scalar;
  216. }
  217. if ($type === 'numeric') {
  218. return is_numeric($var);
  219. }
  220. return gettype($var) === $type;
  221. }
  222. /**
  223. * Removes insecure parts in a path; used before include() or
  224. * require() when a part of the path comes from an insecure source
  225. * like a cookie or form.
  226. *
  227. * @param string $path The path to check
  228. */
  229. public static function securePath(string $path): string
  230. {
  231. // change .. to .
  232. return (string) preg_replace('@\.\.*@', '.', $path);
  233. }
  234. /**
  235. * displays the given error message on phpMyAdmin error page in foreign language,
  236. * ends script execution and closes session
  237. *
  238. * loads language file if not loaded already
  239. *
  240. * @param string $error_message the error message or named error message
  241. * @param string|array $message_args arguments applied to $error_message
  242. */
  243. public static function fatalError(
  244. string $error_message,
  245. $message_args = null
  246. ): void {
  247. global $dbi;
  248. /* Use format string if applicable */
  249. if (is_string($message_args)) {
  250. $error_message = sprintf($error_message, $message_args);
  251. } elseif (is_array($message_args)) {
  252. $error_message = vsprintf($error_message, $message_args);
  253. }
  254. /*
  255. * Avoid using Response class as config does not have to be loaded yet
  256. * (this can happen on early fatal error)
  257. */
  258. if (isset($dbi, $GLOBALS['PMA_Config']) && $dbi !== null
  259. && $GLOBALS['PMA_Config']->get('is_setup') === false
  260. && Response::getInstance()->isAjax()
  261. ) {
  262. $response = Response::getInstance();
  263. $response->setRequestStatus(false);
  264. $response->addJSON('message', Message::error($error_message));
  265. } elseif (! empty($_REQUEST['ajax_request'])) {
  266. // Generate JSON manually
  267. self::headerJSON();
  268. echo json_encode(
  269. [
  270. 'success' => false,
  271. 'message' => Message::error($error_message)->getDisplay(),
  272. ]
  273. );
  274. } else {
  275. $error_message = strtr($error_message, ['<br>' => '[br]']);
  276. $template = new Template();
  277. echo $template->render('error/generic', [
  278. 'lang' => $GLOBALS['lang'] ?? 'en',
  279. 'dir' => $GLOBALS['text_dir'] ?? 'ltr',
  280. 'error_message' => Sanitize::sanitizeMessage($error_message),
  281. ]);
  282. }
  283. if (! defined('TESTSUITE')) {
  284. exit;
  285. }
  286. }
  287. /**
  288. * Returns a link to the PHP documentation
  289. *
  290. * @param string $target anchor in documentation
  291. *
  292. * @return string the URL
  293. *
  294. * @access public
  295. */
  296. public static function getPHPDocLink(string $target): string
  297. {
  298. /* List of PHP documentation translations */
  299. $php_doc_languages = [
  300. 'pt_BR',
  301. 'zh',
  302. 'fr',
  303. 'de',
  304. 'it',
  305. 'ja',
  306. 'ro',
  307. 'ru',
  308. 'es',
  309. 'tr',
  310. ];
  311. $lang = 'en';
  312. if (isset($GLOBALS['lang']) && in_array($GLOBALS['lang'], $php_doc_languages)) {
  313. $lang = $GLOBALS['lang'];
  314. }
  315. return self::linkURL('https://www.php.net/manual/' . $lang . '/' . $target);
  316. }
  317. /**
  318. * Warn or fail on missing extension.
  319. *
  320. * @param string $extension Extension name
  321. * @param bool $fatal Whether the error is fatal.
  322. * @param string $extra Extra string to append to message.
  323. */
  324. public static function warnMissingExtension(
  325. string $extension,
  326. bool $fatal = false,
  327. string $extra = ''
  328. ): void {
  329. /** @var ErrorHandler $error_handler */
  330. global $error_handler;
  331. /* Gettext does not have to be loaded yet here */
  332. if (function_exists('__')) {
  333. $message = __(
  334. 'The %s extension is missing. Please check your PHP configuration.'
  335. );
  336. } else {
  337. $message
  338. = 'The %s extension is missing. Please check your PHP configuration.';
  339. }
  340. $doclink = self::getPHPDocLink('book.' . $extension . '.php');
  341. $message = sprintf(
  342. $message,
  343. '[a@' . $doclink . '@Documentation][em]' . $extension . '[/em][/a]'
  344. );
  345. if ($extra != '') {
  346. $message .= ' ' . $extra;
  347. }
  348. if ($fatal) {
  349. self::fatalError($message);
  350. return;
  351. }
  352. $error_handler->addError(
  353. $message,
  354. E_USER_WARNING,
  355. '',
  356. 0,
  357. false
  358. );
  359. }
  360. /**
  361. * returns count of tables in given db
  362. *
  363. * @param string $db database to count tables for
  364. *
  365. * @return int count of tables in $db
  366. */
  367. public static function getTableCount(string $db): int
  368. {
  369. global $dbi;
  370. $tables = $dbi->tryQuery(
  371. 'SHOW TABLES FROM ' . Util::backquote($db) . ';',
  372. DatabaseInterface::CONNECT_USER,
  373. DatabaseInterface::QUERY_STORE
  374. );
  375. if ($tables) {
  376. $num_tables = $dbi->numRows($tables);
  377. $dbi->freeResult($tables);
  378. } else {
  379. $num_tables = 0;
  380. }
  381. return $num_tables;
  382. }
  383. /**
  384. * Converts numbers like 10M into bytes
  385. * Used with permission from Moodle (https://moodle.org) by Martin Dougiamas
  386. * (renamed with PMA prefix to avoid double definition when embedded
  387. * in Moodle)
  388. *
  389. * @param string|int $size size (Default = 0)
  390. */
  391. public static function getRealSize($size = 0): int
  392. {
  393. if (! $size) {
  394. return 0;
  395. }
  396. $binaryprefixes = [
  397. 'T' => 1099511627776,
  398. 't' => 1099511627776,
  399. 'G' => 1073741824,
  400. 'g' => 1073741824,
  401. 'M' => 1048576,
  402. 'm' => 1048576,
  403. 'K' => 1024,
  404. 'k' => 1024,
  405. ];
  406. if (preg_match('/^([0-9]+)([KMGT])/i', (string) $size, $matches)) {
  407. return (int) ($matches[1] * $binaryprefixes[$matches[2]]);
  408. }
  409. return (int) $size;
  410. }
  411. /**
  412. * Checks given $page against given $allowList and returns true if valid
  413. * it optionally ignores query parameters in $page (script.php?ignored)
  414. *
  415. * @param string $page page to check
  416. * @param array $allowList allow list to check page against
  417. * @param bool $include whether the page is going to be included
  418. *
  419. * @return bool whether $page is valid or not (in $allowList or not)
  420. */
  421. public static function checkPageValidity(&$page, array $allowList = [], $include = false): bool
  422. {
  423. if (empty($allowList)) {
  424. $allowList = ['index.php'];
  425. }
  426. if (empty($page)) {
  427. return false;
  428. }
  429. if (in_array($page, $allowList)) {
  430. return true;
  431. }
  432. if ($include) {
  433. return false;
  434. }
  435. $_page = mb_substr(
  436. $page,
  437. 0,
  438. (int) mb_strpos($page . '?', '?')
  439. );
  440. if (in_array($_page, $allowList)) {
  441. return true;
  442. }
  443. $_page = urldecode($page);
  444. $_page = mb_substr(
  445. $_page,
  446. 0,
  447. (int) mb_strpos($_page . '?', '?')
  448. );
  449. return in_array($_page, $allowList);
  450. }
  451. /**
  452. * tries to find the value for the given environment variable name
  453. *
  454. * searches in $_SERVER, $_ENV then tries getenv() and apache_getenv()
  455. * in this order
  456. *
  457. * @param string $var_name variable name
  458. *
  459. * @return string value of $var or empty string
  460. */
  461. public static function getenv(string $var_name): string
  462. {
  463. if (isset($_SERVER[$var_name])) {
  464. return (string) $_SERVER[$var_name];
  465. }
  466. if (isset($_ENV[$var_name])) {
  467. return (string) $_ENV[$var_name];
  468. }
  469. if (getenv($var_name)) {
  470. return (string) getenv($var_name);
  471. }
  472. if (function_exists('apache_getenv')
  473. && apache_getenv($var_name, true)
  474. ) {
  475. return (string) apache_getenv($var_name, true);
  476. }
  477. return '';
  478. }
  479. /**
  480. * Send HTTP header, taking IIS limits into account (600 seems ok)
  481. *
  482. * @param string $uri the header to send
  483. * @param bool $use_refresh whether to use Refresh: header when running on IIS
  484. */
  485. public static function sendHeaderLocation(string $uri, bool $use_refresh = false): void
  486. {
  487. if ($GLOBALS['PMA_Config']->get('PMA_IS_IIS') && mb_strlen($uri) > 600) {
  488. Response::getInstance()->disable();
  489. $template = new Template();
  490. echo $template->render('header_location', ['uri' => $uri]);
  491. return;
  492. }
  493. /*
  494. * Avoid relative path redirect problems in case user entered URL
  495. * like /phpmyadmin/index.php/ which some web servers happily accept.
  496. */
  497. if ($uri[0] === '.') {
  498. $uri = $GLOBALS['PMA_Config']->getRootPath() . substr($uri, 2);
  499. }
  500. $response = Response::getInstance();
  501. session_write_close();
  502. if ($response->headersSent()) {
  503. trigger_error(
  504. 'Core::sendHeaderLocation called when headers are already sent!',
  505. E_USER_ERROR
  506. );
  507. }
  508. // bug #1523784: IE6 does not like 'Refresh: 0', it
  509. // results in a blank page
  510. // but we need it when coming from the cookie login panel)
  511. if ($GLOBALS['PMA_Config']->get('PMA_IS_IIS') && $use_refresh) {
  512. $response->header('Refresh: 0; ' . $uri);
  513. } else {
  514. $response->header('Location: ' . $uri);
  515. }
  516. }
  517. /**
  518. * Outputs application/json headers. This includes no caching.
  519. */
  520. public static function headerJSON(): void
  521. {
  522. if (defined('TESTSUITE')) {
  523. return;
  524. }
  525. // No caching
  526. self::noCacheHeader();
  527. // MIME type
  528. header('Content-Type: application/json; charset=UTF-8');
  529. // Disable content sniffing in browser
  530. // This is needed in case we include HTML in JSON, browser might assume it's
  531. // html to display
  532. header('X-Content-Type-Options: nosniff');
  533. }
  534. /**
  535. * Outputs headers to prevent caching in browser (and on the way).
  536. */
  537. public static function noCacheHeader(): void
  538. {
  539. if (defined('TESTSUITE')) {
  540. return;
  541. }
  542. // rfc2616 - Section 14.21
  543. header('Expires: ' . gmdate(DATE_RFC1123));
  544. // HTTP/1.1
  545. header(
  546. 'Cache-Control: no-store, no-cache, must-revalidate,'
  547. . ' pre-check=0, post-check=0, max-age=0'
  548. );
  549. header('Pragma: no-cache'); // HTTP/1.0
  550. // test case: exporting a database into a .gz file with Safari
  551. // would produce files not having the current time
  552. // (added this header for Safari but should not harm other browsers)
  553. header('Last-Modified: ' . gmdate(DATE_RFC1123));
  554. }
  555. /**
  556. * Sends header indicating file download.
  557. *
  558. * @param string $filename Filename to include in headers if empty,
  559. * none Content-Disposition header will be sent.
  560. * @param string $mimetype MIME type to include in headers.
  561. * @param int $length Length of content (optional)
  562. * @param bool $no_cache Whether to include no-caching headers.
  563. */
  564. public static function downloadHeader(
  565. string $filename,
  566. string $mimetype,
  567. int $length = 0,
  568. bool $no_cache = true
  569. ): void {
  570. if ($no_cache) {
  571. self::noCacheHeader();
  572. }
  573. /* Replace all possibly dangerous chars in filename */
  574. $filename = Sanitize::sanitizeFilename($filename);
  575. if (! empty($filename)) {
  576. header('Content-Description: File Transfer');
  577. header('Content-Disposition: attachment; filename="' . $filename . '"');
  578. }
  579. header('Content-Type: ' . $mimetype);
  580. // inform the server that compression has been done,
  581. // to avoid a double compression (for example with Apache + mod_deflate)
  582. $notChromeOrLessThan43 = PMA_USR_BROWSER_AGENT != 'CHROME' // see bug #4942
  583. || (PMA_USR_BROWSER_AGENT == 'CHROME' && PMA_USR_BROWSER_VER < 43);
  584. if (strpos($mimetype, 'gzip') !== false && $notChromeOrLessThan43) {
  585. header('Content-Encoding: gzip');
  586. }
  587. header('Content-Transfer-Encoding: binary');
  588. if ($length <= 0) {
  589. return;
  590. }
  591. header('Content-Length: ' . $length);
  592. }
  593. /**
  594. * Returns value of an element in $array given by $path.
  595. * $path is a string describing position of an element in an associative array,
  596. * eg. Servers/1/host refers to $array[Servers][1][host]
  597. *
  598. * @param string $path path in the array
  599. * @param array $array the array
  600. * @param mixed $default default value
  601. *
  602. * @return array|mixed|null array element or $default
  603. */
  604. public static function arrayRead(string $path, array $array, $default = null)
  605. {
  606. $keys = explode('/', $path);
  607. $value =& $array;
  608. foreach ($keys as $key) {
  609. if (! isset($value[$key])) {
  610. return $default;
  611. }
  612. $value =& $value[$key];
  613. }
  614. return $value;
  615. }
  616. /**
  617. * Stores value in an array
  618. *
  619. * @param string $path path in the array
  620. * @param array $array the array
  621. * @param mixed $value value to store
  622. */
  623. public static function arrayWrite(string $path, array &$array, $value): void
  624. {
  625. $keys = explode('/', $path);
  626. $last_key = array_pop($keys);
  627. $a =& $array;
  628. foreach ($keys as $key) {
  629. if (! isset($a[$key])) {
  630. $a[$key] = [];
  631. }
  632. $a =& $a[$key];
  633. }
  634. $a[$last_key] = $value;
  635. }
  636. /**
  637. * Removes value from an array
  638. *
  639. * @param string $path path in the array
  640. * @param array $array the array
  641. */
  642. public static function arrayRemove(string $path, array &$array): void
  643. {
  644. $keys = explode('/', $path);
  645. $keys_last = array_pop($keys);
  646. $path = [];
  647. $depth = 0;
  648. $path[0] =& $array;
  649. $found = true;
  650. // go as deep as required or possible
  651. foreach ($keys as $key) {
  652. if (! isset($path[$depth][$key])) {
  653. $found = false;
  654. break;
  655. }
  656. $depth++;
  657. $path[$depth] =& $path[$depth - 1][$key];
  658. }
  659. // if element found, remove it
  660. if ($found) {
  661. unset($path[$depth][$keys_last]);
  662. $depth--;
  663. }
  664. // remove empty nested arrays
  665. for (; $depth >= 0; $depth--) {
  666. if (isset($path[$depth + 1]) && count($path[$depth + 1]) !== 0) {
  667. break;
  668. }
  669. unset($path[$depth][$keys[$depth]]);
  670. }
  671. }
  672. /**
  673. * Returns link to (possibly) external site using defined redirector.
  674. *
  675. * @param string $url URL where to go.
  676. *
  677. * @return string URL for a link.
  678. */
  679. public static function linkURL(string $url): string
  680. {
  681. if (! preg_match('#^https?://#', $url)) {
  682. return $url;
  683. }
  684. $params = [];
  685. $params['url'] = $url;
  686. $url = Url::getCommon($params);
  687. //strip off token and such sensitive information. Just keep url.
  688. $arr = parse_url($url);
  689. if (! is_array($arr)) {
  690. $arr = [];
  691. }
  692. parse_str($arr['query'] ?? '', $vars);
  693. $query = http_build_query(['url' => $vars['url']]);
  694. if ($GLOBALS['PMA_Config'] !== null && $GLOBALS['PMA_Config']->get('is_setup')) {
  695. $url = '../url.php?' . $query;
  696. } else {
  697. $url = './url.php?' . $query;
  698. }
  699. return $url;
  700. }
  701. /**
  702. * Checks whether domain of URL is an allowed domain or not.
  703. * Use only for URLs of external sites.
  704. *
  705. * @param string $url URL of external site.
  706. *
  707. * @return bool True: if domain of $url is allowed domain,
  708. * False: otherwise.
  709. */
  710. public static function isAllowedDomain(string $url): bool
  711. {
  712. $arr = parse_url($url);
  713. if (! is_array($arr)) {
  714. $arr = [];
  715. }
  716. // We need host to be set
  717. if (! isset($arr['host']) || strlen($arr['host']) == 0) {
  718. return false;
  719. }
  720. // We do not want these to be present
  721. $blocked = [
  722. 'user',
  723. 'pass',
  724. 'port',
  725. ];
  726. foreach ($blocked as $part) {
  727. if (isset($arr[$part]) && strlen((string) $arr[$part]) != 0) {
  728. return false;
  729. }
  730. }
  731. $domain = $arr['host'];
  732. $domainAllowList = [
  733. /* Include current domain */
  734. $_SERVER['SERVER_NAME'],
  735. /* phpMyAdmin domains */
  736. 'wiki.phpmyadmin.net',
  737. 'www.phpmyadmin.net',
  738. 'phpmyadmin.net',
  739. 'demo.phpmyadmin.net',
  740. 'docs.phpmyadmin.net',
  741. /* mysql.com domains */
  742. 'dev.mysql.com',
  743. 'bugs.mysql.com',
  744. /* mariadb domains */
  745. 'mariadb.org',
  746. 'mariadb.com',
  747. /* php.net domains */
  748. 'php.net',
  749. 'www.php.net',
  750. /* Github domains*/
  751. 'github.com',
  752. 'www.github.com',
  753. /* Percona domains */
  754. 'www.percona.com',
  755. /* Following are doubtful ones. */
  756. 'mysqldatabaseadministration.blogspot.com',
  757. ];
  758. return in_array($domain, $domainAllowList);
  759. }
  760. /**
  761. * Replace some html-unfriendly stuff
  762. *
  763. * @param string $buffer String to process
  764. *
  765. * @return string Escaped and cleaned up text suitable for html
  766. */
  767. public static function mimeDefaultFunction(string $buffer): string
  768. {
  769. $buffer = htmlspecialchars($buffer);
  770. $buffer = str_replace(' ', ' &nbsp;', $buffer);
  771. return (string) preg_replace("@((\015\012)|(\015)|(\012))@", '<br>' . "\n", $buffer);
  772. }
  773. /**
  774. * Displays SQL query before executing.
  775. *
  776. * @param array|string $query_data Array containing queries or query itself
  777. */
  778. public static function previewSQL($query_data): void
  779. {
  780. $retval = '<div class="preview_sql">';
  781. if (empty($query_data)) {
  782. $retval .= __('No change');
  783. } elseif (is_array($query_data)) {
  784. foreach ($query_data as $query) {
  785. $retval .= Html\Generator::formatSql($query);
  786. }
  787. } else {
  788. $retval .= Html\Generator::formatSql($query_data);
  789. }
  790. $retval .= '</div>';
  791. $response = Response::getInstance();
  792. $response->addJSON('sql_data', $retval);
  793. }
  794. /**
  795. * recursively check if variable is empty
  796. *
  797. * @param mixed $value the variable
  798. *
  799. * @return bool true if empty
  800. */
  801. public static function emptyRecursive($value): bool
  802. {
  803. $empty = true;
  804. if (is_array($value)) {
  805. array_walk_recursive(
  806. $value,
  807. /**
  808. * @param mixed $item
  809. */
  810. static function ($item) use (&$empty) {
  811. $empty = $empty && empty($item);
  812. }
  813. );
  814. } else {
  815. $empty = empty($value);
  816. }
  817. return $empty;
  818. }
  819. /**
  820. * Creates some globals from $_POST variables matching a pattern
  821. *
  822. * @param array $post_patterns The patterns to search for
  823. */
  824. public static function setPostAsGlobal(array $post_patterns): void
  825. {
  826. global $containerBuilder;
  827. foreach (array_keys($_POST) as $post_key) {
  828. foreach ($post_patterns as $one_post_pattern) {
  829. if (! preg_match($one_post_pattern, $post_key)) {
  830. continue;
  831. }
  832. $GLOBALS[$post_key] = $_POST[$post_key];
  833. $containerBuilder->setParameter($post_key, $GLOBALS[$post_key]);
  834. }
  835. }
  836. }
  837. public static function setDatabaseAndTableFromRequest(ContainerInterface $containerBuilder): void
  838. {
  839. global $db, $table, $url_params;
  840. $databaseFromRequest = $_POST['db'] ?? $_GET['db'] ?? $_REQUEST['db'] ?? null;
  841. $tableFromRequest = $_POST['table'] ?? $_GET['table'] ?? $_REQUEST['table'] ?? null;
  842. $db = self::isValid($databaseFromRequest) ? $databaseFromRequest : '';
  843. $table = self::isValid($tableFromRequest) ? $tableFromRequest : '';
  844. $url_params['db'] = $db;
  845. $url_params['table'] = $table;
  846. $containerBuilder->setParameter('db', $db);
  847. $containerBuilder->setParameter('table', $table);
  848. $containerBuilder->setParameter('url_params', $url_params);
  849. }
  850. /**
  851. * PATH_INFO could be compromised if set, so remove it from PHP_SELF
  852. * and provide a clean PHP_SELF here
  853. */
  854. public static function cleanupPathInfo(): void
  855. {
  856. global $PMA_PHP_SELF;
  857. $PMA_PHP_SELF = self::getenv('PHP_SELF');
  858. if (empty($PMA_PHP_SELF)) {
  859. $PMA_PHP_SELF = urldecode(self::getenv('REQUEST_URI'));
  860. }
  861. $_PATH_INFO = self::getenv('PATH_INFO');
  862. if (! empty($_PATH_INFO) && ! empty($PMA_PHP_SELF)) {
  863. $question_pos = mb_strpos($PMA_PHP_SELF, '?');
  864. if ($question_pos != false) {
  865. $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $question_pos);
  866. }
  867. $path_info_pos = mb_strrpos($PMA_PHP_SELF, $_PATH_INFO);
  868. if ($path_info_pos !== false) {
  869. $path_info_part = mb_substr($PMA_PHP_SELF, $path_info_pos, mb_strlen($_PATH_INFO));
  870. if ($path_info_part == $_PATH_INFO) {
  871. $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $path_info_pos);
  872. }
  873. }
  874. }
  875. $path = [];
  876. foreach (explode('/', $PMA_PHP_SELF) as $part) {
  877. // ignore parts that have no value
  878. if (empty($part) || $part === '.') {
  879. continue;
  880. }
  881. if ($part !== '..') {
  882. // cool, we found a new part
  883. $path[] = $part;
  884. } elseif (count($path) > 0) {
  885. // going back up? sure
  886. array_pop($path);
  887. }
  888. // Here we intentionall ignore case where we go too up
  889. // as there is nothing sane to do
  890. }
  891. $PMA_PHP_SELF = htmlspecialchars('/' . implode('/', $path));
  892. }
  893. /**
  894. * Checks that required PHP extensions are there.
  895. */
  896. public static function checkExtensions(): void
  897. {
  898. /**
  899. * Warning about mbstring.
  900. */
  901. if (! function_exists('mb_detect_encoding')) {
  902. self::warnMissingExtension('mbstring');
  903. }
  904. /**
  905. * We really need this one!
  906. */
  907. if (! function_exists('preg_replace')) {
  908. self::warnMissingExtension('pcre', true);
  909. }
  910. /**
  911. * JSON is required in several places.
  912. */
  913. if (! function_exists('json_encode')) {
  914. self::warnMissingExtension('json', true);
  915. }
  916. /**
  917. * ctype is required for Twig.
  918. */
  919. if (! function_exists('ctype_alpha')) {
  920. self::warnMissingExtension('ctype', true);
  921. }
  922. /**
  923. * hash is required for cookie authentication.
  924. */
  925. if (function_exists('hash_hmac')) {
  926. return;
  927. }
  928. self::warnMissingExtension('hash', true);
  929. }
  930. /**
  931. * Gets the "true" IP address of the current user
  932. *
  933. * @return string|bool the ip of the user
  934. *
  935. * @access private
  936. */
  937. public static function getIp()
  938. {
  939. /* Get the address of user */
  940. if (empty($_SERVER['REMOTE_ADDR'])) {
  941. /* We do not know remote IP */
  942. return false;
  943. }
  944. $direct_ip = $_SERVER['REMOTE_ADDR'];
  945. /* Do we trust this IP as a proxy? If yes we will use it's header. */
  946. if (! isset($GLOBALS['cfg']['TrustedProxies'][$direct_ip])) {
  947. /* Return true IP */
  948. return $direct_ip;
  949. }
  950. /**
  951. * Parse header in form:
  952. * X-Forwarded-For: client, proxy1, proxy2
  953. */
  954. // Get header content
  955. $value = self::getenv($GLOBALS['cfg']['TrustedProxies'][$direct_ip]);
  956. // Grab first element what is client adddress
  957. $value = explode(',', $value)[0];
  958. // checks that the header contains only one IP address,
  959. $is_ip = filter_var($value, FILTER_VALIDATE_IP);
  960. if ($is_ip !== false) {
  961. // True IP behind a proxy
  962. return $value;
  963. }
  964. // We could not parse header
  965. return false;
  966. }
  967. /**
  968. * Sanitizes MySQL hostname
  969. *
  970. * * strips p: prefix(es)
  971. *
  972. * @param string $name User given hostname
  973. */
  974. public static function sanitizeMySQLHost(string $name): string
  975. {
  976. while (strtolower(substr($name, 0, 2)) === 'p:') {
  977. $name = substr($name, 2);
  978. }
  979. return $name;
  980. }
  981. /**
  982. * Sanitizes MySQL username
  983. *
  984. * * strips part behind null byte
  985. *
  986. * @param string $name User given username
  987. */
  988. public static function sanitizeMySQLUser(string $name): string
  989. {
  990. $position = strpos($name, chr(0));
  991. if ($position !== false) {
  992. return substr($name, 0, $position);
  993. }
  994. return $name;
  995. }
  996. /**
  997. * Safe unserializer wrapper
  998. *
  999. * It does not unserialize data containing objects
  1000. *
  1001. * @param string $data Data to unserialize
  1002. *
  1003. * @return mixed|null
  1004. */
  1005. public static function safeUnserialize(string $data)
  1006. {
  1007. if (! is_string($data)) {
  1008. return null;
  1009. }
  1010. /* validate serialized data */
  1011. $length = strlen($data);
  1012. $depth = 0;
  1013. for ($i = 0; $i < $length; $i++) {
  1014. $value = $data[$i];
  1015. switch ($value) {
  1016. case '}':
  1017. /* end of array */
  1018. if ($depth <= 0) {
  1019. return null;
  1020. }
  1021. $depth--;
  1022. break;
  1023. case 's':
  1024. /* string */
  1025. // parse sting length
  1026. $strlen = intval(substr($data, $i + 2));
  1027. // string start
  1028. $i = strpos($data, ':', $i + 2);
  1029. if ($i === false) {
  1030. return null;
  1031. }
  1032. // skip string, quotes and ;
  1033. $i += 2 + $strlen + 1;
  1034. if ($data[$i] !== ';') {
  1035. return null;
  1036. }
  1037. break;
  1038. case 'b':
  1039. case 'i':
  1040. case 'd':
  1041. /* bool, integer or double */
  1042. // skip value to separator
  1043. $i = strpos($data, ';', $i);
  1044. if ($i === false) {
  1045. return null;
  1046. }
  1047. break;
  1048. case 'a':
  1049. /* array */
  1050. // find array start
  1051. $i = strpos($data, '{', $i);
  1052. if ($i === false) {
  1053. return null;
  1054. }
  1055. // remember nesting
  1056. $depth++;
  1057. break;
  1058. case 'N':
  1059. /* null */
  1060. // skip to end
  1061. $i = strpos($data, ';', $i);
  1062. if ($i === false) {
  1063. return null;
  1064. }
  1065. break;
  1066. default:
  1067. /* any other elements are not wanted */
  1068. return null;
  1069. }
  1070. }
  1071. // check unterminated arrays
  1072. if ($depth > 0) {
  1073. return null;
  1074. }
  1075. return unserialize($data);
  1076. }
  1077. /**
  1078. * Applies changes to PHP configuration.
  1079. */
  1080. public static function configure(): void
  1081. {
  1082. /**
  1083. * Set utf-8 encoding for PHP
  1084. */
  1085. ini_set('default_charset', 'utf-8');
  1086. mb_internal_encoding('utf-8');
  1087. /**
  1088. * Set precision to sane value, with higher values
  1089. * things behave slightly unexpectedly, for example
  1090. * round(1.2, 2) returns 1.199999999999999956.
  1091. */
  1092. ini_set('precision', '14');
  1093. /**
  1094. * check timezone setting
  1095. * this could produce an E_WARNING - but only once,
  1096. * if not done here it will produce E_WARNING on every date/time function
  1097. */
  1098. date_default_timezone_set(@date_default_timezone_get());
  1099. }
  1100. /**
  1101. * Check whether PHP configuration matches our needs.
  1102. */
  1103. public static function checkConfiguration(): void
  1104. {
  1105. /**
  1106. * As we try to handle charsets by ourself, mbstring overloads just
  1107. * break it, see bug 1063821.
  1108. *
  1109. * We specifically use empty here as we are looking for anything else than
  1110. * empty value or 0.
  1111. */
  1112. if (extension_loaded('mbstring') && ! empty(ini_get('mbstring.func_overload'))) {
  1113. self::fatalError(
  1114. __(
  1115. 'You have enabled mbstring.func_overload in your PHP '
  1116. . 'configuration. This option is incompatible with phpMyAdmin '
  1117. . 'and might cause some data to be corrupted!'
  1118. )
  1119. );
  1120. }
  1121. /**
  1122. * The ini_set and ini_get functions can be disabled using
  1123. * disable_functions but we're relying quite a lot of them.
  1124. */
  1125. if (function_exists('ini_get') && function_exists('ini_set')) {
  1126. return;
  1127. }
  1128. self::fatalError(
  1129. __(
  1130. 'The ini_get and/or ini_set functions are disabled in php.ini. '
  1131. . 'phpMyAdmin requires these functions!'
  1132. )
  1133. );
  1134. }
  1135. /**
  1136. * Checks request and fails with fatal error if something problematic is found
  1137. */
  1138. public static function checkRequest(): void
  1139. {
  1140. if (isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS'])) {
  1141. self::fatalError(__('GLOBALS overwrite attempt'));
  1142. }
  1143. /**
  1144. * protect against possible exploits - there is no need to have so much variables
  1145. */
  1146. if (count($_REQUEST) <= 1000) {
  1147. return;
  1148. }
  1149. self::fatalError(__('possible exploit'));
  1150. }
  1151. /**
  1152. * Sign the sql query using hmac using the session token
  1153. *
  1154. * @param string $sqlQuery The sql query
  1155. *
  1156. * @return string
  1157. */
  1158. public static function signSqlQuery($sqlQuery)
  1159. {
  1160. global $cfg;
  1161. $secret = $_SESSION[' HMAC_secret '] ?? '';
  1162. return hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']);
  1163. }
  1164. /**
  1165. * Check that the sql query has a valid hmac signature
  1166. *
  1167. * @param string $sqlQuery The sql query
  1168. * @param string $signature The Signature to check
  1169. *
  1170. * @return bool
  1171. */
  1172. public static function checkSqlQuerySignature($sqlQuery, $signature)
  1173. {
  1174. global $cfg;
  1175. $secret = $_SESSION[' HMAC_secret '] ?? '';
  1176. $hmac = hash_hmac('sha256', $sqlQuery, $secret . $cfg['blowfish_secret']);
  1177. return hash_equals($hmac, $signature);
  1178. }
  1179. /**
  1180. * Check whether user supplied token is valid, if not remove any possibly
  1181. * dangerous stuff from request.
  1182. *
  1183. * Check for token mismatch only if the Request method is POST.
  1184. * GET Requests would never have token and therefore checking
  1185. * mis-match does not make sense.
  1186. */
  1187. public static function checkTokenRequestParam(): void
  1188. {
  1189. global $token_mismatch, $token_provided;
  1190. $token_mismatch = true;
  1191. $token_provided = false;
  1192. if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  1193. return;
  1194. }
  1195. if (self::isValid($_POST['token'])) {
  1196. $token_provided = true;
  1197. $token_mismatch = ! @hash_equals($_SESSION[' PMA_token '], $_POST['token']);
  1198. }
  1199. if (! $token_mismatch) {
  1200. return;
  1201. }
  1202. // Warn in case the mismatch is result of failed setting of session cookie
  1203. if (isset($_POST['set_session']) && $_POST['set_session'] !== session_id()) {
  1204. trigger_error(
  1205. __(
  1206. 'Failed to set session cookie. Maybe you are using '
  1207. . 'HTTP instead of HTTPS to access phpMyAdmin.'
  1208. ),
  1209. E_USER_ERROR
  1210. );
  1211. }
  1212. /**
  1213. * We don't allow any POST operation parameters if the token is mismatched
  1214. * or is not provided.
  1215. */
  1216. $allowList = ['ajax_request'];
  1217. Sanitize::removeRequestVars($allowList);
  1218. }
  1219. public static function setGotoAndBackGlobals(ContainerInterface $container, Config $config): void
  1220. {
  1221. global $goto, $back, $url_params;
  1222. // Holds page that should be displayed.
  1223. $goto = '';
  1224. $container->setParameter('goto', $goto);
  1225. if (isset($_REQUEST['goto']) && self::checkPageValidity($_REQUEST['goto'])) {
  1226. $goto = $_REQUEST['goto'];
  1227. $url_params['goto'] = $goto;
  1228. $container->setParameter('goto', $goto);
  1229. $container->setParameter('url_params', $url_params);
  1230. } else {
  1231. if ($config->issetCookie('goto')) {
  1232. $config->removeCookie('goto');
  1233. }
  1234. unset($_REQUEST['goto'], $_GET['goto'], $_POST['goto']);
  1235. }
  1236. if (isset($_REQUEST['back']) && self::checkPageValidity($_REQUEST['back'])) {
  1237. // Returning page.
  1238. $back = $_REQUEST['back'];
  1239. $container->setParameter('back', $back);
  1240. } else {
  1241. if ($config->issetCookie('back')) {
  1242. $config->removeCookie('back');
  1243. }
  1244. unset($_REQUEST['back'], $_GET['back'], $_POST['back']);
  1245. }
  1246. }
  1247. public static function connectToDatabaseServer(DatabaseInterface $dbi, AuthenticationPlugin $auth): void
  1248. {
  1249. global $cfg;
  1250. /**
  1251. * Try to connect MySQL with the control user profile (will be used to get the privileges list for the current
  1252. * user but the true user link must be open after this one so it would be default one for all the scripts).
  1253. */
  1254. $controlLink = false;
  1255. if ($cfg['Server']['controluser'] !== '') {
  1256. $controlLink = $dbi->connect(DatabaseInterface::CONNECT_CONTROL);
  1257. }
  1258. // Connects to the server (validates user's login)
  1259. $userLink = $dbi->connect(DatabaseInterface::CONNECT_USER);
  1260. if ($userLink === false) {
  1261. $auth->showFailure('mysql-denied');
  1262. }
  1263. if ($controlLink) {
  1264. return;
  1265. }
  1266. /**
  1267. * Open separate connection for control queries, this is needed to avoid problems with table locking used in
  1268. * main connection and phpMyAdmin issuing queries to configuration storage, which is not locked by that time.
  1269. */
  1270. $dbi->connect(DatabaseInterface::CONNECT_USER, null, DatabaseInterface::CONNECT_CONTROL);
  1271. }
  1272. /**
  1273. * Get the container builder
  1274. */
  1275. public static function getContainerBuilder(): ContainerBuilder
  1276. {
  1277. $containerBuilder = new ContainerBuilder();
  1278. $loader = new PhpFileLoader($containerBuilder, new FileLocator(ROOT_PATH . 'libraries'));
  1279. $loader->load('services_loader.php');
  1280. return $containerBuilder;
  1281. }
  1282. }