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