PageRenderTime 51ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Common.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 620 lines | 334 code | 108 blank | 178 comment | 57 complexity | 96ef3e9d060ea58bcfc207a83b1d19b9 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use InvalidArgumentException;
  5. use PhpMyAdmin\Dbal\DatabaseName;
  6. use PhpMyAdmin\Dbal\TableName;
  7. use PhpMyAdmin\Http\Factory\ServerRequestFactory;
  8. use PhpMyAdmin\Http\ServerRequest;
  9. use PhpMyAdmin\Plugins\AuthenticationPlugin;
  10. use PhpMyAdmin\SqlParser\Lexer;
  11. use Symfony\Component\DependencyInjection\ContainerInterface;
  12. use Webmozart\Assert\Assert;
  13. use function __;
  14. use function array_pop;
  15. use function count;
  16. use function date_default_timezone_get;
  17. use function date_default_timezone_set;
  18. use function define;
  19. use function defined;
  20. use function explode;
  21. use function extension_loaded;
  22. use function function_exists;
  23. use function hash_equals;
  24. use function htmlspecialchars;
  25. use function implode;
  26. use function ini_get;
  27. use function ini_set;
  28. use function is_array;
  29. use function is_scalar;
  30. use function mb_internal_encoding;
  31. use function mb_strlen;
  32. use function mb_strpos;
  33. use function mb_strrpos;
  34. use function mb_substr;
  35. use function register_shutdown_function;
  36. use function session_id;
  37. use function strlen;
  38. use function trigger_error;
  39. use function urldecode;
  40. use const E_USER_ERROR;
  41. final class Common
  42. {
  43. /**
  44. * Misc stuff and REQUIRED by ALL the scripts.
  45. * MUST be included by every script
  46. *
  47. * Among other things, it contains the advanced authentication work.
  48. *
  49. * Order of sections:
  50. *
  51. * the authentication libraries must be before the connection to db
  52. *
  53. * ... so the required order is:
  54. *
  55. * LABEL_variables_init
  56. * - initialize some variables always needed
  57. * LABEL_parsing_config_file
  58. * - parsing of the configuration file
  59. * LABEL_loading_language_file
  60. * - loading language file
  61. * LABEL_setup_servers
  62. * - check and setup configured servers
  63. * LABEL_theme_setup
  64. * - setting up themes
  65. *
  66. * - load of MySQL extension (if necessary)
  67. * - loading of an authentication library
  68. * - db connection
  69. * - authentication work
  70. */
  71. public static function run(): void
  72. {
  73. global $containerBuilder, $errorHandler, $config, $server, $dbi, $request;
  74. global $lang, $cfg, $isConfigLoading, $auth_plugin, $route, $theme;
  75. global $urlParams, $isMinimumCommon, $sql_query, $token_mismatch;
  76. $request = ServerRequestFactory::createFromGlobals();
  77. $route = Routing::getCurrentRoute();
  78. if ($route === '/import-status') {
  79. $isMinimumCommon = true;
  80. }
  81. $containerBuilder = Core::getContainerBuilder();
  82. /** @var ErrorHandler $errorHandler */
  83. $errorHandler = $containerBuilder->get('error_handler');
  84. self::checkRequiredPhpExtensions();
  85. self::configurePhpSettings();
  86. self::cleanupPathInfo();
  87. /* parsing configuration file LABEL_parsing_config_file */
  88. /** @var bool $isConfigLoading Indication for the error handler */
  89. $isConfigLoading = false;
  90. register_shutdown_function([Config::class, 'fatalErrorHandler']);
  91. /**
  92. * Force reading of config file, because we removed sensitive values
  93. * in the previous iteration.
  94. *
  95. * @var Config $config
  96. */
  97. $config = $containerBuilder->get('config');
  98. /**
  99. * include session handling after the globals, to prevent overwriting
  100. */
  101. if (! defined('PMA_NO_SESSION')) {
  102. Session::setUp($config, $errorHandler);
  103. }
  104. /**
  105. * init some variables LABEL_variables_init
  106. */
  107. /**
  108. * holds parameters to be passed to next page
  109. *
  110. * @global array $urlParams
  111. */
  112. $urlParams = [];
  113. $containerBuilder->setParameter('url_params', $urlParams);
  114. self::setGotoAndBackGlobals($containerBuilder, $config);
  115. self::checkTokenRequestParam();
  116. self::setDatabaseAndTableFromRequest($containerBuilder, $request);
  117. /**
  118. * SQL query to be executed
  119. *
  120. * @global string $sql_query
  121. */
  122. $sql_query = '';
  123. if ($request->isPost()) {
  124. $sql_query = $request->getParsedBodyParam('sql_query', '');
  125. }
  126. $containerBuilder->setParameter('sql_query', $sql_query);
  127. //$_REQUEST['set_theme'] // checked later in this file LABEL_theme_setup
  128. //$_REQUEST['server']; // checked later in this file
  129. //$_REQUEST['lang']; // checked by LABEL_loading_language_file
  130. /* loading language file LABEL_loading_language_file */
  131. /**
  132. * lang detection is done here
  133. */
  134. $language = LanguageManager::getInstance()->selectLanguage();
  135. $language->activate();
  136. /**
  137. * check for errors occurred while loading configuration
  138. * this check is done here after loading language files to present errors in locale
  139. */
  140. $config->checkPermissions();
  141. $config->checkErrors();
  142. self::checkServerConfiguration();
  143. self::checkRequest();
  144. /* setup servers LABEL_setup_servers */
  145. $config->checkServers();
  146. /**
  147. * current server
  148. *
  149. * @global integer $server
  150. */
  151. $server = $config->selectServer();
  152. $urlParams['server'] = $server;
  153. $containerBuilder->setParameter('server', $server);
  154. $containerBuilder->setParameter('url_params', $urlParams);
  155. $cfg = $config->settings;
  156. /* setup themes LABEL_theme_setup */
  157. $theme = ThemeManager::initializeTheme();
  158. /** @var DatabaseInterface $dbi */
  159. $dbi = null;
  160. if (isset($isMinimumCommon)) {
  161. $config->loadUserPreferences();
  162. $containerBuilder->set('theme_manager', ThemeManager::getInstance());
  163. Tracker::enable();
  164. return;
  165. }
  166. /**
  167. * save some settings in cookies
  168. *
  169. * @todo should be done in PhpMyAdmin\Config
  170. */
  171. $config->setCookie('pma_lang', (string) $lang);
  172. ThemeManager::getInstance()->setThemeCookie();
  173. $dbi = DatabaseInterface::load();
  174. $containerBuilder->set(DatabaseInterface::class, $dbi);
  175. $containerBuilder->setAlias('dbi', DatabaseInterface::class);
  176. if (! empty($cfg['Server'])) {
  177. $config->getLoginCookieValidityFromCache($server);
  178. $auth_plugin = Plugins::getAuthPlugin();
  179. $auth_plugin->authenticate();
  180. /* Enable LOAD DATA LOCAL INFILE for LDI plugin */
  181. if ($route === '/import' && ($_POST['format'] ?? '') === 'ldi') {
  182. // Switch this before the DB connection is done
  183. // phpcs:disable PSR1.Files.SideEffects
  184. define('PMA_ENABLE_LDI', 1);
  185. // phpcs:enable
  186. }
  187. self::connectToDatabaseServer($dbi, $auth_plugin);
  188. $auth_plugin->rememberCredentials();
  189. $auth_plugin->checkTwoFactor();
  190. /* Log success */
  191. Logging::logUser($cfg['Server']['user']);
  192. if ($dbi->getVersion() < $cfg['MysqlMinVersion']['internal']) {
  193. Core::fatalError(
  194. __('You should upgrade to %s %s or later.'),
  195. [
  196. 'MySQL',
  197. $cfg['MysqlMinVersion']['human'],
  198. ]
  199. );
  200. }
  201. // Sets the default delimiter (if specified).
  202. $sqlDelimiter = $request->getParam('sql_delimiter', '');
  203. if (strlen($sqlDelimiter) > 0) {
  204. // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  205. Lexer::$DEFAULT_DELIMITER = $sqlDelimiter;
  206. }
  207. // TODO: Set SQL modes too.
  208. } else { // end server connecting
  209. $response = ResponseRenderer::getInstance();
  210. $response->getHeader()->disableMenuAndConsole();
  211. $response->getFooter()->setMinimal();
  212. }
  213. $response = ResponseRenderer::getInstance();
  214. /**
  215. * There is no point in even attempting to process
  216. * an ajax request if there is a token mismatch
  217. */
  218. if ($response->isAjax() && $request->isPost() && $token_mismatch) {
  219. $response->setRequestStatus(false);
  220. $response->addJSON(
  221. 'message',
  222. Message::error(__('Error: Token mismatch'))
  223. );
  224. exit;
  225. }
  226. Profiling::check($dbi, $response);
  227. $containerBuilder->set('response', ResponseRenderer::getInstance());
  228. // load user preferences
  229. $config->loadUserPreferences();
  230. $containerBuilder->set('theme_manager', ThemeManager::getInstance());
  231. /* Tell tracker that it can actually work */
  232. Tracker::enable();
  233. if (empty($server) || ! isset($cfg['ZeroConf']) || $cfg['ZeroConf'] !== true) {
  234. return;
  235. }
  236. $dbi->postConnectControl();
  237. }
  238. /**
  239. * Checks that required PHP extensions are there.
  240. */
  241. private static function checkRequiredPhpExtensions(): void
  242. {
  243. /**
  244. * Warning about mbstring.
  245. */
  246. if (! function_exists('mb_detect_encoding')) {
  247. Core::warnMissingExtension('mbstring');
  248. }
  249. /**
  250. * We really need this one!
  251. */
  252. if (! function_exists('preg_replace')) {
  253. Core::warnMissingExtension('pcre', true);
  254. }
  255. /**
  256. * JSON is required in several places.
  257. */
  258. if (! function_exists('json_encode')) {
  259. Core::warnMissingExtension('json', true);
  260. }
  261. /**
  262. * ctype is required for Twig.
  263. */
  264. if (! function_exists('ctype_alpha')) {
  265. Core::warnMissingExtension('ctype', true);
  266. }
  267. /**
  268. * hash is required for cookie authentication.
  269. */
  270. if (function_exists('hash_hmac')) {
  271. return;
  272. }
  273. Core::warnMissingExtension('hash', true);
  274. }
  275. /**
  276. * Applies changes to PHP configuration.
  277. */
  278. private static function configurePhpSettings(): void
  279. {
  280. /**
  281. * Set utf-8 encoding for PHP
  282. */
  283. ini_set('default_charset', 'utf-8');
  284. mb_internal_encoding('utf-8');
  285. /**
  286. * Set precision to sane value, with higher values
  287. * things behave slightly unexpectedly, for example
  288. * round(1.2, 2) returns 1.199999999999999956.
  289. */
  290. ini_set('precision', '14');
  291. /**
  292. * check timezone setting
  293. * this could produce an E_WARNING - but only once,
  294. * if not done here it will produce E_WARNING on every date/time function
  295. */
  296. date_default_timezone_set(@date_default_timezone_get());
  297. }
  298. /**
  299. * PATH_INFO could be compromised if set, so remove it from PHP_SELF
  300. * and provide a clean PHP_SELF here
  301. */
  302. public static function cleanupPathInfo(): void
  303. {
  304. global $PMA_PHP_SELF;
  305. $PMA_PHP_SELF = Core::getenv('PHP_SELF');
  306. if (empty($PMA_PHP_SELF)) {
  307. $PMA_PHP_SELF = urldecode(Core::getenv('REQUEST_URI'));
  308. }
  309. $_PATH_INFO = Core::getenv('PATH_INFO');
  310. if (! empty($_PATH_INFO) && ! empty($PMA_PHP_SELF)) {
  311. $question_pos = mb_strpos($PMA_PHP_SELF, '?');
  312. if ($question_pos != false) {
  313. $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $question_pos);
  314. }
  315. $path_info_pos = mb_strrpos($PMA_PHP_SELF, $_PATH_INFO);
  316. if ($path_info_pos !== false) {
  317. $path_info_part = mb_substr($PMA_PHP_SELF, $path_info_pos, mb_strlen($_PATH_INFO));
  318. if ($path_info_part == $_PATH_INFO) {
  319. $PMA_PHP_SELF = mb_substr($PMA_PHP_SELF, 0, $path_info_pos);
  320. }
  321. }
  322. }
  323. $path = [];
  324. foreach (explode('/', $PMA_PHP_SELF) as $part) {
  325. // ignore parts that have no value
  326. if (empty($part) || $part === '.') {
  327. continue;
  328. }
  329. if ($part !== '..') {
  330. // cool, we found a new part
  331. $path[] = $part;
  332. } elseif (count($path) > 0) {
  333. // going back up? sure
  334. array_pop($path);
  335. }
  336. // Here we intentionall ignore case where we go too up
  337. // as there is nothing sane to do
  338. }
  339. $PMA_PHP_SELF = htmlspecialchars('/' . implode('/', $path));
  340. }
  341. private static function setGotoAndBackGlobals(ContainerInterface $container, Config $config): void
  342. {
  343. global $goto, $back, $urlParams;
  344. // Holds page that should be displayed.
  345. $goto = '';
  346. $container->setParameter('goto', $goto);
  347. if (isset($_REQUEST['goto']) && Core::checkPageValidity($_REQUEST['goto'])) {
  348. $goto = $_REQUEST['goto'];
  349. $urlParams['goto'] = $goto;
  350. $container->setParameter('goto', $goto);
  351. $container->setParameter('url_params', $urlParams);
  352. } else {
  353. if ($config->issetCookie('goto')) {
  354. $config->removeCookie('goto');
  355. }
  356. unset($_REQUEST['goto'], $_GET['goto'], $_POST['goto']);
  357. }
  358. if (isset($_REQUEST['back']) && Core::checkPageValidity($_REQUEST['back'])) {
  359. // Returning page.
  360. $back = $_REQUEST['back'];
  361. $container->setParameter('back', $back);
  362. return;
  363. }
  364. if ($config->issetCookie('back')) {
  365. $config->removeCookie('back');
  366. }
  367. unset($_REQUEST['back'], $_GET['back'], $_POST['back']);
  368. }
  369. /**
  370. * Check whether user supplied token is valid, if not remove any possibly
  371. * dangerous stuff from request.
  372. *
  373. * Check for token mismatch only if the Request method is POST.
  374. * GET Requests would never have token and therefore checking
  375. * mis-match does not make sense.
  376. */
  377. public static function checkTokenRequestParam(): void
  378. {
  379. global $token_mismatch, $token_provided;
  380. $token_mismatch = true;
  381. $token_provided = false;
  382. if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
  383. return;
  384. }
  385. if (isset($_POST['token']) && is_scalar($_POST['token']) && strlen((string) $_POST['token']) > 0) {
  386. $token_provided = true;
  387. $token_mismatch = ! @hash_equals($_SESSION[' PMA_token '], (string) $_POST['token']);
  388. }
  389. if (! $token_mismatch) {
  390. return;
  391. }
  392. // Warn in case the mismatch is result of failed setting of session cookie
  393. if (isset($_POST['set_session']) && $_POST['set_session'] !== session_id()) {
  394. trigger_error(
  395. __(
  396. 'Failed to set session cookie. Maybe you are using HTTP instead of HTTPS to access phpMyAdmin.'
  397. ),
  398. E_USER_ERROR
  399. );
  400. }
  401. /**
  402. * We don't allow any POST operation parameters if the token is mismatched
  403. * or is not provided.
  404. */
  405. $allowList = ['ajax_request'];
  406. Sanitize::removeRequestVars($allowList);
  407. }
  408. private static function setDatabaseAndTableFromRequest(
  409. ContainerInterface $containerBuilder,
  410. ServerRequest $request
  411. ): void {
  412. global $db, $table, $urlParams;
  413. $databaseFromRequest = $request->getParam('db');
  414. $tableFromRequest = $request->getParam('table');
  415. try {
  416. Assert::string($databaseFromRequest);
  417. $db = DatabaseName::fromString($databaseFromRequest)->getName();
  418. } catch (InvalidArgumentException $exception) {
  419. $db = '';
  420. }
  421. try {
  422. Assert::stringNotEmpty($db);
  423. Assert::string($tableFromRequest);
  424. $table = TableName::fromString($tableFromRequest)->getName();
  425. } catch (InvalidArgumentException $exception) {
  426. $table = '';
  427. }
  428. if (! is_array($urlParams)) {
  429. $urlParams = [];
  430. }
  431. $urlParams['db'] = $db;
  432. $urlParams['table'] = $table;
  433. $containerBuilder->setParameter('db', $db);
  434. $containerBuilder->setParameter('table', $table);
  435. $containerBuilder->setParameter('url_params', $urlParams);
  436. }
  437. /**
  438. * Check whether PHP configuration matches our needs.
  439. */
  440. private static function checkServerConfiguration(): void
  441. {
  442. /**
  443. * As we try to handle charsets by ourself, mbstring overloads just
  444. * break it, see bug 1063821.
  445. *
  446. * We specifically use empty here as we are looking for anything else than
  447. * empty value or 0.
  448. */
  449. if (extension_loaded('mbstring') && ! empty(ini_get('mbstring.func_overload'))) {
  450. Core::fatalError(
  451. __(
  452. 'You have enabled mbstring.func_overload in your PHP '
  453. . 'configuration. This option is incompatible with phpMyAdmin '
  454. . 'and might cause some data to be corrupted!'
  455. )
  456. );
  457. }
  458. /**
  459. * The ini_set and ini_get functions can be disabled using
  460. * disable_functions but we're relying quite a lot of them.
  461. */
  462. if (function_exists('ini_get') && function_exists('ini_set')) {
  463. return;
  464. }
  465. Core::fatalError(
  466. __(
  467. 'The ini_get and/or ini_set functions are disabled in php.ini. phpMyAdmin requires these functions!'
  468. )
  469. );
  470. }
  471. /**
  472. * Checks request and fails with fatal error if something problematic is found
  473. */
  474. private static function checkRequest(): void
  475. {
  476. if (isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS'])) {
  477. Core::fatalError(__('GLOBALS overwrite attempt'));
  478. }
  479. /**
  480. * protect against possible exploits - there is no need to have so much variables
  481. */
  482. if (count($_REQUEST) <= 1000) {
  483. return;
  484. }
  485. Core::fatalError(__('possible exploit'));
  486. }
  487. private static function connectToDatabaseServer(DatabaseInterface $dbi, AuthenticationPlugin $auth): void
  488. {
  489. global $cfg;
  490. /**
  491. * Try to connect MySQL with the control user profile (will be used to get the privileges list for the current
  492. * user but the true user link must be open after this one so it would be default one for all the scripts).
  493. */
  494. $controlLink = false;
  495. if ($cfg['Server']['controluser'] !== '') {
  496. $controlLink = $dbi->connect(DatabaseInterface::CONNECT_CONTROL);
  497. }
  498. // Connects to the server (validates user's login)
  499. $userLink = $dbi->connect(DatabaseInterface::CONNECT_USER);
  500. if ($userLink === false) {
  501. $auth->showFailure('mysql-denied');
  502. }
  503. if ($controlLink) {
  504. return;
  505. }
  506. /**
  507. * Open separate connection for control queries, this is needed to avoid problems with table locking used in
  508. * main connection and phpMyAdmin issuing queries to configuration storage, which is not locked by that time.
  509. */
  510. $dbi->connect(DatabaseInterface::CONNECT_USER, null, DatabaseInterface::CONNECT_CONTROL);
  511. }
  512. }