Merge pull request #19212 from kamil-tekiela/privatize-const
[phpmyadmin.git] / src / Config / ConfigFile.php
bloba30c39abc72ef801637d727e50aec7962b7d0aec
1 <?php
2 /**
3 * Config file management
4 */
6 declare(strict_types=1);
8 namespace PhpMyAdmin\Config;
10 use PhpMyAdmin\Config;
11 use PhpMyAdmin\Core;
12 use PhpMyAdmin\Current;
14 use function __;
15 use function _pgettext;
16 use function array_diff;
17 use function array_flip;
18 use function array_keys;
19 use function array_merge;
20 use function count;
21 use function is_array;
22 use function preg_replace;
24 /**
25 * Config file management class.
26 * Stores its data in $_SESSION
28 class ConfigFile
30 /**
31 * Stores default phpMyAdmin config
33 * @see Settings
35 * @var mixed[]
37 private array $defaultCfg;
39 /**
40 * Stores allowed values for non-standard fields
42 * @var array<string, string|mixed[]>
44 private array $cfgDb;
46 /**
47 * Whether we are currently working in PMA Setup context
49 private bool $isInSetup;
51 /**
52 * Keys which will be always written to config file
54 * @var mixed[]
56 private array $persistKeys = [];
58 /**
59 * Changes keys while updating config in {@link updateWithGlobalConfig()}
60 * or reading by {@link getConfig()} or {@link getConfigArray()}
62 * @var mixed[]
64 private array $cfgUpdateReadMapping = [];
66 /**
67 * Key filter for {@link set()}
69 private array|null $setFilter = null;
71 /**
72 * Instance id (key in $_SESSION array, separate for each server -
73 * ConfigFile{server id})
75 private string $id;
77 /**
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])) {
94 return;
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
109 // to keys
110 $this->persistKeys = array_flip($keys);
114 * Returns flipped array set by {@link setPersistKeys()}
116 * @return mixed[]
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;
134 return;
137 // checking key presence is much faster than searching so move values
138 // to keys
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;
174 * Sets config value
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])) {
183 return;
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);
190 return;
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);
197 } else {
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;
207 if ($removePath) {
208 Core::arrayRemove($path, $_SESSION[$this->id]);
210 return;
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
223 * @return mixed[]
225 private function getFlatArray(array $array, string $prefix = ''): array
227 $result = [];
228 foreach ($array as $key => $value) {
229 if (is_array($value) && ! isset($value[0])) {
230 $result += $this->getFlatArray($value, $prefix . $key . '/');
231 } else {
232 $result[$prefix . $key] = $value;
236 return $result;
240 * Returns default config in a flattened array
242 * @return mixed[]
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]);
305 if ($v !== null) {
306 return $v;
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'])
342 : 0;
346 * Returns server list
348 * @return mixed[]
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])) {
363 return '';
366 $path = 'Servers/' . $server;
367 $dsn = 'mysqli://';
368 if ($this->getValue($path . '/auth_type') === 'config') {
369 $dsn .= $this->getValue($path . '/user');
370 if (! empty($this->getValue($path . '/password'))) {
371 $dsn .= ':***';
374 $dsn .= '@';
377 if ($this->getValue($path . '/host') !== 'localhost') {
378 $dsn .= $this->getValue($path . '/host');
379 $port = $this->getValue($path . '/port');
380 if ($port) {
381 $dsn .= ':' . $port;
383 } else {
384 $dsn .= $this->getValue($path . '/socket');
387 return $dsn;
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])) {
398 return '';
401 $verbose = $this->get('Servers/' . $id . '/verbose');
402 if (! empty($verbose)) {
403 return $verbose;
406 $host = $this->get('Servers/' . $id . '/host');
408 return empty($host) ? 'localhost' : $host;
412 * Removes server
414 * @param int $server server index
416 public function removeServer(int $server): void
418 if (! isset($_SESSION[$this->id]['Servers'][$server])) {
419 return;
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) {
432 return;
435 unset($_SESSION[$this->id]['ServerDefault']);
439 * Returns configuration array (full, multidimensional format)
441 * @return mixed[]
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) {
449 continue;
452 Core::arrayWrite($mapTo, $c, Core::arrayRead($mapFrom, $c));
453 Core::arrayRemove($mapFrom, $c);
456 return $c;
460 * Returns configuration array (flat format)
462 * @return mixed[]
464 public function getConfigArray(): array
466 $c = $this->getFlatArray($_SESSION[$this->id]);
468 $persistKeys = array_diff(
469 array_keys($this->persistKeys),
470 array_keys($c),
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])) {
478 continue;
481 $c[$mapTo] = $c[$mapFrom];
482 unset($c[$mapFrom]);
485 return $c;
489 * Database with allowed values for configuration stored in the $cfg array,
490 * used by setup script and user preferences to generate forms.
492 * Value meaning:
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();
502 return [
503 'Servers' => [
504 1 => [
505 'port' => 'integer',
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' => [
516 // fields list
517 'structure' => __('Structure'),
518 // SQL form
519 'sql' => __('SQL'),
520 // search page
521 'search' => __('Search'),
522 // insert row page
523 'insert' => __('Insert'),
524 // browse page
525 'browse' => __('Browse'),
527 'NavigationTreeDefaultTabTable2' => [
528 //don't display
529 '' => '',
530 // fields list
531 'structure' => __('Structure'),
532 // SQL form
533 'sql' => __('SQL'),
534 // search page
535 'search' => __('Search'),
536 // insert row page
537 'insert' => __('Insert'),
538 // browse page
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' => [
558 'A3' => 'A3',
559 'A4' => 'A4',
560 'A5' => 'A5',
561 'letter' => 'letter',
562 'legal' => 'legal',
564 'ActionLinksMode' => ['icons' => __('Icons'), 'text' => __('Text'), 'both' => __('Both')],
565 'GridEditing' => [
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'),
574 // list of databases
575 'databases' => __('Databases'),
576 // runtime information
577 'status' => __('Status'),
578 // MySQL server variables
579 'variables' => __('Variables'),
580 // user management
581 'privileges' => __('Privileges'),
583 'DefaultTabDatabase' => [
584 // tables list
585 'structure' => __('Structure'),
586 // SQL form
587 'sql' => __('SQL'),
588 // search query
589 'search' => __('Search'),
590 // operations on database
591 'operations' => __('Operations'),
593 'DefaultTabTable' => [
594 // fields list
595 'structure' => __('Structure'),
596 // SQL form
597 'sql' => __('SQL'),
598 // search page
599 'search' => __('Search'),
600 // insert row page
601 'insert' => __('Insert'),
602 // browse page
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'),
626 'Import' => [
627 'format' => [
628 // CSV
629 'csv',
630 // DocSQL
631 'docsql',
632 // CSV using LOAD DATA
633 'ldi',
634 // SQL
635 'sql',
637 'charset' => array_merge([''], $config->settings['AvailableCharsets'] ?? []),
638 'sql_compatibility' => [
639 'NONE',
640 'ANSI',
641 'DB2',
642 'MAXDB',
643 'MYSQL323',
644 'MYSQL40',
645 'MSSQL',
646 'ORACLE',
647 // removed; in MySQL 5.0.33, this produces exports that
648 // can't be read by POSTGRESQL (see our bug #1596328)
649 //'POSTGRESQL',
650 'TRADITIONAL',
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],
661 'Export' => [
662 '_sod_select' => [
663 'structure' => __('structure'),
664 'data' => __('data'),
665 'structure_and_data' => __('structure and data'),
667 'method' => [
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'),
672 'format' => [
673 'codegen',
674 'csv',
675 'excel',
676 'htmlexcel',
677 'htmlword',
678 'latex',
679 'ods',
680 'odt',
681 'pdf',
682 'sql',
683 'texytext',
684 'xml',
685 'yaml',
687 'compression' => ['none', 'zip', 'gzip'],
688 'charset' => array_merge([''], $config->settings['AvailableCharsets'] ?? []),
689 'sql_compatibility' => [
690 'NONE',
691 'ANSI',
692 'DB2',
693 'MAXDB',
694 'MYSQL323',
695 'MYSQL40',
696 'MSSQL',
697 'ORACLE',
698 // removed; in MySQL 5.0.33, this produces exports that
699 // can't be read by POSTGRESQL (see our bug #1596328)
700 //'POSTGRESQL',
701 'TRADITIONAL',
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',
710 'excel_edition' => [
711 'win' => 'Windows',
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',
748 'Console' => [
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
759 '_validators' => [
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']],