1<?php
2/**
3 * Config file management
4 */
5
6declare(strict_types=1);
7
8namespace PhpMyAdmin\Config;
9
10use PhpMyAdmin\Core;
11use function array_diff;
12use function array_flip;
13use function array_keys;
14use function array_walk;
15use function count;
16use function is_array;
17use function preg_replace;
18
19/**
20 * Config file management class.
21 * Stores its data in $_SESSION
22 */
23class ConfigFile
24{
25    /**
26     * Stores default PMA config from config.default.php
27     *
28     * @var array
29     */
30    private $defaultCfg;
31
32    /**
33     * Stores allowed values for non-standard fields
34     *
35     * @var array
36     */
37    private $cfgDb;
38
39    /**
40     * Stores original PMA config, not modified by user preferences
41     *
42     * @var array|null
43     */
44    private $baseCfg;
45
46    /**
47     * Whether we are currently working in PMA Setup context
48     *
49     * @var bool
50     */
51    private $isInSetup;
52
53    /**
54     * Keys which will be always written to config file
55     *
56     * @var array
57     */
58    private $persistKeys = [];
59
60    /**
61     * Changes keys while updating config in {@link updateWithGlobalConfig()}
62     * or reading by {@link getConfig()} or {@link getConfigArray()}
63     *
64     * @var array
65     */
66    private $cfgUpdateReadMapping = [];
67
68    /**
69     * Key filter for {@link set()}
70     *
71     * @var array|null
72     */
73    private $setFilter;
74
75    /**
76     * Instance id (key in $_SESSION array, separate for each server -
77     * ConfigFile{server id})
78     *
79     * @var string
80     */
81    private $id;
82
83    /**
84     * Result for {@link flattenArray()}
85     *
86     * @var array|null
87     */
88    private $flattenArrayResult;
89
90    /**
91     * @param array|null $baseConfig base configuration read from
92     *                               {@link PhpMyAdmin\Config::$base_config},
93     *                               use only when not in PMA Setup
94     */
95    public function __construct($baseConfig = null)
96    {
97        // load default config values
98        $cfg = &$this->defaultCfg;
99        include ROOT_PATH . 'libraries/config.default.php';
100
101        // load additional config information
102        $this->cfgDb = include ROOT_PATH . 'libraries/config.values.php';
103
104        // apply default values overrides
105        if (count($this->cfgDb['_overrides'])) {
106            foreach ($this->cfgDb['_overrides'] as $path => $value) {
107                Core::arrayWrite($path, $cfg, $value);
108            }
109        }
110
111        $this->baseCfg = $baseConfig;
112        $this->isInSetup = $baseConfig === null;
113        $this->id = 'ConfigFile' . $GLOBALS['server'];
114        if (isset($_SESSION[$this->id])) {
115            return;
116        }
117
118        $_SESSION[$this->id] = [];
119    }
120
121    /**
122     * Sets names of config options which will be placed in config file even if
123     * they are set to their default values (use only full paths)
124     *
125     * @param array $keys the names of the config options
126     *
127     * @return void
128     */
129    public function setPersistKeys(array $keys)
130    {
131        // checking key presence is much faster than searching so move values
132        // to keys
133        $this->persistKeys = array_flip($keys);
134    }
135
136    /**
137     * Returns flipped array set by {@link setPersistKeys()}
138     *
139     * @return array
140     */
141    public function getPersistKeysMap()
142    {
143        return $this->persistKeys;
144    }
145
146    /**
147     * By default ConfigFile allows setting of all configuration keys, use
148     * this method to set up a filter on {@link set()} method
149     *
150     * @param array|null $keys array of allowed keys or null to remove filter
151     *
152     * @return void
153     */
154    public function setAllowedKeys($keys)
155    {
156        if ($keys === null) {
157            $this->setFilter = null;
158
159            return;
160        }
161        // checking key presence is much faster than searching so move values
162        // to keys
163        $this->setFilter = array_flip($keys);
164    }
165
166    /**
167     * Sets path mapping for updating config in
168     * {@link updateWithGlobalConfig()} or reading
169     * by {@link getConfig()} or {@link getConfigArray()}
170     *
171     * @param array $mapping Contains the mapping of "Server/config options"
172     *                       to "Server/1/config options"
173     *
174     * @return void
175     */
176    public function setCfgUpdateReadMapping(array $mapping)
177    {
178        $this->cfgUpdateReadMapping = $mapping;
179    }
180
181    /**
182     * Resets configuration data
183     *
184     * @return void
185     */
186    public function resetConfigData()
187    {
188        $_SESSION[$this->id] = [];
189    }
190
191    /**
192     * Sets configuration data (overrides old data)
193     *
194     * @param array $cfg Configuration options
195     *
196     * @return void
197     */
198    public function setConfigData(array $cfg)
199    {
200        $_SESSION[$this->id] = $cfg;
201    }
202
203    /**
204     * Sets config value
205     *
206     * @param string $path          Path
207     * @param mixed  $value         Value
208     * @param string $canonicalPath Canonical path
209     *
210     * @return void
211     */
212    public function set($path, $value, $canonicalPath = null)
213    {
214        if ($canonicalPath === null) {
215            $canonicalPath = $this->getCanonicalPath($path);
216        }
217
218        if ($this->setFilter !== null
219            && ! isset($this->setFilter[$canonicalPath])
220        ) {
221            return;
222        }
223        // if the path isn't protected it may be removed
224        if (isset($this->persistKeys[$canonicalPath])) {
225            Core::arrayWrite($path, $_SESSION[$this->id], $value);
226
227            return;
228        }
229
230        $defaultValue = $this->getDefault($canonicalPath);
231        $removePath = $value === $defaultValue;
232        if ($this->isInSetup) {
233            // remove if it has a default value or is empty
234            $removePath = $removePath
235                || (empty($value) && empty($defaultValue));
236        } else {
237            // get original config values not overwritten by user
238            // preferences to allow for overwriting options set in
239            // config.inc.php with default values
240            $instanceDefaultValue = Core::arrayRead(
241                $canonicalPath,
242                $this->baseCfg
243            );
244            // remove if it has a default value and base config (config.inc.php)
245            // uses default value
246            $removePath = $removePath
247                && ($instanceDefaultValue === $defaultValue);
248        }
249        if ($removePath) {
250            Core::arrayRemove($path, $_SESSION[$this->id]);
251
252            return;
253        }
254
255        Core::arrayWrite($path, $_SESSION[$this->id], $value);
256    }
257
258    /**
259     * Flattens multidimensional array, changes indices to paths
260     * (eg. 'key/subkey').
261     * Used as array_walk() callback.
262     *
263     * @param mixed $value  Value
264     * @param mixed $key    Key
265     * @param mixed $prefix Prefix
266     *
267     * @return void
268     */
269    private function flattenArray($value, $key, $prefix)
270    {
271        // no recursion for numeric arrays
272        if (is_array($value) && ! isset($value[0])) {
273            $prefix .= $key . '/';
274            array_walk(
275                $value,
276                function ($value, $key, $prefix) {
277                    $this->flattenArray($value, $key, $prefix);
278                },
279                $prefix
280            );
281        } else {
282            $this->flattenArrayResult[$prefix . $key] = $value;
283        }
284    }
285
286    /**
287     * Returns default config in a flattened array
288     *
289     * @return array
290     */
291    public function getFlatDefaultConfig()
292    {
293        $this->flattenArrayResult = [];
294        array_walk(
295            $this->defaultCfg,
296            function ($value, $key, $prefix) {
297                $this->flattenArray($value, $key, $prefix);
298            },
299            ''
300        );
301        $flatConfig = $this->flattenArrayResult;
302        $this->flattenArrayResult = null;
303
304        return $flatConfig;
305    }
306
307    /**
308     * Updates config with values read from given array
309     * (config will contain differences to defaults from config.defaults.php).
310     *
311     * @param array $cfg Configuration
312     *
313     * @return void
314     */
315    public function updateWithGlobalConfig(array $cfg)
316    {
317        // load config array and flatten it
318        $this->flattenArrayResult = [];
319        array_walk(
320            $cfg,
321            function ($value, $key, $prefix) {
322                $this->flattenArray($value, $key, $prefix);
323            },
324            ''
325        );
326        $flatConfig = $this->flattenArrayResult;
327        $this->flattenArrayResult = null;
328
329        // save values map for translating a few user preferences paths,
330        // should be complemented by code reading from generated config
331        // to perform inverse mapping
332        foreach ($flatConfig as $path => $value) {
333            if (isset($this->cfgUpdateReadMapping[$path])) {
334                $path = $this->cfgUpdateReadMapping[$path];
335            }
336            $this->set($path, $value, $path);
337        }
338    }
339
340    /**
341     * Returns config value or $default if it's not set
342     *
343     * @param string $path    Path of config file
344     * @param mixed  $default Default values
345     *
346     * @return mixed
347     */
348    public function get($path, $default = null)
349    {
350        return Core::arrayRead($path, $_SESSION[$this->id], $default);
351    }
352
353    /**
354     * Returns default config value or $default it it's not set ie. it doesn't
355     * exist in config.default.php ($cfg) and config.values.php
356     * ($_cfg_db['_overrides'])
357     *
358     * @param string $canonicalPath Canonical path
359     * @param mixed  $default       Default value
360     *
361     * @return mixed
362     */
363    public function getDefault($canonicalPath, $default = null)
364    {
365        return Core::arrayRead($canonicalPath, $this->defaultCfg, $default);
366    }
367
368    /**
369     * Returns config value, if it's not set uses the default one; returns
370     * $default if the path isn't set and doesn't contain a default value
371     *
372     * @param string $path    Path
373     * @param mixed  $default Default value
374     *
375     * @return mixed
376     */
377    public function getValue($path, $default = null)
378    {
379        $v = Core::arrayRead($path, $_SESSION[$this->id], null);
380        if ($v !== null) {
381            return $v;
382        }
383        $path = $this->getCanonicalPath($path);
384
385        return $this->getDefault($path, $default);
386    }
387
388    /**
389     * Returns canonical path
390     *
391     * @param string $path Path
392     *
393     * @return string
394     */
395    public function getCanonicalPath($path)
396    {
397        return preg_replace('#^Servers/([\d]+)/#', 'Servers/1/', $path);
398    }
399
400    /**
401     * Returns config database entry for $path
402     *
403     * @param string $path    path of the variable in config db
404     * @param mixed  $default default value
405     *
406     * @return mixed
407     */
408    public function getDbEntry($path, $default = null)
409    {
410        return Core::arrayRead($path, $this->cfgDb, $default);
411    }
412
413    /**
414     * Returns server count
415     *
416     * @return int
417     */
418    public function getServerCount()
419    {
420        return isset($_SESSION[$this->id]['Servers'])
421            ? count($_SESSION[$this->id]['Servers'])
422            : 0;
423    }
424
425    /**
426     * Returns server list
427     *
428     * @return array|null
429     */
430    public function getServers()
431    {
432        return $_SESSION[$this->id]['Servers'] ?? null;
433    }
434
435    /**
436     * Returns DSN of given server
437     *
438     * @param int $server server index
439     *
440     * @return string
441     */
442    public function getServerDSN($server)
443    {
444        if (! isset($_SESSION[$this->id]['Servers'][$server])) {
445            return '';
446        }
447
448        $path = 'Servers/' . $server;
449        $dsn = 'mysqli://';
450        if ($this->getValue($path . '/auth_type') === 'config') {
451            $dsn .= $this->getValue($path . '/user');
452            if (! empty($this->getValue($path . '/password'))) {
453                $dsn .= ':***';
454            }
455            $dsn .= '@';
456        }
457        if ($this->getValue($path . '/host') !== 'localhost') {
458            $dsn .= $this->getValue($path . '/host');
459            $port = $this->getValue($path . '/port');
460            if ($port) {
461                $dsn .= ':' . $port;
462            }
463        } else {
464            $dsn .= $this->getValue($path . '/socket');
465        }
466
467        return $dsn;
468    }
469
470    /**
471     * Returns server name
472     *
473     * @param int $id server index
474     *
475     * @return string
476     */
477    public function getServerName($id)
478    {
479        if (! isset($_SESSION[$this->id]['Servers'][$id])) {
480            return '';
481        }
482        $verbose = $this->get('Servers/' . $id . '/verbose');
483        if (! empty($verbose)) {
484            return $verbose;
485        }
486        $host = $this->get('Servers/' . $id . '/host');
487
488        return empty($host) ? 'localhost' : $host;
489    }
490
491    /**
492     * Removes server
493     *
494     * @param int $server server index
495     *
496     * @return void
497     */
498    public function removeServer($server)
499    {
500        if (! isset($_SESSION[$this->id]['Servers'][$server])) {
501            return;
502        }
503        $lastServer = $this->getServerCount();
504
505        for ($i = $server; $i < $lastServer; $i++) {
506            $_SESSION[$this->id]['Servers'][$i]
507                = $_SESSION[$this->id]['Servers'][$i + 1];
508        }
509        unset($_SESSION[$this->id]['Servers'][$lastServer]);
510
511        if (! isset($_SESSION[$this->id]['ServerDefault'])
512            || $_SESSION[$this->id]['ServerDefault'] != $lastServer
513        ) {
514            return;
515        }
516
517        unset($_SESSION[$this->id]['ServerDefault']);
518    }
519
520    /**
521     * Returns configuration array (full, multidimensional format)
522     *
523     * @return array
524     */
525    public function getConfig()
526    {
527        $c = $_SESSION[$this->id];
528        foreach ($this->cfgUpdateReadMapping as $mapTo => $mapFrom) {
529            // if the key $c exists in $map_to
530            if (Core::arrayRead($mapTo, $c) === null) {
531                continue;
532            }
533
534            Core::arrayWrite($mapTo, $c, Core::arrayRead($mapFrom, $c));
535            Core::arrayRemove($mapFrom, $c);
536        }
537
538        return $c;
539    }
540
541    /**
542     * Returns configuration array (flat format)
543     *
544     * @return array
545     */
546    public function getConfigArray()
547    {
548        $this->flattenArrayResult = [];
549        array_walk(
550            $_SESSION[$this->id],
551            function ($value, $key, $prefix) {
552                $this->flattenArray($value, $key, $prefix);
553            },
554            ''
555        );
556        $c = $this->flattenArrayResult;
557        $this->flattenArrayResult = null;
558
559        $persistKeys = array_diff(
560            array_keys($this->persistKeys),
561            array_keys($c)
562        );
563        foreach ($persistKeys as $k) {
564            $c[$k] = $this->getDefault($this->getCanonicalPath($k));
565        }
566
567        foreach ($this->cfgUpdateReadMapping as $mapTo => $mapFrom) {
568            if (! isset($c[$mapFrom])) {
569                continue;
570            }
571            $c[$mapTo] = $c[$mapFrom];
572            unset($c[$mapFrom]);
573        }
574
575        return $c;
576    }
577}
578