3 * Config file management
6 declare(strict_types
=1);
8 namespace PhpMyAdmin\Config
;
10 use PhpMyAdmin\Config
;
12 use PhpMyAdmin\Current
;
15 use function _pgettext
;
16 use function array_diff
;
17 use function array_flip
;
18 use function array_keys
;
19 use function array_merge
;
21 use function is_array
;
22 use function preg_replace
;
25 * Config file management class.
26 * Stores its data in $_SESSION
31 * Stores default phpMyAdmin config
37 private array $defaultCfg;
40 * Stores allowed values for non-standard fields
42 * @var array<string, string|mixed[]>
47 * Whether we are currently working in PMA Setup context
49 private bool $isInSetup;
52 * Keys which will be always written to config file
56 private array $persistKeys = [];
59 * Changes keys while updating config in {@link updateWithGlobalConfig()}
60 * or reading by {@link getConfig()} or {@link getConfigArray()}
64 private array $cfgUpdateReadMapping = [];
67 * Key filter for {@link set()}
69 private array|
null $setFilter = null;
72 * Instance id (key in $_SESSION array, separate for each server -
73 * ConfigFile{server id})
78 * @param mixed[]|null $baseConfig base configuration read from
79 {@link PhpMyAdmin\Config::$base_config},
80 use only when not in PMA Setup
81 Stores original PMA config, not modified by user preferences
83 public function __construct(private array|
null $baseConfig = null)
85 // load default config values
86 $settings = new Settings([]);
87 $this->defaultCfg
= $settings->asArray();
89 // load additional config information
90 $this->cfgDb
= $this->getAllowedValues();
91 $this->isInSetup
= $baseConfig === null;
92 $this->id
= 'ConfigFile' . Current
::$server;
93 if (isset($_SESSION[$this->id
])) {
97 $_SESSION[$this->id
] = [];
101 * Sets names of config options which will be placed in config file even if
102 * they are set to their default values (use only full paths)
104 * @param mixed[] $keys the names of the config options
106 public function setPersistKeys(array $keys): void
108 // checking key presence is much faster than searching so move values
110 $this->persistKeys
= array_flip($keys);
114 * Returns flipped array set by {@link setPersistKeys()}
118 public function getPersistKeysMap(): array
120 return $this->persistKeys
;
124 * By default ConfigFile allows setting of all configuration keys, use
125 * this method to set up a filter on {@link set()} method
127 * @param mixed[]|null $keys array of allowed keys or null to remove filter
129 public function setAllowedKeys(array|
null $keys): void
131 if ($keys === null) {
132 $this->setFilter
= null;
137 // checking key presence is much faster than searching so move values
139 $this->setFilter
= array_flip($keys);
143 * Sets path mapping for updating config in
144 * {@link updateWithGlobalConfig()} or reading
145 * by {@link getConfig()} or {@link getConfigArray()}
147 * @param mixed[] $mapping Contains the mapping of "Server/config options"
148 * to "Server/1/config options"
150 public function setCfgUpdateReadMapping(array $mapping): void
152 $this->cfgUpdateReadMapping
= $mapping;
156 * Resets configuration data
158 public function resetConfigData(): void
160 $_SESSION[$this->id
] = [];
164 * Sets configuration data (overrides old data)
166 * @param mixed[] $cfg Configuration options
168 public function setConfigData(array $cfg): void
170 $_SESSION[$this->id
] = $cfg;
176 public function set(string $path, mixed $value, string|
null $canonicalPath = null): void
178 if ($canonicalPath === null) {
179 $canonicalPath = $this->getCanonicalPath($path);
182 if ($this->setFilter
!== null && ! isset($this->setFilter
[$canonicalPath])) {
186 // if the path isn't protected it may be removed
187 if (isset($this->persistKeys
[$canonicalPath])) {
188 Core
::arrayWrite($path, $_SESSION[$this->id
], $value);
193 $defaultValue = $this->getDefault($canonicalPath);
194 if ($this->isInSetup
) {
195 // remove if it has a default value or is empty
196 $removePath = $value === $defaultValue ||
empty($value) && empty($defaultValue);
198 // get original config values not overwritten by user
199 // preferences to allow for overwriting options set in
200 // config.inc.php with default values
201 $instanceDefaultValue = Core
::arrayRead($canonicalPath, $this->baseConfig
);
202 // remove if it has a default value and base config (config.inc.php)
203 // uses default value
204 $removePath = $value === $defaultValue && $instanceDefaultValue === $defaultValue;
208 Core
::arrayRemove($path, $_SESSION[$this->id
]);
213 Core
::arrayWrite($path, $_SESSION[$this->id
], $value);
217 * Flattens multidimensional array, changes indices to paths
218 * (eg. 'key/subkey').
220 * @param mixed[] $array Multidimensional array
221 * @param string $prefix Prefix
225 private function getFlatArray(array $array, string $prefix = ''): array
228 foreach ($array as $key => $value) {
229 if (is_array($value) && ! isset($value[0])) {
230 $result +
= $this->getFlatArray($value, $prefix . $key . '/');
232 $result[$prefix . $key] = $value;
240 * Returns default config in a flattened array
244 public function getFlatDefaultConfig(): array
246 return $this->getFlatArray($this->defaultCfg
);
250 * Updates config with values read from given array
251 * (config will contain differences to defaults from {@see \PhpMyAdmin\Config\Settings}).
253 * @param mixed[] $cfg Configuration
255 public function updateWithGlobalConfig(array $cfg): void
257 // load config array and flatten it
258 $flatConfig = $this->getFlatArray($cfg);
260 // save values map for translating a few user preferences paths,
261 // should be complemented by code reading from generated config
262 // to perform inverse mapping
263 foreach ($flatConfig as $path => $value) {
264 if (isset($this->cfgUpdateReadMapping
[$path])) {
265 $path = $this->cfgUpdateReadMapping
[$path];
268 $this->set($path, $value, $path);
273 * Returns config value or $default if it's not set
275 * @param string $path Path of config file
276 * @param mixed $default Default values
278 public function get(string $path, mixed $default = null): mixed
280 return Core
::arrayRead($path, $_SESSION[$this->id
], $default);
284 * Returns default config value or $default it it's not set ie. it doesn't
285 * exist in {@see \PhpMyAdmin\Config\Settings} ($cfg).
287 * @param string $canonicalPath Canonical path
288 * @param mixed $default Default value
290 public function getDefault(string $canonicalPath, mixed $default = null): mixed
292 return Core
::arrayRead($canonicalPath, $this->defaultCfg
, $default);
296 * Returns config value, if it's not set uses the default one; returns
297 * $default if the path isn't set and doesn't contain a default value
299 * @param string $path Path
300 * @param mixed $default Default value
302 public function getValue(string $path, mixed $default = null): mixed
304 $v = Core
::arrayRead($path, $_SESSION[$this->id
]);
309 $path = $this->getCanonicalPath($path);
311 return $this->getDefault($path, $default);
315 * Returns canonical path
317 * @param string $path Path
319 public function getCanonicalPath(string $path): string
321 return preg_replace('#^Servers/([\d]+)/#', 'Servers/1/', $path);
325 * Returns config database entry for $path
327 * @param string $path path of the variable in config db
328 * @param mixed $default default value
330 public function getDbEntry(string $path, mixed $default = null): mixed
332 return Core
::arrayRead($path, $this->cfgDb
, $default);
336 * Returns server count
338 public function getServerCount(): int
340 return isset($_SESSION[$this->id
]['Servers'])
341 ?
count($_SESSION[$this->id
]['Servers'])
346 * Returns server list
350 public function getServers(): array
352 return $_SESSION[$this->id
]['Servers'] ??
[];
356 * Returns DSN of given server
358 * @param int $server server index
360 public function getServerDSN(int $server): string
362 if (! isset($_SESSION[$this->id
]['Servers'][$server])) {
366 $path = 'Servers/' . $server;
368 if ($this->getValue($path . '/auth_type') === 'config') {
369 $dsn .= $this->getValue($path . '/user');
370 if (! empty($this->getValue($path . '/password'))) {
377 if ($this->getValue($path . '/host') !== 'localhost') {
378 $dsn .= $this->getValue($path . '/host');
379 $port = $this->getValue($path . '/port');
384 $dsn .= $this->getValue($path . '/socket');
391 * Returns server name
393 * @param int $id server index
395 public function getServerName(int $id): string
397 if (! isset($_SESSION[$this->id
]['Servers'][$id])) {
401 $verbose = $this->get('Servers/' . $id . '/verbose');
402 if (! empty($verbose)) {
406 $host = $this->get('Servers/' . $id . '/host');
408 return empty($host) ?
'localhost' : $host;
414 * @param int $server server index
416 public function removeServer(int $server): void
418 if (! isset($_SESSION[$this->id
]['Servers'][$server])) {
422 $lastServer = $this->getServerCount();
424 /** @infection-ignore-all */
425 for ($i = $server; $i < $lastServer; $i++
) {
426 $_SESSION[$this->id
]['Servers'][$i] = $_SESSION[$this->id
]['Servers'][$i +
1];
429 unset($_SESSION[$this->id
]['Servers'][$lastServer]);
431 if (! isset($_SESSION[$this->id
]['ServerDefault']) ||
$_SESSION[$this->id
]['ServerDefault'] != $lastServer) {
435 unset($_SESSION[$this->id
]['ServerDefault']);
439 * Returns configuration array (full, multidimensional format)
443 public function getConfig(): array
445 $c = $_SESSION[$this->id
];
446 foreach ($this->cfgUpdateReadMapping
as $mapTo => $mapFrom) {
447 // if the key $c exists in $map_to
448 if (Core
::arrayRead($mapTo, $c) === null) {
452 Core
::arrayWrite($mapTo, $c, Core
::arrayRead($mapFrom, $c));
453 Core
::arrayRemove($mapFrom, $c);
460 * Returns configuration array (flat format)
464 public function getConfigArray(): array
466 $c = $this->getFlatArray($_SESSION[$this->id
]);
468 $persistKeys = array_diff(
469 array_keys($this->persistKeys
),
472 foreach ($persistKeys as $k) {
473 $c[$k] = $this->getDefault($this->getCanonicalPath($k));
476 foreach ($this->cfgUpdateReadMapping
as $mapTo => $mapFrom) {
477 if (! isset($c[$mapFrom])) {
481 $c[$mapTo] = $c[$mapFrom];
489 * Database with allowed values for configuration stored in the $cfg array,
490 * used by setup script and user preferences to generate forms.
493 * array - select field, array contains allowed values
494 * string - type override
496 * @return array<string, string|mixed[]>
498 public function getAllowedValues(): array
500 $config = Config
::getInstance();
506 'auth_type' => ['config', 'http', 'signon', 'cookie'],
507 'AllowDeny' => ['order' => ['', 'deny,allow', 'allow,deny', 'explicit']],
508 'only_db' => 'array',
511 'RecodingEngine' => ['auto', 'iconv', 'mb', 'none'],
512 'OBGzip' => ['auto', true, false],
513 'MemoryLimit' => 'short_string',
514 'NavigationLogoLinkWindow' => ['main', 'new'],
515 'NavigationTreeDefaultTabTable' => [
517 'structure' => __('Structure'),
521 'search' => __('Search'),
523 'insert' => __('Insert'),
525 'browse' => __('Browse'),
527 'NavigationTreeDefaultTabTable2' => [
531 'structure' => __('Structure'),
535 'search' => __('Search'),
537 'insert' => __('Insert'),
539 'browse' => __('Browse'),
541 'NavigationTreeDbSeparator' => 'short_string',
542 'NavigationTreeTableSeparator' => 'short_string',
543 'NavigationWidth' => 'integer',
544 'TableNavigationLinksMode' => ['icons' => __('Icons'), 'text' => __('Text'), 'both' => __('Both')],
545 'MaxRows' => [25, 50, 100, 250, 500],
546 'Order' => ['ASC', 'DESC', 'SMART'],
547 'RowActionLinks' => [
548 'none' => __('Nowhere'),
549 'left' => __('Left'),
550 'right' => __('Right'),
551 'both' => __('Both'),
553 'TablePrimaryKeyOrder' => ['NONE' => __('None'), 'ASC' => __('Ascending'), 'DESC' => __('Descending')],
554 'ProtectBinary' => [false, 'blob', 'noblob', 'all'],
555 'CharEditing' => ['input', 'textarea'],
556 'TabsMode' => ['icons' => __('Icons'), 'text' => __('Text'), 'both' => __('Both')],
557 'PDFDefaultPageSize' => [
561 'letter' => 'letter',
564 'ActionLinksMode' => ['icons' => __('Icons'), 'text' => __('Text'), 'both' => __('Both')],
566 'click' => __('Click'),
567 'double-click' => __('Double click'),
568 'disabled' => __('Disabled'),
570 'RelationalDisplay' => ['K' => __('key'), 'D' => __('display column')],
571 'DefaultTabServer' => [
572 // the welcome page (recommended for multiuser setups)
573 'welcome' => __('Welcome'),
575 'databases' => __('Databases'),
576 // runtime information
577 'status' => __('Status'),
578 // MySQL server variables
579 'variables' => __('Variables'),
581 'privileges' => __('Privileges'),
583 'DefaultTabDatabase' => [
585 'structure' => __('Structure'),
589 'search' => __('Search'),
590 // operations on database
591 'operations' => __('Operations'),
593 'DefaultTabTable' => [
595 'structure' => __('Structure'),
599 'search' => __('Search'),
601 'insert' => __('Insert'),
603 'browse' => __('Browse'),
605 'InitialSlidersState' => ['open' => __('Open'), 'closed' => __('Closed'), 'disabled' => __('Disabled')],
606 'FirstDayOfCalendar' => [
607 1 => _pgettext('Week day name', 'Monday'),
608 2 => _pgettext('Week day name', 'Tuesday'),
609 3 => _pgettext('Week day name', 'Wednesday'),
610 4 => _pgettext('Week day name', 'Thursday'),
611 5 => _pgettext('Week day name', 'Friday'),
612 6 => _pgettext('Week day name', 'Saturday'),
613 7 => _pgettext('Week day name', 'Sunday'),
615 'SendErrorReports' => [
616 'ask' => __('Ask before sending error reports'),
617 'always' => __('Always send error reports'),
618 'never' => __('Never send error reports'),
620 'DefaultForeignKeyChecks' => [
621 'default' => __('Server default'),
622 'enable' => __('Enable'),
623 'disable' => __('Disable'),
632 // CSV using LOAD DATA
637 'charset' => array_merge([''], $config->settings
['AvailableCharsets'] ??
[]),
638 'sql_compatibility' => [
647 // removed; in MySQL 5.0.33, this produces exports that
648 // can't be read by POSTGRESQL (see our bug #1596328)
652 'csv_terminated' => 'short_string',
653 'csv_enclosed' => 'short_string',
654 'csv_escaped' => 'short_string',
655 'ldi_terminated' => 'short_string',
656 'ldi_enclosed' => 'short_string',
657 'ldi_escaped' => 'short_string',
658 'ldi_local_option' => ['auto', true, false],
663 'structure' => __('structure'),
664 'data' => __('data'),
665 'structure_and_data' => __('structure and data'),
668 'quick' => __('Quick - display only the minimal options to configure'),
669 'custom' => __('Custom - display all possible options to configure'),
670 'custom-no-form' => __('Custom - like above, but without the quick/custom choice'),
687 'compression' => ['none', 'zip', 'gzip'],
688 'charset' => array_merge([''], $config->settings
['AvailableCharsets'] ??
[]),
689 'sql_compatibility' => [
698 // removed; in MySQL 5.0.33, this produces exports that
699 // can't be read by POSTGRESQL (see our bug #1596328)
703 'codegen_format' => ['#', 'NHibernate C# DO', 'NHibernate XML'],
704 'csv_separator' => 'short_string',
705 'csv_terminated' => 'short_string',
706 'csv_enclosed' => 'short_string',
707 'csv_escaped' => 'short_string',
708 'csv_null' => 'short_string',
709 'excel_null' => 'short_string',
712 'mac_excel2003' => 'Excel 2003 / Macintosh',
713 'mac_excel2008' => 'Excel 2008 / Macintosh',
715 'sql_structure_or_data' => [
716 'structure' => __('structure'),
717 'data' => __('data'),
718 'structure_and_data' => __('structure and data'),
720 'sql_type' => ['INSERT', 'UPDATE', 'REPLACE'],
721 'sql_insert_syntax' => [
722 'complete' => __('complete inserts'),
723 'extended' => __('extended inserts'),
724 'both' => __('both of the above'),
725 'none' => __('neither of the above'),
727 'htmlword_structure_or_data' => [
728 'structure' => __('structure'),
729 'data' => __('data'),
730 'structure_and_data' => __('structure and data'),
732 'htmlword_null' => 'short_string',
733 'ods_null' => 'short_string',
734 'odt_null' => 'short_string',
735 'odt_structure_or_data' => [
736 'structure' => __('structure'),
737 'data' => __('data'),
738 'structure_and_data' => __('structure and data'),
740 'texytext_structure_or_data' => [
741 'structure' => __('structure'),
742 'data' => __('data'),
743 'structure_and_data' => __('structure and data'),
745 'texytext_null' => 'short_string',
749 'Mode' => ['info', 'show', 'collapse'],
750 'OrderBy' => ['exec', 'time', 'count'],
751 'Order' => ['asc', 'desc'],
755 * Basic validator assignments (functions from libraries/config/Validator.php
756 * and 'window.validators' object in js/config.js)
757 * Use only full paths and form ids
760 'Console/Height' => 'validateNonNegativeNumber',
761 'CharTextareaCols' => 'validatePositiveNumber',
762 'CharTextareaRows' => 'validatePositiveNumber',
763 'ExecTimeLimit' => 'validateNonNegativeNumber',
764 'Export/sql_max_query_size' => 'validatePositiveNumber',
765 'FirstLevelNavigationItems' => 'validatePositiveNumber',
766 'ForeignKeyMaxLimit' => 'validatePositiveNumber',
767 'Import/csv_enclosed' => [['validateByRegex', '/^.?$/']],
768 'Import/csv_escaped' => [['validateByRegex', '/^.$/']],
769 'Import/csv_terminated' => [['validateByRegex', '/^.$/']],
770 'Import/ldi_enclosed' => [['validateByRegex', '/^.?$/']],
771 'Import/ldi_escaped' => [['validateByRegex', '/^.$/']],
772 'Import/ldi_terminated' => [['validateByRegex', '/^.$/']],
773 'Import/skip_queries' => 'validateNonNegativeNumber',
774 'InsertRows' => 'validatePositiveNumber',
775 'NumRecentTables' => 'validateNonNegativeNumber',
776 'NumFavoriteTables' => 'validateNonNegativeNumber',
777 'LimitChars' => 'validatePositiveNumber',
778 'LoginCookieValidity' => 'validatePositiveNumber',
779 'LoginCookieStore' => 'validateNonNegativeNumber',
780 'MaxDbList' => 'validatePositiveNumber',
781 'MaxNavigationItems' => 'validatePositiveNumber',
782 'MaxCharactersInDisplayedSQL' => 'validatePositiveNumber',
783 'MaxRows' => 'validatePositiveNumber',
784 'MaxSizeForInputField' => 'validatePositiveNumber',
785 'MinSizeForInputField' => 'validateNonNegativeNumber',
786 'MaxTableList' => 'validatePositiveNumber',
787 'MemoryLimit' => [['validateByRegex', '/^(-1|(\d+(?:[kmg])?))$/i']],
788 'NavigationTreeDisplayItemFilterMinimum' => 'validatePositiveNumber',
789 'NavigationTreeTableLevel' => 'validatePositiveNumber',
790 'NavigationWidth' => 'validateNonNegativeNumber',
791 'QueryHistoryMax' => 'validatePositiveNumber',
792 'RepeatCells' => 'validateNonNegativeNumber',
793 'Server' => 'validateServer',
794 'Server_pmadb' => 'validatePMAStorage',
795 'Servers/1/port' => 'validatePortNumber',
796 'Servers/1/hide_db' => 'validateRegex',
797 'TextareaCols' => 'validatePositiveNumber',
798 'TextareaRows' => 'validatePositiveNumber',
799 'TrustedProxies' => 'validateTrustedProxies',
803 * Additional validators used for user preferences
805 '_userValidators' => [
806 'MaxDbList' => [['validateUpperBound', 'value:MaxDbList']],
807 'MaxTableList' => [['validateUpperBound', 'value:MaxTableList']],
808 'QueryHistoryMax' => [['validateUpperBound', 'value:QueryHistoryMax']],