diff --git a/README.md b/README.md index 99bc7b32c..f5313327d 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ For an overview of the software quality over time and the evolving metrics, chec | functions.php | ❌ | yyyy-mm-dd | | groups.php | ❌ | yyyy-mm-dd | | help.php | ❌ | yyyy-mm-dd | -| history.php | ❌ | yyyy-mm-dd | +| history.php | ✅ | 2025-09-14 | | index.php | ✅ | 2024-12-26 | | indexes.php | ❌ | yyyy-mm-dd | | info.php | ❌ | yyyy-mm-dd | diff --git a/all_db.php b/all_db.php index 881e3cc0e..b7fbda72c 100644 --- a/all_db.php +++ b/all_db.php @@ -1,5 +1,7 @@ printHeader($lang['strhistory']); - - // Bring to the front always - echo "\n"; - - echo "
\n"; - $misc->printConnection($onchange); - echo "

"; - - if (!isset($_REQUEST['database'])) { - echo "

{$lang['strnodatabaseselected']}

\n"; - return; - } - - if (isset($_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']])) { - include_once('classes/ArrayRecordSet.php'); - - $history = new ArrayRecordSet($_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']]); - - $columns = array( - 'query' => array( - 'title' => $lang['strsql'], - 'field' => field('query'), - ), - 'paginate' => array( - 'title' => $lang['strpaginate'], - 'field' => field('paginate'), - 'type' => 'yesno', - ), - 'actions' => array( - 'title' => $lang['stractions'], - ), - ); - - $actions = array( - 'run' => array( - 'content' => $lang['strexecute'], - 'attr' => array ( - 'href' => array ( - 'url' => 'sql.php', - 'urlvars' => array ( - 'subject' => 'history', - 'nohistory' => 't', - 'queryid' => field('queryid'), - 'paginate' => field('paginate') - ) - ), - 'target' => 'detail' - ) - ), - 'remove' => array( - 'content' => $lang['strdelete'], - 'attr' => array ( - 'href' => array ( - 'url' => 'history.php', - 'urlvars' => array ( - 'action' => 'confdelhistory', - 'queryid' => field('queryid'), - ) - ) - ) - ) - ); - - $misc->printTable($history, $columns, $actions, 'history-history', $lang['strnohistory']); - } else { - echo "

{$lang['strnohistory']}

\n"; - } - - $navlinks = array ( - 'refresh' => array ( - 'attr' => array ( - 'href' => array ( - 'url' => 'history.php', - 'urlvars' => array ( - 'action' => 'history', - 'server' => $_REQUEST['server'], - 'database' => $_REQUEST['database'], - ) - ) - ), - 'content' => $lang['strrefresh'] - ) - ); - - if ( - isset($_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']]) - && count($_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']]) - ) { - $navlinks['download'] = array ( - 'attr' => array ( - 'href' => array ( - 'url' => 'history.php', - 'urlvars' => array ( - 'action' => 'download', - 'server' => $_REQUEST['server'], - 'database' => $_REQUEST['database'] - ) - ) - ), - 'content' => $lang['strdownload'] - ); - $navlinks['clear'] = array ( - 'attr' => array ( - 'href' => array ( - 'url' => 'history.php', - 'urlvars' => array( - 'action' => 'confclearhistory', - 'server' => $_REQUEST['server'], - 'database' => $_REQUEST['database'] - ) - ) - ), - 'content' => $lang['strclearhistory'] - ); - } - - $misc->printNavLinks($navlinks, 'history-history', get_defined_vars()); -} - -function doDelHistory($qid, $confirm) -{ - global $misc, $lang; - - if ($confirm) { - $misc->printHeader($lang['strhistory']); - - // Bring to the front always - echo "\n"; - - echo "

{$lang['strdelhistory']}

\n"; - echo "

{$lang['strconfdelhistory']}

\n"; - - echo "
", htmlentities($_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']][$qid]['query'], ENT_QUOTES, 'UTF-8'), "
"; - echo "
\n"; - echo "\n"; - echo "\n"; - echo $misc->form; - echo "\n"; - echo "\n"; - echo "
\n"; - } else { - unset($_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']][$qid]); - } -} - -function doClearHistory($confirm) -{ - global $misc, $lang; - - if ($confirm) { - $misc->printHeader($lang['strhistory']); - - // Bring to the front always - echo "\n"; - - echo "

{$lang['strclearhistory']}

\n"; - echo "

{$lang['strconfclearhistory']}

\n"; - - echo "
\n"; - echo "\n"; - echo $misc->form; - echo "\n"; - echo "\n"; - echo "
\n"; - } else { - unset($_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']]); - } -} - -function doDownloadHistory() -{ - header('Content-Type: application/download'); - $datetime = date('YmdHis'); - header("Content-Disposition: attachment; filename=history{$datetime}.sql"); - - foreach ($_SESSION['history'][$_REQUEST['server']][$_REQUEST['database']] as $queries) { - $query = rtrim($queries['query']); - echo $query; - if (substr($query, -1) != ';') { - echo ';'; - } - echo "\n"; - } - - exit; -} - -switch ($action) { - case 'confdelhistory': - doDelHistory($_REQUEST['queryid'], true); - break; - case 'delhistory': - if (isset($_POST['yes'])) { - doDelHistory($_REQUEST['queryid'], false); - } - doDefault(); - break; - case 'confclearhistory': - doClearHistory(true); - break; - case 'clearhistory': - if (isset($_POST['yes'])) { - doClearHistory(false); - } - doDefault(); - break; - case 'download': - doDownloadHistory(); - break; - default: - doDefault(); -} - - // Set the name of the window - $misc->setWindowName('history'); - $misc->printFooter(); +$website = new History(); +echo $website->buildHtmlString(); diff --git a/history_clear.php b/history_clear.php new file mode 100644 index 000000000..5af80847b --- /dev/null +++ b/history_clear.php @@ -0,0 +1,10 @@ +buildHtmlString(); diff --git a/history_delete.php b/history_delete.php new file mode 100644 index 000000000..ef3b527ef --- /dev/null +++ b/history_delete.php @@ -0,0 +1,10 @@ +buildHtmlString(); diff --git a/history_download.php b/history_download.php new file mode 100644 index 000000000..d25035c63 --- /dev/null +++ b/history_download.php @@ -0,0 +1,10 @@ +buildHtmlString(); diff --git a/intro.php b/intro.php index 24bf91f5a..03ded545e 100644 --- a/intro.php +++ b/intro.php @@ -1,5 +1,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Api/Servers/Tree.php b/src/Api/Servers/Tree.php index edca84dae..5b06b7856 100644 --- a/src/Api/Servers/Tree.php +++ b/src/Api/Servers/Tree.php @@ -22,14 +22,20 @@ public function outputXmlTree(): void $dom->formatOutput = true; $dom->encoding = 'utf-8'; - $logins = isset($_SESSION['webdbLogin']) && is_array($_SESSION['webdbLogin']) ? $_SESSION['webdbLogin'] : []; + $logins = isset($_SESSION['webdbLogin']) && is_array($_SESSION['webdbLogin']) + ? $_SESSION['webdbLogin'] + : []; $root = $dom->createElement('tree'); $serverIdParam = RequestParameter::getString('server'); - $serverSession = !is_null($serverIdParam) ? ServerSession::fromServerId($serverIdParam) : null; + $serverSession = !is_null($serverIdParam) + ? ServerSession::fromServerId($serverIdParam) + : null; + if (!is_null($serverSession)) { $dbConnection = $serverSession->getDatabaseConnection(); $dbs = $dbConnection->getDatabases(); + foreach ($dbs as $dbData) { $actionUrl = 'redirect.php'; $actionUrlParams = [ @@ -53,6 +59,7 @@ public function outputXmlTree(): void } } else { $configuredServers = Config::getServers(); + foreach ($configuredServers as $configuredServer) { $tree = $dom->createElement('tree'); $tree->setAttribute('text', (string)$configuredServer->Name); @@ -65,6 +72,7 @@ public function outputXmlTree(): void $tree->setAttribute('action', $actionUrl); $username = ''; + if ( isset($logins[$serverId]) && is_array($logins[$serverId]) && @@ -80,7 +88,9 @@ public function outputXmlTree(): void $tree->setAttribute('src', $srcUrl); } - $iconName = $username !== '' ? 'Server' : 'DisconnectedServer'; + $iconName = $username !== '' + ? 'Server' + : 'DisconnectedServer'; $tree->setAttribute('icon', Config::getIcon($iconName)); $tree->setAttribute('openicon', Config::getIcon($iconName)); $tree->setAttribute('tooltip', $serverId); diff --git a/src/Config.php b/src/Config.php index f1925673d..056c32569 100644 --- a/src/Config.php +++ b/src/Config.php @@ -13,7 +13,7 @@ * 'extra_session_security'?: bool, * 'left_width'?: int, * 'owned_only'?: bool, - * 'servers'?: Server[], + * 'servers'?: array, * 'show_advanced'?: bool, * 'show_system'?: bool * } @@ -38,6 +38,7 @@ final class Config public static function extraSessionSecurity(): bool { $conf = self::tryGetConfigFileData(); + return ($conf['extra_session_security'] ?? true) === true; } @@ -48,18 +49,22 @@ public static function getAvailableLocales(): array { $localeDir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'locale'; $langDir = dir($localeDir); + if ($langDir === false) { return []; } self::$availableLocales = []; - while (false !== ($entry = $langDir->read())) { + + while (($entry = $langDir->read()) !== false) { if ($entry === '.' || $entry === '..') { continue; } + if (!is_dir($localeDir . DIRECTORY_SEPARATOR . $entry)) { continue; } + if (preg_match('/^(?P[a-z]{2})_(?P[A-Z]{2})$/', $entry, $matches)) { self::$availableLocales[] = $entry; } @@ -74,6 +79,7 @@ public static function getAvailableLocales(): array public static function getIcon(string|array $icon): string { $possiblePaths = []; + if (is_string($icon)) { $theme = self::theme(); $path = "images/themes/{$theme}/{$icon}"; @@ -99,17 +105,19 @@ public static function getIcon(string|array $icon): string } /** - * @return Server[] + * @return array */ public static function getServers(): array { $conf = self::tryGetConfigFileData(); + return $conf['servers'] ?? []; } public static function leftWidth(): int { $conf = self::tryGetConfigFileData(); + return $conf['left_width'] ?? 200; } @@ -117,17 +125,19 @@ public static function locale(): string { if (!isset(self::$data['locale'])) { $locale = null; + if ( isset($_REQUEST['language']) && is_string($_REQUEST['language']) && self::languageIsAvailable($_REQUEST['language']) ) { $locale = self::getNormalizedLocaleFromLocaleOrLanguage($_REQUEST['language']); + if (!is_null($locale)) { setcookie( name: 'webdbLanguage', value: $locale, - expires_or_options: time() + 31_536_000 // 1 year. + expires_or_options: time() + 31_536_000, // 1 year. ); } } @@ -151,6 +161,7 @@ public static function locale(): string } $conf = self::tryGetConfigFileData(); + if ( is_null($locale) && isset($conf['default_lang']) && @@ -164,21 +175,26 @@ public static function locale(): string '/\s*(?P[a-z]{1,8}(?:-[a-z]{1,8})*)(?:;q=(?P[01](?:.\d{0,3})?))?\s*(?:,|$)/', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']), $matches, - PREG_SET_ORDER + PREG_SET_ORDER, ); $acceptLanguages = []; - foreach ($matches as $match) { // $match[1] = language tag, [2] = quality + + foreach ($matches as $match) { // $match[1] = language tag, [2] = quality if (!isset($match['quality'])) { - $match['quality'] = 1; // Default quality to 1 + $match['quality'] = 1; // Default quality to 1 } + if ($match['quality'] <= 0 || $match['quality'] > 1) { continue; } + if (!self::languageIsAvailable($match['language'])) { continue; } + $normalizedLocale = self::getNormalizedLocaleFromLocaleOrLanguage($match['language']); + if (is_null($normalizedLocale)) { continue; } @@ -224,6 +240,7 @@ public static function locale(): string public static function getServerById(string $serverId): ?Server { $servers = self::getServers(); + foreach ($servers as $server) { if ($serverId === $server->id()) { return $server; @@ -236,6 +253,7 @@ public static function getServerById(string $serverId): ?Server public static function ownedOnly(): bool { $conf = self::tryGetConfigFileData(); + return $conf['owned_only'] ?? false; } @@ -245,6 +263,7 @@ public static function ownedOnly(): bool public static function serverExists(string $serverId): bool { $servers = self::getServers(); + foreach ($servers as $server) { if ($serverId === $server->id()) { return true; @@ -257,12 +276,14 @@ public static function serverExists(string $serverId): bool public static function showAdvanced(): bool { $conf = self::tryGetConfigFileData(); + return $conf['show_advanced'] ?? false; } public static function showSystem(): bool { $conf = self::tryGetConfigFileData(); + return $conf['show_system'] ?? false; } @@ -273,6 +294,7 @@ public static function theme(): string if (isset($_REQUEST['server']) && is_string($_REQUEST['server'])) { $serverIdTheme = self::tryGetThemeByServerId($_REQUEST['server']); + if ($serverIdTheme !== '') { self::$data['theme'] = $serverIdTheme; } @@ -294,15 +316,11 @@ public static function theme(): string self::$data['theme'] = $_SESSION['ppaTheme']; } - if ( - isset($_REQUEST['theme']) && - is_string($_REQUEST['theme']) && - Themes::cssExists($_REQUEST['theme']) - ) { + if (isset($_REQUEST['theme']) && is_string($_REQUEST['theme']) && Themes::cssExists($_REQUEST['theme'])) { setcookie( name: 'ppaTheme', value: $_REQUEST['theme'], - expires_or_options: time() + 31_536_000 // 1 year. + expires_or_options: time() + 31_536_000, // 1 year. ); self::$data['theme'] = $_REQUEST['theme']; } @@ -321,6 +339,7 @@ private static function getNormalizedLocaleFromLocaleOrLanguage(string $localeOr $lowerCasedLocaleOrLanguage = strtolower($localeOrLanguage); $languageIdsWithLocales = Language::getAvailableLanguageIdsWithLocales(); + if (isset($languageIdsWithLocales[$lowerCasedLocaleOrLanguage])) { return $languageIdsWithLocales[$lowerCasedLocaleOrLanguage]; } @@ -331,10 +350,13 @@ private static function getNormalizedLocaleFromLocaleOrLanguage(string $localeOr private static function languageIsAvailable(string $language): bool { $normalizedLocale = self::getNormalizedLocaleFromLocaleOrLanguage($language); + if (is_null($normalizedLocale)) { return false; } + $availableLocales = self::getAvailableLocales(); + foreach ($availableLocales as $locale) { if ($locale === $normalizedLocale) { return true; @@ -354,29 +376,37 @@ private static function tryGetConfigFileData(): array $confDir = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'conf'; $yamlConfigFile = $confDir . DIRECTORY_SEPARATOR . 'config.yaml'; + if (!file_exists($yamlConfigFile)) { $yamlConfigFile = $confDir . DIRECTORY_SEPARATOR . 'config.yml'; } + $configYamlFileEnv = getenv('PHPPGADMIN_CONFIG_YAML_FILE'); + if (is_string($configYamlFileEnv) && $configYamlFileEnv !== '') { $yamlConfigFile = $confDir . DIRECTORY_SEPARATOR . $configYamlFileEnv; } if (file_exists($yamlConfigFile)) { $yaml = (new YamlParser())->parseFile($yamlConfigFile); + if (is_array($yaml)) { if (isset($yaml['default_lang']) && is_string($yaml['default_lang'])) { self::$conf['default_lang'] = $yaml['default_lang']; } + if (isset($yaml['extra_session_security']) && is_bool($yaml['extra_session_security'])) { self::$conf['extra_session_security'] = $yaml['extra_session_security']; } + if (isset($yaml['left_width']) && is_int($yaml['left_width'])) { self::$conf['left_width'] = $yaml['left_width']; } + if (isset($yaml['owned_only']) && is_bool($yaml['owned_only'])) { self::$conf['owned_only'] = $yaml['owned_only']; } + if (isset($yaml['servers']) && is_array($yaml['servers'])) { self::$conf['servers'] = []; @@ -417,9 +447,11 @@ private static function tryGetConfigFileData(): array */ } } + if (isset($yaml['show_advanced']) && is_bool($yaml['show_advanced'])) { self::$conf['show_advanced'] = $yaml['show_advanced']; } + if (isset($yaml['show_system']) && is_bool($yaml['show_system'])) { self::$conf['show_system'] = $yaml['show_system']; } @@ -438,6 +470,7 @@ private static function tryGetThemeByServerId(string $serverId): string $servers = self::getServers(); $tmpTheme = ''; + foreach ($servers as $info) { if ($serverId !== $info->id()) { continue; diff --git a/src/DDD/Entities/Server.php b/src/DDD/Entities/Server.php index 964db3bde..e4bb5966f 100644 --- a/src/DDD/Entities/Server.php +++ b/src/DDD/Entities/Server.php @@ -24,7 +24,7 @@ public function __construct( protected SslMode $sslMode = SslMode::ALLOW, protected DatabaseName $defaultDb = new DatabaseName('template1'), protected Filename $pgDumpPath = new Filename('/usr/bin/pg_dump'), - protected Filename $pgDumpAllPath = new Filename('/usr/bin/pg_dumpall') + protected Filename $pgDumpAllPath = new Filename('/usr/bin/pg_dumpall'), ) { } @@ -34,31 +34,37 @@ public function __construct( public static function fromArray(array $input): self { $name = ''; + $host = '127.0.0.1'; + $port = 5_432; + $sslMode = 'allow'; + $defaultDb = 'template1'; + $pgDumpPath = '/usr/bin/pg_dump'; + $pgDumpAllPath = '/usr/bin/pg_dumpall'; + if (isset($input['desc']) && is_string($input['desc'])) { $name = $input['desc']; } - $host = '127.0.0.1'; if (isset($input['host']) && is_string($input['host'])) { $host = $input['host']; } - $port = 5432; + if (isset($input['port']) && is_int($input['port'])) { $port = $input['port']; } - $sslMode = 'allow'; + if (isset($input['sslmode']) && is_string($input['sslmode'])) { $sslMode = strtolower($input['sslmode']); } - $defaultDb = 'template1'; + if (isset($input['defaultdb']) && is_string($input['defaultdb'])) { $defaultDb = $input['defaultdb']; } - $pgDumpPath = '/usr/bin/pg_dump'; + if (isset($input['pgdumppath']) && is_string($input['pgdumppath'])) { $pgDumpPath = $input['pgdumppath']; } - $pgDumpAllPath = '/usr/bin/pg_dumpall'; + if (isset($input['pg_dumpall_path']) && is_string($input['pg_dumpall_path'])) { $pgDumpAllPath = $input['pg_dumpall_path']; } @@ -70,7 +76,7 @@ public static function fromArray(array $input): self sslMode: SslMode::from($sslMode), defaultDb: new DatabaseName($defaultDb), pgDumpPath: new Filename($pgDumpPath), - pgDumpAllPath: new Filename($pgDumpAllPath) + pgDumpAllPath: new Filename($pgDumpAllPath), ); } diff --git a/src/DDD/Entities/ServerSession.php b/src/DDD/Entities/ServerSession.php index a76c4dd1d..c1ecedaed 100644 --- a/src/DDD/Entities/ServerSession.php +++ b/src/DDD/Entities/ServerSession.php @@ -33,7 +33,7 @@ public function __construct( DatabaseName $defaultDb = new DatabaseName('template1'), Filename $pgDumpPath = new Filename('/usr/bin/pg_dump'), Filename $pgDumpAllPath = new Filename('/usr/bin/pg_dumpall'), - private Platform $platform = new Platform('PostgreSQL') + private Platform $platform = new Platform('PostgreSQL'), ) { parent::__construct( name: $name, @@ -42,13 +42,14 @@ public function __construct( sslMode: $sslMode, defaultDb: $defaultDb, pgDumpPath: $pgDumpPath, - pgDumpAllPath: $pgDumpAllPath + pgDumpAllPath: $pgDumpAllPath, ); } public static function fromServerId(string $serverId): ?self { $servers = Config::getServers(); + foreach ($servers as $server) { if ($server->id() !== $serverId) { continue; @@ -106,7 +107,7 @@ public static function fromServerId(string $serverId): ?self defaultDb: new DatabaseName((string)$server->DefaultDb), pgDumpPath: new Filename((string)$server->PgDumpPath), pgDumpAllPath: new Filename((string)$server->PgDumpAllPath), - platform: new Platform($platform) + platform: new Platform($platform), ); } } @@ -131,7 +132,7 @@ public function getDatabaseConnection(): PhpPgAdminConnection sslmode: $this->SslMode->value, user: (string)$this->Username, password: (string)$this->Password, - database: 'postgres' + database: 'postgres', ); $connection->exec("SET client_encoding TO 'UTF-8'"); @@ -142,22 +143,29 @@ public function getDatabaseConnection(): PhpPgAdminConnection public static function isLoggedIn(string $serverId): bool { + if (!isset($_SESSION['webdbLogin']) || !is_array($_SESSION['webdbLogin'])) { + return false; + } + + if (!isset($_SESSION['webdbLogin'][$serverId]) || !is_array($_SESSION['webdbLogin'][$serverId])) { + return false; + } + if ( - isset($_SESSION['webdbLogin']) && - is_array($_SESSION['webdbLogin']) && - isset($_SESSION['webdbLogin'][$serverId]) && - is_array($_SESSION['webdbLogin'][$serverId]) && - isset($_SESSION['webdbLogin'][$serverId]['username']) && - is_string($_SESSION['webdbLogin'][$serverId]['username']) && - $_SESSION['webdbLogin'][$serverId]['username'] !== '' && - isset($_SESSION['webdbLogin'][$serverId]['password']) && - is_string($_SESSION['webdbLogin'][$serverId]['password']) && - $_SESSION['webdbLogin'][$serverId]['password'] !== '' + !isset($_SESSION['webdbLogin'][$serverId]['username']) || + !isset($_SESSION['webdbLogin'][$serverId]['password']) ) { - return true; + return false; + } + + $sessionUsername = $_SESSION['webdbLogin'][$serverId]['username']; + $sessionPassword = $_SESSION['webdbLogin'][$serverId]['password']; + + if (!is_string($sessionUsername) || !is_string($sessionPassword)) { + return false; } - return false; + return $sessionUsername !== '' && $sessionPassword !== ''; } public function __get(string $name): mixed diff --git a/src/DDD/Repositories/History.php b/src/DDD/Repositories/History.php new file mode 100644 index 000000000..365506f4b --- /dev/null +++ b/src/DDD/Repositories/History.php @@ -0,0 +1,93 @@ + + */ + public static function getHistory(string $serverId, string $database): array + { + $history = []; + + if (!isset($_SESSION['history']) || !is_array($_SESSION['history'])) { + return $history; + } + + if (!isset($_SESSION['history'][$serverId]) || !is_array($_SESSION['history'][$serverId])) { + return $history; + } + + if ( + !isset($_SESSION['history'][$serverId][$database]) || + !is_array($_SESSION['history'][$serverId][$database]) + ) { + return $history; + } + + foreach ($_SESSION['history'][$serverId][$database] as $queryId => $entry) { + if (!is_string($queryId) || !is_array($entry)) { + continue; + } + + if (!isset($entry['query']) || !is_string($entry['query'])) { + continue; + } + + $paginate = false; + + if (isset($entry['paginate']) && is_string($entry['paginate'])) { + $paginate = $entry['paginate'] === 't'; + } + + $history[$queryId] = [ + 'paginate' => $paginate, + 'query' => $entry['query'], + ]; + } + + return $history; + } + + /** + * @return array{'query': string, 'paginate': bool}|null + */ + public static function getHistoryEntry(string $serverId, string $database, string $queryId): ?array + { + $history = self::getHistory($serverId, $database); + + return $history[$queryId] ?? null; + } +} diff --git a/src/DDD/ValueObjects/DbSize.php b/src/DDD/ValueObjects/DbSize.php index 13dfc87f8..10a54b448 100644 --- a/src/DDD/ValueObjects/DbSize.php +++ b/src/DDD/ValueObjects/DbSize.php @@ -28,14 +28,17 @@ public function prettyFormat(): string _('GB'), _('TB'), ]; + foreach ($unitStrings as $unitString) { if ($this->size < $limit * $multiplier) { return sprintf('%d %s', floor(($this->size + $multiplier / 2) / $multiplier), $unitString); } + $multiplier *= 1_024; } $multiplier /= 1_024; + return sprintf('%d %s', floor(($this->size + $multiplier / 2) / $multiplier), $unitString); } diff --git a/src/DDD/ValueObjects/Server/DatabaseName.php b/src/DDD/ValueObjects/Server/DatabaseName.php index 515fd6fc3..4f70e52c9 100644 --- a/src/DDD/ValueObjects/Server/DatabaseName.php +++ b/src/DDD/ValueObjects/Server/DatabaseName.php @@ -11,6 +11,7 @@ final class DatabaseName implements \Stringable public function __construct(string $name) { $name = trim($name); + if (empty($name)) { throw new \InvalidArgumentException('Database name cannot be empty!'); } diff --git a/src/DDD/ValueObjects/Server/Host.php b/src/DDD/ValueObjects/Server/Host.php index 5d1af6c59..690d6cae9 100644 --- a/src/DDD/ValueObjects/Server/Host.php +++ b/src/DDD/ValueObjects/Server/Host.php @@ -11,6 +11,7 @@ final class Host implements \Stringable public function __construct(string $host = '127.0.0.1') { $host = trim($host); + if (empty($host)) { throw new \InvalidArgumentException('Host cannot be empty!'); } diff --git a/src/DDD/ValueObjects/Server/Name.php b/src/DDD/ValueObjects/Server/Name.php index c11dc2435..96f93aefa 100644 --- a/src/DDD/ValueObjects/Server/Name.php +++ b/src/DDD/ValueObjects/Server/Name.php @@ -11,6 +11,7 @@ final class Name implements \Stringable public function __construct(string $name) { $name = trim($name); + if (empty($name)) { throw new \InvalidArgumentException('Server name cannot be empty!'); } diff --git a/src/DDD/ValueObjects/Server/Port.php b/src/DDD/ValueObjects/Server/Port.php index 4567f7ec9..201461e18 100644 --- a/src/DDD/ValueObjects/Server/Port.php +++ b/src/DDD/ValueObjects/Server/Port.php @@ -9,9 +9,9 @@ */ final class Port { - public function __construct(private int $port = 5432) + public function __construct(private int $port = 5_432) { - if ($port < 1 || $port > 65535) { + if ($port < 1 || $port > 65_535) { throw new \InvalidArgumentException('Port must be between 1 and 65535'); } } diff --git a/src/DDD/ValueObjects/ServerSession/Password.php b/src/DDD/ValueObjects/ServerSession/Password.php index 412ddf582..9cc9edfbf 100644 --- a/src/DDD/ValueObjects/ServerSession/Password.php +++ b/src/DDD/ValueObjects/ServerSession/Password.php @@ -11,6 +11,7 @@ final class Password implements \Stringable public function __construct(string $password) { $password = trim($password); + if (empty($password)) { throw new \InvalidArgumentException('ServerSession password cannot be empty!'); } diff --git a/src/DDD/ValueObjects/ServerSession/Platform.php b/src/DDD/ValueObjects/ServerSession/Platform.php index ab1a4baea..add478c00 100644 --- a/src/DDD/ValueObjects/ServerSession/Platform.php +++ b/src/DDD/ValueObjects/ServerSession/Platform.php @@ -11,6 +11,7 @@ final class Platform implements \Stringable public function __construct(string $name) { $name = trim($name); + if (empty($name)) { throw new \InvalidArgumentException('ServerSession platform cannot be empty!'); } diff --git a/src/DDD/ValueObjects/ServerSession/Username.php b/src/DDD/ValueObjects/ServerSession/Username.php index 110211a08..6e96ab041 100644 --- a/src/DDD/ValueObjects/ServerSession/Username.php +++ b/src/DDD/ValueObjects/ServerSession/Username.php @@ -11,6 +11,7 @@ final class Username implements \Stringable public function __construct(string $name) { $name = trim($name); + if (empty($name)) { throw new \InvalidArgumentException('ServerSession username cannot be empty!'); } diff --git a/src/Database/PhpPgAdminConnection.php b/src/Database/PhpPgAdminConnection.php index 14faf90ec..ae33f7b71 100644 --- a/src/Database/PhpPgAdminConnection.php +++ b/src/Database/PhpPgAdminConnection.php @@ -11,11 +11,9 @@ final class PhpPgAdminConnection extends \PDO { /** - * Map of database encoding names to HTTP encoding names. If a + * Map of database encoding names to HTTP encoding names. If a * database encoding does not appear in this list, then its HTTP * encoding name is the same as its database encoding name. - * - * @var array */ public const CODEMAP = [ 'BIG5' => 'BIG5', @@ -74,7 +72,7 @@ public function alterDatabase( string $dbName, string $newName, ?string $newOwner = null, - ?string $comment = null + ?string $comment = null, ): void { if (!$this->beginTransaction()) { throw new \PDOException('Failed to begin transaction.'); @@ -150,7 +148,7 @@ public function createDatabase( string $comment = '', string $template = 'template1', string $lcCollate = '', - string $lcCType = '' + string $lcCType = '', ): void { $escapedDatabase = self::escapeIdentifier($database); $escapedTemplate = self::escapeIdentifier($template); @@ -158,23 +156,28 @@ public function createDatabase( $sqlParams = []; if ($encoding !== '') { - if (!in_array($encoding, array_keys(self::CODEMAP))) { + if (!in_array(needle: $encoding, haystack: array_keys(self::CODEMAP), strict: true)) { throw new \InvalidArgumentException("Invalid encoding: {$encoding}"); } + $sql .= " ENCODING = '{$encoding}'"; } $availableCollations = $this->getAvailableCollations(); + if ($lcCollate !== '') { - if (!in_array($lcCollate, $availableCollations)) { + if (!in_array(needle: $lcCollate, haystack: $availableCollations, strict: true)) { throw new \InvalidArgumentException("Invalid LC_COLLATE: {$lcCollate}"); } + $sql .= " LC_COLLATE = '{$lcCollate}'"; } + if ($lcCType !== '') { - if (!in_array($lcCType, $availableCollations)) { + if (!in_array(needle: $lcCType, haystack: $availableCollations, strict: true)) { throw new \InvalidArgumentException("Invalid LC_CTYPE: {$lcCType}"); } + $sql .= " LC_CTYPE = '{$lcCType}'"; } @@ -184,6 +187,7 @@ public function createDatabase( } $statement = $this->prepare($sql); + if ($statement === false) { throw new \PDOException('Failed to prepare SQL statement for creating database.'); } @@ -201,26 +205,30 @@ public function dropDatabase(string $database): void { $escapedDatabase = self::escapeIdentifier($database); $statement = "DROP DATABASE \"{$escapedDatabase}\""; + if ($this->exec($statement) === false) { throw new \PDOException('Failed to execute SQL statement for dropping database.'); } } /** - * @return string[] + * @return array */ public function getAvailableCollations(): array { $query = "SELECT collname FROM pg_collation WHERE collname LIKE '%.%' ORDER BY collname"; $statement = $this->prepare($query); + if ($statement === false) { throw new \PDOException('Failed to prepare SQL statement for getting available collations.'); } + if (!$statement->execute()) { throw new \PDOException('Failed to execute SQL statement for getting available collations.'); } $collations = []; + while ($row = $statement->fetch()) { if (is_array($row) && isset($row['collname']) && is_string($row['collname'])) { $collations[] = $row['collname']; @@ -238,14 +246,17 @@ public function getDatabaseComment(string $database): string WHERE pg_database.datname = :database"; $sqlParams = ['database' => $database]; $statement = $this->prepare($sql); + if ($statement === false) { throw new \PDOException('Failed to prepare SQL statement for getting database comment.'); } + if (!$statement->execute($sqlParams)) { throw new \PDOException('Failed to execute SQL statement for getting database comment.'); } $result = $statement->fetch(); + if ($result === false) { // no comment found return ''; } @@ -270,14 +281,17 @@ public function getDatabaseOwner(string $database): string 'database' => $database, ]; $statement = $this->prepare($sql); + if ($statement === false) { throw new \PDOException('Failed to prepare SQL statement for getting database owner.'); } + if (!$statement->execute($sqlParams)) { throw new \PDOException('Failed to execute SQL statement for getting database owner.'); } $result = $statement->fetch(); + if (!is_array($result)) { throw new \PDOException('Failed to fetch database owner.'); } @@ -305,7 +319,9 @@ public function getDatabases(): array { $serverInfo = ServerSession::fromRequestParameter(); - $whereClause = Config::showSystem() ? 'pdb.datallowconn' : 'NOT pdb.datistemplate'; + $whereClause = Config::showSystem() + ? 'pdb.datallowconn' + : 'NOT pdb.datistemplate'; if (!is_null($serverInfo) && Config::ownedOnly() && !$this->isSuperUser()) { $whereClause = " AND pg_has_role('{$serverInfo->Username}'::name, pr.rolname, 'USAGE')"; @@ -334,6 +350,7 @@ public function getDatabases(): array ORDER BY {$orderBy}"; $statement = $this->prepare($sql); + if ($statement === false) { throw new \PDOException('Failed to prepare SQL statement for getting databases.'); } @@ -343,32 +360,51 @@ public function getDatabases(): array } $result = []; + $requiredFields = [ + 'datname', + 'datowner', + 'datencoding', + 'datcollate', + 'datctype', + 'tablespace', + 'dbsize', + ]; + while ($row = $statement->fetch()) { if (!is_array($row)) { continue; } + foreach ($requiredFields as $field) { + if (!isset($row[$field])) { + continue 2; + } + } + if ( - isset($row['datname']) && is_string($row['datname']) && - isset($row['datowner']) && is_string($row['datowner']) && - isset($row['datencoding']) && is_string($row['datencoding']) && - isset($row['datcollate']) && is_string($row['datcollate']) && - isset($row['datctype']) && is_string($row['datctype']) && - isset($row['tablespace']) && is_string($row['tablespace']) && - isset($row['dbsize']) && is_int($row['dbsize']) + !is_string($row['datname']) || + !is_string($row['datowner']) || + !is_string($row['datencoding']) || + !is_string($row['datcollate']) || + !is_string($row['datctype']) || + !is_string($row['tablespace']) || + !is_int($row['dbsize']) ) { - $result[] = [ - 'datcollate' => $row['datcollate'], - 'datcomment' => isset($row['datcomment']) && is_string($row['datcomment']) ? - $row['datcomment'] : '', - 'datctype' => $row['datctype'], - 'datencoding' => $row['datencoding'], - 'datname' => $row['datname'], - 'datowner' => $row['datowner'], - 'dbsize' => $row['dbsize'], - 'tablespace' => $row['tablespace'], - ]; + continue; } + + $result[] = [ + 'datcollate' => $row['datcollate'], + 'datcomment' => isset($row['datcomment']) && is_string($row['datcomment']) + ? $row['datcomment'] + : '', + 'datctype' => $row['datctype'], + 'datencoding' => $row['datencoding'], + 'datname' => $row['datname'], + 'datowner' => $row['datowner'], + 'dbsize' => $row['dbsize'], + 'tablespace' => $row['tablespace'], + ]; } return $result; @@ -389,34 +425,48 @@ public function getUsers(): array FROM pg_user ORDER BY usename"; $statement = $this->prepare($sql); + if ($statement === false) { throw new \PDOException('Failed to prepare SQL statement for getting users.'); } + if (!$statement->execute()) { throw new \PDOException('Failed to execute SQL statement for getting users.'); } $result = []; + $requiredFields = [ + 'usename', + 'usesuper', + 'usecreatedb', + ]; + while ($row = $statement->fetch()) { if (!is_array($row)) { continue; } - if ( - isset($row['usename']) && is_string($row['usename']) && - isset($row['usesuper']) && is_bool($row['usesuper']) && - isset($row['usecreatedb']) && is_bool($row['usecreatedb']) - ) { - $result[] = [ - 'useconfig' => isset($row['useconfig']) && is_string($row['useconfig']) ? - $row['useconfig'] : '', - 'usecreatedb' => $row['usecreatedb'], - 'useexpires' => isset($row['useexpires']) && is_string($row['useexpires']) ? - $row['useexpires'] : '', - 'usename' => $row['usename'], - 'usesuper' => $row['usesuper'], - ]; + foreach ($requiredFields as $field) { + if (!isset($row[$field])) { + continue 2; + } + } + + if (!is_string($row['usename']) || !is_bool($row['usesuper']) || !is_bool($row['usecreatedb'])) { + continue; } + + $result[] = [ + 'useconfig' => isset($row['useconfig']) && is_string($row['useconfig']) + ? $row['useconfig'] + : '', + 'usecreatedb' => $row['usecreatedb'], + 'useexpires' => isset($row['useexpires']) && is_string($row['useexpires']) + ? $row['useexpires'] + : '', + 'usename' => $row['usename'], + 'usesuper' => $row['usesuper'], + ]; } return $result; @@ -432,22 +482,27 @@ public function isSuperUser(string $username = ''): bool { if (empty($username)) { $statement = $this->query("SHOW is_superuser"); + if ($statement !== false) { $isSuperUserColumn = $statement->fetchColumn(); + return $isSuperUserColumn === 'on'; } } $sql = "SELECT usesuper FROM pg_user WHERE usename = :username"; $statement = $this->prepare($sql); + if ($statement === false) { return false; } + if (!$statement->execute(['username' => $username])) { return false; } $rows = $statement->fetchAll(); + if (empty($rows)) { return false; } @@ -465,7 +520,9 @@ public function setDatabaseComment(string $database, ?string $comment = null): v { $escapedDatabase = self::escapeIdentifier($database); $statement = "COMMENT ON DATABASE \"{$escapedDatabase}\" IS "; - $statement .= !is_null($comment) ? $this->quote($comment) : 'NULL'; + $statement .= !is_null($comment) + ? $this->quote($comment) + : 'NULL'; if ($this->exec($statement) === false) { throw new \PDOException('Failed to execute SQL statement for setting database comment.'); @@ -477,13 +534,17 @@ public static function loginDataIsValid( int $port, SslMode $sslmode, string $user, - string $password + string $password, ): bool { $dsn = "pgsql:host={$host};port={$port};sslmode={$sslmode->value}"; + try { - $_ = new \PDO(dsn: $dsn, username: $user, password: $password); + new \PDO(dsn: $dsn, username: $user, password: $password); + return true; } catch (\PDOException $e) { + error_log($e->getMessage()); + return false; } } diff --git a/src/RequestParameter.php b/src/RequestParameter.php index 2bfe328f7..455f05188 100644 --- a/src/RequestParameter.php +++ b/src/RequestParameter.php @@ -9,9 +9,11 @@ final class RequestParameter public static function getString(string $name, ?string $default = null): ?string { $value = filter_input(INPUT_GET, $name, FILTER_DEFAULT); + if (!is_string($value)) { $value = filter_input(INPUT_POST, $name, FILTER_DEFAULT); } + if (!is_string($value)) { $value = $default; } diff --git a/src/Session.php b/src/Session.php index c8ba1eef1..94f749d95 100644 --- a/src/Session.php +++ b/src/Session.php @@ -14,6 +14,7 @@ public static function destroy(): void session_name(self::NAME); session_start(); } + unset($_SESSION); session_destroy(); } @@ -23,10 +24,12 @@ public static function start(): void if (Config::extraSessionSecurity()) { if (ini_get('session.auto_start')) { $setting = strtolower(ini_get('session.cookie_samesite') ?: ''); + if (session_name() !== self::NAME && $setting !== 'lax' && $setting !== 'strict') { session_destroy(); session_name(self::NAME); ini_set('session.cookie_samesite', 'Strict'); + if (!session_start()) { throw new \RuntimeException('Session could not be started'); } @@ -34,12 +37,14 @@ public static function start(): void } elseif (session_status() !== PHP_SESSION_ACTIVE) { session_name(self::NAME); ini_set('session.cookie_samesite', 'Strict'); + if (!session_start()) { throw new \RuntimeException('Session could not be started'); } } } elseif (!ini_get('session.auto_start')) { session_name(self::NAME); + if (!session_start()) { throw new \RuntimeException('Session could not be started'); } diff --git a/src/Website.php b/src/Website.php index 35e2781ce..133947681 100644 --- a/src/Website.php +++ b/src/Website.php @@ -51,6 +51,7 @@ public function buildHtmlDocument(): \DOMDocument public function buildHtmlString(): string { $dom = $this->buildHtmlDocument(); + return $dom->saveHTML() ?: ''; } @@ -63,17 +64,22 @@ protected function buildHtmlHead(\DOMDocument $dom): \DOMElement { $head = $dom->createElement('head'); - $meta = $dom->createElement('meta'); - $meta->setAttribute('http-equiv', 'Content-Type'); - $meta->setAttribute('content', 'text/html; charset=utf-8'); - $head->appendChild($meta); + $metaContentType = $dom->createElement('meta'); + $metaContentType->setAttribute('http-equiv', 'Content-Type'); + $metaContentType->setAttribute('content', 'text/html; charset=utf-8'); + $head->appendChild($metaContentType); + + $metaColorScheme = $dom->createElement('meta'); + $metaColorScheme->setAttribute('name', 'color-scheme'); + $metaColorScheme->setAttribute('content', 'light dark'); + $head->appendChild($metaColorScheme); $formatTitle = ''; - if (!empty($this->title)) { - $formatTitle = self::APP_NAME . ' - ' . $this->title; - } else { - $formatTitle = self::APP_NAME; - } + + $formatTitle = !empty($this->title) + ? self::APP_NAME . ' - ' . $this->title + : self::APP_NAME; + $title = $dom->createElement('title'); $title->appendChild($dom->createTextNode($formatTitle)); $head->appendChild($title); diff --git a/src/Website/AllDb.php b/src/Website/AllDb.php index d301ba1f7..749aa02e7 100644 --- a/src/Website/AllDb.php +++ b/src/Website/AllDb.php @@ -91,6 +91,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $body->appendChild(WebsiteComponents::buildServerDatabasesTabs($dom, $tabLinks)); $message = RequestParameter::getString('message') ?? ''; + if (!empty($message)) { $body->appendChild(WebsiteComponents::buildMessage($dom, $message)); } diff --git a/src/Website/AllDbExport.php b/src/Website/AllDbExport.php index 9b0d24bcb..c90fbb273 100644 --- a/src/Website/AllDbExport.php +++ b/src/Website/AllDbExport.php @@ -122,11 +122,13 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $formatSelect = $dom->createElement('select'); $formatSelect->setAttribute('name', 'd_format'); $formatOptions = ['copy', 'sql']; + foreach ($formatOptions as $option) { $optionElement = $dom->createElement('option', strtoupper($option)); $optionElement->setAttribute('value', $option); $formatSelect->appendChild($optionElement); } + $rowOnlyDataColumnOptions->appendChild($formatSelect); $rowOnlyData->appendChild($rowOnlyDataColumnFormat); $rowOnlyData->appendChild($rowOnlyDataColumnOptions); @@ -173,13 +175,16 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $rowStructureAndDataColumnOptions->appendChild($dom->createTextNode(_('Format'))); $rowStructureAndDataColumnOptions->appendChild($dom->createEntityReference('nbsp')); $clonedFormatSelect = $formatSelect->cloneNode(true); + if ($clonedFormatSelect instanceof \DOMElement) { $clonedFormatSelect->setAttribute('name', 'sd_format'); $rowStructureAndDataColumnOptions->appendChild($clonedFormatSelect); } + $rowStructureAndDataColumnOptions->appendChild($dom->createElement('br')); $clonedCleanInputForStructureOnly = $cleanInputForStructureOnly->cloneNode(true); $clonedCleanInputForStructureOnlyLabel = $cleanInputForStructureOnlyLabel->cloneNode(true); + if ( $clonedCleanInputForStructureOnly instanceof \DOMElement && $clonedCleanInputForStructureOnlyLabel instanceof \DOMElement diff --git a/src/Website/AlterDb.php b/src/Website/AlterDb.php index 588c7f37f..1be19ff59 100644 --- a/src/Website/AlterDb.php +++ b/src/Website/AlterDb.php @@ -16,41 +16,49 @@ public function __construct() { parent::__construct(); - if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') { - $serverId = RequestParameter::getString('server') ?? ''; - - $oldName = RequestParameter::getString('oldname'); - $newName = RequestParameter::getString('newname'); - if (is_null($oldName) || is_null($newName)) { - $this->message = _('Database alter failed.'); - return; - } + if (!(isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST')) { + return; + } - $serverSession = ServerSession::fromServerId($serverId); - if (!is_null($serverSession)) { - $db = $serverSession->getDatabaseConnection(); - try { - $db->alterDatabase( - dbName: $oldName, - newName: $newName, - newOwner: RequestParameter::getString('owner'), - comment: RequestParameter::getString('dbcomment'), - ); - - if (!headers_sent()) { - $redirectUrl = 'all_db.php'; - $redirectUrlParams = [ - 'server' => $serverId, - 'subject' => 'server', - ]; - - header('Location: ' . $redirectUrl . '?' . http_build_query($redirectUrlParams)); - die(); - } - } catch (\PDOException $e) { - $this->message = _('Database alter failed.'); - } + $serverId = RequestParameter::getString('server') ?? ''; + + $oldName = RequestParameter::getString('oldname'); + $newName = RequestParameter::getString('newname'); + + if (is_null($oldName) || is_null($newName)) { + $this->message = _('Database alter failed.'); + + return; + } + + $serverSession = ServerSession::fromServerId($serverId); + + if (is_null($serverSession)) { + return; + } + + $db = $serverSession->getDatabaseConnection(); + + try { + $db->alterDatabase( + dbName: $oldName, + newName: $newName, + newOwner: RequestParameter::getString('owner'), + comment: RequestParameter::getString('dbcomment'), + ); + + if (!headers_sent()) { + $redirectUrl = 'all_db.php'; + $redirectUrlParams = [ + 'server' => $serverId, + 'subject' => 'server', + ]; + + header('Location: ' . $redirectUrl . '?' . http_build_query($redirectUrlParams)); + die; } + } catch (\PDOException $e) { + $this->message = _('Database alter failed.') . ' - ' . $e->getMessage(); } } @@ -73,7 +81,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement urlParams: [ 'help' => 'pg.database.alter', 'server' => $serverId, - ] + ], ); $h2->appendChild($aHelp); $body->appendChild($h2); @@ -121,17 +129,21 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $db = $serverSession?->getDatabaseConnection(); $dbOwner = $db?->getDatabaseOwner($database); $dbUsers = $db?->getUsers(); + if (is_iterable($dbUsers)) { foreach ($dbUsers as $dbUser) { $username = $dbUser['usename']; $optionOwner = $dom->createElement('option', $username); $optionOwner->setAttribute('value', $username); + if ($dbOwner === $username) { $optionOwner->setAttribute('selected', 'selected'); } + $selectOwner->appendChild($optionOwner); } } + $tdOwner->appendChild($selectOwner); $trOwner->appendChild($thOwner); $trOwner->appendChild($tdOwner); diff --git a/src/Website/Browser.php b/src/Website/Browser.php index 8a4880fc5..914d401c8 100644 --- a/src/Website/Browser.php +++ b/src/Website/Browser.php @@ -99,7 +99,7 @@ protected function buildHtmlHead(\DOMDocument $dom): \DOMElement $style = $dom->createElement('style'); $style->setAttribute('type', 'text/css'); $style->appendChild($dom->createTextNode( - '.webfx-tree-children { background-image: url("' . Config::getIcon('I') . '"); }' + '.webfx-tree-children { background-image: url("' . Config::getIcon('I') . '"); }', )); $head->appendChild($style); diff --git a/src/Website/CreateDb.php b/src/Website/CreateDb.php index 8dc8377dc..348e10302 100644 --- a/src/Website/CreateDb.php +++ b/src/Website/CreateDb.php @@ -18,37 +18,7 @@ public function __construct() parent::__construct(); if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') { - $formName = RequestParameter::getString('formName') ?? ''; - - if (!empty($formName)) { - $serverId = RequestParameter::getString('server') ?? ''; - $serverSession = ServerSession::fromServerId($serverId); - if (!is_null($serverSession)) { - $db = $serverSession->getDatabaseConnection(); - $db->createDatabase( - database: $formName, - encoding: RequestParameter::getString('formEncoding') ?? '', - tablespace: RequestParameter::getString('formTablespace') ?? '', - comment: RequestParameter::getString('formComment') ?? '', - template: RequestParameter::getString('formTemplate') ?? 'template1', - lcCollate: RequestParameter::getString('formCollate') ?? '', - lcCType: RequestParameter::getString('formCType') ?? '' - ); - if (!headers_sent()) { - $redirectUrl = 'all_db.php'; - $redirectUrlParams = [ - 'server' => $serverId, - 'subject' => 'server', - ]; - header('Location: ' . $redirectUrl . '?' . http_build_query($redirectUrlParams)); - die(); - } - - $this->message = _('Database creation failed.'); - } - } else { - $this->message = _('You must give a name for your database.'); - } + $this->handlePostRequest(); } } @@ -69,7 +39,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement urlParams: [ 'help' => 'pg.database.create', 'server' => $serverId, - ] + ], ); $h2->appendChild($aHelp); $body->appendChild($h2); @@ -118,37 +88,50 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $selectTemplate->setAttribute('id', 'db-template'); $db = $serverSession?->getDatabaseConnection(); $dbs = $db?->getDatabases(); + if (is_iterable($dbs)) { $formTemplate = RequestParameter::getString('formTemplate') ?? ''; $optionTemplate0 = $dom->createElement('option', 'template0'); $optionTemplate0->setAttribute('value', 'template0'); + if ($formTemplate === 'template0') { $optionTemplate0->setAttribute('selected', 'selected'); } + $selectTemplate->appendChild($optionTemplate0); $optionTemplate1 = $dom->createElement('option', 'template1'); $optionTemplate1->setAttribute('value', 'template1'); + if ($formTemplate === 'template1' || $formTemplate === '') { $optionTemplate1->setAttribute('selected', 'selected'); } + $formTemplate1 = RequestParameter::getString('formTemplate') ?? ''; + if ($formTemplate1 === 'template1') { $optionTemplate1->setAttribute('selected', 'selected'); } + $selectTemplate->appendChild($optionTemplate1); foreach ($dbs as $dbData) { $dbName = $dbData['datname']; - if ($dbName !== 'template1') { - $optionTemplate = $dom->createElement('option', $dbName); - $optionTemplate->setAttribute('value', $dbName); - if ($formTemplate === $dbName) { - $optionTemplate->setAttribute('selected', 'selected'); - } - $selectTemplate->appendChild($optionTemplate); + + if ($dbName === 'template1') { + continue; } + + $optionTemplate = $dom->createElement('option', $dbName); + $optionTemplate->setAttribute('value', $dbName); + + if ($formTemplate === $dbName) { + $optionTemplate->setAttribute('selected', 'selected'); + } + + $selectTemplate->appendChild($optionTemplate); } } + $tdTemplateValue->appendChild($selectTemplate); $trTemplate->appendChild($thTemplate); $trTemplate->appendChild($tdTemplateValue); @@ -169,14 +152,18 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $emptyOption->setAttribute('value', ''); $selectEncoding->appendChild($emptyOption); $formEncoding = RequestParameter::getString('formEncoding') ?? ''; + foreach (PhpPgAdminConnection::CODEMAP as $key => $value) { $optionEncoding = $dom->createElement('option', $key); $optionEncoding->setAttribute('value', $key); + if ($formEncoding === $key) { $optionEncoding->setAttribute('selected', 'selected'); } + $selectEncoding->appendChild($optionEncoding); } + $tdEncodingValue->appendChild($selectEncoding); $trEncoding->appendChild($thEncoding); $trEncoding->appendChild($tdEncodingValue); @@ -198,16 +185,20 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $emptyOption->setAttribute('value', ''); $selectCollation->appendChild($emptyOption); $availableCollations = $db?->getAvailableCollations(); + if (!is_null($availableCollations)) { foreach ($availableCollations as $collation) { $option = $dom->createElement('option', $collation); $option->setAttribute('value', $collation); + if ($formCollate === $collation) { $option->setAttribute('selected', 'selected'); } + $selectCollation->appendChild($option); } } + $tdCollationValue->appendChild($selectCollation); $trCollation->appendChild($thCollation); $trCollation->appendChild($tdCollationValue); @@ -228,16 +219,20 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $emptyOption = $dom->createElement('option'); $emptyOption->setAttribute('value', ''); $selectCType->appendChild($emptyOption); + if (!is_null($availableCollations)) { foreach ($availableCollations as $collation) { $option = $dom->createElement('option', $collation); $option->setAttribute('value', $collation); + if ($formCType === $collation) { $option->setAttribute('selected', 'selected'); } + $selectCType->appendChild($option); } } + $tdCTypeValue->appendChild($selectCType); $trCType->appendChild($thCType); $trCType->appendChild($tdCTypeValue); @@ -302,4 +297,41 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement return $body; } + + private function handlePostRequest(): void + { + $formName = RequestParameter::getString('formName') ?? ''; + + if (!empty($formName)) { + $serverId = RequestParameter::getString('server') ?? ''; + $serverSession = ServerSession::fromServerId($serverId); + + if (!is_null($serverSession)) { + $db = $serverSession->getDatabaseConnection(); + $db->createDatabase( + database: $formName, + encoding: RequestParameter::getString('formEncoding') ?? '', + tablespace: RequestParameter::getString('formTablespace') ?? '', + comment: RequestParameter::getString('formComment') ?? '', + template: RequestParameter::getString('formTemplate') ?? 'template1', + lcCollate: RequestParameter::getString('formCollate') ?? '', + lcCType: RequestParameter::getString('formCType') ?? '', + ); + + if (!headers_sent()) { + $redirectUrl = 'all_db.php'; + $redirectUrlParams = [ + 'server' => $serverId, + 'subject' => 'server', + ]; + header('Location: ' . $redirectUrl . '?' . http_build_query($redirectUrlParams)); + die; + } + + $this->message = _('Database creation failed.'); + } + } else { + $this->message = _('You must give a name for your database.'); + } + } } diff --git a/src/Website/DropDb.php b/src/Website/DropDb.php index 4ba05ed64..7b3f452bd 100644 --- a/src/Website/DropDb.php +++ b/src/Website/DropDb.php @@ -15,29 +15,7 @@ public function __construct() parent::__construct(); if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') { - $database = RequestParameter::getString('database') ?? ''; - $serverId = RequestParameter::getString('server') ?? ''; - $serverSession = ServerSession::fromServerId($serverId); - if (!is_null($serverSession) && !empty($database)) { - $redirectUrl = 'all_db.php'; - $redirectUrlParams = [ - 'server' => $serverId, - 'subject' => 'server', - ]; - - $db = $serverSession->getDatabaseConnection(); - try { - $db->dropDatabase($database); - $redirectUrlParams['message'] = _('Database dropped.'); - } catch (\Exception $e) { - $redirectUrlParams['message'] = _('Database drop failed.'); - } - - if (!headers_sent()) { - header('Location: ' . $redirectUrl . '?' . http_build_query($redirectUrlParams)); - die(); - } - } + $this->handlePostRequest(); } } @@ -58,7 +36,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement urlParams: [ 'help' => 'pg.database.drop', 'server' => $serverId, - ] + ], ); $h2->appendChild($aHelp); $body->appendChild($h2); @@ -67,7 +45,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $form->setAttribute('method', 'post'); $p = $dom->createElement( 'p', - sprintf(_('Are you sure you want to drop the database "%s"?'), $database) + sprintf(_('Are you sure you want to drop the database "%s"?'), $database), ); $inputHiddenDatabase = $dom->createElement('input'); $inputHiddenDatabase->setAttribute('type', 'hidden'); @@ -101,4 +79,35 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement return $body; } + + private function handlePostRequest(): void + { + $database = RequestParameter::getString('database') ?? ''; + $serverId = RequestParameter::getString('server') ?? ''; + $serverSession = ServerSession::fromServerId($serverId); + + if (is_null($serverSession) || empty($database)) { + return; + } + + $redirectUrl = 'all_db.php'; + $redirectUrlParams = [ + 'server' => $serverId, + 'subject' => 'server', + ]; + + $db = $serverSession->getDatabaseConnection(); + + try { + $db->dropDatabase($database); + $redirectUrlParams['message'] = _('Database dropped.'); + } catch (\Throwable $e) { + $redirectUrlParams['message'] = _('Database drop failed.') . ' - ' . $e->getMessage(); + } + + if (!headers_sent()) { + header('Location: ' . $redirectUrl . '?' . http_build_query($redirectUrlParams)); + die; + } + } } diff --git a/src/Website/Exception.php b/src/Website/Exception.php index 29b565ca6..a52eb3944 100644 --- a/src/Website/Exception.php +++ b/src/Website/Exception.php @@ -16,10 +16,12 @@ public function __construct(private \Throwable $exception) public static function handle(\Throwable $exception): void { $exceptionCode = $exception->getCode(); + if (is_int($exceptionCode)) { $statusCode = StatusCode::tryFrom($exceptionCode) ?? StatusCode::InternalServerError; header($statusCode->buildHeader()); } + $website = new self($exception); print $website->buildHtmlString(); } @@ -30,7 +32,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $h1 = $dom->createElement( 'h1', - htmlentities($this->exception->getCode() . ': ' . $this->exception->getMessage()) + htmlentities($this->exception->getCode() . ': ' . $this->exception->getMessage()), ); $body->appendChild($h1); diff --git a/src/Website/History.php b/src/Website/History.php new file mode 100644 index 000000000..3bf9e9980 --- /dev/null +++ b/src/Website/History.php @@ -0,0 +1,278 @@ +scripts['history'] = [ + 'src' => 'js/history.js', + ]; + } + + protected function buildHtmlBody(\DOMDocument $dom): \DOMElement + { + $body = parent::buildHtmlBody($dom); + + $serverId = RequestParameter::getString('server') ?? ''; + $selectedDatabase = RequestParameter::getString('database') ?? ''; + + $form = $dom->createElement('form'); + $form->setAttribute('method', 'post'); + + $serverSession = ServerSession::fromServerId($serverId); + $db = $serverSession?->getDatabaseConnection(); + + $table = $dom->createElement('table'); + $table->setAttribute('style', 'width: 100%;'); + $tBody = $dom->createElement('tbody'); + $tr = $dom->createElement('tr'); + + $tdLeft = $dom->createElement('td'); + $labelForServer = $dom->createElement('label'); + $labelForServer->setAttribute('for', 'server'); + $labelForServer->appendChild($dom->createTextNode(_('Server'))); + $serverHelpLink = WebsiteComponents::buildHelpLink( + dom: $dom, + url: 'help.php', + urlParams: [ + 'help' => 'pg.server', + 'server' => $serverId, + ], + ); + $serverSelect = $dom->createElement('select'); + $serverSelect->setAttribute('name', 'server'); + $serverSelect->setAttribute('id', 'server'); + $serverSelect->setAttribute('onchange', 'changeServer(event)'); + $servers = Config::getServers(); + + foreach ($servers as $server) { + $options = $dom->createElement('option'); + $options->setAttribute('value', $server->id()); + + if ($serverId === $server->id()) { + $options->setAttribute('selected', 'selected'); + } + + $options->appendChild($dom->createTextNode((string)$server->Name . ' (' . $server->id() . ')')); + $serverSelect->appendChild($options); + } + + $tdLeft->appendChild($labelForServer); + $tdLeft->appendChild($serverHelpLink); + $tdLeft->appendChild($dom->createTextNode(': ')); + $tdLeft->appendChild($serverSelect); + + $tdRight = $dom->createElement('td'); + $tdRight->setAttribute('style', 'text-align: right;'); + $labelForDatabase = $dom->createElement('label'); + $labelForDatabase->setAttribute('for', 'database'); + $labelForDatabase->appendChild($dom->createTextNode(_('Database'))); + $databaseHelpLink = WebsiteComponents::buildHelpLink( + dom: $dom, + url: 'help.php', + urlParams: [ + 'help' => 'pg.database', + 'server' => $serverId, + ], + ); + $databaseSelect = $dom->createElement('select'); + $databaseSelect->setAttribute('name', 'database'); + $databaseSelect->setAttribute('id', 'database'); + $databaseSelect->setAttribute('onchange', 'changeServer(event)'); + $databases = $db?->getDatabases() ?? []; + $emptyOption = $dom->createElement('option'); + $emptyOption->setAttribute('value', ''); + $emptyOption->appendChild($dom->createTextNode('---')); + $databaseSelect->appendChild($emptyOption); + + foreach ($databases as $database) { + $options = $dom->createElement('option'); + $options->setAttribute('value', $database['datname']); + + if ($selectedDatabase === $database['datname']) { + $options->setAttribute('selected', 'selected'); + } + + $options->appendChild($dom->createTextNode($database['datname'])); + $databaseSelect->appendChild($options); + } + + $tdRight->appendChild($labelForDatabase); + $tdRight->appendChild($databaseHelpLink); + $tdRight->appendChild($dom->createTextNode(': ')); + $tdRight->appendChild($databaseSelect); + + $tr->appendChild($tdLeft); + $tr->appendChild($tdRight); + $tBody->appendChild($tr); + $table->appendChild($tBody); + + $form->appendChild($table); + $body->appendChild($form); + $body->appendChild($dom->createElement('br')); + + if ($selectedDatabase !== '') { + $history = HistoryRepository::getHistory($serverId, $selectedDatabase); + $historyCount = count($history); + + $body->appendChild( + $this->buildHistoryTable( + $dom, + $history, + $serverId, + $selectedDatabase, + ), + ); + + if ($historyCount === 0) { + $pNoHistory = $dom->createElement('p'); + $pNoHistory->appendChild($dom->createTextNode(_('No history.'))); + $body->appendChild($pNoHistory); + } + + $ulNavLink = $dom->createElement('ul'); + $ulNavLink->setAttribute('class', 'navlink'); + + $liRefresh = $dom->createElement('li'); + $aRefresh = $dom->createElement('a'); + $requestUri = ''; + + if (isset($_SERVER['REQUEST_URI']) && is_string($_SERVER['REQUEST_URI'])) { + $requestUri = $_SERVER['REQUEST_URI']; + } + + $aRefresh->setAttribute('href', $requestUri); + $aRefresh->appendChild($dom->createTextNode(_('Refresh'))); + $liRefresh->appendChild($aRefresh); + + $ulNavLink->appendChild($liRefresh); + + if ($historyCount > 0) { + $liDownload = $dom->createElement('li'); + $aDownload = $dom->createElement('a'); + $downloadUrl = 'history_download.php'; + $downloadUrlParams = [ + 'database' => $selectedDatabase, + 'server' => $serverId, + ]; + $aDownload->setAttribute('href', $downloadUrl . '?' . http_build_query($downloadUrlParams)); + $aDownload->appendChild($dom->createTextNode(_('Download'))); + $liDownload->appendChild($aDownload); + $ulNavLink->appendChild($liDownload); + + $liClear = $dom->createElement('li'); + $aClear = $dom->createElement('a'); + $clearUrl = 'history_clear.php'; + $clearUrlParams = [ + 'database' => $selectedDatabase, + 'server' => $serverId, + ]; + $aClear->setAttribute('href', $clearUrl . '?' . http_build_query($clearUrlParams)); + $aClear->appendChild($dom->createTextNode(_('Clear history'))); + $liClear->appendChild($aClear); + $ulNavLink->appendChild($liClear); + } + + $body->appendChild($ulNavLink); + } else { + $pPleaseSelectDatabase = $dom->createElement('p'); + $pPleaseSelectDatabase->appendChild($dom->createTextNode(_('Please, select a database.'))); + $body->appendChild($pPleaseSelectDatabase); + } + + return $body; + } + + /** + * @param array $history + */ + private function buildHistoryTable( + \DOMDocument $dom, + array $history, + string $serverId, + string $selectedDatabase, + ): \DOMElement { + $table = $dom->createElement('table'); + + $tHead = $dom->createElement('thead'); + $trHead = $dom->createElement('tr'); + $thQuery = $dom->createElement('th'); + $thQuery->setAttribute('class', 'data'); + $thQuery->appendChild($dom->createTextNode(_('SQL'))); + $trHead->appendChild($thQuery); + $thPaginate = $dom->createElement('th'); + $thPaginate->setAttribute('class', 'data'); + $thPaginate->appendChild($dom->createTextNode(_('Paginate results'))); + $trHead->appendChild($thPaginate); + $thActions = $dom->createElement('th'); + $thActions->setAttribute('class', 'data'); + $thActions->setAttribute('colspan', '2'); + $thActions->appendChild($dom->createTextNode(_('Actions'))); + $trHead->appendChild($thActions); + $tHead->appendChild($trHead); + $table->appendChild($tHead); + + $tBody = $dom->createElement('tbody'); + $historyCounter = 0; + + foreach ($history as $queryId => $historyData) { + $historyCounter++; + + $tr = $dom->createElement('tr'); + $tr->setAttribute('class', 'data' . ($historyCounter % 2 !== 0 ? '1' : '2')); + $tdQuery = $dom->createElement('td'); + $tdQuery->appendChild($dom->createTextNode($historyData['query'])); + $tdPaginate = $dom->createElement('td'); + $tdPaginate->setAttribute('style', 'text-align: center;'); + $tdPaginate->appendChild($dom->createTextNode($historyData['paginate'] ? _('Yes') : _('No'))); + $tdExecute = $dom->createElement('td'); + $tdExecute->setAttribute('class', 'opbutton' . ($historyCounter % 2 !== 0 ? '1' : '2')); + $executeLink = $dom->createElement('a'); + $executeUrl = 'sql.php'; + $executeUrlParams = [ + 'database' => $selectedDatabase, + 'nohistory' => 't', + 'paginate' => $historyData['paginate'] ? 't' : 'f', + 'queryid' => $queryId, + 'server' => $serverId, + 'subject' => 'history', + ]; + $executeLink->setAttribute('href', $executeUrl . '?' . http_build_query($executeUrlParams)); + $executeLink->setAttribute('target', 'detail'); + $executeLink->appendChild($dom->createTextNode(_('Execute'))); + $tdExecute->appendChild($executeLink); + $tdDelete = $dom->createElement('td'); + $tdDelete->setAttribute('class', 'opbutton' . ($historyCounter % 2 !== 0 ? '1' : '2')); + $deleteLink = $dom->createElement('a'); + $deleteUrl = 'history_delete.php'; + $deleteUrlParams = [ + 'database' => $selectedDatabase, + 'queryid' => $queryId, + 'server' => $serverId, + ]; + $deleteLink->setAttribute('href', $deleteUrl . '?' . http_build_query($deleteUrlParams)); + $deleteLink->appendChild($dom->createTextNode(_('Delete'))); + $tdDelete->appendChild($deleteLink); + $tr->appendChild($tdQuery); + $tr->appendChild($tdPaginate); + $tr->appendChild($tdExecute); + $tr->appendChild($tdDelete); + + $tBody->appendChild($tr); + } + + $table->appendChild($tBody); + + return $table; + } +} diff --git a/src/Website/HistoryClear.php b/src/Website/HistoryClear.php new file mode 100644 index 000000000..db467755a --- /dev/null +++ b/src/Website/HistoryClear.php @@ -0,0 +1,89 @@ + $selectedDatabase, + 'server' => $serverId, + 'subject' => 'table', + ]; + header('Location: ' . $redirectUrl . '?' . http_build_query($redirectUrlParams)); + } + + protected function buildHtmlBody(\DOMDocument $dom): \DOMElement + { + $body = parent::buildHtmlBody($dom); + + $serverId = RequestParameter::getString('server') ?? ''; + $selectedDatabase = RequestParameter::getString('database') ?? ''; + + $h3 = $dom->createElement('h3'); + $h3->appendChild($dom->createTextNode(_('Clear history'))); + $body->appendChild($h3); + + $p = $dom->createElement('p'); + $p->appendChild($dom->createTextNode(_('Really clear history?'))); + $body->appendChild($p); + + $form = $dom->createElement('form'); + $form->setAttribute('method', 'post'); + $inputServer = $dom->createElement('input'); + $inputServer->setAttribute('type', 'hidden'); + $inputServer->setAttribute('name', 'server'); + $inputServer->setAttribute('value', $serverId); + $inputDatabase = $dom->createElement('input'); + $inputDatabase->setAttribute('type', 'hidden'); + $inputDatabase->setAttribute('name', 'database'); + $inputDatabase->setAttribute('value', $selectedDatabase); + $inputYes = $dom->createElement('input'); + $inputYes->setAttribute('type', 'submit'); + $inputYes->setAttribute('name', 'yes'); + $inputYes->setAttribute('value', _('Yes')); + $inputNo = $dom->createElement('input'); + $inputNo->setAttribute('type', 'submit'); + $inputNo->setAttribute('name', 'no'); + $inputNo->setAttribute('value', _('No')); + $form->appendChild($inputServer); + $form->appendChild($inputDatabase); + $form->appendChild($inputYes); + $form->appendChild($dom->createTextNode(' ')); + $form->appendChild($inputNo); + $body->appendChild($form); + + return $body; + } +} diff --git a/src/Website/HistoryDelete.php b/src/Website/HistoryDelete.php new file mode 100644 index 000000000..3a0da2053 --- /dev/null +++ b/src/Website/HistoryDelete.php @@ -0,0 +1,106 @@ + $selectedDatabase, + 'server' => $serverId, + 'subject' => 'table', + ]; + header('Location: ' . $redirectUrl . '?' . http_build_query($redirectUrlParams)); + } + + protected function buildHtmlBody(\DOMDocument $dom): \DOMElement + { + $body = parent::buildHtmlBody($dom); + + $queryId = RequestParameter::getString('queryid') ?? ''; + $serverId = RequestParameter::getString('server') ?? ''; + $selectedDatabase = RequestParameter::getString('database') ?? ''; + + $historyEntry = History::getHistoryEntry($serverId, $selectedDatabase, $queryId); + + if (is_null($historyEntry)) { + return $body; + } + + $h3 = $dom->createElement('h3'); + $h3->appendChild($dom->createTextNode(_('Delete from history'))); + $body->appendChild($h3); + + $p = $dom->createElement('p'); + $p->appendChild($dom->createTextNode(_('Really remove this request from history?'))); + $body->appendChild($p); + + $pre = $dom->createElement('pre'); + $pre->appendChild($dom->createTextNode($historyEntry['query'])); + $body->appendChild($pre); + + $form = $dom->createElement('form'); + $form->setAttribute('method', 'post'); + $inputQueryId = $dom->createElement('input'); + $inputQueryId->setAttribute('type', 'hidden'); + $inputQueryId->setAttribute('name', 'queryid'); + $inputQueryId->setAttribute('value', $queryId); + $inputServer = $dom->createElement('input'); + $inputServer->setAttribute('type', 'hidden'); + $inputServer->setAttribute('name', 'server'); + $inputServer->setAttribute('value', $serverId); + $inputDatabase = $dom->createElement('input'); + $inputDatabase->setAttribute('type', 'hidden'); + $inputDatabase->setAttribute('name', 'database'); + $inputDatabase->setAttribute('value', $selectedDatabase); + $inputYes = $dom->createElement('input'); + $inputYes->setAttribute('type', 'submit'); + $inputYes->setAttribute('name', 'yes'); + $inputYes->setAttribute('value', _('Yes')); + $inputNo = $dom->createElement('input'); + $inputNo->setAttribute('type', 'submit'); + $inputNo->setAttribute('name', 'no'); + $inputNo->setAttribute('value', _('No')); + $form->appendChild($inputQueryId); + $form->appendChild($inputServer); + $form->appendChild($inputDatabase); + $form->appendChild($inputYes); + $form->appendChild($dom->createTextNode(' ')); + $form->appendChild($inputNo); + $body->appendChild($form); + + return $body; + } +} diff --git a/src/Website/HistoryDownload.php b/src/Website/HistoryDownload.php new file mode 100644 index 000000000..a94631e6a --- /dev/null +++ b/src/Website/HistoryDownload.php @@ -0,0 +1,42 @@ +format('Y-m-d_H-i-s') . '.sql'; + + header('Content-Type: application/download'); + header("Content-Disposition: attachment; filename={$filename}"); + } + + foreach ($history as $queries) { + $query = trim($queries['query']); + print $query; + + if (substr($query, -1) !== ';') { + print ';'; + } + + print "\n"; + } + + exit; + } +} diff --git a/src/Website/Intro.php b/src/Website/Intro.php index be0bc6056..98b293342 100644 --- a/src/Website/Intro.php +++ b/src/Website/Intro.php @@ -35,17 +35,21 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $currentLocale = Config::locale(); $languageIdsWithLocales = Language::getAvailableLanguageIdsWithLocales(); + foreach ($languageIdsWithLocales as $languageId => $locale) { Language::setLocale($locale); $option = $dom->createElement('option', _('applang')); $option->setAttribute('value', $languageId); $option->setAttribute('data-locale', $locale); $option->setAttribute('data-language-id', $languageId); + if ($locale === $currentLocale) { $option->setAttribute('selected', 'selected'); } + $select->appendChild($option); } + // Reset locale Language::setLocale($currentLocale); @@ -62,14 +66,18 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $select->setAttribute('name', 'theme'); $select->setAttribute('onchange', 'this.form.submit()'); $themes = Themes::available(); + foreach ($themes as $theme => $label) { $option = $dom->createElement('option', $label); $option->setAttribute('value', $theme); + if ($theme === Config::theme()) { $option->setAttribute('selected', 'selected'); } + $select->appendChild($option); } + $td->appendChild($select); $tr->appendChild($td); $table->appendChild($tr); @@ -101,6 +109,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $body->appendChild(WebsiteComponents::buildBackToTopLink($dom)); $languageParam = RequestParameter::getString('language'); + if (!is_null($languageParam)) { $scriptElement = $dom->createElement('script'); $scriptElement->setAttribute('type', 'text/javascript'); diff --git a/src/Website/Login.php b/src/Website/Login.php index 5e98d3857..0c0833d93 100644 --- a/src/Website/Login.php +++ b/src/Website/Login.php @@ -17,64 +17,8 @@ public function __construct() $this->title = _('Login'); - if ( - isset($_SERVER['REQUEST_METHOD']) && - is_string($_SERVER['REQUEST_METHOD']) && - $_SERVER['REQUEST_METHOD'] === 'POST' - ) { - $loginServer = RequestParameter::getString('loginServer'); - if (is_null($loginServer)) { - throw new \InvalidArgumentException('Parameter "loginServer" is required'); - } - $loginUsername = RequestParameter::getString('loginUsername'); - if (is_null($loginUsername)) { - throw new \InvalidArgumentException('Parameter "loginUsername" is required'); - } - - $server = Config::getServerById($loginServer); - if (is_null($server)) { - throw new \InvalidArgumentException('Server not found'); - } - $loginPassword = RequestParameter::getString('loginPassword_' . hash('sha256', (string)$server->Name)); - if (is_null($loginPassword)) { - throw new \InvalidArgumentException('Parameter "loginPassword" is required'); - } - - if ( - !PhpPgAdminConnection::loginDataIsValid( - host: (string)$server->Host, - port: $server->Port->Value, - sslmode: $server->SslMode, - user: $loginUsername, - password: $loginPassword - ) - ) { - $this->message = _('Login failed'); - return; - } - - if (!isset($_SESSION['webdbLogin']) || !is_array($_SESSION['webdbLogin'])) { - $_SESSION['webdbLogin'] = []; - } - $_SESSION['webdbLogin'][$loginServer] = [ - 'defaultdb' => (string)$server->DefaultDb, - 'desc' => (string)$server->Name, - 'host' => (string)$server->Host, - 'password' => $loginPassword, - 'pg_dumpall_path' => '/usr/bin/pg_dumpall', - 'pg_dump_path' => '/usr/bin/pg_dump', - //'pgVersion' => $server->Version->Value, - //'platform' => 'PostgreSQL ' . $server->Version->Value, - 'port' => $server->Port->Value, - 'sslmode' => $server->SslMode->value, - 'username' => $loginUsername, - ]; - - $redirectLocationUrlParams = [ - 'server' => $loginServer, - ]; - header('Location: ./all_db.php?' . http_build_query($redirectLocationUrlParams)); - exit; + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') { + $this->handlePostRequest(); } } @@ -86,11 +30,13 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $body->appendChild(WebsiteComponents::buildTrail($dom)); $serverId = RequestParameter::getString('server'); + if (is_null($serverId)) { throw new \InvalidArgumentException('Parameter "server" is required'); } $server = Config::getServerById($serverId); + if (is_null($server)) { throw new \InvalidArgumentException('Server not found'); } @@ -104,9 +50,11 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $form = $dom->createElement('form'); $form->setAttribute('id', 'login_form'); $loginFormAction = ''; + if (isset($_SERVER['SCRIPT_NAME']) && is_string($_SERVER['SCRIPT_NAME'])) { $loginFormAction = $_SERVER['SCRIPT_NAME']; } + $form->setAttribute('action', $loginFormAction); $form->setAttribute('method', 'post'); $form->setAttribute('name', 'login_form'); @@ -178,4 +126,69 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement return $body; } + + private function handlePostRequest(): void + { + $loginServer = RequestParameter::getString('loginServer'); + + if (is_null($loginServer)) { + throw new \InvalidArgumentException('Parameter "loginServer" is required'); + } + + $loginUsername = RequestParameter::getString('loginUsername'); + + if (is_null($loginUsername)) { + throw new \InvalidArgumentException('Parameter "loginUsername" is required'); + } + + $server = Config::getServerById($loginServer); + + if (is_null($server)) { + throw new \InvalidArgumentException('Server not found'); + } + + $loginPassword = RequestParameter::getString('loginPassword_' . hash('sha256', (string)$server->Name)); + + if (is_null($loginPassword)) { + throw new \InvalidArgumentException('Parameter "loginPassword" is required'); + } + + $loginIsValid = PhpPgAdminConnection::loginDataIsValid( + host: (string)$server->Host, + port: $server->Port->Value, + sslmode: $server->SslMode, + user: $loginUsername, + password: $loginPassword, + ); + + if (!$loginIsValid) { + $this->message = _('Login failed'); + + return; + } + + if (!isset($_SESSION['webdbLogin']) || !is_array($_SESSION['webdbLogin'])) { + $_SESSION['webdbLogin'] = []; + } + + $_SESSION['webdbLogin'][$loginServer] = [ + 'defaultdb' => (string)$server->DefaultDb, + 'desc' => (string)$server->Name, + 'host' => (string)$server->Host, + 'password' => $loginPassword, + 'pg_dumpall_path' => '/usr/bin/pg_dumpall', + 'pg_dump_path' => '/usr/bin/pg_dump', + //'pgVersion' => $server->Version->Value, + //'platform' => 'PostgreSQL ' . $server->Version->Value, + 'port' => $server->Port->Value, + 'sslmode' => $server->SslMode->value, + 'username' => $loginUsername, + ]; + + $redirectLocationUrlParams = [ + 'server' => $loginServer, + ]; + header('Location: ./all_db.php?' . http_build_query($redirectLocationUrlParams)); + exit; + } } diff --git a/src/Website/Redirect.php b/src/Website/Redirect.php index 59de0bc37..a8c237fd6 100644 --- a/src/Website/Redirect.php +++ b/src/Website/Redirect.php @@ -13,11 +13,9 @@ final class Redirect extends Website public function tryRedirect(): void { $subject = RequestParameter::getString('subject'); + if (is_null($subject)) { - throw new \InvalidArgumentException( - 'Missing required parameter: subject', - StatusCode::BadRequest->value - ); + throw new \InvalidArgumentException('Missing required parameter: subject', StatusCode::BadRequest->value); } match ($subject) { @@ -25,7 +23,7 @@ public function tryRedirect(): void 'server' => $this->redirectToServer(), default => trigger_error( 'Redirecting subject ("' . $subject . '") not found the new way. Continue the old way!', - E_USER_DEPRECATED + E_USER_DEPRECATED, ) }; } @@ -40,6 +38,7 @@ private function redirectToRoot(): void private function redirectToServer(): void { $server = RequestParameter::getString('server'); + if (is_null($server)) { throw new \InvalidArgumentException('Missing required parameter: server', StatusCode::BadRequest->value); } @@ -47,7 +46,7 @@ private function redirectToServer(): void if (!Config::serverExists($server)) { throw new \InvalidArgumentException( _('Attempt to connect with invalid server parameter, possibly someone is trying to hack your system.'), - StatusCode::BadRequest->value + StatusCode::BadRequest->value, ); } @@ -62,7 +61,7 @@ private function redirectToServer(): void unset($locationUrlParams['subject']); } - $locationUrl = $locationUrl . '?' . http_build_query($locationUrlParams); + $locationUrl .= '?' . http_build_query($locationUrlParams); header('Location: ' . $locationUrl); exit; diff --git a/src/Website/ServerLogout.php b/src/Website/ServerLogout.php index 845ad1545..c0c1f77a7 100644 --- a/src/Website/ServerLogout.php +++ b/src/Website/ServerLogout.php @@ -41,6 +41,7 @@ protected function buildHtmlHead(\DOMDocument $dom): \DOMElement $head = parent::buildHtmlHead($dom); $redirectUrl = './servers.php'; + if (isset($_SERVER['HTTP_REFERER']) && is_string($_SERVER['HTTP_REFERER'])) { $redirectUrl = $_SERVER['HTTP_REFERER']; } diff --git a/src/Website/Servers.php b/src/Website/Servers.php index db620dd29..10e7f7bf4 100644 --- a/src/Website/Servers.php +++ b/src/Website/Servers.php @@ -48,8 +48,11 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $table->appendChild($tHead); $tBody = $dom->createElement('tbody'); - $logins = isset($_SESSION['webdbLogin']) && is_array($_SESSION['webdbLogin']) ? $_SESSION['webdbLogin'] : []; + $logins = isset($_SESSION['webdbLogin']) && is_array($_SESSION['webdbLogin']) + ? $_SESSION['webdbLogin'] + : []; $servers = Config::getServers(); + foreach ($servers as $index => $server) { $tr = $dom->createElement('tr'); $tr->setAttribute('class', 'data' . ($index + 1)); @@ -69,6 +72,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $td = $dom->createElement('td', (string)$server->Port->Value); $tr->appendChild($td); $username = ''; + if ( isset($logins[$serverId]) && is_array($logins[$serverId]) && @@ -77,9 +81,11 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement ) { $username = $logins[$serverId]['username']; } + $td = $dom->createElement('td', $username); $tr->appendChild($td); $td = $dom->createElement('td'); + if ($username !== '') { $td->setAttribute('class', 'opbutton' . ($index + 1)); $a = $dom->createElement('a', _('Logout')); @@ -90,9 +96,11 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $a->setAttribute('href', $logoutUrl); $td->appendChild($a); } + $tr->appendChild($td); $tBody->appendChild($tr); } + $table->appendChild($tBody); $body->appendChild($table); diff --git a/src/Website/SqlEdit.php b/src/Website/SqlEdit.php index 2f1f0b3c2..faf0e0ff3 100644 --- a/src/Website/SqlEdit.php +++ b/src/Website/SqlEdit.php @@ -89,7 +89,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement urlParams: [ 'help' => 'pg.server', 'server' => $serverId, - ] + ], ); $labelForServer->appendChild($helpLink); $tdServerSelection->appendChild($labelForServer); @@ -99,15 +99,19 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $serverSelection->setAttribute('id', 'server'); $serverSelection->setAttribute('onchange', 'changeServer(event)'); $servers = Config::getServers(); + foreach ($servers as $server) { $option = $dom->createElement('option'); $option->setAttribute('value', $server->id()); + if ($server->id() === $serverId) { $option->setAttribute('selected', 'selected'); } + $option->appendChild($dom->createTextNode($server->Name . ' (' . $server->id() . ')')); $serverSelection->appendChild($option); } + $tdServerSelection->appendChild($serverSelection); $tdDatabaseSelection = $dom->createElement('td'); @@ -121,7 +125,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement urlParams: [ 'help' => 'pg.database', 'server' => $serverId, - ] + ], ); $labelForDatabase->appendChild($helpLink); $labelForDatabase->appendChild($dom->createTextNode(': ')); @@ -134,21 +138,26 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement $emptyOption->setAttribute('value', ''); $databaseSelection->appendChild($emptyOption); $serverSession = ServerSession::fromServerId($serverId); + if (!is_null($serverSession)) { $databaseParam = RequestParameter::getString('database'); $db = $serverSession->getDatabaseConnection(); $dbs = $db->getDatabases(); + foreach ($dbs as $dbMetaData) { $option = $dom->createElement('option'); $option->setAttribute('value', $dbMetaData['datname']); + if ($dbMetaData['datname'] === $databaseParam) { $option->setAttribute('selected', 'selected'); } + $option->appendChild($dom->createTextNode($dbMetaData['datname'])); $databaseSelection->appendChild($option); } } + $tdDatabaseSelection->appendChild($databaseSelection); $tr->appendChild($tdServerSelection); @@ -173,6 +182,7 @@ protected function buildHtmlBody(\DOMDocument $dom): \DOMElement private function appendFindPart(\DOMElement $element): void { $dom = $element->ownerDocument; + if (is_null($dom)) { throw new \RuntimeException('DOMDocument is not set for the element.'); } @@ -203,6 +213,7 @@ private function appendFindPart(\DOMElement $element): void 'TRIGGER' => _('Triggers'), 'VIEW' => _('Views'), ]; + foreach ($filterArray as $key => $value) { $option = $dom->createElement('option'); $option->setAttribute('value', $key); @@ -233,6 +244,7 @@ private function appendFindPart(\DOMElement $element): void private function appendSqlPart(\DOMElement $element, string $serverId): void { $dom = $element->ownerDocument; + if (is_null($dom)) { throw new \RuntimeException('DOMDocument is not set for the element.'); } @@ -248,7 +260,7 @@ private function appendSqlPart(\DOMElement $element, string $serverId): void urlParams: [ 'help' => 'pg.schema.search_path', 'server' => $serverId, - ] + ], ); $pSchemaSearchPath->appendChild($searchPathHelp); $pSchemaSearchPath->appendChild($dom->createTextNode(': ')); diff --git a/src/WebsiteComponents.php b/src/WebsiteComponents.php index af8aa26c9..3b2507e39 100644 --- a/src/WebsiteComponents.php +++ b/src/WebsiteComponents.php @@ -62,8 +62,10 @@ public static function buildDatabasesTable(\DOMDocument $dom, ?ServerSession $se $tBody = $dom->createElement('tbody'); $dbs = $dbConnection?->getDatabases(); + if (is_iterable($dbs)) { $dbCounter = 0; + foreach ($dbs as $db) { $dbCounter++; @@ -163,15 +165,17 @@ public static function buildDatabasesTable(\DOMDocument $dom, ?ServerSession $se } /** - * @param array $urlParams + * @param array $urlParams */ public static function buildHelpLink(\DOMDocument $dom, string $url, ?array $urlParams = null): \DOMElement { $a = $dom->createElement('a', '?'); $href = $url; + if (!is_null($urlParams)) { $href .= '?' . http_build_query($urlParams); } + $a->setAttribute('href', $href); $a->setAttribute('class', 'help'); $a->setAttribute('title', _('Help')); @@ -185,12 +189,13 @@ public static function buildMessage(\DOMDocument $dom, string $message): \DOMEle $pMessage = $dom->createElement('p'); $pMessage->setAttribute('class', 'message'); $pMessage->appendChild($dom->createTextNode($message)); + return $pMessage; } public static function buildMultipleActionsTableForDatabases( \DOMDocument $dom, - ?ServerSession $serverSession + ?ServerSession $serverSession, ): \DOMElement { $table = $dom->createElement('table'); @@ -254,7 +259,7 @@ public static function buildMultipleActionsTableForDatabases( } /** - * @param array{'url': string, 'url-params'?: array, 'label': string}[] $navLinks + * @param array, 'label': string}> $navLinks */ public static function buildNavLinks(\DOMDocument $dom, array $navLinks): \DOMElement { @@ -265,9 +270,11 @@ public static function buildNavLinks(\DOMDocument $dom, array $navLinks): \DOMEl $li = $dom->createElement('li'); $a = $dom->createElement('a', $navLink['label']); $href = $navLink['url']; + if (isset($navLink['url-params'])) { $href .= '?' . http_build_query($navLink['url-params']); } + $a->setAttribute('href', $href); $li->appendChild($a); $ul->appendChild($li); @@ -326,7 +333,7 @@ public static function buildRootTabs(\DOMDocument $dom, string $activeTab): \DOM } /** - * @param array{ + * @param array, * 'label': string, @@ -336,7 +343,7 @@ public static function buildRootTabs(\DOMDocument $dom, string $activeTab): \DOM * 'url': string, * 'url-params'?: array * } - * }[] $tabLinks + * }> $tabLinks */ public static function buildServerDatabasesTabs(\DOMDocument $dom, array $tabLinks): \DOMElement { @@ -349,16 +356,20 @@ public static function buildServerDatabasesTabs(\DOMDocument $dom, array $tabLin foreach ($tabLinks as $tabLink) { $td = $dom->createElement('td'); $tdClass = 'tab'; + if (isset($tabLink['active']) && $tabLink['active']) { $tdClass .= ' active'; } + $td->setAttribute('class', $tdClass); $td->setAttribute('style', 'width: 20%'); $href = $tabLink['url']; + if (isset($tabLink['url-params'])) { $href .= '?' . http_build_query($tabLink['url-params']); } + $a = $dom->createElement('a'); $a->setAttribute('href', $href); $spanIcon = $dom->createElement('span'); @@ -377,7 +388,7 @@ public static function buildServerDatabasesTabs(\DOMDocument $dom, array $tabLin $aHelp = self::buildHelpLink( dom: $dom, url: $tabLink['help']['url'], - urlParams: $tabLink['help']['url-params'] ?? [] + urlParams: $tabLink['help']['url-params'] ?? [], ); $td->appendChild($aHelp); } @@ -403,12 +414,12 @@ public static function buildTopBar(\DOMDocument $dom): \DOMElement $aAlertBanner = $dom->createElement('a'); $aAlertBanner->setAttribute( 'href', - 'https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite' + 'https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite', ); $aAlertBanner->setAttribute('target', '_blank'); $aAlertBanner->setAttribute('rel', 'noopener noreferrer'); $aAlertBanner->appendChild($dom->createTextNode( - _('You are running phpPgAdmin with session security disabled. This is a potential security risk!') + _('You are running phpPgAdmin with session security disabled. This is a potential security risk!'), )); $pAlertBanner->appendChild($aAlertBanner); $divAlertBanner->appendChild($pAlertBanner); @@ -423,13 +434,14 @@ public static function buildTopBar(\DOMDocument $dom): \DOMElement $trTopbar = $dom->createElement('tr'); $serverSession = ServerSession::fromRequestParameter(); + if (!is_null($serverSession)) { $topLeftContent = sprintf( _("%s running on %s:%s -- You are logged in as user \"%s\""), '' . htmlspecialchars((string)$serverSession->Platform) . '', '' . htmlspecialchars((string)$serverSession->Host) . '', '' . $serverSession->Port->Value . '', - '' . htmlspecialchars((string)$serverSession->Username) . '' + '' . htmlspecialchars((string)$serverSession->Username) . '', ); $fragment = $dom->createDocumentFragment(); $fragment->appendXML($topLeftContent); @@ -519,7 +531,6 @@ private static function buildTopBarLinks(\DOMDocument $dom, ServerSession $serve 'text' => _('History'), 'url' => 'history.php', 'url-params' => [ - 'action' => 'pophistory', 'server' => $serverSession->id(), 'subject' => 'table', ], @@ -546,9 +557,11 @@ private static function buildTopBarLinks(\DOMDocument $dom, ServerSession $serve $li = $dom->createElement('li'); $a = $dom->createElement('a', $link['text']); $a->setAttribute('href', $link['url'] . '?' . http_build_query($link['url-params'])); + if (isset($link['target'])) { $a->setAttribute('target', $link['target']); } + $a->setAttribute('id', 'toplink_' . $key); $li->appendChild($a); $ulTopLinks->appendChild($li); @@ -581,6 +594,7 @@ private static function buildTrailForServer(\DOMDocument $dom): \DOMElement $imgIcon->setAttribute('alt', _('Server')); $spanIcon->appendChild($imgIcon); $a->appendChild($spanIcon); + if (!is_null($serverSession)) { $spanLabel = $dom->createElement('span', (string)$serverSession->Name); $spanLabel->setAttribute('class', 'label'); @@ -595,7 +609,7 @@ private static function buildTrailForServer(\DOMDocument $dom): \DOMElement urlParams: [ 'help' => 'pg.server', 'server' => $serverId, - ] + ], ); $td->appendChild($aHelp); $td->appendChild($dom->createTextNode(': '));