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