PageRenderTime 33ms CodeModel.GetById 4ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Config/Validator.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 613 lines | 379 code | 72 blank | 162 comment | 51 complexity | 0072616298b2d584873cb30f29f66b33 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. /**
  3. * Form validation for configuration editor
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin\Config;
  7. use PhpMyAdmin\Core;
  8. use PhpMyAdmin\Util;
  9. use function __;
  10. use function array_map;
  11. use function array_merge;
  12. use function array_shift;
  13. use function call_user_func_array;
  14. use function count;
  15. use function error_clear_last;
  16. use function error_get_last;
  17. use function explode;
  18. use function filter_var;
  19. use function htmlspecialchars;
  20. use function intval;
  21. use function is_array;
  22. use function is_object;
  23. use function mb_strpos;
  24. use function mb_substr;
  25. use function mysqli_close;
  26. use function mysqli_connect;
  27. use function mysqli_report;
  28. use function preg_match;
  29. use function preg_replace;
  30. use function sprintf;
  31. use function str_replace;
  32. use function trim;
  33. use const FILTER_FLAG_IPV4;
  34. use const FILTER_FLAG_IPV6;
  35. use const FILTER_VALIDATE_IP;
  36. use const MYSQLI_REPORT_OFF;
  37. use const PHP_INT_MAX;
  38. /**
  39. * Validation class for various validation functions
  40. *
  41. * Validation function takes two argument: id for which it is called
  42. * and array of fields' values (usually values for entire formset).
  43. * The function must always return an array with an error (or error array)
  44. * assigned to a form element (formset name or field path). Even if there are
  45. * no errors, key must be set with an empty value.
  46. *
  47. * Validation functions are assigned in $cfg_db['_validators'] (config.values.php).
  48. */
  49. class Validator
  50. {
  51. /**
  52. * Returns validator list
  53. *
  54. * @param ConfigFile $cf Config file instance
  55. *
  56. * @return array
  57. */
  58. public static function getValidators(ConfigFile $cf)
  59. {
  60. static $validators = null;
  61. if ($validators !== null) {
  62. return $validators;
  63. }
  64. $validators = $cf->getDbEntry('_validators', []);
  65. if ($GLOBALS['config']->get('is_setup')) {
  66. return $validators;
  67. }
  68. // not in setup script: load additional validators for user
  69. // preferences we need original config values not overwritten
  70. // by user preferences, creating a new PhpMyAdmin\Config instance is a
  71. // better idea than hacking into its code
  72. $uvs = $cf->getDbEntry('_userValidators', []);
  73. foreach ($uvs as $field => $uvList) {
  74. $uvList = (array) $uvList;
  75. foreach ($uvList as &$uv) {
  76. if (! is_array($uv)) {
  77. continue;
  78. }
  79. for ($i = 1, $nb = count($uv); $i < $nb; $i++) {
  80. if (mb_substr($uv[$i], 0, 6) !== 'value:') {
  81. continue;
  82. }
  83. $uv[$i] = Core::arrayRead(
  84. mb_substr($uv[$i], 6),
  85. $GLOBALS['config']->baseSettings
  86. );
  87. }
  88. }
  89. $validators[$field] = isset($validators[$field])
  90. ? array_merge((array) $validators[$field], $uvList)
  91. : $uvList;
  92. }
  93. return $validators;
  94. }
  95. /**
  96. * Runs validation $validator_id on values $values and returns error list.
  97. *
  98. * Return values:
  99. * o array, keys - field path or formset id, values - array of errors
  100. * when $isPostSource is true values is an empty array to allow for error list
  101. * cleanup in HTML document
  102. * o false - when no validators match name(s) given by $validator_id
  103. *
  104. * @param ConfigFile $cf Config file instance
  105. * @param string|array $validatorId ID of validator(s) to run
  106. * @param array $values Values to validate
  107. * @param bool $isPostSource tells whether $values are directly from
  108. * POST request
  109. *
  110. * @return bool|array
  111. */
  112. public static function validate(
  113. ConfigFile $cf,
  114. $validatorId,
  115. array &$values,
  116. $isPostSource
  117. ) {
  118. // find validators
  119. $validatorId = (array) $validatorId;
  120. $validators = static::getValidators($cf);
  121. $vids = [];
  122. foreach ($validatorId as &$vid) {
  123. $vid = $cf->getCanonicalPath($vid);
  124. if (! isset($validators[$vid])) {
  125. continue;
  126. }
  127. $vids[] = $vid;
  128. }
  129. if (empty($vids)) {
  130. return false;
  131. }
  132. // create argument list with canonical paths and remember path mapping
  133. $arguments = [];
  134. $keyMap = [];
  135. foreach ($values as $k => $v) {
  136. $k2 = $isPostSource ? str_replace('-', '/', $k) : $k;
  137. $k2 = mb_strpos($k2, '/')
  138. ? $cf->getCanonicalPath($k2)
  139. : $k2;
  140. $keyMap[$k2] = $k;
  141. $arguments[$k2] = $v;
  142. }
  143. // validate
  144. $result = [];
  145. foreach ($vids as $vid) {
  146. // call appropriate validation functions
  147. foreach ((array) $validators[$vid] as $validator) {
  148. $vdef = (array) $validator;
  149. $vname = array_shift($vdef);
  150. /** @var callable $vname */
  151. $vname = 'PhpMyAdmin\Config\Validator::' . $vname;
  152. $args = array_merge([$vid, &$arguments], $vdef);
  153. $r = call_user_func_array($vname, $args);
  154. // merge results
  155. if (! is_array($r)) {
  156. continue;
  157. }
  158. foreach ($r as $key => $errorList) {
  159. // skip empty values if $isPostSource is false
  160. if (! $isPostSource && empty($errorList)) {
  161. continue;
  162. }
  163. if (! isset($result[$key])) {
  164. $result[$key] = [];
  165. }
  166. $errorList = array_map('PhpMyAdmin\Sanitize::sanitizeMessage', (array) $errorList);
  167. $result[$key] = array_merge($result[$key], $errorList);
  168. }
  169. }
  170. }
  171. // restore original paths
  172. $newResult = [];
  173. foreach ($result as $k => $v) {
  174. $k2 = $keyMap[$k] ?? $k;
  175. $newResult[$k2] = $v;
  176. }
  177. return empty($newResult) ? true : $newResult;
  178. }
  179. /**
  180. * Test database connection
  181. *
  182. * @param string $host host name
  183. * @param string $port tcp port to use
  184. * @param string $socket socket to use
  185. * @param string $user username to use
  186. * @param string $pass password to use
  187. * @param string $errorKey key to use in return array
  188. *
  189. * @return bool|array
  190. */
  191. public static function testDBConnection(
  192. $host,
  193. $port,
  194. $socket,
  195. $user,
  196. $pass = null,
  197. $errorKey = 'Server'
  198. ) {
  199. if ($GLOBALS['cfg']['DBG']['demo']) {
  200. // Connection test disabled on the demo server!
  201. return true;
  202. }
  203. $error = null;
  204. $host = Core::sanitizeMySQLHost($host);
  205. error_clear_last();
  206. /** @var string $socket */
  207. $socket = empty($socket) ? null : $socket;
  208. /** @var int $port */
  209. $port = empty($port) ? null : (int) $port;
  210. mysqli_report(MYSQLI_REPORT_OFF);
  211. $conn = @mysqli_connect($host, $user, (string) $pass, '', $port, $socket);
  212. if (! $conn) {
  213. $error = __('Could not connect to the database server!');
  214. } else {
  215. mysqli_close($conn);
  216. }
  217. if ($error !== null) {
  218. $lastError = error_get_last();
  219. if ($lastError !== null) {
  220. $error .= ' - ' . $lastError['message'];
  221. }
  222. }
  223. return $error === null ? true : [$errorKey => $error];
  224. }
  225. /**
  226. * Validate server config
  227. *
  228. * @param string $path path to config, not used
  229. * keep this parameter since the method is invoked using
  230. * reflection along with other similar methods
  231. * @param array $values config values
  232. *
  233. * @return array
  234. */
  235. public static function validateServer($path, array $values)
  236. {
  237. $result = [
  238. 'Server' => '',
  239. 'Servers/1/user' => '',
  240. 'Servers/1/SignonSession' => '',
  241. 'Servers/1/SignonURL' => '',
  242. ];
  243. $error = false;
  244. if (empty($values['Servers/1/auth_type'])) {
  245. $values['Servers/1/auth_type'] = '';
  246. $result['Servers/1/auth_type'] = __('Invalid authentication type!');
  247. $error = true;
  248. }
  249. if ($values['Servers/1/auth_type'] === 'config' && empty($values['Servers/1/user'])) {
  250. $result['Servers/1/user'] = __('Empty username while using [kbd]config[/kbd] authentication method!');
  251. $error = true;
  252. }
  253. if ($values['Servers/1/auth_type'] === 'signon' && empty($values['Servers/1/SignonSession'])) {
  254. $result['Servers/1/SignonSession'] = __(
  255. 'Empty signon session name while using [kbd]signon[/kbd] authentication method!'
  256. );
  257. $error = true;
  258. }
  259. if ($values['Servers/1/auth_type'] === 'signon' && empty($values['Servers/1/SignonURL'])) {
  260. $result['Servers/1/SignonURL'] = __(
  261. 'Empty signon URL while using [kbd]signon[/kbd] authentication method!'
  262. );
  263. $error = true;
  264. }
  265. if (! $error && $values['Servers/1/auth_type'] === 'config') {
  266. $password = '';
  267. if (! empty($values['Servers/1/password'])) {
  268. $password = $values['Servers/1/password'];
  269. }
  270. $test = static::testDBConnection(
  271. empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'],
  272. empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'],
  273. empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'],
  274. empty($values['Servers/1/user']) ? '' : $values['Servers/1/user'],
  275. $password,
  276. 'Server'
  277. );
  278. if (is_array($test)) {
  279. $result = array_merge($result, $test);
  280. }
  281. }
  282. return $result;
  283. }
  284. /**
  285. * Validate pmadb config
  286. *
  287. * @param string $path path to config, not used
  288. * keep this parameter since the method is invoked using
  289. * reflection along with other similar methods
  290. * @param array $values config values
  291. *
  292. * @return array
  293. */
  294. public static function validatePMAStorage($path, array $values)
  295. {
  296. $result = [
  297. 'Server_pmadb' => '',
  298. 'Servers/1/controluser' => '',
  299. 'Servers/1/controlpass' => '',
  300. ];
  301. $error = false;
  302. if (empty($values['Servers/1/pmadb'])) {
  303. return $result;
  304. }
  305. $result = [];
  306. if (empty($values['Servers/1/controluser'])) {
  307. $result['Servers/1/controluser'] = __(
  308. 'Empty phpMyAdmin control user while using phpMyAdmin configuration storage!'
  309. );
  310. $error = true;
  311. }
  312. if (empty($values['Servers/1/controlpass'])) {
  313. $result['Servers/1/controlpass'] = __(
  314. 'Empty phpMyAdmin control user password while using phpMyAdmin configuration storage!'
  315. );
  316. $error = true;
  317. }
  318. if (! $error) {
  319. $test = static::testDBConnection(
  320. empty($values['Servers/1/host']) ? '' : $values['Servers/1/host'],
  321. empty($values['Servers/1/port']) ? '' : $values['Servers/1/port'],
  322. empty($values['Servers/1/socket']) ? '' : $values['Servers/1/socket'],
  323. empty($values['Servers/1/controluser']) ? '' : $values['Servers/1/controluser'],
  324. empty($values['Servers/1/controlpass']) ? '' : $values['Servers/1/controlpass'],
  325. 'Server_pmadb'
  326. );
  327. if (is_array($test)) {
  328. $result = array_merge($result, $test);
  329. }
  330. }
  331. return $result;
  332. }
  333. /**
  334. * Validates regular expression
  335. *
  336. * @param string $path path to config
  337. * @param array $values config values
  338. *
  339. * @return array
  340. */
  341. public static function validateRegex($path, array $values)
  342. {
  343. $result = [$path => ''];
  344. if (empty($values[$path])) {
  345. return $result;
  346. }
  347. error_clear_last();
  348. $matches = [];
  349. // in libraries/ListDatabase.php _checkHideDatabase(),
  350. // a '/' is used as the delimiter for hide_db
  351. @preg_match('/' . Util::requestString($values[$path]) . '/', '', $matches);
  352. $currentError = error_get_last();
  353. if ($currentError !== null) {
  354. $error = preg_replace('/^preg_match\(\): /', '', $currentError['message']);
  355. return [$path => $error];
  356. }
  357. return $result;
  358. }
  359. /**
  360. * Validates TrustedProxies field
  361. *
  362. * @param string $path path to config
  363. * @param array $values config values
  364. *
  365. * @return array
  366. */
  367. public static function validateTrustedProxies($path, array $values)
  368. {
  369. $result = [$path => []];
  370. if (empty($values[$path])) {
  371. return $result;
  372. }
  373. if (is_array($values[$path]) || is_object($values[$path])) {
  374. // value already processed by FormDisplay::save
  375. $lines = [];
  376. foreach ($values[$path] as $ip => $v) {
  377. $v = Util::requestString($v);
  378. $lines[] = preg_match('/^-\d+$/', $ip)
  379. ? $v
  380. : $ip . ': ' . $v;
  381. }
  382. } else {
  383. // AJAX validation
  384. $lines = explode("\n", $values[$path]);
  385. }
  386. foreach ($lines as $line) {
  387. $line = trim($line);
  388. $matches = [];
  389. // we catch anything that may (or may not) be an IP
  390. if (! preg_match('/^(.+):(?:[ ]?)\\w+$/', $line, $matches)) {
  391. $result[$path][] = __('Incorrect value:') . ' '
  392. . htmlspecialchars($line);
  393. continue;
  394. }
  395. // now let's check whether we really have an IP address
  396. if (
  397. filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false
  398. && filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false
  399. ) {
  400. $ip = htmlspecialchars(trim($matches[1]));
  401. $result[$path][] = sprintf(__('Incorrect IP address: %s'), $ip);
  402. continue;
  403. }
  404. }
  405. return $result;
  406. }
  407. /**
  408. * Tests integer value
  409. *
  410. * @param string $path path to config
  411. * @param array $values config values
  412. * @param bool $allowNegative allow negative values
  413. * @param bool $allowZero allow zero
  414. * @param int $maxValue max allowed value
  415. * @param string $errorString error message string
  416. *
  417. * @return string empty string if test is successful
  418. */
  419. public static function validateNumber(
  420. $path,
  421. array $values,
  422. $allowNegative,
  423. $allowZero,
  424. $maxValue,
  425. $errorString
  426. ) {
  427. if (empty($values[$path])) {
  428. return '';
  429. }
  430. $value = Util::requestString($values[$path]);
  431. if (
  432. intval($value) != $value
  433. || (! $allowNegative && $value < 0)
  434. || (! $allowZero && $value == 0)
  435. || $value > $maxValue
  436. ) {
  437. return $errorString;
  438. }
  439. return '';
  440. }
  441. /**
  442. * Validates port number
  443. *
  444. * @param string $path path to config
  445. * @param array $values config values
  446. *
  447. * @return array
  448. */
  449. public static function validatePortNumber($path, array $values)
  450. {
  451. return [
  452. $path => static::validateNumber(
  453. $path,
  454. $values,
  455. false,
  456. false,
  457. 65535,
  458. __('Not a valid port number!')
  459. ),
  460. ];
  461. }
  462. /**
  463. * Validates positive number
  464. *
  465. * @param string $path path to config
  466. * @param array $values config values
  467. *
  468. * @return array
  469. */
  470. public static function validatePositiveNumber($path, array $values)
  471. {
  472. return [
  473. $path => static::validateNumber(
  474. $path,
  475. $values,
  476. false,
  477. false,
  478. PHP_INT_MAX,
  479. __('Not a positive number!')
  480. ),
  481. ];
  482. }
  483. /**
  484. * Validates non-negative number
  485. *
  486. * @param string $path path to config
  487. * @param array $values config values
  488. *
  489. * @return array
  490. */
  491. public static function validateNonNegativeNumber($path, array $values)
  492. {
  493. return [
  494. $path => static::validateNumber(
  495. $path,
  496. $values,
  497. false,
  498. true,
  499. PHP_INT_MAX,
  500. __('Not a non-negative number!')
  501. ),
  502. ];
  503. }
  504. /**
  505. * Validates value according to given regular expression
  506. * Pattern and modifiers must be a valid for PCRE <b>and</b> JavaScript RegExp
  507. *
  508. * @param string $path path to config
  509. * @param array $values config values
  510. * @param string $regex regular expression to match
  511. *
  512. * @return array|string
  513. */
  514. public static function validateByRegex($path, array $values, $regex)
  515. {
  516. if (! isset($values[$path])) {
  517. return '';
  518. }
  519. $result = preg_match($regex, Util::requestString($values[$path]));
  520. return [$path => $result ? '' : __('Incorrect value!')];
  521. }
  522. /**
  523. * Validates upper bound for numeric inputs
  524. *
  525. * @param string $path path to config
  526. * @param array $values config values
  527. * @param int $maxValue maximal allowed value
  528. *
  529. * @return array
  530. */
  531. public static function validateUpperBound($path, array $values, $maxValue)
  532. {
  533. $result = $values[$path] <= $maxValue;
  534. return [
  535. $path => $result ? '' : sprintf(
  536. __('Value must be less than or equal to %s!'),
  537. $maxValue
  538. ),
  539. ];
  540. }
  541. }