1<?php
2/**
3 * Configuration handling.
4 */
5
6declare(strict_types=1);
7
8namespace PhpMyAdmin;
9
10use const DIRECTORY_SEPARATOR;
11use const E_USER_ERROR;
12use const PHP_OS;
13use const PHP_URL_PATH;
14use const PHP_URL_SCHEME;
15use const PHP_VERSION_ID;
16use function array_filter;
17use function array_flip;
18use function array_intersect_key;
19use function array_keys;
20use function array_merge;
21use function array_replace_recursive;
22use function array_slice;
23use function count;
24use function define;
25use function defined;
26use function error_get_last;
27use function error_reporting;
28use function explode;
29use function fclose;
30use function file_exists;
31use function filemtime;
32use function fileperms;
33use function fopen;
34use function fread;
35use function function_exists;
36use function gd_info;
37use function implode;
38use function ini_get;
39use function intval;
40use function is_dir;
41use function is_int;
42use function is_numeric;
43use function is_readable;
44use function is_string;
45use function is_writable;
46use function max;
47use function mb_strstr;
48use function mb_strtolower;
49use function md5;
50use function min;
51use function mkdir;
52use function ob_end_clean;
53use function ob_get_clean;
54use function ob_start;
55use function parse_url;
56use function preg_match;
57use function realpath;
58use function rtrim;
59use function setcookie;
60use function sprintf;
61use function str_replace;
62use function stripos;
63use function strlen;
64use function strpos;
65use function strtolower;
66use function substr;
67use function sys_get_temp_dir;
68use function time;
69use function trigger_error;
70use function trim;
71use function crc32;
72
73/**
74 * Configuration class
75 */
76class Config
77{
78    /** @var string  default config source */
79    public $defaultSource = ROOT_PATH . 'libraries/config.default.php';
80
81    /** @var array   default configuration settings */
82    public $default = [];
83
84    /** @var array   configuration settings, without user preferences applied */
85    public $baseSettings = [];
86
87    /** @var array   configuration settings */
88    public $settings = [];
89
90    /** @var string  config source */
91    public $source = '';
92
93    /** @var int     source modification time */
94    public $sourceMtime = 0;
95
96    /** @var int */
97    public $defaultSourceMtime = 0;
98
99    /** @var int */
100    public $setMtime = 0;
101
102    /** @var bool */
103    public $errorConfigFile = false;
104
105    /** @var bool */
106    public $errorConfigDefaultFile = false;
107
108    /** @var array */
109    public $defaultServer = [];
110
111    /**
112     * @var bool whether init is done or not
113     * set this to false to force some initial checks
114     * like checking for required functions
115     */
116    public $done = false;
117
118    /**
119     * @param string $source source to read config from
120     */
121    public function __construct(?string $source = null)
122    {
123        $this->settings = ['is_setup' => false];
124
125        // functions need to refresh in case of config file changed goes in
126        // PhpMyAdmin\Config::load()
127        $this->load($source);
128
129        // other settings, independent from config file, comes in
130        $this->checkSystem();
131
132        $this->baseSettings = $this->settings;
133    }
134
135    /**
136     * sets system and application settings
137     */
138    public function checkSystem(): void
139    {
140        // All the version handling is now done in the Version class
141        $this->set('PMA_VERSION', Version::VERSION);
142        $this->set('PMA_MAJOR_VERSION', Version::SERIES);
143
144        $this->checkWebServerOs();
145        $this->checkWebServer();
146        $this->checkGd2();
147        $this->checkClient();
148        $this->checkUpload();
149        $this->checkUploadSize();
150        $this->checkOutputCompression();
151    }
152
153    /**
154     * whether to use gzip output compression or not
155     */
156    public function checkOutputCompression(): void
157    {
158        // If zlib output compression is set in the php configuration file, no
159        // output buffering should be run
160        if (ini_get('zlib.output_compression')) {
161            $this->set('OBGzip', false);
162        }
163
164        // enable output-buffering (if set to 'auto')
165        if (strtolower((string) $this->get('OBGzip')) !== 'auto') {
166            return;
167        }
168
169        $this->set('OBGzip', true);
170    }
171
172    /**
173     * Sets the client platform based on user agent
174     *
175     * @param string $user_agent the user agent
176     */
177    private function setClientPlatform(string $user_agent): void
178    {
179        if (mb_strstr($user_agent, 'Win')) {
180            $this->set('PMA_USR_OS', 'Win');
181        } elseif (mb_strstr($user_agent, 'Mac')) {
182            $this->set('PMA_USR_OS', 'Mac');
183        } elseif (mb_strstr($user_agent, 'Linux')) {
184            $this->set('PMA_USR_OS', 'Linux');
185        } elseif (mb_strstr($user_agent, 'Unix')) {
186            $this->set('PMA_USR_OS', 'Unix');
187        } elseif (mb_strstr($user_agent, 'OS/2')) {
188            $this->set('PMA_USR_OS', 'OS/2');
189        } else {
190            $this->set('PMA_USR_OS', 'Other');
191        }
192    }
193
194    /**
195     * Determines platform (OS), browser and version of the user
196     * Based on a phpBuilder article:
197     *
198     * @see http://www.phpbuilder.net/columns/tim20000821.php
199     */
200    public function checkClient(): void
201    {
202        if (Core::getenv('HTTP_USER_AGENT')) {
203            $HTTP_USER_AGENT = Core::getenv('HTTP_USER_AGENT');
204        } else {
205            $HTTP_USER_AGENT = '';
206        }
207
208        // 1. Platform
209        $this->setClientPlatform($HTTP_USER_AGENT);
210
211        // 2. browser and version
212        // (must check everything else before Mozilla)
213
214        $is_mozilla = preg_match(
215            '@Mozilla/([0-9]\.[0-9]{1,2})@',
216            $HTTP_USER_AGENT,
217            $mozilla_version
218        );
219
220        if (preg_match(
221            '@Opera(/| )([0-9]\.[0-9]{1,2})@',
222            $HTTP_USER_AGENT,
223            $log_version
224        )) {
225            $this->set('PMA_USR_BROWSER_VER', $log_version[2]);
226            $this->set('PMA_USR_BROWSER_AGENT', 'OPERA');
227        } elseif (preg_match(
228            '@(MS)?IE ([0-9]{1,2}\.[0-9]{1,2})@',
229            $HTTP_USER_AGENT,
230            $log_version
231        )) {
232            $this->set('PMA_USR_BROWSER_VER', $log_version[2]);
233            $this->set('PMA_USR_BROWSER_AGENT', 'IE');
234        } elseif (preg_match(
235            '@Trident/(7)\.0@',
236            $HTTP_USER_AGENT,
237            $log_version
238        )) {
239            $this->set('PMA_USR_BROWSER_VER', intval($log_version[1]) + 4);
240            $this->set('PMA_USR_BROWSER_AGENT', 'IE');
241        } elseif (preg_match(
242            '@OmniWeb/([0-9]{1,3})@',
243            $HTTP_USER_AGENT,
244            $log_version
245        )) {
246            $this->set('PMA_USR_BROWSER_VER', $log_version[1]);
247            $this->set('PMA_USR_BROWSER_AGENT', 'OMNIWEB');
248            // Konqueror 2.2.2 says Konqueror/2.2.2
249            // Konqueror 3.0.3 says Konqueror/3
250        } elseif (preg_match(
251            '@(Konqueror/)(.*)(;)@',
252            $HTTP_USER_AGENT,
253            $log_version
254        )) {
255            $this->set('PMA_USR_BROWSER_VER', $log_version[2]);
256            $this->set('PMA_USR_BROWSER_AGENT', 'KONQUEROR');
257            // must check Chrome before Safari
258        } elseif ($is_mozilla
259            && preg_match('@Chrome/([0-9.]*)@', $HTTP_USER_AGENT, $log_version)
260        ) {
261            $this->set('PMA_USR_BROWSER_VER', $log_version[1]);
262            $this->set('PMA_USR_BROWSER_AGENT', 'CHROME');
263            // newer Safari
264        } elseif ($is_mozilla
265            && preg_match('@Version/(.*) Safari@', $HTTP_USER_AGENT, $log_version)
266        ) {
267            $this->set(
268                'PMA_USR_BROWSER_VER',
269                $log_version[1]
270            );
271            $this->set('PMA_USR_BROWSER_AGENT', 'SAFARI');
272            // older Safari
273        } elseif ($is_mozilla
274            && preg_match('@Safari/([0-9]*)@', $HTTP_USER_AGENT, $log_version)
275        ) {
276            $this->set(
277                'PMA_USR_BROWSER_VER',
278                $mozilla_version[1] . '.' . $log_version[1]
279            );
280            $this->set('PMA_USR_BROWSER_AGENT', 'SAFARI');
281            // Firefox
282        } elseif (! mb_strstr($HTTP_USER_AGENT, 'compatible')
283            && preg_match('@Firefox/([\w.]+)@', $HTTP_USER_AGENT, $log_version)
284        ) {
285            $this->set(
286                'PMA_USR_BROWSER_VER',
287                $log_version[1]
288            );
289            $this->set('PMA_USR_BROWSER_AGENT', 'FIREFOX');
290        } elseif (preg_match('@rv:1\.9(.*)Gecko@', $HTTP_USER_AGENT)) {
291            $this->set('PMA_USR_BROWSER_VER', '1.9');
292            $this->set('PMA_USR_BROWSER_AGENT', 'GECKO');
293        } elseif ($is_mozilla) {
294            $this->set('PMA_USR_BROWSER_VER', $mozilla_version[1]);
295            $this->set('PMA_USR_BROWSER_AGENT', 'MOZILLA');
296        } else {
297            $this->set('PMA_USR_BROWSER_VER', 0);
298            $this->set('PMA_USR_BROWSER_AGENT', 'OTHER');
299        }
300    }
301
302    /**
303     * Whether GD2 is present
304     */
305    public function checkGd2(): void
306    {
307        if ($this->get('GD2Available') === 'yes') {
308            $this->set('PMA_IS_GD2', 1);
309
310            return;
311        }
312
313        if ($this->get('GD2Available') === 'no') {
314            $this->set('PMA_IS_GD2', 0);
315
316            return;
317        }
318
319        if (! function_exists('imagecreatetruecolor')) {
320            $this->set('PMA_IS_GD2', 0);
321
322            return;
323        }
324
325        if (function_exists('gd_info')) {
326            $gd_nfo = gd_info();
327            if (mb_strstr($gd_nfo['GD Version'], '2.')) {
328                $this->set('PMA_IS_GD2', 1);
329            } else {
330                $this->set('PMA_IS_GD2', 0);
331            }
332        } else {
333            $this->set('PMA_IS_GD2', 0);
334        }
335    }
336
337    /**
338     * Whether the Web server php is running on is IIS
339     */
340    public function checkWebServer(): void
341    {
342        // some versions return Microsoft-IIS, some Microsoft/IIS
343        // we could use a preg_match() but it's slower
344        if (Core::getenv('SERVER_SOFTWARE')
345            && stripos(Core::getenv('SERVER_SOFTWARE'), 'Microsoft') !== false
346            && stripos(Core::getenv('SERVER_SOFTWARE'), 'IIS') !== false
347        ) {
348            $this->set('PMA_IS_IIS', 1);
349        } else {
350            $this->set('PMA_IS_IIS', 0);
351        }
352    }
353
354    /**
355     * Whether the os php is running on is windows or not
356     */
357    public function checkWebServerOs(): void
358    {
359        // Default to Unix or Equiv
360        $this->set('PMA_IS_WINDOWS', false);
361        // If PHP_OS is defined then continue
362        if (! defined('PHP_OS')) {
363            return;
364        }
365
366        if (stripos(PHP_OS, 'win') !== false && stripos(PHP_OS, 'darwin') === false) {
367            // Is it some version of Windows
368            $this->set('PMA_IS_WINDOWS', true);
369        } elseif (stripos(PHP_OS, 'OS/2') !== false) {
370            // Is it OS/2 (No file permissions like Windows)
371            $this->set('PMA_IS_WINDOWS', true);
372        }
373    }
374
375    /**
376     * loads default values from default source
377     *
378     * @return bool success
379     */
380    public function loadDefaults(): bool
381    {
382        global $isConfigLoading;
383
384        /** @var array<string,mixed> $cfg */
385        $cfg = [];
386        if (! @file_exists($this->defaultSource)) {
387            $this->errorConfigDefaultFile = true;
388
389            return false;
390        }
391        $canUseErrorReporting = Util::isErrorReportingAvailable();
392        $oldErrorReporting = null;
393        if ($canUseErrorReporting) {
394            $oldErrorReporting = error_reporting(0);
395        }
396
397        ob_start();
398        $isConfigLoading = true;
399        $eval_result = include $this->defaultSource;
400        $isConfigLoading = false;
401        ob_end_clean();
402
403        if ($canUseErrorReporting) {
404            error_reporting($oldErrorReporting);
405        }
406
407        if ($eval_result === false) {
408            $this->errorConfigDefaultFile = true;
409
410            return false;
411        }
412
413        $this->defaultSourceMtime = filemtime($this->defaultSource);
414
415        $this->defaultServer = $cfg['Servers'][1];
416        unset($cfg['Servers']);
417
418        $this->default = $cfg;
419        $this->settings = array_replace_recursive($this->settings, $cfg);
420
421        $this->errorConfigDefaultFile = false;
422
423        return true;
424    }
425
426    /**
427     * loads configuration from $source, usually the config file
428     * should be called on object creation
429     *
430     * @param string $source config file
431     */
432    public function load(?string $source = null): bool
433    {
434        global $isConfigLoading;
435
436        $this->loadDefaults();
437
438        if ($source !== null) {
439            $this->setSource($source);
440        }
441
442        if (! $this->checkConfigSource()) {
443            return false;
444        }
445
446        $cfg = [];
447
448        /**
449         * Parses the configuration file, we throw away any errors or
450         * output.
451         */
452        $canUseErrorReporting = Util::isErrorReportingAvailable();
453        $oldErrorReporting = null;
454        if ($canUseErrorReporting) {
455            $oldErrorReporting = error_reporting(0);
456        }
457
458        ob_start();
459        $isConfigLoading = true;
460        $eval_result = include $this->getSource();
461        $isConfigLoading = false;
462        ob_end_clean();
463
464        if ($canUseErrorReporting) {
465            error_reporting($oldErrorReporting);
466        }
467
468        if ($eval_result === false) {
469            $this->errorConfigFile = true;
470        } else {
471            $this->errorConfigFile = false;
472            $this->sourceMtime = filemtime($this->getSource());
473        }
474
475        /**
476         * Ignore keys with / as we do not use these
477         *
478         * These can be confusing for user configuration layer as it
479         * flatten array using / and thus don't see difference between
480         * $cfg['Export/method'] and $cfg['Export']['method'], while rest
481         * of the code uses the setting only in latter form.
482         *
483         * This could be removed once we consistently handle both values
484         * in the functional code as well.
485         *
486         * It could use array_filter(...ARRAY_FILTER_USE_KEY), but it's not
487         * supported on PHP 5.5 and HHVM.
488         */
489        $matched_keys = array_filter(
490            array_keys($cfg),
491            static function ($key) {
492                return strpos($key, '/') === false;
493            }
494        );
495
496        $cfg = array_intersect_key($cfg, array_flip($matched_keys));
497
498        $this->settings = array_replace_recursive($this->settings, $cfg);
499
500        return true;
501    }
502
503    /**
504     * Sets the connection collation
505     */
506    private function setConnectionCollation(): void
507    {
508        global $dbi;
509
510        $collation_connection = $this->get('DefaultConnectionCollation');
511        if (empty($collation_connection)
512            || $collation_connection == $GLOBALS['collation_connection']
513        ) {
514            return;
515        }
516
517        $dbi->setCollation($collation_connection);
518    }
519
520    /**
521     * Loads user preferences and merges them with current config
522     * must be called after control connection has been established
523     */
524    public function loadUserPreferences(): void
525    {
526        $userPreferences = new UserPreferences();
527        // index.php should load these settings, so that phpmyadmin.css.php
528        // will have everything available in session cache
529        $server = $GLOBALS['server'] ?? (! empty($GLOBALS['cfg']['ServerDefault'])
530                ? $GLOBALS['cfg']['ServerDefault']
531                : 0);
532        $cache_key = 'server_' . $server;
533        if ($server > 0 && ! defined('PMA_MINIMUM_COMMON')) {
534            $config_mtime = max($this->defaultSourceMtime, $this->sourceMtime);
535            // cache user preferences, use database only when needed
536            if (! isset($_SESSION['cache'][$cache_key]['userprefs'])
537                || $_SESSION['cache'][$cache_key]['config_mtime'] < $config_mtime
538            ) {
539                $prefs = $userPreferences->load();
540                $_SESSION['cache'][$cache_key]['userprefs']
541                    = $userPreferences->apply($prefs['config_data']);
542                $_SESSION['cache'][$cache_key]['userprefs_mtime'] = $prefs['mtime'];
543                $_SESSION['cache'][$cache_key]['userprefs_type'] = $prefs['type'];
544                $_SESSION['cache'][$cache_key]['config_mtime'] = $config_mtime;
545            }
546        } elseif ($server == 0
547            || ! isset($_SESSION['cache'][$cache_key]['userprefs'])
548        ) {
549            $this->set('user_preferences', false);
550
551            return;
552        }
553        $config_data = $_SESSION['cache'][$cache_key]['userprefs'];
554        // type is 'db' or 'session'
555        $this->set(
556            'user_preferences',
557            $_SESSION['cache'][$cache_key]['userprefs_type']
558        );
559        $this->set(
560            'user_preferences_mtime',
561            $_SESSION['cache'][$cache_key]['userprefs_mtime']
562        );
563
564        // load config array
565        $this->settings = array_replace_recursive($this->settings, $config_data);
566        $GLOBALS['cfg'] = array_replace_recursive($GLOBALS['cfg'], $config_data);
567        if (defined('PMA_MINIMUM_COMMON')) {
568            return;
569        }
570
571        // settings below start really working on next page load, but
572        // changes are made only in index.php so everything is set when
573        // in frames
574
575        // save theme
576        /** @var ThemeManager $tmanager */
577        $tmanager = ThemeManager::getInstance();
578        if ($tmanager->getThemeCookie() || isset($_REQUEST['set_theme'])) {
579            if ((! isset($config_data['ThemeDefault'])
580                && $tmanager->theme->getId() !== 'original')
581                || isset($config_data['ThemeDefault'])
582                && $config_data['ThemeDefault'] != $tmanager->theme->getId()
583            ) {
584                // new theme was set in common.inc.php
585                $this->setUserValue(
586                    null,
587                    'ThemeDefault',
588                    $tmanager->theme->getId(),
589                    'original'
590                );
591            }
592        } else {
593            // no cookie - read default from settings
594            if ($tmanager->theme !== null
595                && $this->settings['ThemeDefault'] != $tmanager->theme->getId()
596                && $tmanager->checkTheme($this->settings['ThemeDefault'])
597            ) {
598                $tmanager->setActiveTheme($this->settings['ThemeDefault']);
599                $tmanager->setThemeCookie();
600            }
601        }
602
603        // save language
604        if ($this->issetCookie('pma_lang') || isset($_POST['lang'])) {
605            if ((! isset($config_data['lang'])
606                && $GLOBALS['lang'] !== 'en')
607                || isset($config_data['lang'])
608                && $GLOBALS['lang'] != $config_data['lang']
609            ) {
610                $this->setUserValue(null, 'lang', $GLOBALS['lang'], 'en');
611            }
612        } else {
613            // read language from settings
614            if (isset($config_data['lang'])) {
615                $language = LanguageManager::getInstance()->getLanguage(
616                    $config_data['lang']
617                );
618                if ($language !== false) {
619                    $language->activate();
620                    $this->setCookie('pma_lang', $language->getCode());
621                }
622            }
623        }
624
625        // set connection collation
626        $this->setConnectionCollation();
627    }
628
629    /**
630     * Sets config value which is stored in user preferences (if available)
631     * or in a cookie.
632     *
633     * If user preferences are not yet initialized, option is applied to
634     * global config and added to a update queue, which is processed
635     * by {@link loadUserPreferences()}
636     *
637     * @param string|null $cookie_name   can be null
638     * @param string      $cfg_path      configuration path
639     * @param string      $new_cfg_value new value
640     * @param string|null $default_value default value
641     *
642     * @return true|Message
643     */
644    public function setUserValue(
645        ?string $cookie_name,
646        string $cfg_path,
647        $new_cfg_value,
648        $default_value = null
649    ) {
650        $userPreferences = new UserPreferences();
651        $result = true;
652        // use permanent user preferences if possible
653        $prefs_type = $this->get('user_preferences');
654        if ($prefs_type) {
655            if ($default_value === null) {
656                $default_value = Core::arrayRead($cfg_path, $this->default);
657            }
658            $result = $userPreferences->persistOption($cfg_path, $new_cfg_value, $default_value);
659        }
660        if ($prefs_type !== 'db' && $cookie_name) {
661            // fall back to cookies
662            if ($default_value === null) {
663                $default_value = Core::arrayRead($cfg_path, $this->settings);
664            }
665            $this->setCookie($cookie_name, $new_cfg_value, $default_value);
666        }
667        Core::arrayWrite($cfg_path, $GLOBALS['cfg'], $new_cfg_value);
668        Core::arrayWrite($cfg_path, $this->settings, $new_cfg_value);
669
670        return $result;
671    }
672
673    /**
674     * Reads value stored by {@link setUserValue()}
675     *
676     * @param string $cookie_name cookie name
677     * @param mixed  $cfg_value   config value
678     *
679     * @return mixed
680     */
681    public function getUserValue(string $cookie_name, $cfg_value)
682    {
683        $cookie_exists = ! empty($this->getCookie($cookie_name));
684        $prefs_type = $this->get('user_preferences');
685        if ($prefs_type === 'db') {
686            // permanent user preferences value exists, remove cookie
687            if ($cookie_exists) {
688                $this->removeCookie($cookie_name);
689            }
690        } elseif ($cookie_exists) {
691            return $this->getCookie($cookie_name);
692        }
693
694        // return value from $cfg array
695        return $cfg_value;
696    }
697
698    /**
699     * set source
700     *
701     * @param string $source source
702     */
703    public function setSource(string $source): void
704    {
705        $this->source = trim($source);
706    }
707
708    /**
709     * check config source
710     *
711     * @return bool whether source is valid or not
712     */
713    public function checkConfigSource(): bool
714    {
715        if (! $this->getSource()) {
716            // no configuration file set at all
717            return false;
718        }
719
720        if (! @file_exists($this->getSource())) {
721            $this->sourceMtime = 0;
722
723            return false;
724        }
725
726        if (! @is_readable($this->getSource())) {
727            // manually check if file is readable
728            // might be bug #3059806 Supporting running from CIFS/Samba shares
729
730            $contents = false;
731            $handle = @fopen($this->getSource(), 'r');
732            if ($handle !== false) {
733                $contents = @fread($handle, 1); // reading 1 byte is enough to test
734                fclose($handle);
735            }
736            if ($contents === false) {
737                $this->sourceMtime = 0;
738                Core::fatalError(
739                    sprintf(
740                        function_exists('__')
741                        ? __('Existing configuration file (%s) is not readable.')
742                        : 'Existing configuration file (%s) is not readable.',
743                        $this->getSource()
744                    )
745                );
746
747                return false;
748            }
749        }
750
751        return true;
752    }
753
754    /**
755     * verifies the permissions on config file (if asked by configuration)
756     * (must be called after config.inc.php has been merged)
757     */
758    public function checkPermissions(): void
759    {
760        // Check for permissions (on platforms that support it):
761        if (! $this->get('CheckConfigurationPermissions') || ! @file_exists($this->getSource())) {
762            return;
763        }
764
765        $perms = @fileperms($this->getSource());
766        if ($perms === false || (! ($perms & 2))) {
767            return;
768        }
769
770        // This check is normally done after loading configuration
771        $this->checkWebServerOs();
772        if ($this->get('PMA_IS_WINDOWS') === true) {
773            return;
774        }
775
776        $this->sourceMtime = 0;
777        Core::fatalError(
778            __(
779                'Wrong permissions on configuration file, '
780                . 'should not be world writable!'
781            )
782        );
783    }
784
785    /**
786     * Checks for errors
787     * (must be called after config.inc.php has been merged)
788     */
789    public function checkErrors(): void
790    {
791        if ($this->errorConfigDefaultFile) {
792            Core::fatalError(
793                sprintf(
794                    __('Could not load default configuration from: %1$s'),
795                    $this->defaultSource
796                )
797            );
798        }
799
800        if (! $this->errorConfigFile) {
801            return;
802        }
803
804        $error = '[strong]' . __('Failed to read configuration file!') . '[/strong]'
805            . '[br][br]'
806            . __(
807                'This usually means there is a syntax error in it, '
808                . 'please check any errors shown below.'
809            )
810            . '[br][br]'
811            . '[conferr]';
812        trigger_error($error, E_USER_ERROR);
813    }
814
815    /**
816     * returns specific config setting
817     *
818     * @param string $setting config setting
819     *
820     * @return mixed|null value
821     */
822    public function get(string $setting)
823    {
824        if (isset($this->settings[$setting])) {
825            return $this->settings[$setting];
826        }
827
828        return null;
829    }
830
831    /**
832     * sets configuration variable
833     *
834     * @param string $setting configuration option
835     * @param mixed  $value   new value for configuration option
836     */
837    public function set(string $setting, $value): void
838    {
839        if (isset($this->settings[$setting])
840            && $this->settings[$setting] === $value
841        ) {
842            return;
843        }
844
845        $this->settings[$setting] = $value;
846        $this->setMtime = time();
847    }
848
849    /**
850     * returns source for current config
851     *
852     * @return string  config source
853     */
854    public function getSource(): string
855    {
856        return $this->source;
857    }
858
859    /**
860     * returns a unique value to force a CSS reload if either the config
861     * or the theme changes
862     *
863     * @return int Summary of unix timestamps, to be unique on theme parameters
864     *             change
865     */
866    public function getThemeUniqueValue(): int
867    {
868        global $PMA_Theme;
869
870        return crc32(
871            $this->sourceMtime .
872            $this->defaultSourceMtime .
873            $this->get('user_preferences_mtime') .
874            ($PMA_Theme->mtimeInfo ?? 0) .
875            ($PMA_Theme->filesizeInfo ?? 0)
876        );
877    }
878
879    /**
880     * checks if upload is enabled
881     */
882    public function checkUpload(): void
883    {
884        if (! ini_get('file_uploads')) {
885            $this->set('enable_upload', false);
886
887            return;
888        }
889
890        $this->set('enable_upload', true);
891        // if set "php_admin_value file_uploads Off" in httpd.conf
892        // ini_get() also returns the string "Off" in this case:
893        if (strtolower((string) ini_get('file_uploads')) !== 'off') {
894            return;
895        }
896
897        $this->set('enable_upload', false);
898    }
899
900    /**
901     * Maximum upload size as limited by PHP
902     * Used with permission from Moodle (https://moodle.org/) by Martin Dougiamas
903     *
904     * this section generates $max_upload_size in bytes
905     */
906    public function checkUploadSize(): void
907    {
908        $fileSize = ini_get('upload_max_filesize');
909
910        if (! $fileSize) {
911            $fileSize = '5M';
912        }
913
914        $size = Core::getRealSize($fileSize);
915        $postSize = ini_get('post_max_size');
916
917        if ($postSize) {
918            $size = min($size, Core::getRealSize($postSize));
919        }
920
921        $this->set('max_upload_size', $size);
922    }
923
924    /**
925     * Checks if protocol is https
926     *
927     * This function checks if the https protocol on the active connection.
928     */
929    public function isHttps(): bool
930    {
931        if ($this->get('is_https') !== null) {
932            return $this->get('is_https');
933        }
934
935        $url = $this->get('PmaAbsoluteUri');
936
937        $is_https = false;
938        if (! empty($url) && parse_url($url, PHP_URL_SCHEME) === 'https') {
939            $is_https = true;
940        } elseif (strtolower(Core::getenv('HTTP_SCHEME')) === 'https') {
941            $is_https = true;
942        } elseif (strtolower(Core::getenv('HTTPS')) === 'on') {
943            $is_https = true;
944        } elseif (strtolower(substr(Core::getenv('REQUEST_URI'), 0, 6)) === 'https:') {
945            $is_https = true;
946        } elseif (strtolower(Core::getenv('HTTP_HTTPS_FROM_LB')) === 'on') {
947            // A10 Networks load balancer
948            $is_https = true;
949        } elseif (strtolower(Core::getenv('HTTP_FRONT_END_HTTPS')) === 'on') {
950            $is_https = true;
951        } elseif (strtolower(Core::getenv('HTTP_X_FORWARDED_PROTO')) === 'https') {
952            $is_https = true;
953        } elseif (strtolower(Core::getenv('HTTP_CLOUDFRONT_FORWARDED_PROTO')) === 'https') {
954            // Amazon CloudFront, issue #15621
955            $is_https = true;
956        } elseif (Util::getProtoFromForwardedHeader(Core::getenv('HTTP_FORWARDED')) === 'https') {
957            // RFC 7239 Forwarded header
958            $is_https = true;
959        } elseif (Core::getenv('SERVER_PORT') == 443) {
960            $is_https = true;
961        }
962
963        $this->set('is_https', $is_https);
964
965        return $is_https;
966    }
967
968    /**
969     * Get phpMyAdmin root path
970     */
971    public function getRootPath(): string
972    {
973        static $cookie_path = null;
974
975        if ($cookie_path !== null && ! defined('TESTSUITE')) {
976            return $cookie_path;
977        }
978
979        $url = $this->get('PmaAbsoluteUri');
980
981        if (! empty($url)) {
982            $path = parse_url($url, PHP_URL_PATH);
983            if (! empty($path)) {
984                if (substr($path, -1) !== '/') {
985                    return $path . '/';
986                }
987
988                return $path;
989            }
990        }
991
992        $parsedUrlPath = parse_url($GLOBALS['PMA_PHP_SELF'], PHP_URL_PATH);
993
994        $parts = explode(
995            '/',
996            rtrim(str_replace('\\', '/', $parsedUrlPath), '/')
997        );
998
999        /* Remove filename */
1000        if (substr($parts[count($parts) - 1], -4) === '.php') {
1001            $parts = array_slice($parts, 0, count($parts) - 1);
1002        }
1003
1004        /* Remove extra path from javascript calls */
1005        if (defined('PMA_PATH_TO_BASEDIR')) {
1006            $parts = array_slice($parts, 0, count($parts) - 1);
1007        }
1008
1009        $parts[] = '';
1010
1011        return implode('/', $parts);
1012    }
1013
1014    /**
1015     * enables backward compatibility
1016     */
1017    public function enableBc(): void
1018    {
1019        $GLOBALS['cfg']             = $this->settings;
1020        $GLOBALS['default_server']  = $this->defaultServer;
1021        unset($this->defaultServer);
1022        $GLOBALS['is_upload']       = $this->get('enable_upload');
1023        $GLOBALS['max_upload_size'] = $this->get('max_upload_size');
1024        $GLOBALS['is_https']        = $this->get('is_https');
1025
1026        $defines = [
1027            'PMA_VERSION',
1028            'PMA_MAJOR_VERSION',
1029            'PMA_THEME_VERSION',
1030            'PMA_THEME_GENERATION',
1031            'PMA_IS_WINDOWS',
1032            'PMA_IS_GD2',
1033            'PMA_USR_OS',
1034            'PMA_USR_BROWSER_VER',
1035            'PMA_USR_BROWSER_AGENT',
1036        ];
1037
1038        foreach ($defines as $define) {
1039            if (defined($define)) {
1040                continue;
1041            }
1042
1043            define($define, $this->get($define));
1044        }
1045    }
1046
1047    /**
1048     * removes cookie
1049     *
1050     * @param string $cookieName name of cookie to remove
1051     *
1052     * @return bool result of setcookie()
1053     */
1054    public function removeCookie(string $cookieName): bool
1055    {
1056        $httpCookieName = $this->getCookieName($cookieName);
1057
1058        if ($this->issetCookie($cookieName)) {
1059            unset($_COOKIE[$httpCookieName]);
1060        }
1061        if (defined('TESTSUITE')) {
1062            return true;
1063        }
1064
1065        return setcookie(
1066            $httpCookieName,
1067            '',
1068            time() - 3600,
1069            $this->getRootPath(),
1070            '',
1071            $this->isHttps()
1072        );
1073    }
1074
1075    /**
1076     * sets cookie if value is different from current cookie value,
1077     * or removes if value is equal to default
1078     *
1079     * @param string $cookie   name of cookie to remove
1080     * @param string $value    new cookie value
1081     * @param string $default  default value
1082     * @param int    $validity validity of cookie in seconds (default is one month)
1083     * @param bool   $httponly whether cookie is only for HTTP (and not for scripts)
1084     *
1085     * @return bool result of setcookie()
1086     */
1087    public function setCookie(
1088        string $cookie,
1089        string $value,
1090        ?string $default = null,
1091        ?int $validity = null,
1092        bool $httponly = true
1093    ): bool {
1094        global $cfg;
1095
1096        if (strlen($value) > 0 && $default !== null && $value === $default
1097        ) {
1098            // default value is used
1099            if ($this->issetCookie($cookie)) {
1100                // remove cookie
1101                return $this->removeCookie($cookie);
1102            }
1103
1104            return false;
1105        }
1106
1107        if (strlen($value) === 0 && $this->issetCookie($cookie)) {
1108            // remove cookie, value is empty
1109            return $this->removeCookie($cookie);
1110        }
1111
1112        $httpCookieName = $this->getCookieName($cookie);
1113
1114        if (! $this->issetCookie($cookie) || $this->getCookie($cookie) !== $value) {
1115            // set cookie with new value
1116            /* Calculate cookie validity */
1117            if ($validity === null) {
1118                /* Valid for one month */
1119                $validity = time() + 2592000;
1120            } elseif ($validity == 0) {
1121                /* Valid for session */
1122                $validity = 0;
1123            } else {
1124                $validity = time() + $validity;
1125            }
1126            if (defined('TESTSUITE')) {
1127                $_COOKIE[$httpCookieName] = $value;
1128
1129                return true;
1130            }
1131
1132            if (PHP_VERSION_ID < 70300) {
1133                return setcookie(
1134                    $httpCookieName,
1135                    $value,
1136                    $validity,
1137                    $this->getRootPath() . '; samesite=' . $cfg['CookieSameSite'],
1138                    '',
1139                    $this->isHttps(),
1140                    $httponly
1141                );
1142            }
1143            $optionalParams = [
1144                'expires' => $validity,
1145                'path' => $this->getRootPath(),
1146                'domain' => '',
1147                'secure' => $this->isHttps(),
1148                'httponly' => $httponly,
1149                'samesite' => $cfg['CookieSameSite'],
1150            ];
1151
1152            return setcookie(
1153                $httpCookieName,
1154                $value,
1155                $optionalParams
1156            );
1157        }
1158
1159        // cookie has already $value as value
1160        return true;
1161    }
1162
1163    /**
1164     * get cookie
1165     *
1166     * @param string $cookieName The name of the cookie to get
1167     *
1168     * @return mixed|null result of getCookie()
1169     */
1170    public function getCookie(string $cookieName)
1171    {
1172        if (isset($_COOKIE[$this->getCookieName($cookieName)])) {
1173            return $_COOKIE[$this->getCookieName($cookieName)];
1174        }
1175
1176        return null;
1177    }
1178
1179    /**
1180     * Get the real cookie name
1181     *
1182     * @param string $cookieName The name of the cookie
1183     */
1184    public function getCookieName(string $cookieName): string
1185    {
1186        return $cookieName . ( $this->isHttps() ? '_https' : '' );
1187    }
1188
1189    /**
1190     * isset cookie
1191     *
1192     * @param string $cookieName The name of the cookie to check
1193     *
1194     * @return bool result of issetCookie()
1195     */
1196    public function issetCookie(string $cookieName): bool
1197    {
1198        return isset($_COOKIE[$this->getCookieName($cookieName)]);
1199    }
1200
1201    /**
1202     * Error handler to catch fatal errors when loading configuration
1203     * file
1204     */
1205    public static function fatalErrorHandler(): void
1206    {
1207        global $isConfigLoading;
1208
1209        if (! isset($isConfigLoading) || ! $isConfigLoading) {
1210            return;
1211        }
1212
1213        $error = error_get_last();
1214        if ($error === null) {
1215            return;
1216        }
1217
1218        Core::fatalError(
1219            sprintf(
1220                'Failed to load phpMyAdmin configuration (%s:%s): %s',
1221                Error::relPath($error['file']),
1222                $error['line'],
1223                $error['message']
1224            )
1225        );
1226    }
1227
1228    /**
1229     * Wrapper for footer/header rendering
1230     *
1231     * @param string $filename File to check and render
1232     * @param string $id       Div ID
1233     */
1234    private static function renderCustom(string $filename, string $id): string
1235    {
1236        $retval = '';
1237        if (@file_exists($filename)) {
1238            $retval .= '<div id="' . $id . '">';
1239            ob_start();
1240            include $filename;
1241            $retval .= ob_get_clean();
1242            $retval .= '</div>';
1243        }
1244
1245        return $retval;
1246    }
1247
1248    /**
1249     * Renders user configured footer
1250     */
1251    public static function renderFooter(): string
1252    {
1253        return self::renderCustom(CUSTOM_FOOTER_FILE, 'pma_footer');
1254    }
1255
1256    /**
1257     * Renders user configured footer
1258     */
1259    public static function renderHeader(): string
1260    {
1261        return self::renderCustom(CUSTOM_HEADER_FILE, 'pma_header');
1262    }
1263
1264    /**
1265     * Returns temporary dir path
1266     *
1267     * @param string $name Directory name
1268     */
1269    public function getTempDir(string $name): ?string
1270    {
1271        static $temp_dir = [];
1272
1273        if (isset($temp_dir[$name]) && ! defined('TESTSUITE')) {
1274            return $temp_dir[$name];
1275        }
1276
1277        $path = $this->get('TempDir');
1278        if (empty($path)) {
1279            $path = null;
1280        } else {
1281            $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name;
1282            if (! @is_dir($path)) {
1283                @mkdir($path, 0770, true);
1284            }
1285            if (! @is_dir($path) || ! @is_writable($path)) {
1286                $path = null;
1287            }
1288        }
1289
1290        $temp_dir[$name] = $path;
1291
1292        return $path;
1293    }
1294
1295    /**
1296     * Returns temporary directory
1297     */
1298    public function getUploadTempDir(): ?string
1299    {
1300        // First try configured temp dir
1301        // Fallback to PHP upload_tmp_dir
1302        $dirs = [
1303            $this->getTempDir('upload'),
1304            ini_get('upload_tmp_dir'),
1305            sys_get_temp_dir(),
1306        ];
1307
1308        foreach ($dirs as $dir) {
1309            if (! empty($dir) && @is_writable($dir)) {
1310                return realpath($dir);
1311            }
1312        }
1313
1314        return null;
1315    }
1316
1317    /**
1318     * Selects server based on request parameters.
1319     */
1320    public function selectServer(): int
1321    {
1322        $request = empty($_REQUEST['server']) ? 0 : $_REQUEST['server'];
1323
1324        /**
1325         * Lookup server by name
1326         * (see FAQ 4.8)
1327         */
1328        if (! is_numeric($request)) {
1329            foreach ($this->settings['Servers'] as $i => $server) {
1330                $verboseToLower = mb_strtolower($server['verbose']);
1331                $serverToLower = mb_strtolower($request);
1332                if ($server['host'] == $request
1333                    || $server['verbose'] == $request
1334                    || $verboseToLower == $serverToLower
1335                    || md5($verboseToLower) === $serverToLower
1336                ) {
1337                    $request = $i;
1338                    break;
1339                }
1340            }
1341            if (is_string($request)) {
1342                $request = 0;
1343            }
1344        }
1345
1346        /**
1347         * If no server is selected, make sure that $this->settings['Server'] is empty (so
1348         * that nothing will work), and skip server authentication.
1349         * We do NOT exit here, but continue on without logging into any server.
1350         * This way, the welcome page will still come up (with no server info) and
1351         * present a choice of servers in the case that there are multiple servers
1352         * and '$this->settings['ServerDefault'] = 0' is set.
1353         */
1354
1355        if (is_numeric($request) && ! empty($request) && ! empty($this->settings['Servers'][$request])) {
1356            $server = $request;
1357            $this->settings['Server'] = $this->settings['Servers'][$server];
1358        } else {
1359            if (! empty($this->settings['Servers'][$this->settings['ServerDefault']])) {
1360                $server = $this->settings['ServerDefault'];
1361                $this->settings['Server'] = $this->settings['Servers'][$server];
1362            } else {
1363                $server = 0;
1364                $this->settings['Server'] = [];
1365            }
1366        }
1367
1368        return (int) $server;
1369    }
1370
1371    /**
1372     * Checks whether Servers configuration is valid and possibly apply fixups.
1373     */
1374    public function checkServers(): void
1375    {
1376        // Do we have some server?
1377        if (! isset($this->settings['Servers']) || count($this->settings['Servers']) === 0) {
1378            // No server => create one with defaults
1379            $this->settings['Servers'] = [1 => $this->defaultServer];
1380        } else {
1381            // We have server(s) => apply default configuration
1382            $new_servers = [];
1383
1384            foreach ($this->settings['Servers'] as $server_index => $each_server) {
1385                // Detect wrong configuration
1386                if (! is_int($server_index) || $server_index < 1) {
1387                    trigger_error(
1388                        sprintf(__('Invalid server index: %s'), $server_index),
1389                        E_USER_ERROR
1390                    );
1391                }
1392
1393                $each_server = array_merge($this->defaultServer, $each_server);
1394
1395                // Final solution to bug #582890
1396                // If we are using a socket connection
1397                // and there is nothing in the verbose server name
1398                // or the host field, then generate a name for the server
1399                // in the form of "Server 2", localized of course!
1400                if (empty($each_server['host']) && empty($each_server['verbose'])) {
1401                    $each_server['verbose'] = sprintf(__('Server %d'), $server_index);
1402                }
1403
1404                $new_servers[$server_index] = $each_server;
1405            }
1406            $this->settings['Servers'] = $new_servers;
1407        }
1408    }
1409
1410    /**
1411     * Return connection parameters for the database server
1412     *
1413     * @param int        $mode   Connection mode on of CONNECT_USER, CONNECT_CONTROL
1414     *                           or CONNECT_AUXILIARY.
1415     * @param array|null $server Server information like host/port/socket/persistent
1416     *
1417     * @return array user, host and server settings array
1418     */
1419    public static function getConnectionParams(int $mode, ?array $server = null): array
1420    {
1421        global $cfg;
1422
1423        $user = null;
1424        $password = null;
1425
1426        if ($mode == DatabaseInterface::CONNECT_USER) {
1427            $user = $cfg['Server']['user'];
1428            $password = $cfg['Server']['password'];
1429            $server = $cfg['Server'];
1430        } elseif ($mode == DatabaseInterface::CONNECT_CONTROL) {
1431            $user = $cfg['Server']['controluser'];
1432            $password = $cfg['Server']['controlpass'];
1433
1434            $server = [];
1435
1436            if (! empty($cfg['Server']['controlhost'])) {
1437                $server['host'] = $cfg['Server']['controlhost'];
1438            } else {
1439                $server['host'] = $cfg['Server']['host'];
1440            }
1441            // Share the settings if the host is same
1442            if ($server['host'] == $cfg['Server']['host']) {
1443                $shared = [
1444                    'port',
1445                    'socket',
1446                    'compress',
1447                    'ssl',
1448                    'ssl_key',
1449                    'ssl_cert',
1450                    'ssl_ca',
1451                    'ssl_ca_path',
1452                    'ssl_ciphers',
1453                    'ssl_verify',
1454                ];
1455                foreach ($shared as $item) {
1456                    if (! isset($cfg['Server'][$item])) {
1457                        continue;
1458                    }
1459
1460                    $server[$item] = $cfg['Server'][$item];
1461                }
1462            }
1463            // Set configured port
1464            if (! empty($cfg['Server']['controlport'])) {
1465                $server['port'] = $cfg['Server']['controlport'];
1466            }
1467            // Set any configuration with control_ prefix
1468            foreach ($cfg['Server'] as $key => $val) {
1469                if (substr($key, 0, 8) !== 'control_') {
1470                    continue;
1471                }
1472
1473                $server[substr($key, 8)] = $val;
1474            }
1475        } else {
1476            if ($server === null) {
1477                return [
1478                    null,
1479                    null,
1480                    null,
1481                ];
1482            }
1483            if (isset($server['user'])) {
1484                $user = $server['user'];
1485            }
1486            if (isset($server['password'])) {
1487                $password = $server['password'];
1488            }
1489        }
1490
1491        // Perform sanity checks on some variables
1492        $server['port'] = empty($server['port']) ? 0 : (int) $server['port'];
1493
1494        if (empty($server['socket'])) {
1495            $server['socket'] = null;
1496        }
1497        if (empty($server['host'])) {
1498            $server['host'] = 'localhost';
1499        }
1500        if (! isset($server['ssl'])) {
1501            $server['ssl'] = false;
1502        }
1503        if (! isset($server['compress'])) {
1504            $server['compress'] = false;
1505        }
1506
1507        return [
1508            $user,
1509            $password,
1510            $server,
1511        ];
1512    }
1513
1514    /**
1515     * Get LoginCookieValidity from preferences cache.
1516     *
1517     * No generic solution for loading preferences from cache as some settings
1518     * need to be kept for processing in loadUserPreferences().
1519     *
1520     * @see loadUserPreferences()
1521     */
1522    public function getLoginCookieValidityFromCache(int $server): void
1523    {
1524        global $cfg;
1525
1526        $cacheKey = 'server_' . $server;
1527
1528        if (! isset($_SESSION['cache'][$cacheKey]['userprefs']['LoginCookieValidity'])) {
1529            return;
1530        }
1531
1532        $value = $_SESSION['cache'][$cacheKey]['userprefs']['LoginCookieValidity'];
1533        $this->set('LoginCookieValidity', $value);
1534        $cfg['LoginCookieValidity'] = $value;
1535    }
1536}
1537