Validator.php 18 KB

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