*/ public int $server = 0; /** @var array $tempDir */ private static array $tempDir = []; private bool $hasSelectedServer = false; /** @psalm-var ServerSettingsType */ public array $selectedServer; private bool $isSetup = false; public function __construct() { $this->config = new Settings([]); $config = $this->config->asArray(); $this->settings = $config; $this->baseSettings = $config; $this->selectedServer = (new Server())->asArray(); } /** @deprecated Use dependency injection instead. */ public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } public function setSetup(bool $isSetup): void { $this->isSetup = $isSetup; } public function isSetup(): bool { return $this->isSetup; } /** * @param string|null $source source to read config from * * @throws ConfigException */ public function loadFromFile(string|null $source = null): void { if ($source !== null) { $this->setSource($source); } if ($this->configFileExists()) { $this->load(); } $this->baseSettings = $this->settings; } /** * Determines if GD2+ is available. * * Respects the config override ('yes' / 'no') if set, * otherwise checks the `GD_MAJOR_VERSION` constant (>= 2). */ public function isGd2Available(): bool { if ($this->config->GD2Available === 'yes') { return true; } if ($this->config->GD2Available === 'no') { return false; } return (get_defined_constants()['GD_MAJOR_VERSION'] ?? 0) >= 2; } public function isWindows(): bool { if (PHP_OS_FAMILY === 'Windows') { return true; } // Is it OS/2 (No file permissions like Windows) return stripos(PHP_OS, 'OS/2') !== false; } /** * loads configuration from $source, usually the config file * should be called on object creation * * @throws ConfigException */ private function load(): void { /** @var mixed $cfg */ $cfg = []; ob_start(); try { /** * Suppress any warnings generated by require or inside the included file * * @psalm-suppress UnresolvableInclude */ @require $this->source; } catch (Throwable) { throw new ConfigException('Failed to load phpMyAdmin configuration.'); } ob_end_clean(); $this->sourceMtime = (int) filemtime($this->source); if (! is_array($cfg)) { return; } $this->config = new Settings($cfg); $this->settings = $this->config->asArray(); } /** * set source * * @param string $source source */ public function setSource(string $source): void { $this->source = trim($source); } /** @throws ConfigException */ public function configFileExists(): bool { if ($this->source === '') { // no configuration file set at all return false; } if (! @file_exists($this->source)) { return false; } if (! @is_readable($this->source)) { // manually check if file is readable // might be bug #3059806 Supporting running from CIFS/Samba shares $contents = false; $handle = @fopen($this->source, 'r'); if ($handle !== false) { $contents = @fread($handle, 1); // reading 1 byte is enough to test fclose($handle); } if ($contents === false) { throw new ConfigException(sprintf( function_exists('__') ? __('Existing configuration file (%s) is not readable.') : 'Existing configuration file (%s) is not readable.', $this->source, )); } } return true; } /** * verifies the permissions on config file (if asked by configuration) * (must be called after config.inc.php has been merged) * * @throws ConfigException */ public function checkPermissions(): void { // Check for permissions (on platforms that support it): if (! $this->config->CheckConfigurationPermissions || ! @file_exists($this->source)) { return; } $perms = @fileperms($this->source); if ($perms === false || ! ($perms & 2)) { return; } // This check is normally done after loading configuration if ($this->isWindows()) { return; } throw new ConfigException(__('Wrong permissions on configuration file, should not be world writable!')); } /** * sets configuration variable * * @param string $setting configuration option * @param mixed $value new value for configuration option * * @throws ConfigException */ public function set(string $setting, mixed $value): void { $parts = explode('/', $setting); if (! $this->setValueRecursive($this->settings, $parts, $value)) { return; } $this->config = new Settings($this->settings); $this->settings = $this->config->asArray(); } /** * @param mixed[] $array * @param array-key[] $parts * * @throws ConfigException */ private function setValueRecursive(array &$array, array $parts, mixed $value): bool { $part = array_shift($parts); if ($parts === []) { if (isset($array[$part]) && $array[$part] === $value) { return false; } $array[$part] = $value; return true; } if (! is_array($array[$part])) { throw new ConfigException('Failed to set configuration value.'); } return $this->setValueRecursive($array[$part], $parts, $value); } /** * returns source for current config * * @return string config source */ public function getSource(): string { return $this->source; } public function isUploadEnabled(): bool { $iniValue = ini_get('file_uploads'); // if set "php_admin_value file_uploads Off" in httpd.conf // ini_get() also returns the string "Off" in this case: return $iniValue !== false && $iniValue !== '' && $iniValue !== '0' && strtolower($iniValue) !== 'off'; } /** * Checks if protocol is https * * This function checks if the https protocol on the active connection. */ public function isHttps(): bool { if ($this->isHttps !== null) { return $this->isHttps; } $url = $this->config->PmaAbsoluteUri; $isHttps = false; if ($url !== '' && parse_url($url, PHP_URL_SCHEME) === 'https') { $isHttps = true; } elseif (strtolower(Core::getEnv('HTTP_SCHEME')) === 'https') { $isHttps = true; } elseif (strtolower(Core::getEnv('HTTPS')) === 'on') { $isHttps = true; } elseif (stripos(Core::getEnv('REQUEST_URI'), 'https:') === 0) { $isHttps = true; } elseif (strtolower(Core::getEnv('HTTP_HTTPS_FROM_LB')) === 'on') { // A10 Networks load balancer $isHttps = true; } elseif (strtolower(Core::getEnv('HTTP_FRONT_END_HTTPS')) === 'on') { $isHttps = true; } elseif (strtolower(Core::getEnv('HTTP_X_FORWARDED_PROTO')) === 'https') { $isHttps = true; } elseif (strtolower(Core::getEnv('HTTP_CLOUDFRONT_FORWARDED_PROTO')) === 'https') { // Amazon CloudFront, issue #15621 $isHttps = true; } elseif (Util::getProtoFromForwardedHeader(Core::getEnv('HTTP_FORWARDED')) === 'https') { // RFC 7239 Forwarded header $isHttps = true; } elseif ((int) Core::getEnv('SERVER_PORT') === 443) { $isHttps = true; } $this->isHttps = $isHttps; return $isHttps; } /** * Get phpMyAdmin root path */ public function getRootPath(): string { $url = $this->config->PmaAbsoluteUri; if ($url !== '') { $path = parse_url($url, PHP_URL_PATH); if (! empty($path)) { if (! str_ends_with($path, '/')) { return $path . '/'; } return $path; } } $parsedUrlPath = Routing::getCleanPathInfo(); $parts = explode('/', $parsedUrlPath); /* Remove filename */ if (str_ends_with($parts[count($parts) - 1], '.php')) { $parts = array_slice($parts, 0, count($parts) - 1); } // Add one more part to make the path end in slash unless it already ends with slash if (count($parts) < 2 || $parts[array_key_last($parts)] !== '') { $parts[] = ''; } return implode('/', $parts); } /** * removes cookie * * @param string $cookieName name of cookie to remove */ public function removeCookie(string $cookieName): bool { $httpCookieName = $this->getCookieName($cookieName); if ($this->issetCookie($cookieName)) { unset($_COOKIE[$httpCookieName]); } if (defined('TESTSUITE')) { return true; } return setcookie( $httpCookieName, '', time() - 3600, $this->getRootPath(), '', $this->isHttps(), ); } /** * sets cookie if value is different from current cookie value, * or removes if value is equal to default * * @param string $cookie name of cookie to remove * @param string $value new cookie value * @param string|null $default default value * @param int|null $validity validity of cookie in seconds (default is one month) * @param bool $httponly whether cookie is only for HTTP (and not for scripts) */ public function setCookie( string $cookie, string $value, string|null $default = null, int|null $validity = null, bool $httponly = true, ): bool { if ($value !== '' && $value === $default) { // default value is used if ($this->issetCookie($cookie)) { // remove cookie return $this->removeCookie($cookie); } return false; } if ($value === '' && $this->issetCookie($cookie)) { // remove cookie, value is empty return $this->removeCookie($cookie); } $httpCookieName = $this->getCookieName($cookie); if (! $this->issetCookie($cookie) || $this->getCookie($cookie) !== $value) { // set cookie with new value /* Calculate cookie validity */ if ($validity === null) { /* Valid for one month */ $validity = time() + 2592000; } elseif ($validity === 0) { /* Valid for session */ $validity = 0; } else { $validity += time(); } if (defined('TESTSUITE')) { $_COOKIE[$httpCookieName] = $value; return true; } $cookieSameSite = $this->config->CookieSameSite; $optionalParams = [ 'expires' => $validity, 'path' => $this->getRootPath(), 'domain' => '', 'secure' => $this->isHttps(), 'httponly' => $httponly, 'samesite' => $cookieSameSite, ]; return setcookie($httpCookieName, $value, $optionalParams); } // cookie has already $value as value return true; } /** * get cookie * * @param string $cookieName The name of the cookie to get * * @return mixed result of getCookie() */ public function getCookie(string $cookieName): mixed { return $_COOKIE[$this->getCookieName($cookieName)] ?? null; } /** * Get the real cookie name * * @param string $cookieName The name of the cookie */ public function getCookieName(string $cookieName): string { return ($this->isHttps() ? '__Secure-' : '') . $cookieName . ($this->isHttps() ? '_https' : ''); } /** * isset cookie * * @param string $cookieName The name of the cookie to check */ public function issetCookie(string $cookieName): bool { return isset($_COOKIE[$this->getCookieName($cookieName)]); } /** * Returns temporary dir path * * @param string $name Directory name */ public function getTempDir(string $name): string|null { if (isset(self::$tempDir[$name])) { return self::$tempDir[$name]; } $path = $this->config->TempDir; if ($path === '') { return null; } $path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name; if (! @is_dir($path)) { @mkdir($path, 0770, true); } if (! @is_dir($path) || ! @is_writable($path)) { return null; } self::$tempDir[$name] = $path; return $path; } /** * Returns temporary directory */ public function getUploadTempDir(): string|null { // First try configured temp dir // Fallback to PHP upload_tmp_dir $dirs = [$this->getTempDir('upload'), ini_get('upload_tmp_dir'), sys_get_temp_dir()]; foreach ($dirs as $dir) { if (! in_array($dir, ['', '0', null, false], true) && @is_writable($dir)) { return realpath($dir); } } return null; } /** @return int<0, max> */ public function selectServer(mixed $serverParamFromRequest): int { $serverNumber = 0; if (is_numeric($serverParamFromRequest)) { $serverNumber = (int) $serverParamFromRequest; $serverNumber = $serverNumber >= 1 ? $serverNumber : 0; } elseif (is_string($serverParamFromRequest) && $serverParamFromRequest !== '') { /** Lookup server by name (see FAQ 4.8) */ foreach ($this->config->Servers as $i => $server) { if ($server->host === $serverParamFromRequest || $server->verbose === $serverParamFromRequest) { $serverNumber = $i; break; } $verboseToLower = mb_strtolower($server->verbose); $serverToLower = mb_strtolower($serverParamFromRequest); if ($verboseToLower === $serverToLower || md5($verboseToLower) === $serverToLower) { $serverNumber = $i; break; } } } /** * If no server is selected, make sure that $this->settings['Server'] is empty (so * that nothing will work), and skip server authentication. * We do NOT exit here, but continue on without logging into any server. * This way, the welcome page will still come up (with no server info) and * present a choice of servers in the case that there are multiple servers * and '$this->config->ServerDefault = 0' is set. */ if (isset($this->config->Servers[$serverNumber])) { $this->hasSelectedServer = true; $this->selectedServer = $this->config->Servers[$serverNumber]->asArray(); } elseif (isset($this->config->Servers[$this->config->ServerDefault])) { $this->hasSelectedServer = true; $serverNumber = $this->config->ServerDefault; $this->selectedServer = $this->config->Servers[$this->config->ServerDefault]->asArray(); } else { $this->hasSelectedServer = false; $serverNumber = 0; $this->selectedServer = (new Server())->asArray(); } $this->server = $serverNumber; return $this->server; } /** * Return connection parameters for the database server */ public static function getConnectionParams(Server $currentServer, ConnectionType $connectionType): Server { if ($connectionType !== ConnectionType::ControlUser) { if ($currentServer->host !== '' && $currentServer->port !== '') { return $currentServer; } $server = $currentServer->asArray(); $server['host'] = $server['host'] === '' ? 'localhost' : $server['host']; $server['port'] = $server['port'] === '' ? '0' : $server['port']; return new Server($server); } $server = [ 'user' => $currentServer->controlUser, 'password' => $currentServer->controlPass, 'host' => $currentServer->controlHost !== '' ? $currentServer->controlHost : $currentServer->host, 'port' => '0', 'socket' => null, 'compress' => null, 'ssl' => null, 'ssl_key' => null, 'ssl_cert' => null, 'ssl_ca' => null, 'ssl_ca_path' => null, 'ssl_ciphers' => null, 'ssl_verify' => null, 'hide_connection_errors' => null, ]; // Share the settings if the host is same if ($server['host'] === $currentServer->host) { $server['port'] = $currentServer->port !== '' ? $currentServer->port : '0'; $server['socket'] = $currentServer->socket; $server['compress'] = $currentServer->compress; $server['ssl'] = $currentServer->ssl; $server['ssl_key'] = $currentServer->sslKey; $server['ssl_cert'] = $currentServer->sslCert; $server['ssl_ca'] = $currentServer->sslCa; $server['ssl_ca_path'] = $currentServer->sslCaPath; $server['ssl_ciphers'] = $currentServer->sslCiphers; $server['ssl_verify'] = $currentServer->sslVerify; $server['hide_connection_errors'] = $currentServer->hideConnectionErrors; } // Set configured port if ($currentServer->controlPort !== '') { $server['port'] = $currentServer->controlPort; } // Set any configuration with control_ prefix $server['socket'] = $currentServer->controlSocket ?? $server['socket']; $server['compress'] = $currentServer->controlCompress ?? $server['compress']; $server['ssl'] = $currentServer->controlSsl ?? $server['ssl']; $server['ssl_key'] = $currentServer->controlSslKey ?? $server['ssl_key']; $server['ssl_cert'] = $currentServer->controlSslCert ?? $server['ssl_cert']; $server['ssl_ca'] = $currentServer->controlSslCa ?? $server['ssl_ca']; $server['ssl_ca_path'] = $currentServer->controlSslCaPath ?? $server['ssl_ca_path']; $server['ssl_ciphers'] = $currentServer->controlSslCiphers ?? $server['ssl_ciphers']; $server['ssl_verify'] = $currentServer->controlSslVerify ?? $server['ssl_verify']; $server['hide_connection_errors'] = $currentServer->controlHideConnectionErrors ?? $server['hide_connection_errors']; if ($server['host'] === '') { $server['host'] = 'localhost'; } return new Server($server); } public function getSettings(): Settings { return $this->config; } public function hasSelectedServer(): bool { return $this->hasSelectedServer; } public function getChangeLogFilePath(): string { return CHANGELOG_FILE; } }