Import postgresql plugin

This commit is contained in:
Marek Miklewicz
2026-07-04 16:00:04 +02:00
commit ddc971567f
74 changed files with 18735 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
.DS_Store
._*
__MACOSX/
.claude/
error.log
data/*
+5
View File
@@ -0,0 +1,5 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
header('Location: /CMD_PLUGINS/da-postgresql/index.html', true, 302);
exit;
+14
View File
@@ -0,0 +1,14 @@
Bundled Adminer binary for deterministic plugin installs.
- Version: 5.4.2 (pgsql-only build)
- Source repo/tag: https://github.com/vrana/adminer/tree/v5.4.2
- Build command: `php compile.php pgsql`
- Output file: `adminer-5.4.2-pgsql.php`
- SHA256: 059505abc2b56487d78bbfbeb9485ed8c6a10fe357f4ddec9fc69b1043bff4af
- Bundled extension file: `adminer-extension.php`
- Extension SHA256: 6e2f098b172838ccb3736506867396dc0954a6383fdd7b7c5e7739ab21baafeb
The installer `scripts/setup/adminer_install.sh` verifies both checksums before
copying files to:
- `/var/www/html/adminer/adminer-core.php`
- `/var/www/html/adminer/adminer-extension.php`
+2
View File
@@ -0,0 +1,2 @@
6e2f098b172838ccb3736506867396dc0954a6383fdd7b7c5e7739ab21baafeb adminer-extension.php
059505abc2b56487d78bbfbeb9485ed8c6a10fe357f4ddec9fc69b1043bff4af adminer-5.4.2-pgsql.php
File diff suppressed because one or more lines are too long
+101
View File
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
function da_adminer_sso_auth(): ?array
{
$auth = $GLOBALS['da_adminer_sso_auth'] ?? null;
if (!is_array($auth)) {
return null;
}
$user = isset($auth['user']) && is_string($auth['user']) ? trim($auth['user']) : '';
$password = isset($auth['password']) && is_string($auth['password']) ? $auth['password'] : '';
$database = isset($auth['database']) && is_string($auth['database']) ? $auth['database'] : '';
if ($user === '' || $password === '') {
return null;
}
return [
'user' => $user,
'password' => $password,
'database' => $database,
];
}
function da_adminer_bootstrap_auth(array $auth): void
{
$user = isset($auth['user']) && is_string($auth['user']) ? trim($auth['user']) : '';
$password = isset($auth['password']) && is_string($auth['password']) ? $auth['password'] : '';
$database = isset($auth['database']) && is_string($auth['database']) ? trim($auth['database']) : '';
if ($user === '' || $password === '') {
return;
}
$driver = 'pgsql';
$server = 'localhost';
$_GET[$driver] = $server;
$_GET['username'] = $user;
if ($database !== '' && (!isset($_GET['db']) || !is_string($_GET['db']) || $_GET['db'] === '')) {
$_GET['db'] = $database;
}
if (!isset($_SESSION['pwds']) || !is_array($_SESSION['pwds'])) {
$_SESSION['pwds'] = [];
}
$_SESSION['pwds'][$driver][$server][$user] = $password;
if ($database !== '') {
if (!isset($_SESSION['db']) || !is_array($_SESSION['db'])) {
$_SESSION['db'] = [];
}
$_SESSION['db'][$driver][$server][$user][$database] = true;
}
}
function adminer_object()
{
static $bootstrapped = false;
if (!$bootstrapped) {
$auth = da_adminer_sso_auth();
if ($auth !== null) {
da_adminer_bootstrap_auth($auth);
}
$bootstrapped = true;
}
if (!class_exists('DirectAdminAdminerExtension', false)) {
class DirectAdminAdminerExtension extends \Adminer\Adminer
{
public function credentials()
{
$auth = da_adminer_sso_auth();
if ($auth !== null) {
return ['localhost', (string)$auth['user'], (string)$auth['password']];
}
$username = isset($_GET['username']) && is_string($_GET['username']) ? $_GET['username'] : '';
$password = \Adminer\get_password();
if (!is_string($password)) {
$password = '';
}
return ['localhost', $username, $password];
}
public function loginFormField($name, $heading, $value)
{
if ($name === 'driver') {
return "<tr style='display:none'><th></th><td><input type='hidden' name='auth[driver]' value='pgsql'></td></tr>\n";
}
if ($name === 'server') {
return "<tr style='display:none'><th></th><td><input type='hidden' name='auth[server]' value='localhost'></td></tr>\n";
}
return parent::loginFormField($name, $heading, $value);
}
}
}
return new DirectAdminAdminerExtension();
}
+156
View File
@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
if (!defined('IN_DA_PLUGIN') || IN_DA_PLUGIN !== true) {
http_response_code(403);
echo 'Forbidden';
exit;
}
if (!defined('PLUGIN_ACTION')) {
http_response_code(400);
echo 'Missing PLUGIN_ACTION';
exit;
}
define('PLUGIN_ROOT', dirname(__DIR__));
define('PLUGIN_EXEC_DIR', __DIR__);
define('PLUGIN_HANDLERS_DIR', __DIR__ . '/handlers');
$pluginErrorLog = PLUGIN_ROOT . '/error.log';
@ini_set('log_errors', '1');
@ini_set('error_log', $pluginErrorLog);
if (!is_file($pluginErrorLog)) {
@file_put_contents($pluginErrorLog, '');
}
register_shutdown_function(static function () use ($pluginErrorLog): void {
$last = error_get_last();
if ($last === null) {
return;
}
$fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
if (!in_array((int)$last['type'], $fatalTypes, true)) {
return;
}
$msg = sprintf(
"[%s] FATAL type=%d message=%s file=%s line=%d\n",
date('c'),
(int)$last['type'],
(string)$last['message'],
(string)$last['file'],
(int)$last['line']
);
@error_log($msg, 3, $pluginErrorLog);
});
try {
autoload_plugin_libs();
Http::bootstrapGlobals();
$settings = Settings::load(PLUGIN_ROOT . '/plugin-settings.conf');
AdminerRuntimeConfig::sync($settings, $pluginErrorLog);
$daUser = DirectAdminUser::fromEnvironment($settings);
if (!$daUser->hasPluginAccess()) {
http_response_code(403);
$langCode = $daUser->language();
$lang = Lang::load($daUser->language());
render_access_denied_message($lang, $daUser->skin(), $langCode);
exit;
}
$credentials = Settings::loadPostgresqlCredentials('/usr/local/directadmin/conf/postgresql.conf');
$pg = new PostgresService($credentials, $settings);
$pg->ensureMetadataSchema();
$secret = $settings->loadOrCreateSecret(PLUGIN_ROOT . '/data/secret.key');
$csrf = new CsrfGuard($secret, $daUser->username(), Http::server('SESSION_ID'));
$lang = Lang::load($daUser->language());
$ctx = new AppContext($daUser, $settings, $pg, $csrf, $lang);
$handlerFile = PLUGIN_HANDLERS_DIR . '/' . basename((string)PLUGIN_ACTION) . '.php';
if (!is_file($handlerFile)) {
throw new RuntimeException('Brak handlera dla akcji: ' . (string)PLUGIN_ACTION);
}
$handler = require $handlerFile;
if (!is_callable($handler)) {
throw new RuntimeException('Handler akcji nie jest wywoływalny: ' . (string)PLUGIN_ACTION);
}
$handler($ctx);
exit;
} catch (Throwable $e) {
$msg = sprintf(
"[%s] EXCEPTION class=%s message=%s file=%s line=%d action=%s\n",
date('c'),
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
defined('PLUGIN_ACTION') ? (string)PLUGIN_ACTION : 'unknown'
);
@error_log($msg, 3, $pluginErrorLog);
@error_log("[stacktrace]\n" . $e->getTraceAsString() . "\n", 3, $pluginErrorLog);
http_response_code(500);
header('Content-Type: text/html; charset=UTF-8');
echo '<!doctype html><html><head><meta charset="utf-8"><title>Błąd pluginu</title></head><body>';
echo '<h1>Błąd pluginu PostgreSQL</h1>';
echo '<p>' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>';
echo '<p>Sprawdź konfigurację i uprawnienia pluginu.</p>';
echo '</body></html>';
exit;
}
function autoload_plugin_libs(): void
{
$files = [
PLUGIN_EXEC_DIR . '/lib/Http.php',
PLUGIN_EXEC_DIR . '/lib/Settings.php',
PLUGIN_EXEC_DIR . '/lib/AdminerRuntimeConfig.php',
PLUGIN_EXEC_DIR . '/lib/DirectAdminUser.php',
PLUGIN_EXEC_DIR . '/lib/Lang.php',
PLUGIN_EXEC_DIR . '/lib/LocalizedException.php',
PLUGIN_EXEC_DIR . '/lib/CsrfGuard.php',
PLUGIN_EXEC_DIR . '/lib/NamePolicy.php',
PLUGIN_EXEC_DIR . '/lib/PostgresService.php',
PLUGIN_EXEC_DIR . '/lib/AdminerSso.php',
PLUGIN_EXEC_DIR . '/lib/BackupQueueService.php',
PLUGIN_EXEC_DIR . '/lib/FilePicker.php',
PLUGIN_EXEC_DIR . '/lib/AppContext.php',
];
foreach ($files as $file) {
require_once $file;
}
}
function render_access_denied_message(Lang $lang, string $skin, string $langCode): void
{
$path = PLUGIN_ROOT . '/user/' . strtolower(trim($skin)) . '.css';
if (!is_file($path)) {
$path = PLUGIN_ROOT . '/user/enhanced.css';
}
$css = is_file($path) ? (string)file_get_contents($path) : '';
$message = $lang->t('Bazy danych PostgreSQL nie są dostępne dla Twojego konta - skontaktuj się z pomocą techniczną aby uzyskać pomoc i więcej informacji');
$title = $lang->t('Błąd');
header('Content-Type: text/html; charset=UTF-8');
echo '<!doctype html><html lang="' . htmlspecialchars($langCode, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '"><head><meta charset="utf-8">';
echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
echo '<title>' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</title>';
echo '<style>' . $css . '</style>';
echo '</head><body>';
echo '<div class="wrap">';
echo '<section class="main-section">';
echo '<div class="alert alert-err">' . htmlspecialchars($message, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</div>';
echo '</section>';
echo '</div>';
echo '</body></html>';
}
+88
View File
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$dbNames = array_map(static fn(array $db): string => $db['name'], $ctx->pg->listDatabasesForUser($daUser));
$roleNames = array_map(static fn(array $r): string => $r['name'], $ctx->pg->listRolesForUser($daUser));
if ($ctx->isPost()) {
try {
$ctx->requireCsrf('assign_database_submit');
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('db_name'), $prefix);
$roleName = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if (!in_array($dbName, $dbNames, true)) {
throw new RuntimeException('Baza nie należy do bieżącego użytkownika DA: ' . $dbName);
}
if (!in_array($roleName, $roleNames, true)) {
throw new RuntimeException('Użytkownik PostgreSQL nie należy do bieżącego konta DA: ' . $roleName);
}
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$ctx->pg->grantPrivileges($dbName, $roleName, $privileges);
$hostEntries = $ctx->pg->getRoleHostEntries($daUser, $roleName);
$hasLocalhost = false;
foreach ($hostEntries as $entry) {
if ($entry['host'] === 'localhost') {
$hasLocalhost = true;
break;
}
}
if (!$hasLocalhost) {
$hostEntries[] = ['host' => 'localhost', 'note' => ''];
$ctx->pg->replaceRoleHostsWithNotes($daUser, $roleName, $hostEntries);
}
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Przypisanie wykonane, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Przypisano bazę ' . $dbName . ' do użytkownika ' . $roleName . '.';
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$dbOptions = '<option value="">-- wybierz bazę --</option>';
foreach ($dbNames as $dbName) {
$dbOptions .= '<option value="' . AppContext::e($dbName) . '">' . AppContext::e($dbName) . '</option>';
}
$roleOptions = '<option value="">-- wybierz użytkownika --</option>';
foreach ($roleNames as $roleName) {
$roleOptions .= '<option value="' . AppContext::e($roleName) . '">' . AppContext::e($roleName) . '</option>';
}
$privCheckboxes = '';
foreach (NamePolicy::PRIVILEGE_LABELS as $code => $label) {
$checked = ($code === 'ALL') ? ' checked' : '';
$privCheckboxes .= '<label class="inline"><input type="checkbox" name="privileges[]" value="' . AppContext::e($code) . '"' . $checked . '> ' . AppContext::e($label) . '</label>';
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('assign_database_submit');
$body .= '<div class="row">';
$body .= '<div><label>Baza danych</label><select name="db_name" required>' . $dbOptions . '</select></div>';
$body .= '<div><label>Użytkownik PostgreSQL</label><select name="role_name" required>' . $roleOptions . '</select></div>';
$body .= '</div>';
$body .= '<div class="panel"><h3>Uprawnienia</h3><div class="privileges-group" data-privileges-group>' . $privCheckboxes . '</div></div>';
$body .= '<div class="actions"><button class="primary" type="submit">Przypisz bazę</button></div>';
$body .= '</form>';
$ctx->renderPage('Przypisywanie Bazy do Użytkownika', $body, $ok, $errors);
};
+586
View File
@@ -0,0 +1,586 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$showRemoteHosts = $ctx->settings->allowRemoteHosts();
$postgresPort = $ctx->pg->serverPort();
$passwordChangedFor = '';
$changedPassword = '';
$createdRoleName = '';
$createdRolePassword = '';
$createdRoleAssignedDbs = [];
$showCreateUserForm = (strtolower(trim($ctx->queryString('add_user'))) === '1');
$deletePromptRoleRaw = trim($ctx->postString('delete_role'));
if ($deletePromptRoleRaw === '') {
$deletePromptRoleRaw = trim($ctx->queryString('delete_role'));
}
$deletePromptRole = '';
$deletePromptAssignedDbs = [];
$createRoleSuffixInput = trim($ctx->postString('create_role_name'));
if (strpos($createRoleSuffixInput, $prefix) === 0) {
$createRoleSuffixInput = substr($createRoleSuffixInput, strlen($prefix));
}
$createPasswordInput = $ctx->postString('create_password');
$createSelectedDbsRaw = $ctx->postArray('create_user_databases');
$createSelectedDbs = array_values(array_unique(array_filter($createSelectedDbsRaw, static fn(string $db): bool => $db !== '')));
$databases = $ctx->pg->listDatabasesForUser($daUser);
$allDbs = array_map(static fn(array $db): string => $db['name'], $databases);
$roles = $ctx->pg->listRolesForUser($daUser);
$roleNames = array_map(static fn(array $r): string => $r['name'], $roles);
$preferredRoleRaw = $ctx->postString('role_name');
if ($preferredRoleRaw === '') {
$preferredRoleRaw = $ctx->queryString('role');
}
if ($ctx->isPost()) {
$postAction = $ctx->postString('form_action');
try {
if ($postAction === 'create_user_inline') {
$ctx->requireCsrf('create_user_inline_submit');
$showCreateUserForm = true;
$newRole = NamePolicy::normalizeRoleName($ctx->postString('create_role_name'), $prefix);
if ($ctx->pg->roleExists($newRole)) {
throw new RuntimeException('Użytkownik PostgreSQL już istnieje: ' . $newRole);
}
$newPassword = $ctx->postString('create_password');
if (strlen($newPassword) < 12) {
throw new RuntimeException('Hasło użytkownika musi mieć co najmniej 12 znaków.');
}
$selectedDbs = [];
foreach ($createSelectedDbs as $rawDbName) {
$dbName = NamePolicy::normalizeDatabaseName($rawDbName, $prefix);
if (!in_array($dbName, $allDbs, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $dbName);
}
if (!in_array($dbName, $selectedDbs, true)) {
$selectedDbs[] = $dbName;
}
}
$ctx->pg->createRole($newRole, $newPassword);
try {
$ctx->pg->replaceRoleHostsWithNotes($daUser, $newRole, [['host' => 'localhost', 'note' => '']]);
foreach ($selectedDbs as $dbName) {
$ctx->pg->grantPrivileges($dbName, $newRole, NamePolicy::defaultPrivileges());
}
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Użytkownik został utworzony, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
$createdRoleName = $newRole;
$createdRolePassword = $newPassword;
$createdRoleAssignedDbs = $selectedDbs;
if (empty($selectedDbs)) {
$ok[] = 'Utworzono użytkownika ' . $newRole . ' bez przypisanych baz danych.';
} else {
$ok[] = 'Utworzono użytkownika ' . $newRole . ' i przypisano do ' . count($selectedDbs) . ' baz(y).';
}
$preferredRoleRaw = $newRole;
$showCreateUserForm = false;
$createRoleSuffixInput = '';
$createPasswordInput = '';
$createSelectedDbs = [];
} catch (Throwable $inner) {
try {
$ctx->pg->deleteRoleHosts($daUser, $newRole);
} catch (Throwable $ignored) {
}
try {
$ctx->pg->dropRole($newRole);
} catch (Throwable $ignored) {
}
throw $inner;
}
} elseif ($postAction === 'delete_user_confirm') {
$ctx->requireCsrf('delete_user_submit');
$roleToDelete = NamePolicy::normalizeRoleName($ctx->postString('delete_role'), $prefix);
if (!in_array($roleToDelete, $roleNames, true)) {
throw new RuntimeException('Użytkownik PostgreSQL nie należy do konta DA: ' . $roleToDelete);
}
$owned = array_values(array_unique($ctx->pg->roleOwnedDatabases($roleToDelete, $daUser)));
if (!empty($owned)) {
throw new RuntimeException(
'Nie można usunąć użytkownika, ponieważ jest właścicielem baz: ' . implode(', ', $owned)
);
}
$assigned = array_values(array_unique($ctx->pg->roleAssignedDatabases($roleToDelete, $daUser)));
foreach ($assigned as $dbName) {
$owner = $ctx->pg->getDatabaseOwner($dbName);
if ($owner !== null && $owner !== $roleToDelete) {
$ctx->pg->reassignOwnedObjects($dbName, $roleToDelete, $owner);
}
$ctx->pg->revokePrivileges($dbName, $roleToDelete);
}
$ctx->pg->deleteRoleHosts($daUser, $roleToDelete);
$ctx->pg->dropRole($roleToDelete);
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Usuwanie wykonane, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Usunięto użytkownika ' . $roleToDelete . '.';
if ($preferredRoleRaw === $roleToDelete) {
$preferredRoleRaw = '';
}
$deletePromptRoleRaw = '';
} else {
$ctx->requireCsrf('user_manage_submit');
if (empty($roleNames)) {
throw new RuntimeException('Brak użytkowników PostgreSQL do zarządzania.');
}
$selectedRole = NamePolicy::normalizeRoleName($ctx->postString('role_name', $preferredRoleRaw), $prefix);
if (!in_array($selectedRole, $roleNames, true)) {
throw new RuntimeException('Użytkownik PostgreSQL nie należy do konta DA: ' . $selectedRole);
}
$preferredRoleRaw = $selectedRole;
if ($postAction === 'change_password') {
$password = $ctx->postString('password');
if (strlen($password) < 12) {
throw new RuntimeException('Nowe hasło musi mieć minimum 12 znaków.');
}
$ctx->pg->changeRolePassword($selectedRole, $password);
$passwordChangedFor = $selectedRole;
$changedPassword = $password;
} elseif ($postAction === 'grant_db') {
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('db_name'), $prefix);
if (!in_array($dbName, $allDbs, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $dbName);
}
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$ctx->pg->grantPrivileges($dbName, $selectedRole, $privileges);
$ok[] = 'Przyznano dostęp do bazy ' . $dbName . ' użytkownikowi ' . $selectedRole . '.';
} elseif ($postAction === 'revoke_db') {
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('db_name'), $prefix);
$ctx->pg->revokePrivileges($dbName, $selectedRole);
$ctx->pg->deleteRoleHosts($daUser, $selectedRole, $dbName);
$ok[] = 'Odebrano dostęp do bazy ' . $dbName . ' użytkownikowi ' . $selectedRole . '.';
} elseif ($postAction === 'add_host' && $showRemoteHosts) {
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('db_name'), $prefix);
if (!in_array($dbName, $allDbs, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $dbName);
}
if (!in_array($selectedRole, $ctx->pg->listDatabaseUsers($daUser, $dbName), true)) {
throw new RuntimeException('Użytkownik PostgreSQL nie ma dostępu do wybranej bazy: ' . $selectedRole);
}
$allowAllHosts = $ctx->postString('allow_all_hosts') === '1';
$host = $allowAllHosts
? NamePolicy::ALL_IPV4_HOST_PATTERN
: NamePolicy::normalizeRemoteIpv4AccessPattern($ctx->postString('host'));
$note = NamePolicy::normalizeHostComment($ctx->postString('host_note'));
if ($allowAllHosts && $note === '') {
$note = 'Dostęp z każdego adresu IPv4';
}
$entries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole, $dbName);
$map = [];
foreach ($entries as $entry) {
$map[$entry['host']] = $entry['note'];
}
$map[$host] = $note;
if (count($map) > $ctx->settings->maxHostsPerUser()) {
throw new RuntimeException('Przekroczono limit hostów dla wybranej bazy i użytkownika PostgreSQL.');
}
$save = [];
foreach ($map as $h => $n) {
$save[] = ['host' => $h, 'note' => $n];
}
$ctx->pg->replaceRoleHostsWithNotes($daUser, $selectedRole, $save, $dbName);
$restartPostgresql = $ctx->postString('restart_postgresql') === '1';
$sync = $ctx->pg->syncHbaRules($restartPostgresql);
if (!$sync['ok']) {
$errors[] = 'Host dodany, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
} elseif ($restartPostgresql) {
$ok[] = 'Synchronizacja konfiguracji PostgreSQL zakończona i usługa PostgreSQL została zrestartowana.';
}
$ok[] = 'Dodano host ' . $host . ' dla użytkownika ' . $selectedRole . ' i bazy ' . $dbName . '.';
} elseif ($postAction === 'remove_host' && $showRemoteHosts) {
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('db_name'), $prefix);
if (!in_array($dbName, $allDbs, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $dbName);
}
$host = trim($ctx->postString('host'));
$entries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole, $dbName);
$save = [];
foreach ($entries as $entry) {
if ($entry['host'] === $host) {
continue;
}
$save[] = $entry;
}
$ctx->pg->replaceRoleHostsWithNotes($daUser, $selectedRole, $save, $dbName);
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Host usunięty, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Usunięto host ' . $host . ' dla użytkownika ' . $selectedRole . ' i bazy ' . $dbName . '.';
}
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
if ($postAction === 'create_user_inline') {
$showCreateUserForm = true;
}
if ($postAction === 'delete_user_confirm') {
$deletePromptRoleRaw = trim($ctx->postString('delete_role'));
}
}
}
$databases = $ctx->pg->listDatabasesForUser($daUser);
$allDbs = array_map(static fn(array $db): string => $db['name'], $databases);
$roles = $ctx->pg->listRolesForUser($daUser);
$roleNames = array_map(static fn(array $r): string => $r['name'], $roles);
$selectedRole = '';
if (!empty($roleNames)) {
$candidateRaw = $preferredRoleRaw !== '' ? $preferredRoleRaw : $roleNames[0];
try {
$candidate = NamePolicy::normalizeRoleName($candidateRaw, $prefix);
} catch (Throwable $e) {
$candidate = $roleNames[0];
}
if (!in_array($candidate, $roleNames, true)) {
$candidate = $roleNames[0];
}
$selectedRole = $candidate;
}
if ($deletePromptRoleRaw !== '') {
try {
$deletePromptRole = NamePolicy::normalizeRoleName($deletePromptRoleRaw, $prefix);
if (!in_array($deletePromptRole, $roleNames, true)) {
throw new RuntimeException('Użytkownik PostgreSQL nie należy do konta DA: ' . $deletePromptRole);
}
$deletePromptAssignedDbs = array_values(array_unique($ctx->pg->roleAssignedDatabases($deletePromptRole, $daUser)));
} catch (Throwable $e) {
$errors[] = $e->getMessage();
$deletePromptRole = '';
$deletePromptAssignedDbs = [];
}
}
$createSelectedDbSet = [];
foreach ($createSelectedDbs as $rawDbName) {
try {
$dbName = NamePolicy::normalizeDatabaseName($rawDbName, $prefix);
} catch (Throwable $e) {
continue;
}
if (in_array($dbName, $allDbs, true)) {
$createSelectedDbSet[$dbName] = true;
}
}
$roleOptions = '';
foreach ($roleNames as $roleName) {
$sel = ($roleName === $selectedRole) ? ' selected' : '';
$roleOptions .= '<option value="' . AppContext::e($roleName) . '"' . $sel . '>' . AppContext::e($roleName) . '</option>';
}
$assignedDbs = [];
$grantableDbs = $allDbs;
if ($selectedRole !== '') {
$assignedDbs = $ctx->pg->roleAssignedDatabases($selectedRole, $daUser);
$grantableDbs = array_values(array_diff($allDbs, $assignedDbs));
}
$dbOptions = '<option value="">-- wybierz bazę danych --</option>';
foreach ($grantableDbs as $dbName) {
$dbOptions .= '<option value="' . AppContext::e($dbName) . '">' . AppContext::e($dbName) . '</option>';
}
$assignedDbOptions = '';
foreach ($assignedDbs as $dbName) {
$assignedDbOptions .= '<option value="' . AppContext::e($dbName) . '">' . AppContext::e($dbName) . '</option>';
}
$dbRows = '';
foreach ($assignedDbs as $dbName) {
$profile = $ctx->pg->getGrantProfile($dbName, $selectedRole);
$label = (empty($profile) || in_array('ALL', $profile, true))
? 'Pełny dostęp'
: implode(', ', $profile);
$dbRows .= '<tr>';
$dbRows .= '<td>' . AppContext::e($dbName) . '</td>';
$dbRows .= '<td><span class="badge">' . AppContext::e($label) . '</span></td>';
$dbRows .= '<td class="actions">';
$dbRows .= '<a class="btn" href="' . AppContext::e($ctx->url('modify_privileges.html', ['db' => $dbName, 'role' => $selectedRole])) . '">Zarządzaj</a>';
$dbRows .= '<a class="btn" href="' . AppContext::e($ctx->url('modify_privileges.html', ['db' => $dbName, 'role' => $selectedRole])) . '#przywileje">Przywileje</a>';
$dbRows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
$dbRows .= $ctx->csrfField('user_manage_submit');
$dbRows .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$dbRows .= '<input type="hidden" name="db_name" value="' . AppContext::e($dbName) . '">';
$dbRows .= '<input type="hidden" name="form_action" value="revoke_db">';
$dbRows .= '<button class="btn danger" type="submit">Odbierz dostęp</button>';
$dbRows .= '</form>';
$dbRows .= '</td>';
$dbRows .= '</tr>';
}
if ($dbRows === '') {
$dbRows = '<tr><td colspan="3" class="muted">Brak przypisanych baz danych.</td></tr>';
}
$hostsRows = '';
$hostsCount = 0;
$hasAllHostsEntry = false;
if ($showRemoteHosts && $selectedRole !== '') {
foreach ($assignedDbs as $dbName) {
$hostEntries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole, $dbName);
$hostsCount += count($hostEntries);
foreach ($hostEntries as $entry) {
$host = $entry['host'];
$note = $entry['note'];
if ($host === NamePolicy::ALL_IPV4_HOST_PATTERN) {
$hasAllHostsEntry = true;
}
$hostsRows .= '<tr>';
$hostsRows .= '<td>' . AppContext::e($dbName) . '</td>';
$hostsRows .= '<td>' . AppContext::e($host) . '</td>';
$hostsRows .= '<td>' . AppContext::e($note) . '</td>';
$hostsRows .= '<td class="actions">';
$hostsRows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
$hostsRows .= $ctx->csrfField('user_manage_submit');
$hostsRows .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$hostsRows .= '<input type="hidden" name="db_name" value="' . AppContext::e($dbName) . '">';
$hostsRows .= '<input type="hidden" name="host" value="' . AppContext::e($host) . '">';
$hostsRows .= '<input type="hidden" name="form_action" value="remove_host">';
$hostsRows .= '<button class="btn danger" type="submit">Usuń</button>';
$hostsRows .= '</form>';
$hostsRows .= '</td>';
$hostsRows .= '</tr>';
}
}
if ($hostsRows === '') {
$hostsRows = '<tr><td colspan="4" class="muted">Brak dodatkowych hostów dla przypisanych baz.</td></tr>';
}
}
$endpoint = $ctx->pg->connectionEndpoint();
$endpointHost = trim($endpoint['host']) !== '' ? $endpoint['host'] : 'localhost';
$endpointPort = $endpoint['port'] > 0 ? (string)$endpoint['port'] : '5432';
$createdUserCard = '';
if ($createdRoleName !== '' && $createdRolePassword !== '') {
$assignedDbsHtml = '';
if (!empty($createdRoleAssignedDbs)) {
$assignedItems = '';
foreach ($createdRoleAssignedDbs as $assignedDbName) {
$assignedItems .= '<li><code>' . AppContext::e($assignedDbName) . '</code></li>';
}
$assignedDbsHtml .= '<div><strong>Przypisane bazy danych:</strong></div>';
$assignedDbsHtml .= '<div><ul class="list-clean">' . $assignedItems . '</ul></div>';
}
$createdUserCard .= '<section class="success-credentials">';
$createdUserCard .= '<div class="left">✓</div>';
$createdUserCard .= '<div class="right">';
$createdUserCard .= '<h4>Dane dostępowe użytkownika PostgreSQL</h4>';
$createdUserCard .= '<div class="kv">';
$createdUserCard .= '<div><strong>Nazwa hosta:</strong></div><div><code>' . AppContext::e($endpointHost) . ' (Port: ' . AppContext::e($endpointPort) . ')</code></div>';
$createdUserCard .= '<div><strong>Nazwa użytkownika:</strong></div><div><code>' . AppContext::e($createdRoleName) . '</code></div>';
$createdUserCard .= '<div><strong>Hasło:</strong></div><div><code>' . AppContext::e($createdRolePassword) . '</code></div>';
$createdUserCard .= $assignedDbsHtml;
$createdUserCard .= '</div></div></section>';
}
$passwordChangedCard = '';
if ($passwordChangedFor !== '' && $changedPassword !== '') {
$passwordChangedCard .= '<section class="success-credentials">';
$passwordChangedCard .= '<div class="left">✓</div>';
$passwordChangedCard .= '<div class="right">';
$passwordChangedCard .= '<h4>Hasło zostało zmienione</h4>';
$passwordChangedCard .= '<div class="kv">';
$passwordChangedCard .= '<div><strong>Nazwa hosta:</strong></div><div><code>' . AppContext::e($endpointHost) . ' (Port: ' . AppContext::e($endpointPort) . ')</code></div>';
$passwordChangedCard .= '<div><strong>Nazwa użytkownika:</strong></div><div><code>' . AppContext::e($passwordChangedFor) . '</code></div>';
$passwordChangedCard .= '<div><strong>Hasło:</strong></div><div><code>' . AppContext::e($changedPassword) . '</code></div>';
$passwordChangedCard .= '</div></div></section>';
}
$body = '';
$body .= $createdUserCard;
$body .= $passwordChangedCard;
$body .= '<section class="panel">';
$body .= '<h3>Zarządzaj użytkownikami</h3>';
$body .= '<div class="actions" style="justify-content:space-between;align-items:center">';
$body .= '<div class="actions" style="margin-top:0">';
$body .= '<a class="btn primary" href="' . AppContext::e($ctx->url('change_password.html', ['add_user' => 1])) . '#add-user-form">Dodaj użytkownika</a>';
if ($showCreateUserForm) {
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('change_password.html')) . '">Anuluj</a>';
}
$body .= '</div>';
if ($selectedRole !== '') {
$body .= '<div class="actions" style="margin-top:0">';
$body .= '<form method="get" action="' . AppContext::e($ctx->url('change_password.html')) . '" class="actions" style="margin-top:0">';
$body .= '<label style="margin:0">Użytkownik: <select name="role" style="width:auto;display:inline-block;margin-left:8px">' . $roleOptions . '</select></label>';
$body .= '<button class="btn" type="submit">Przełącz</button>';
$body .= '</form>';
$body .= '<a class="btn danger-fill" href="' . AppContext::e($ctx->url('change_password.html', ['role' => $selectedRole, 'delete_role' => $selectedRole])) . '">Usuń użytkownika</a>';
$body .= '</div>';
} else {
$body .= '<span class="muted">Brak użytkowników PostgreSQL. Dodaj pierwszego użytkownika.</span>';
}
$body .= '</div>';
$body .= '<p class="section-text">Szczegółowe informacje o użytkowniku.</p>';
if ($selectedRole !== '') {
$body .= '<table><tbody>';
$body .= '<tr><td><strong>Nazwa użytkownika</strong><br>' . AppContext::e($selectedRole) . '</td><td><strong>Bazy danych</strong><br>' . AppContext::e((string)count($assignedDbs)) . '</td>';
if ($showRemoteHosts) {
$body .= '<td><strong>Dozwolone hosty</strong><br>' . AppContext::e((string)$hostsCount) . '</td>';
} else {
$body .= '<td><strong>Dozwolone hosty</strong><br>localhost</td>';
}
$body .= '</tr></tbody></table>';
} else {
$body .= '<p class="muted">Aby zarządzać użytkownikami najpierw utwórz użytkownika PostgreSQL.</p>';
}
$body .= '</section>';
if ($deletePromptRole !== '') {
$cancelUrl = $ctx->url('change_password.html', $selectedRole !== '' ? ['role' => $selectedRole] : []);
$body .= '<section class="panel" style="margin-bottom:14px;border-color:#f0b4a7;background:#fff5f3">';
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Czy na pewno chcesz usunąć użytkownika <strong>' . AppContext::e($deletePromptRole) . '</strong>?</div>';
if (count($deletePromptAssignedDbs) === 1) {
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Uwaga użytkownik <strong>' . AppContext::e($deletePromptRole) . '</strong> jest przypisany do następującej bazy danych <strong>' . AppContext::e($deletePromptAssignedDbs[0]) . '</strong>.</div>';
} elseif (count($deletePromptAssignedDbs) > 1) {
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Uwaga użytkownik <strong>' . AppContext::e($deletePromptRole) . '</strong> jest przypisany do następujących baz danych:</div>';
$body .= '<ul class="list-clean" style="margin:0 0 12px 18px">';
foreach ($deletePromptAssignedDbs as $assignedDbName) {
$body .= '<li>' . AppContext::e($assignedDbName) . '</li>';
}
$body .= '</ul>';
}
$body .= '<form method="post">';
$body .= $ctx->csrfField('delete_user_submit');
$body .= '<input type="hidden" name="form_action" value="delete_user_confirm">';
$body .= '<input type="hidden" name="delete_role" value="' . AppContext::e($deletePromptRole) . '">';
$body .= '<div class="actions">';
$body .= '<button class="danger-fill" type="submit">Potwierdź usunięcie użytkownika</button>';
$body .= '<a class="btn" href="' . AppContext::e($cancelUrl) . '">Anuluj</a>';
$body .= '</div>';
$body .= '</form>';
$body .= '</section>';
}
if ($showCreateUserForm) {
$body .= '<section class="panel" id="add-user-form" style="margin-top:14px">';
$body .= '<h3>Utwórz nowego użytkownika PostgreSQL</h3>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('create_user_inline_submit');
$body .= '<input type="hidden" name="form_action" value="create_user_inline">';
$body .= '<div class="row">';
$body .= '<div><label>Nazwa użytkownika PostgreSQL</label><div class="input-prefix"><span class="prefix">' . AppContext::e($prefix) . '</span><input type="text" name="create_role_name" value="' . AppContext::e($createRoleSuffixInput) . '" autocomplete="off" required></div></div>';
$body .= '<div><label>Hasło użytkownika</label>';
$body .= '<div class="input-with-tools"><input type="password" id="create-role-password" name="create_password" value="' . AppContext::e($createPasswordInput) . '" autocomplete="new-password" required>';
$body .= '<span class="field-tools">';
$body .= '<button type="button" class="tool-btn" data-generate-target="create-role-password" title="Generuj hasło" aria-label="Generuj hasło"><svg viewBox="0 0 24 24"><path d="M20 7v5h-5"></path><path d="M20 12a8 8 0 1 1-2.34-5.66L20 7"></path></svg></button>';
$body .= '<button type="button" class="tool-btn" data-toggle-target="create-role-password" title="Pokaż lub ukryj hasło" aria-label="Pokaż lub ukryj hasło"><svg viewBox="0 0 24 24"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>';
$body .= '</span></div>';
$body .= '</div>';
$body .= '</div>';
$body .= '<details class="details-advanced">';
$body .= '<summary>Przypisz do baz danych (opcjonalnie)</summary>';
if (empty($allDbs)) {
$body .= '<p class="muted">Brak baz do przypisania.</p>';
} else {
$body .= '<div class="db-choice-grid">';
foreach ($allDbs as $dbName) {
$checked = isset($createSelectedDbSet[$dbName]) ? ' checked' : '';
$body .= '<label class="db-choice-card"><input type="checkbox" name="create_user_databases[]" value="' . AppContext::e($dbName) . '"' . $checked . '><span>' . AppContext::e($dbName) . '</span></label>';
}
$body .= '</div>';
}
$body .= '<p class="muted" style="margin-top:8px">Możesz pozostawić użytkownika bez przypisania do baz i nadać dostęp później.</p>';
$body .= '</details>';
$body .= '<div class="actions"><button class="primary" type="submit">Utwórz użytkownika</button></div>';
$body .= '</form>';
$body .= '</section>';
}
if ($selectedRole !== '') {
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Zarządzanie hasłami</h3>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('user_manage_submit');
$body .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$body .= '<input type="hidden" name="form_action" value="change_password">';
$body .= '<label>Nowe hasło</label>';
$body .= '<div class="input-with-tools"><input type="password" id="change-role-password" name="password" placeholder="Wpisz nowe hasło" autocomplete="new-password" required>';
$body .= '<span class="field-tools">';
$body .= '<button type="button" class="tool-btn" data-generate-target="change-role-password" title="Generuj hasło" aria-label="Generuj hasło"><svg viewBox="0 0 24 24"><path d="M20 7v5h-5"></path><path d="M20 12a8 8 0 1 1-2.34-5.66L20 7"></path></svg></button>';
$body .= '<button type="button" class="tool-btn" data-toggle-target="change-role-password" title="Pokaż lub ukryj hasło" aria-label="Pokaż lub ukryj hasło"><svg viewBox="0 0 24 24"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>';
$body .= '</span></div>';
$body .= '<div class="actions"><button class="primary" type="submit">Zmień hasło</button></div>';
$body .= '</form>';
$body .= '</section>';
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Dostęp do baz danych</h3>';
$body .= '<table><thead><tr><th>Nazwa bazy danych</th><th>Przywileje</th><th>Akcje</th></tr></thead><tbody>' . $dbRows . '</tbody></table>';
$body .= '<form method="post" style="margin-top:10px">';
$body .= $ctx->csrfField('user_manage_submit');
$body .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$body .= '<input type="hidden" name="form_action" value="grant_db">';
$body .= '<label>Przyznaj dostęp do kolejnej bazy danych</label>';
$body .= '<select name="db_name">' . $dbOptions . '</select>';
$body .= '<input type="hidden" name="privileges[]" value="ALL">';
$body .= '<div class="actions"><button class="btn" type="submit">+ Przyznaj pełny dostęp</button></div>';
$body .= '</form>';
$body .= '</section>';
if ($showRemoteHosts) {
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Dozwolone hosty</h3>';
if ($hasAllHostsEntry) {
$body .= '<div class="alert alert-err" style="border:1px solid #b42318;background:#fff1f0;color:#7a1b13">Dla co najmniej jednej bazy włączono dostęp z każdego adresu IPv4. Plugin nie otwiera portu PostgreSQL w CSF dla tego trybu. Port PostgreSQL do otwarcia w CSF: <code>' . AppContext::e((string)$postgresPort) . '</code>.</div>';
}
$body .= '<p class="section-text">Lista źródłowych adresów IP/CIDR, które mogą łączyć się z konkretną bazą przy użyciu poświadczeń użytkownika.</p>';
$body .= '<table><thead><tr><th>Baza</th><th>Host/CIDR</th><th>Komentarz</th><th>Akcje</th></tr></thead><tbody>' . $hostsRows . '</tbody></table>';
$body .= '<form method="post" style="margin-top:10px">';
$body .= $ctx->csrfField('user_manage_submit');
$body .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$body .= '<input type="hidden" name="form_action" value="add_host">';
$body .= '<div class="row">';
$body .= '<div><label>Baza danych</label><select name="db_name">' . $assignedDbOptions . '</select></div>';
$body .= '<div><label>Zezwalaj na dostęp z IPv4/CIDR</label><input type="text" name="host" placeholder="203.0.113.10 lub 203.0.113.0/24"></div>';
$body .= '<div><label>Komentarz</label><input type="text" name="host_note" placeholder="np. biuro / serwer aplikacji"></div>';
$body .= '</div>';
$body .= '<div class="actions" style="justify-content:space-between;align-items:center;margin-top:10px">';
$body .= '<label class="inline" style="margin:0"><input type="checkbox" name="allow_all_hosts" value="1"> Dostęp dla wszystkich hostów IPv4 (<code>0.0.0.0/0</code>)</label>';
$body .= '<button class="danger" type="submit" name="restart_postgresql" value="1">Zapisz i restartuj PostgreSQL</button>';
$body .= '</div>';
$body .= '<p class="muted">Ten checkbox ignoruje pole IPv4/CIDR i wymaga ręcznego otwarcia portu PostgreSQL w CSF. Port PostgreSQL do otwarcia w CSF: <code>' . AppContext::e((string)$postgresPort) . '</code>.</p>';
$body .= '<div class="actions"><button class="btn" type="submit">+ Dodaj host</button></div>';
$body .= '</form>';
$body .= '</section>';
}
}
$ctx->renderPage('Zarządzaj użytkownikami', $body, $ok, $errors);
};
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
if ($ctx->isPost()) {
if ($ctx->postString('form_action') === '') {
$_POST['form_action'] = 'create_database';
}
if ($ctx->postString('db_suffix') === '' && $ctx->postString('db_name') !== '') {
$_POST['db_suffix'] = $ctx->postString('db_name');
}
$handler = require __DIR__ . '/index.php';
$handler($ctx);
return;
}
Http::redirect($ctx->url('index.html'));
};
+137
View File
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$showRemoteHosts = $ctx->settings->allowRemoteHosts();
$databases = $ctx->pg->listDatabasesForUser($daUser);
$dbNames = array_map(static fn(array $db): string => $db['name'], $databases);
if ($ctx->isPost()) {
try {
$ctx->requireCsrf('create_user_submit');
$role = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if ($ctx->pg->roleExists($role)) {
throw new RuntimeException('Użytkownik PostgreSQL już istnieje: ' . $role);
}
$password = $ctx->postString('password');
if (strlen($password) < 12) {
throw new RuntimeException('Hasło użytkownika musi mieć co najmniej 12 znaków.');
}
$selectedDbsRaw = $ctx->postArray('databases');
$selectedDbs = [];
foreach ($selectedDbsRaw as $db) {
$normalizedDb = NamePolicy::normalizeDatabaseName($db, $prefix);
if (!in_array($normalizedDb, $dbNames, true)) {
throw new RuntimeException('Nieprawidłowa baza do przypisania: ' . $normalizedDb);
}
$selectedDbs[] = $normalizedDb;
}
$selectedDbs = array_values(array_unique($selectedDbs));
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$globalHostEntries = [['host' => 'localhost', 'note' => '']];
$remoteHostEntry = null;
if ($showRemoteHosts) {
$remoteHostRaw = trim($ctx->postString('remote_host'));
if ($remoteHostRaw !== '') {
$remoteHost = NamePolicy::normalizeRemoteIpv4AccessPattern($remoteHostRaw);
$remoteComment = NamePolicy::normalizeHostComment($ctx->postString('remote_comment'));
$remoteHostEntry = ['host' => $remoteHost, 'note' => $remoteComment];
}
}
if ($remoteHostEntry !== null && empty($selectedDbs)) {
throw new RuntimeException('Host zdalny można dodać tylko dla wybranej bazy.');
}
$ctx->pg->createRole($role, $password);
try {
$ctx->pg->replaceRoleHostsWithNotes($daUser, $role, $globalHostEntries);
foreach ($selectedDbs as $dbName) {
$ctx->pg->grantPrivileges($dbName, $role, $privileges);
if ($remoteHostEntry !== null) {
$ctx->pg->replaceRoleHostsWithNotes($daUser, $role, [$remoteHostEntry], $dbName);
}
}
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Użytkownik został utworzony, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
$ok[] = 'Utworzono użytkownika ' . $role . ' i przypisano ' . count($selectedDbs) . ' baz(y).';
} catch (Throwable $inner) {
try {
foreach ($selectedDbs as $dbName) {
$ctx->pg->revokePrivileges($dbName, $role);
}
$ctx->pg->deleteRoleHosts($daUser, $role);
$ctx->pg->dropRole($role);
} catch (Throwable $ignored) {
}
throw $inner;
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$dbCheckboxes = '';
foreach ($dbNames as $dbName) {
$dbCheckboxes .= '<label class="inline"><input type="checkbox" name="databases[]" value="' . AppContext::e($dbName) . '"> ' . AppContext::e($dbName) . '</label>';
}
if ($dbCheckboxes === '') {
$dbCheckboxes = '<p class="muted">Brak baz do przypisania.</p>';
}
$privCheckboxes = '';
foreach (NamePolicy::PRIVILEGE_LABELS as $code => $label) {
$checked = ($code === 'ALL') ? ' checked' : '';
$privCheckboxes .= '<label class="inline"><input type="checkbox" name="privileges[]" value="' . AppContext::e($code) . '"' . $checked . '> ' . AppContext::e($label) . '</label>';
}
$remoteFields = '';
if ($showRemoteHosts) {
$remoteFields .= '<div class="row">';
$remoteFields .= '<div><label>Dodatkowy host IPv4/CIDR</label><input type="text" name="remote_host" placeholder="203.0.113.10 lub 203.0.113.0/24"></div>';
$remoteFields .= '<div><label>Komentarz hosta</label><input type="text" name="remote_comment" placeholder="np. Laptop administratora"></div>';
$remoteFields .= '</div>';
} else {
$remoteFields .= '<p class="muted">Hosty zdalne są wyłączone w konfiguracji pluginu (`allow_remote_hosts=0`).</p>';
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('create_user_submit');
$body .= '<div class="row">';
$body .= '<div><label>Nazwa użytkownika PostgreSQL</label><input type="text" name="role_name" placeholder="' . AppContext::e($prefix) . 'app_user" required></div>';
$body .= '<div><label>Hasło</label><input type="password" name="password" placeholder="min. 12 znaków" required></div>';
$body .= '</div>';
$body .= '<div class="row">';
$body .= '<div><label>Domyślny dostęp</label><p class="muted">`localhost` jest dodawany automatycznie.</p></div>';
$body .= '<div><label>Przypisz do baz</label><div>' . $dbCheckboxes . '</div></div>';
$body .= '</div>';
$body .= $remoteFields;
$body .= '<div class="panel"><h3>Uprawnienia dla przypisywanych baz</h3><div class="privileges-group" data-privileges-group>' . $privCheckboxes . '</div></div>';
$body .= '<div class="actions"><button class="primary" type="submit">Utwórz użytkownika</button></div>';
$body .= '</form>';
$ctx->renderPage('Tworzenie Użytkownika PostgreSQL', $body, $ok, $errors);
};
+171
View File
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$availableDbs = array_map(static fn(array $db): string => $db['name'], $ctx->pg->listDatabasesForUser($daUser));
$selectedDatabases = $ctx->postArray('databases');
if (empty($selectedDatabases)) {
$queryDb = $ctx->queryString('db');
if ($queryDb !== '') {
$selectedDatabases = [$queryDb];
}
}
$selectedDatabases = array_values(array_unique(array_filter($selectedDatabases, static fn(string $v): bool => $v !== '')));
$confirmDelete = $ctx->postString('confirm_delete') === '1';
if ($ctx->isPost() && !$confirmDelete) {
try {
$ctx->requireCsrf('select_delete_databases');
} catch (Throwable $e) {
$errors[] = $e->getMessage();
$selectedDatabases = [];
}
}
if ($ctx->isPost() && $confirmDelete) {
try {
$ctx->requireCsrf('delete_databases_confirm');
if (empty($selectedDatabases)) {
throw new RuntimeException('Nie wybrano baz danych do usunięcia.');
}
$deleteRelatedUsers = $ctx->postString('delete_related_users') === '1';
$deletedDbs = [];
$deletedUsers = [];
$keptUsers = [];
foreach ($selectedDatabases as $rawDb) {
try {
$dbName = NamePolicy::normalizeDatabaseName($rawDb, $prefix);
if (!in_array($dbName, $availableDbs, true)) {
$errors[] = 'Pominięto bazę spoza konta: ' . $dbName;
continue;
}
$users = $ctx->pg->listDatabaseUsers($daUser, $dbName);
$users = array_values(array_unique($users));
foreach ($users as $roleName) {
$ctx->pg->revokePrivileges($dbName, $roleName);
$ctx->pg->deleteRoleHosts($daUser, $roleName, $dbName);
}
$ctx->pg->dropDatabase($dbName);
$deletedDbs[] = $dbName;
if ($deleteRelatedUsers) {
foreach ($users as $roleName) {
$assigned = $ctx->pg->roleAssignedDatabases($roleName, $daUser);
$owned = $ctx->pg->roleOwnedDatabases($roleName, $daUser);
if (empty($assigned) && empty($owned)) {
try {
$ctx->pg->deleteRoleHosts($daUser, $roleName);
$ctx->pg->dropRole($roleName);
$deletedUsers[] = $roleName;
} catch (Throwable $dropErr) {
$keptUsers[] = $roleName . ' (' . $dropErr->getMessage() . ')';
}
} else {
$keptUsers[] = $roleName . ' (ma nadal przypisane bazy)';
}
}
}
} catch (Throwable $inner) {
$errors[] = $rawDb . ': ' . $inner->getMessage();
}
}
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Operacja wykonana, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
if (!empty($deletedDbs)) {
$ok[] = 'Usunięto bazy: ' . implode(', ', array_values(array_unique($deletedDbs)));
}
if (!empty($deletedUsers)) {
$ok[] = 'Usunięto użytkowników powiązanych z usuniętymi bazami: ' . implode(', ', array_values(array_unique($deletedUsers)));
}
if (!empty($keptUsers)) {
$errors[] = 'Użytkownicy pozostawieni: ' . implode('; ', array_values(array_unique($keptUsers)));
}
$selectedDatabases = [];
$availableDbs = array_map(static fn(array $db): string => $db['name'], $ctx->pg->listDatabasesForUser($daUser));
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if (empty($selectedDatabases)) {
$rows = '';
foreach ($availableDbs as $dbName) {
$users = $ctx->pg->listDatabaseUsers($daUser, $dbName);
$rows .= '<tr>';
$rows .= '<td><input type="checkbox" name="databases[]" value="' . AppContext::e($dbName) . '"></td>';
$rows .= '<td>' . AppContext::e($dbName) . '</td>';
$rows .= '<td>' . AppContext::e(implode(', ', $users)) . '</td>';
$rows .= '</tr>';
}
if ($rows === '') {
$rows = '<tr><td colspan="3" class="muted">Brak baz do usunięcia.</td></tr>';
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('select_delete_databases');
$body .= '<table><thead><tr><th></th><th>Baza</th><th>Przypisani użytkownicy</th></tr></thead><tbody>' . $rows . '</tbody></table>';
$body .= '<div class="actions"><button class="danger" type="submit">Usuń zaznaczone bazy</button></div>';
$body .= '</form>';
$ctx->renderPage('Usuwanie Baz Danych', $body, $ok, $errors);
return;
}
$confirmRows = '';
foreach ($selectedDatabases as $rawDb) {
try {
$dbName = NamePolicy::normalizeDatabaseName($rawDb, $prefix);
$users = $ctx->pg->listDatabaseUsers($daUser, $dbName);
$confirmRows .= '<tr>';
$confirmRows .= '<td>' . AppContext::e($dbName) . '<input type="hidden" name="databases[]" value="' . AppContext::e($dbName) . '"></td>';
$confirmRows .= '<td>' . AppContext::e(implode(', ', $users)) . '</td>';
$confirmRows .= '</tr>';
} catch (Throwable $e) {
$errors[] = $rawDb . ': ' . $e->getMessage();
}
}
if ($confirmRows === '') {
$ctx->renderPage('Usuwanie Baz Danych', '<p class="muted">Brak poprawnych baz do usunięcia.</p>', $ok, $errors);
return;
}
$body = '';
$body .= '<p>Potwierdź usunięcie baz danych. Operacja jest nieodwracalna.</p>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('delete_databases_confirm');
$body .= '<input type="hidden" name="confirm_delete" value="1">';
$body .= '<table><thead><tr><th>Baza</th><th>Przypisani użytkownicy</th></tr></thead><tbody>' . $confirmRows . '</tbody></table>';
$body .= '<p><label class="inline"><input type="checkbox" name="delete_related_users" value="1"> Usuń także użytkowników przypisanych do usuwanych baz (jeżeli po usunięciu baz nie mają już innych przypisań)</label></p>';
$body .= '<div class="actions">';
$body .= '<button class="danger" type="submit">Potwierdź usunięcie</button>';
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('delete_databases.html')) . '">Anuluj</a>';
$body .= '</div>';
$body .= '</form>';
$ctx->renderPage('Potwierdzenie Usuwania Baz Danych', $body, $ok, $errors);
};
+158
View File
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$availableRoles = array_map(static fn(array $r): string => $r['name'], $ctx->pg->listRolesForUser($daUser));
$selectedRoles = $ctx->postArray('roles');
if (empty($selectedRoles)) {
$queryRole = $ctx->queryString('role');
if ($queryRole !== '') {
$selectedRoles = [$queryRole];
}
}
$selectedRoles = array_values(array_unique(array_filter($selectedRoles, static fn(string $v): bool => $v !== '')));
$confirmDelete = $ctx->postString('confirm_delete') === '1';
if ($ctx->isPost() && !$confirmDelete) {
try {
$ctx->requireCsrf('select_delete_users');
} catch (Throwable $e) {
$errors[] = $e->getMessage();
$selectedRoles = [];
}
}
if ($ctx->isPost() && $confirmDelete) {
try {
$ctx->requireCsrf('delete_users_confirm');
if (empty($selectedRoles)) {
throw new RuntimeException('Nie wybrano użytkowników do usunięcia.');
}
$deleted = [];
$skipped = [];
foreach ($selectedRoles as $rawRole) {
try {
$role = NamePolicy::normalizeRoleName($rawRole, $prefix);
if (!in_array($role, $availableRoles, true)) {
$skipped[] = $role . ' (nie istnieje)';
continue;
}
$owned = $ctx->pg->roleOwnedDatabases($role, $daUser);
if (!empty($owned)) {
$skipped[] = $role . ' (właściciel baz: ' . implode(', ', $owned) . ')';
continue;
}
$assigned = $ctx->pg->roleAssignedDatabases($role, $daUser);
foreach ($assigned as $dbName) {
$owner = $ctx->pg->getDatabaseOwner($dbName);
if ($owner !== null && $owner !== $role) {
$ctx->pg->reassignOwnedObjects($dbName, $role, $owner);
}
$ctx->pg->revokePrivileges($dbName, $role);
}
$ctx->pg->deleteRoleHosts($daUser, $role);
$ctx->pg->dropRole($role);
$deleted[] = $role;
} catch (Throwable $inner) {
$skipped[] = $rawRole . ' (' . $inner->getMessage() . ')';
}
}
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Usuwanie wykonane, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
if (!empty($deleted)) {
$ok[] = 'Usunięto użytkowników: ' . implode(', ', $deleted);
}
if (!empty($skipped)) {
$errors[] = 'Nie usunięto: ' . implode('; ', $skipped);
}
$selectedRoles = [];
$availableRoles = array_map(static fn(array $r): string => $r['name'], $ctx->pg->listRolesForUser($daUser));
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if (empty($selectedRoles)) {
$rows = '';
foreach ($availableRoles as $roleName) {
$assigned = $ctx->pg->roleAssignedDatabases($roleName, $daUser);
$owned = $ctx->pg->roleOwnedDatabases($roleName, $daUser);
$rows .= '<tr>';
$rows .= '<td><input type="checkbox" name="roles[]" value="' . AppContext::e($roleName) . '"></td>';
$rows .= '<td>' . AppContext::e($roleName) . '</td>';
$rows .= '<td>' . AppContext::e(implode(', ', $assigned)) . '</td>';
$rows .= '<td>' . AppContext::e(implode(', ', $owned)) . '</td>';
$rows .= '</tr>';
}
if ($rows === '') {
$rows = '<tr><td colspan="4" class="muted">Brak użytkowników do usunięcia.</td></tr>';
}
$body = '';
$body .= '<form method="post">';
$body .= $ctx->csrfField('select_delete_users');
$body .= '<table><thead><tr><th></th><th>Użytkownik</th><th>Przypisane bazy</th><th>Bazy, których jest właścicielem</th></tr></thead><tbody>' . $rows . '</tbody></table>';
$body .= '<div class="actions"><button class="danger" type="submit">Usuń zaznaczonych użytkowników</button></div>';
$body .= '</form>';
$ctx->renderPage('Usuwanie Użytkowników', $body, $ok, $errors);
return;
}
$confirmRows = '';
foreach ($selectedRoles as $rawRole) {
try {
$role = NamePolicy::normalizeRoleName($rawRole, $prefix);
$assigned = $ctx->pg->roleAssignedDatabases($role, $daUser);
$owned = $ctx->pg->roleOwnedDatabases($role, $daUser);
$confirmRows .= '<tr>';
$confirmRows .= '<td>' . AppContext::e($role) . '<input type="hidden" name="roles[]" value="' . AppContext::e($role) . '"></td>';
$confirmRows .= '<td>' . AppContext::e(implode(', ', $assigned)) . '</td>';
$confirmRows .= '<td>' . AppContext::e(implode(', ', $owned)) . '</td>';
$confirmRows .= '</tr>';
} catch (Throwable $e) {
$errors[] = $rawRole . ': ' . $e->getMessage();
}
}
if ($confirmRows === '') {
$ctx->renderPage('Usuwanie Użytkowników', '<p class="muted">Brak poprawnych użytkowników do usunięcia.</p>', $ok, $errors);
return;
}
$body = '';
$body .= '<p>Potwierdź usunięcie zaznaczonych użytkowników PostgreSQL. Operacja jest nieodwracalna.</p>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('delete_users_confirm');
$body .= '<input type="hidden" name="confirm_delete" value="1">';
$body .= '<table><thead><tr><th>Użytkownik</th><th>Przypisane bazy</th><th>Bazy, których jest właścicielem</th></tr></thead><tbody>';
$body .= $confirmRows;
$body .= '</tbody></table>';
$body .= '<div class="actions">';
$body .= '<button class="danger" type="submit">Potwierdź usunięcie</button>';
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('delete_users.html')) . '">Anuluj</a>';
$body .= '</div>';
$body .= '</form>';
$ctx->renderPage('Potwierdzenie Usuwania Użytkowników', $body, $ok, $errors);
};
+111
View File
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$pluginErrorLog = PLUGIN_ROOT . '/error.log';
$rawMode = defined('DA_POSTGRESQL_RAW_MODE') && DA_POSTGRESQL_RAW_MODE === true;
$log = static function (string $message) use ($pluginErrorLog, $ctx): void {
$entry = sprintf(
"[%s] DOWNLOAD_DIRECT action=%s user=%s remote=%s method=%s uri=%s %s\n",
date('c'),
$ctx->action(),
$ctx->daUser->username(),
Http::server('REMOTE_ADDR'),
Http::method(),
Http::server('REQUEST_URI'),
$message
);
@error_log($entry, 3, $pluginErrorLog);
};
try {
if (!$ctx->daUser->hasPluginAccess()) {
throw new RuntimeException('Brak dostępu do pluginu.');
}
$queue = new BackupQueueService($ctx->settings, $ctx->daUser);
$jobId = trim($ctx->queryString('job'));
$backupPath = trim($ctx->queryString('path'));
$downloadPath = '';
$logLabel = '';
if ($jobId !== '') {
$logLabel = 'job_id=' . $jobId;
$downloadPath = $queue->resolveBackupTargetFileForCurrentUser($jobId);
} elseif ($backupPath !== '') {
$logLabel = 'path=' . $backupPath;
$downloadPath = $queue->resolveUserBackupFileForCurrentUser($backupPath);
} else {
throw new RuntimeException('Brak parametru job lub path.');
}
// Reuse streaming logic from index handler (simplified)
$log('start ' . $logLabel);
if (!is_file($downloadPath) || !is_readable($downloadPath)) {
throw new RuntimeException('Plik backupu nie istnieje lub brak odczytu.');
}
$downloadName = basename($downloadPath);
$downloadName = preg_replace('/[^A-Za-z0-9._-]+/', '_', $downloadName) ?? 'backup.sql';
$contentType = 'application/octet-stream';
if (preg_match('/\\.sql$/i', $downloadName)) {
$contentType = 'application/sql';
} elseif (preg_match('/\\.gz$/i', $downloadName)) {
$contentType = 'application/gzip';
}
clearstatcache(true, $downloadPath);
$size = @filesize($downloadPath);
while (ob_get_level() > 0) {
@ob_end_clean();
}
if (function_exists('apache_setenv')) {
@apache_setenv('no-gzip', '1');
}
@ini_set('zlib.output_compression', '0');
@ini_set('output_buffering', '0');
if ($rawMode) {
$statusLine = 'HTTP/1.1 200 OK';
echo $statusLine . "\r\n";
echo 'Content-Type: ' . $contentType . "\r\n";
echo 'Content-Disposition: attachment; filename="' . str_replace('"', '', $downloadName) . "\"\r\n";
echo "X-Content-Type-Options: nosniff\r\n";
echo "Cache-Control: no-store, no-cache, must-revalidate\r\n";
echo "Pragma: no-cache\r\n";
if (is_int($size) && $size > 0) {
echo 'Content-Length: ' . (string)$size . "\r\n";
}
echo "\r\n";
} else {
if (headers_sent($hsFile, $hsLine)) {
$log('headers_sent ' . $logLabel . ' file=' . $hsFile . ' line=' . (string)$hsLine);
throw new RuntimeException('Nagłówki zostały już wysłane.');
}
header_remove('Content-Encoding');
header('Content-Type: ' . $contentType);
header('Content-Disposition: attachment; filename="' . str_replace('"', '', $downloadName) . '"');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
if (is_int($size) && $size > 0) {
header('Content-Length: ' . (string)$size);
}
}
$bytes = @readfile($downloadPath);
$log('done ' . $logLabel . ' bytes=' . (string)$bytes);
exit;
} catch (Throwable $e) {
$log('error message=' . $e->getMessage());
if ($rawMode) {
echo "HTTP/1.1 404 Not Found\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo 'Nie można pobrać backupu: ' . $e->getMessage();
} else {
http_response_code(404);
header('Content-Type: text/plain; charset=UTF-8');
echo 'Nie można pobrać backupu: ' . $e->getMessage();
}
exit;
}
};
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$queue = new BackupQueueService($ctx->settings, $ctx->daUser);
$root = '/home/' . $ctx->daUser->username();
if (FilePicker::handleDirList($ctx, $queue, $root)) {
return;
}
if (FilePicker::handleDirCreate($ctx, $root, $ctx->daUser->username())) {
return;
}
http_response_code(404);
header('Content-Type: text/plain; charset=UTF-8');
echo 'Not Found';
};
+788
View File
@@ -0,0 +1,788 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$showRemoteHosts = $ctx->settings->allowRemoteHosts();
$dbLimit = $ctx->daUser->maxDatabases();
$generatedPassword = '';
$generatedRole = '';
$generatedDb = '';
$advancedPosted = false;
$formAction = $ctx->postString('form_action');
$deletePromptDbRaw = '';
$deletePromptSelectedRolesRaw = [];
$deleteBackupJobIdRaw = '';
$deleteBackupJobId = '';
$deleteBackupDbName = '';
$backupQueue = new BackupQueueService($ctx->settings, $ctx->daUser);
$backupEnabled = $ctx->settings->enableBackup();
$uploadBackupEnabled = $ctx->settings->enableUploadBackup();
$pluginErrorLog = PLUGIN_ROOT . '/error.log';
$logDownload = static function (string $message) use ($pluginErrorLog, $ctx, $daUser): void {
$entry = sprintf(
"[%s] DOWNLOAD action=%s user=%s remote=%s method=%s uri=%s %s\n",
date('c'),
$ctx->action(),
$daUser,
Http::server('REMOTE_ADDR'),
Http::method(),
Http::server('REQUEST_URI'),
$message
);
@error_log($entry, 3, $pluginErrorLog);
};
$streamBackupDownload = static function (string $downloadJobId) use ($backupQueue, $logDownload): void {
$downloadJobId = trim($downloadJobId);
if ($downloadJobId === '') {
throw new RuntimeException('Brak identyfikatora zadania backupu.');
}
$logDownload('start job_id=' . $downloadJobId);
$downloadPath = $backupQueue->resolveBackupTargetFileForCurrentUser($downloadJobId);
if (!is_file($downloadPath)) {
throw new RuntimeException('Plik backupu nie istnieje na serwerze.');
}
if (!is_readable($downloadPath)) {
throw new RuntimeException('Brak uprawnień odczytu pliku backupu.');
}
$downloadName = basename($downloadPath);
$downloadName = preg_replace('/[^A-Za-z0-9._-]+/', '_', $downloadName) ?? 'backup.sql';
$contentType = 'application/octet-stream';
if (preg_match('/\.sql$/i', $downloadName)) {
$contentType = 'application/sql';
} elseif (preg_match('/\.gz$/i', $downloadName)) {
$contentType = 'application/gzip';
}
clearstatcache(true, $downloadPath);
$contentLength = @filesize($downloadPath);
$logDownload(
'prepare job_id=' . $downloadJobId .
' file=' . $downloadPath .
' size=' . (is_int($contentLength) ? (string)$contentLength : 'unknown') .
' ob_level=' . (string)ob_get_level()
);
while (ob_get_level() > 0) {
@ob_end_clean();
}
if (function_exists('apache_setenv')) {
@apache_setenv('no-gzip', '1');
}
@ini_set('zlib.output_compression', '0');
@ini_set('output_buffering', '0');
if (headers_sent($hsFile, $hsLine)) {
$logDownload('headers_sent job_id=' . $downloadJobId . ' file=' . $hsFile . ' line=' . (string)$hsLine);
throw new RuntimeException('Nie można pobrać backupu (nagłówki zostały już wysłane).');
}
header_remove('Content-Encoding');
header('Content-Type: ' . $contentType);
header('Content-Disposition: attachment; filename="' . str_replace('"', '', $downloadName) . '"');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
if (is_int($contentLength) && $contentLength > 0) {
header('Content-Length: ' . (string)$contentLength);
}
$bytes = @readfile($downloadPath);
if ($bytes === false) {
throw new RuntimeException('Nie można odczytać pliku backupu do pobrania.');
}
$logDownload('done job_id=' . $downloadJobId . ' bytes=' . (string)$bytes);
exit;
};
$action = Http::getString($_REQUEST, 'action');
if ($action === 'download_backup') {
try {
$ctx->requireCsrf('download_backup_file_submit');
$streamBackupDownload(Http::getString($_REQUEST, 'job'));
} catch (Throwable $e) {
$logDownload('error job_id=' . trim(Http::getString($_REQUEST, 'job')) . ' message=' . $e->getMessage());
http_response_code(400);
header('Content-Type: text/plain; charset=UTF-8');
echo 'Nie można pobrać backupu: ' . $e->getMessage();
exit;
}
}
if ($ctx->isPost() && $formAction === 'queue_backup') {
try {
$ctx->requireCsrf('queue_backup_submit');
if (!$backupEnabled) {
throw new RuntimeException('Tworzenie backupu jest wyłączone w konfiguracji pluginu.');
}
$dbName = NamePolicy::normalizeDatabaseName($ctx->postString('backup_db'), $prefix);
$availableDbs = array_map(static fn(array $db): string => $db['name'], $ctx->pg->listDatabasesForUser($daUser));
if (!in_array($dbName, $availableDbs, true)) {
throw new RuntimeException('Wskazana baza danych nie należy do konta: ' . $dbName);
}
$backupQueue->queueBackupJob($dbName);
$ok[] = 'Kopia zapasowa bazy danych ' . $dbName . ' została dodana do kolejki';
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if ($ctx->isPost() && $formAction === 'delete_backup_file') {
try {
$ctx->requireCsrf('delete_backup_file_submit');
$jobId = trim($ctx->postString('backup_job_id'));
if ($jobId === '') {
throw new RuntimeException('Brak identyfikatora zadania backupu.');
}
$deletedPath = $backupQueue->deleteBackupTargetFileForCurrentUser($jobId);
try {
$backupQueue->deleteBackupLogForCurrentUser($jobId);
} catch (Throwable $logErr) {
// Ignore log cleanup failure.
}
$ok[] = 'Usunięto plik backupu: ' . $deletedPath;
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if ($ctx->isPost() && $formAction === 'delete_backup_confirm') {
try {
$ctx->requireCsrf('delete_backup_confirm_submit');
$jobId = trim($ctx->postString('backup_job_id'));
if ($jobId === '') {
throw new RuntimeException('Brak identyfikatora zadania backupu.');
}
$deletedPath = $backupQueue->deleteBackupTargetFileForCurrentUser($jobId);
try {
$backupQueue->deleteBackupLogForCurrentUser($jobId);
} catch (Throwable $logErr) {
// Ignore log cleanup failure.
}
$ok[] = 'Usunięto plik backupu: ' . $deletedPath;
$deleteBackupJobIdRaw = '';
} catch (Throwable $e) {
$errors[] = $e->getMessage();
$deleteBackupJobIdRaw = trim($ctx->postString('backup_job_id'));
}
}
if ($ctx->isPost() && $formAction === 'download_backup_file') {
try {
$ctx->requireCsrf('download_backup_file_submit');
$streamBackupDownload($ctx->postString('backup_job_id'));
} catch (Throwable $e) {
$logDownload('error job_id=' . trim($ctx->postString('backup_job_id')) . ' message=' . $e->getMessage());
$errors[] = 'Nie można pobrać backupu: ' . $e->getMessage();
}
}
if ($ctx->isPost() && $formAction === 'create_database') {
try {
$ctx->requireCsrf('create_database_submit');
$limit = $ctx->daUser->maxDatabases();
$used = $ctx->pg->countDatabasesForUser($daUser);
if ($limit > 0 && $used >= $limit) {
throw new RuntimeException('Osiągnięto limit baz danych dla konta (' . $used . '/' . $limit . ').');
}
$dbRaw = trim($ctx->postString('db_suffix'));
if ($dbRaw === '') {
$dbRaw = trim($ctx->postString('db_name'));
}
$dbName = NamePolicy::normalizeDatabaseName($dbRaw, $prefix);
if ($ctx->pg->databaseExists($dbName)) {
throw new RuntimeException('Baza danych już istnieje: ' . $dbName);
}
$creationMode = strtolower($ctx->postString('creation_mode', 'simple'));
$roleModeRaw = strtolower($ctx->postString('role_mode', 'new'));
$advancedPosted = ($creationMode === 'advanced');
if (!$advancedPosted) {
$advancedPosted =
trim($ctx->postString('role_name')) !== '' ||
trim($ctx->postString('password')) !== '' ||
trim($ctx->postString('existing_role')) !== '' ||
$roleModeRaw === 'existing';
}
$roleMode = ($advancedPosted && $roleModeRaw === 'existing') ? 'existing' : 'new';
$selectedRole = '';
$createdRole = false;
$createdDatabase = false;
if ($roleMode === 'existing') {
$selectedRole = NamePolicy::normalizeRoleName($ctx->postString('existing_role'), $prefix);
if (!$ctx->pg->roleExists($selectedRole)) {
throw new RuntimeException('Wybrany użytkownik PostgreSQL nie istnieje: ' . $selectedRole);
}
} else {
$roleInput = $ctx->postString('role_name');
if ($roleInput === '') {
$roleInput = $dbName;
}
$selectedRole = NamePolicy::normalizeRoleName($roleInput, $prefix);
if ($ctx->pg->roleExists($selectedRole)) {
throw new RuntimeException('Użytkownik PostgreSQL już istnieje: ' . $selectedRole);
}
$password = $ctx->postString('password');
if ($advancedPosted && $password === '') {
throw new RuntimeException('Hasło użytkownika jest wymagane w trybie zaawansowanym.');
}
if (!$advancedPosted) {
$password = rtrim(strtr(base64_encode(random_bytes(18)), '+/', 'AZ'), '=');
}
if (strlen($password) < 12) {
throw new RuntimeException('Hasło użytkownika musi mieć co najmniej 12 znaków.');
}
$ctx->pg->createRole($selectedRole, $password);
$createdRole = true;
$generatedPassword = $password;
}
try {
$ctx->pg->createDatabase($dbName, $selectedRole);
$createdDatabase = true;
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$ctx->pg->grantPrivileges($dbName, $selectedRole, $privileges);
$hostEntries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole);
$hostMap = [];
foreach ($hostEntries as $entry) {
$hostMap[$entry['host']] = $entry['note'];
}
$needsHostUpdate = $createdRole;
if (!array_key_exists('localhost', $hostMap)) {
$hostMap['localhost'] = '';
$needsHostUpdate = true;
}
$remoteHostEntry = null;
if ($showRemoteHosts && $advancedPosted) {
$remoteHostRaw = trim($ctx->postString('remote_host'));
if ($remoteHostRaw !== '') {
$remoteHost = NamePolicy::normalizeRemoteIpv4AccessPattern($remoteHostRaw);
$remoteComment = NamePolicy::normalizeHostComment($ctx->postString('remote_comment'));
$remoteHostEntry = ['host' => $remoteHost, 'note' => $remoteComment];
}
}
if ($needsHostUpdate) {
if (count($hostMap) > $ctx->settings->maxHostsPerUser()) {
throw new RuntimeException('Przekroczono limit hostów dla użytkownika PostgreSQL.');
}
$finalEntries = [];
foreach ($hostMap as $host => $note) {
$finalEntries[] = ['host' => $host, 'note' => $note];
}
$ctx->pg->replaceRoleHostsWithNotes($daUser, $selectedRole, $finalEntries);
}
if ($remoteHostEntry !== null) {
$dbHostEntries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole, $dbName);
$dbHostMap = [];
foreach ($dbHostEntries as $entry) {
$dbHostMap[$entry['host']] = $entry['note'];
}
$dbHostMap[$remoteHostEntry['host']] = $remoteHostEntry['note'];
if (count($dbHostMap) > $ctx->settings->maxHostsPerUser()) {
throw new RuntimeException('Przekroczono limit hostów dla wybranej bazy i użytkownika PostgreSQL.');
}
$dbFinalEntries = [];
foreach ($dbHostMap as $host => $note) {
$dbFinalEntries[] = ['host' => $host, 'note' => $note];
}
$ctx->pg->replaceRoleHostsWithNotes($daUser, $selectedRole, $dbFinalEntries, $dbName);
}
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Baza i uprawnienia zapisane, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
$generatedRole = $selectedRole;
$generatedDb = $dbName;
$ok[] = 'Baza danych została utworzona.';
} catch (Throwable $inner) {
if ($createdDatabase) {
try {
$ctx->pg->dropDatabase($dbName);
} catch (Throwable $ignored) {
}
}
if ($createdRole) {
try {
$ctx->pg->dropRole($selectedRole);
} catch (Throwable $ignored) {
}
}
throw $inner;
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if ($ctx->isPost() && $formAction === 'delete_database_confirm') {
try {
$ctx->requireCsrf('delete_database_submit');
$deletePromptDbRaw = trim($ctx->postString('delete_db'));
if ($deletePromptDbRaw === '') {
throw new RuntimeException('Nie wskazano bazy danych do usunięcia.');
}
$deletePromptSelectedRolesRaw = $ctx->postArray('delete_roles');
$deleteMode = strtolower(trim($ctx->postString('delete_mode')));
$dbToDelete = NamePolicy::normalizeDatabaseName($deletePromptDbRaw, $prefix);
$availableBeforeDelete = array_map(static fn(array $db): string => $db['name'], $ctx->pg->listDatabasesForUser($daUser));
if (!in_array($dbToDelete, $availableBeforeDelete, true)) {
throw new RuntimeException('Wskazana baza danych nie należy do konta: ' . $dbToDelete);
}
$assignedRoles = array_values(array_unique($ctx->pg->listDatabaseUsers($daUser, $dbToDelete)));
$singleMatchingRole = count($assignedRoles) === 1 && $assignedRoles[0] === $dbToDelete;
foreach ($assignedRoles as $roleName) {
$ctx->pg->revokePrivileges($dbToDelete, $roleName);
$ctx->pg->deleteRoleHosts($daUser, $roleName, $dbToDelete);
}
$ctx->pg->dropDatabase($dbToDelete);
$rolesToDelete = [];
if ($deleteMode === 'with_user' && $singleMatchingRole) {
$rolesToDelete[] = $assignedRoles[0];
} elseif ($deleteMode !== 'db_only') {
foreach ($deletePromptSelectedRolesRaw as $rawRole) {
$roleName = NamePolicy::normalizeRoleName($rawRole, $prefix);
if (!in_array($roleName, $assignedRoles, true)) {
continue;
}
if (!in_array($roleName, $rolesToDelete, true)) {
$rolesToDelete[] = $roleName;
}
}
}
$deletedUsers = [];
$skippedUsers = [];
foreach ($rolesToDelete as $roleName) {
$remainingAssigned = array_values(array_unique($ctx->pg->roleAssignedDatabases($roleName, $daUser)));
foreach ($remainingAssigned as $remainingDb) {
$ctx->pg->revokePrivileges($remainingDb, $roleName);
}
$remainingOwned = array_values(array_unique($ctx->pg->roleOwnedDatabases($roleName, $daUser)));
if (!empty($remainingOwned)) {
$skippedUsers[] = $roleName . ' (jest właścicielem baz: ' . implode(', ', $remainingOwned) . ')';
continue;
}
try {
$ctx->pg->deleteRoleHosts($daUser, $roleName);
$ctx->pg->dropRole($roleName);
$deletedUsers[] = $roleName;
} catch (Throwable $dropErr) {
$skippedUsers[] = $roleName . ' (' . $dropErr->getMessage() . ')';
}
}
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Usuwanie wykonane, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
}
if (count($deletedUsers) === 1) {
$ok[] = 'Usunięto bazę danych ' . $dbToDelete . ' i użytkownika ' . $deletedUsers[0] . '.';
} elseif (count($deletedUsers) > 1) {
$listItems = '';
foreach ($deletedUsers as $deletedUser) {
$listItems .= "\n" . $deletedUser;
}
$ok[] = 'Usunięto bazę danych ' . $dbToDelete . ' i następujących użytkowników przypisanych do bazy danych ' . $dbToDelete . ':' . $listItems;
} else {
$ok[] = 'Usunięto bazę danych ' . $dbToDelete . '.';
}
if (!empty($skippedUsers)) {
$errors[] = 'Nie udało się usunąć części użytkowników: ' . implode('; ', $skippedUsers) . '.';
}
$deletePromptDbRaw = '';
$deletePromptSelectedRolesRaw = [];
} catch (Throwable $e) {
$errors[] = $e->getMessage();
if ($deletePromptDbRaw === '') {
$deletePromptDbRaw = trim($ctx->postString('delete_db'));
}
if (empty($deletePromptSelectedRolesRaw)) {
$deletePromptSelectedRolesRaw = $ctx->postArray('delete_roles');
}
}
}
$showTableCount = $ctx->settings->enableTableCount();
$showDbSize = $ctx->settings->countDatabaseSize();
$databases = $ctx->pg->listDatabasesForUser($daUser, $showDbSize);
$dbUsersMap = $ctx->pg->listDatabaseUsersMap($daUser);
$roles = $ctx->pg->listRolesForUser($daUser);
$roleNames = array_map(static fn(array $r): string => $r['name'], $roles);
$dbCount = count($databases);
$endpoint = $ctx->pg->connectionEndpoint();
$endpointHost = trim($endpoint['host']) !== '' ? $endpoint['host'] : 'localhost';
$endpointPort = $endpoint['port'] > 0 ? (string)$endpoint['port'] : '5432';
$serverVersion = '';
try {
$serverVersion = $ctx->pg->serverVersion();
} catch (Throwable $e) {
$serverVersion = '';
}
$advancedOpen = $advancedPosted || (strtolower($ctx->postString('creation_mode', 'simple')) === 'advanced');
if (!$ctx->isPost() && $deletePromptDbRaw === '') {
$deletePromptDbRaw = trim($ctx->queryString('delete_db'));
}
if (!$ctx->isPost() && $deleteBackupJobIdRaw === '') {
$deleteBackupJobIdRaw = trim($ctx->queryString('delete_backup'));
}
/** @var array<string,string> $overlayHtmlById */
$overlayHtmlById = [];
$deletePromptDb = '';
$deletePromptRows = '';
$deletePromptWarnings = [];
$deletePromptSelectedRoles = [];
$deletePromptSingleRoleMode = false;
$deletePromptSingleRoleName = '';
$limitText = $dbLimit === 0 ? 'Bez ograniczeń' : (string)$dbLimit;
$usageWarn = '';
$tableCounts = [];
if ($showTableCount) {
foreach ($databases as $dbRow) {
$dbName = $dbRow['name'];
try {
$stats = $ctx->pg->getDatabaseObjectStats($dbName);
$tableCounts[$dbName] = (int)$stats['tables'];
} catch (Throwable $e) {
$tableCounts[$dbName] = 0;
}
}
}
$dbRows = '';
foreach ($databases as $db) {
$name = $db['name'];
$size = $db['size'];
$users = $dbUsersMap[$name] ?? [];
$dbRows .= '<tr>';
$dbRows .= '<td><strong>' . AppContext::e($name) . '</strong></td>';
if ($showDbSize) {
$dbRows .= '<td>' . AppContext::e($size) . '</td>';
}
$dbRows .= '<td>' . AppContext::e((string)count($users)) . '</td>';
if ($showTableCount) {
$dbRows .= '<td>' . AppContext::e((string)($tableCounts[$name] ?? 0)) . '</td>';
}
$dbRows .= '<td class="actions">';
if ($ctx->settings->enableAdminer()) {
$dbRows .= '<form method="post" target="_top" action="' . AppContext::e($ctx->url('open_adminer.raw')) . '" style="display:inline-flex;gap:6px;margin:0">';
$dbRows .= $ctx->csrfField('open_adminer_submit');
$dbRows .= '<input type="hidden" name="adminer_db" value="' . AppContext::e($name) . '">';
$dbRows .= '<button class="btn adminer-login" type="submit">Zaloguj do bazy</button>';
$dbRows .= '</form>';
}
if ($backupEnabled) {
$locked = $backupQueue->isDbLocked($name);
if (!$locked) {
$dbRows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
$dbRows .= $ctx->csrfField('queue_backup_submit');
$dbRows .= '<input type="hidden" name="form_action" value="queue_backup">';
$dbRows .= '<input type="hidden" name="backup_db" value="' . AppContext::e($name) . '">';
$dbRows .= '<button class="btn" type="submit">Wykonaj Backup</button>';
$dbRows .= '</form>';
} else {
$dbRows .= '<button class="btn" type="button" disabled title="Na tej bazie trwa już operacja backup/restore">Wykonaj Backup</button>';
}
}
$dbRows .= '<a class="btn" href="' . AppContext::e($ctx->url('modify_privileges.html', ['db' => $name])) . '">Zarządzaj</a>';
$dbRows .= '<a class="btn danger-fill" href="' . AppContext::e($ctx->url('index.html', ['delete_db' => $name])) . '">Usuń bazę</a>';
$dbRows .= '</td>';
$dbRows .= '</tr>';
}
if ($dbRows === '') {
$colspan = 3 + ($showDbSize ? 1 : 0) + ($showTableCount ? 1 : 0);
$dbRows = '<tr><td colspan="' . AppContext::e((string)$colspan) . '" class="muted">Nie znaleziono baz danych...</td></tr>';
}
if ($deletePromptDbRaw !== '') {
try {
$deletePromptDb = NamePolicy::normalizeDatabaseName($deletePromptDbRaw, $prefix);
$dbNames = array_map(static fn(array $db): string => $db['name'], $databases);
if (!in_array($deletePromptDb, $dbNames, true)) {
throw new RuntimeException('Wskazana baza danych nie należy do konta: ' . $deletePromptDb);
}
foreach ($deletePromptSelectedRolesRaw as $rawRole) {
$roleName = NamePolicy::normalizeRoleName($rawRole, $prefix);
if (!in_array($roleName, $deletePromptSelectedRoles, true)) {
$deletePromptSelectedRoles[] = $roleName;
}
}
$assignedRoles = array_values(array_unique($ctx->pg->listDatabaseUsers($daUser, $deletePromptDb)));
if (count($assignedRoles) === 1 && $assignedRoles[0] === $deletePromptDb) {
$deletePromptSingleRoleMode = true;
$deletePromptSingleRoleName = $assignedRoles[0];
} else {
foreach ($assignedRoles as $roleName) {
$assignedDbs = array_values(array_unique($ctx->pg->roleAssignedDatabases($roleName, $daUser)));
$isChecked = in_array($roleName, $deletePromptSelectedRoles, true) ? ' checked' : '';
$deletePromptRows .= '<tr>';
$deletePromptRows .= '<td><input type="checkbox" class="delete-role-checkbox" name="delete_roles[]" value="' . AppContext::e($roleName) . '"' . $isChecked . '></td>';
$deletePromptRows .= '<td>' . AppContext::e($roleName) . '</td>';
if (empty($assignedDbs)) {
$deletePromptRows .= '<td class="muted">Brak przypisań</td>';
} else {
$dbList = '';
foreach ($assignedDbs as $dbName) {
$dbList .= '<li>' . AppContext::e($dbName) . '</li>';
}
$deletePromptRows .= '<td><ul class="list-clean">' . $dbList . '</ul></td>';
}
$deletePromptRows .= '</tr>';
if (count($assignedDbs) > 1) {
$deletePromptWarnings[] = 'Uwaga: wskazany użytkownik jest przypisany do wielu baz danych i jego usunięcie spowoduje brak możliwości dostępu do innych baz danych przy użyciu danych logowania użytkownika ' . $roleName . '.';
}
}
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
$deletePromptDb = '';
$deletePromptRows = '';
$deletePromptWarnings = [];
$deletePromptSelectedRoles = [];
$deletePromptSingleRoleMode = false;
$deletePromptSingleRoleName = '';
}
}
$roleOptions = '<option value="">-- wybierz istniejącego użytkownika --</option>';
$postedExistingRole = $ctx->postString('existing_role');
foreach ($roleNames as $roleName) {
$selected = ($postedExistingRole === $roleName) ? ' selected' : '';
$roleOptions .= '<option value="' . AppContext::e($roleName) . '"' . $selected . '>' . AppContext::e($roleName) . '</option>';
}
$privCheckboxes = '';
foreach (NamePolicy::PRIVILEGE_LABELS as $code => $label) {
$checked = ($code === 'ALL') ? ' checked' : '';
$privCheckboxes .= '<label class="inline"><input type="checkbox" name="privileges[]" value="' . AppContext::e($code) . '"' . $checked . '> ' . AppContext::e($label) . '</label>';
}
$successCard = '';
if ($generatedDb !== '' && $generatedRole !== '') {
$passwordLine = $generatedPassword !== ''
? '<div><strong>Hasło:</strong></div><div><code>' . AppContext::e($generatedPassword) . '</code></div>'
: '';
$successCard .= '<section class="success-credentials">';
$successCard .= '<div class="left">✓</div>';
$successCard .= '<div class="right">';
$successCard .= '<h4>Dane dostępowe do bazy danych</h4>';
$successCard .= '<div class="kv">';
$successCard .= '<div><strong>Nazwa hosta:</strong></div><div><code>' . AppContext::e($endpointHost) . ' (Port: ' . AppContext::e($endpointPort) . ')</code></div>';
$successCard .= '<div><strong>Baza danych:</strong></div><div><code>' . AppContext::e($generatedDb) . '</code></div>';
$successCard .= '<div><strong>Nazwa użytkownika:</strong></div><div><code>' . AppContext::e($generatedRole) . '</code></div>';
$successCard .= $passwordLine;
$successCard .= '</div></div></section>';
}
$body = '';
$body .= $successCard;
$body .= $usageWarn;
if ($deleteBackupJobIdRaw !== '') {
try {
$deleteBackupJobId = trim($deleteBackupJobIdRaw);
$job = $backupQueue->getJobForCurrentUser($deleteBackupJobId);
if (($job['type'] ?? '') !== 'backup') {
throw new RuntimeException('Wskazane zadanie nie jest zadaniem backupu.');
}
if (($job['status'] ?? '') !== 'done_ok') {
throw new RuntimeException('Backup nie został zakończony powodzeniem.');
}
$deleteBackupDbName = (string)($job['db_name'] ?? '');
if ($deleteBackupDbName === '') {
$deleteBackupDbName = 'nieznana baza';
}
$body .= '<section class="panel" style="margin-bottom:14px;border-color:#f0b4a7;background:#fff5f3">';
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Czy na pewno chcesz usunąć backup bazy danych - <strong>' . AppContext::e($deleteBackupDbName) . '</strong>?</div>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('delete_backup_confirm_submit');
$body .= '<input type="hidden" name="form_action" value="delete_backup_confirm">';
$body .= '<input type="hidden" name="backup_job_id" value="' . AppContext::e($deleteBackupJobId) . '">';
$body .= '<div class="actions">';
$body .= '<button class="danger" type="submit">Potwierdź usunięcie backupu</button>';
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">Anuluj</a>';
$body .= '</div>';
$body .= '</form>';
$body .= '</section>';
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
if ($deletePromptDb !== '') {
$body .= '<section class="panel" style="margin-bottom:14px;border-color:#f0b4a7;background:#fff5f3">';
if ($deletePromptSingleRoleMode && $deletePromptSingleRoleName !== '') {
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Czy na pewno chcesz usunąć następującą bazę danych - <strong>' . AppContext::e($deletePromptDb) . '</strong> i przypisanego do niej użytkownika <strong>' . AppContext::e($deletePromptSingleRoleName) . '</strong>.</div>';
} else {
$body .= '<div class="alert alert-err" style="margin:0 0 10px">Czy na pewno chcesz usunąć następującą bazę danych - <strong>' . AppContext::e($deletePromptDb) . '</strong>.</div>';
}
$body .= '<form method="post">';
$body .= $ctx->csrfField('delete_database_submit');
$body .= '<input type="hidden" name="form_action" value="delete_database_confirm">';
$body .= '<input type="hidden" name="delete_db" value="' . AppContext::e($deletePromptDb) . '">';
if ($deletePromptSingleRoleMode && $deletePromptSingleRoleName !== '') {
$body .= '<div class="actions">';
$body .= '<button class="danger-fill" type="submit" name="delete_mode" value="with_user">Tak</button>';
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">Nie</a>';
$body .= '<button class="btn warn" type="submit" name="delete_mode" value="db_only">Usuń tylko bazę danych</button>';
$body .= '</div>';
} else {
if ($deletePromptRows !== '') {
$body .= '<p class="section-text">Do bazy danych ' . AppContext::e($deletePromptDb) . ' przypisani są użytkownicy, z poniższej listy wybierz użytkowników, których chcesz usunąć.</p>';
$body .= '<table><thead><tr>';
$body .= '<th data-select-all-for="delete-role-checkbox" title="Kliknij, aby zaznaczyć lub odznaczyć wszystkich użytkowników">Wybierz</th>';
$body .= '<th>Nazwa użytkownika</th>';
$body .= '<th>Przypisane Bazy danych</th>';
$body .= '</tr></thead><tbody>' . $deletePromptRows . '</tbody></table>';
} else {
$body .= '<p class="muted">Do wskazanej bazy nie są przypisani użytkownicy PostgreSQL.</p>';
}
if (!empty($deletePromptWarnings)) {
foreach (array_values(array_unique($deletePromptWarnings)) as $warning) {
$body .= '<div class="alert alert-err">' . AppContext::e($warning) . '</div>';
}
}
$body .= '<div class="actions">';
$body .= '<button class="danger" type="submit">Potwierdź usunięcie bazy</button>';
$body .= '<a class="btn" href="' . AppContext::e($ctx->url('index.html')) . '">Anuluj</a>';
$body .= '</div>';
}
$body .= '</form>';
$body .= '</section>';
}
$dbSuffixValue = $ctx->postString('db_suffix');
if (strpos($dbSuffixValue, $prefix) === 0) {
$dbSuffixValue = substr($dbSuffixValue, strlen($prefix));
}
$roleSuffixValue = $ctx->postString('role_name');
if (strpos($roleSuffixValue, $prefix) === 0) {
$roleSuffixValue = substr($roleSuffixValue, strlen($prefix));
}
$body .= '<section class="panel">';
$body .= '<h3 class="section-title-center">Lista baz danych</h3>';
$body .= '<div class="actions" style="justify-content:space-between;align-items:center;margin-bottom:8px">';
if ($serverVersion !== '') {
$body .= '<span class="muted">' . AppContext::e($ctx->t('Wersja serwera PostgreSQL')) . ': <strong>' . AppContext::e($serverVersion) . '</strong></span>';
}
$body .= '<span class="muted">Ilość baz danych <strong>' . AppContext::e((string)$dbCount) . '/' . AppContext::e($limitText) . '</strong></span>';
$body .= '</div>';
$header = '<th>Nazwa bazy danych</th>';
if ($showDbSize) {
$header .= '<th>Rozmiar</th>';
}
$header .= '<th>Użytkownicy</th>';
if ($showTableCount) {
$header .= '<th>Liczba Tabel</th>';
}
$header .= '<th>Akcje</th>';
$body .= '<table><thead><tr>' . $header . '</tr></thead><tbody>' . $dbRows . '</tbody></table>';
$body .= '</section>';
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Utwórz bazę danych</h3>';
$body .= '<p class="section-text">Tryb prosty tworzy użytkownika o tej samej nazwie co baza i automatycznie generuje hasło.</p>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('create_database_submit');
$body .= '<input type="hidden" name="form_action" value="create_database">';
$body .= '<input type="hidden" id="create-db-mode" name="creation_mode" value="' . ($advancedOpen ? 'advanced' : 'simple') . '">';
$body .= '<input type="hidden" name="role_mode" value="new">';
$body .= '<div><label>Nazwa bazy danych</label>';
$body .= '<div class="input-prefix"><span class="prefix">' . AppContext::e($prefix) . '</span><input type="text" name="db_suffix" value="' . AppContext::e($dbSuffixValue) . '" required></div>';
$body .= '</div>';
$body .= '<p class="muted" style="margin-top:6px">Użytkownik o tej samej nazwie i bezpiecznym haśle zostanie wygenerowany automatycznie.</p>';
$body .= '<details class="details-advanced" id="create-db-advanced"' . ($advancedOpen ? ' open' : '') . '>';
$body .= '<summary>Tryb zaawansowany</summary>';
$body .= '<div class="row" style="margin-top:10px">';
$body .= '<div>';
$roleModeCurrent = strtolower($ctx->postString('role_mode', 'new'));
$body .= '<label class="inline"><input type="radio" id="role-mode-new" name="role_mode" value="new"' . ($roleModeCurrent !== 'existing' ? ' checked' : '') . '> Nowy użytkownik</label>';
$body .= '<label class="inline"><input type="radio" id="role-mode-existing" name="role_mode" value="existing"' . ($roleModeCurrent === 'existing' ? ' checked' : '') . '> Istniejący użytkownik</label>';
$body .= '</div>';
$body .= '<div><label>Istniejący użytkownik</label><select id="existing-role-name" name="existing_role">' . $roleOptions . '</select></div>';
$body .= '</div>';
$body .= '<div class="row">';
$body .= '<div><label>Nazwa użytkownika PostgreSQL</label><div class="input-prefix"><span class="prefix">' . AppContext::e($prefix) . '</span><input type="text" id="advanced-role-name" name="role_name" value="' . AppContext::e($roleSuffixValue) . '" autocomplete="off"></div></div>';
$body .= '<div><label>Hasło użytkownika</label>';
$body .= '<div class="input-with-tools"><input type="password" id="advanced-role-password" name="password" value="' . AppContext::e($ctx->postString('password')) . '" autocomplete="new-password">';
$body .= '<span class="field-tools">';
$body .= '<button type="button" class="tool-btn" data-generate-target="advanced-role-password" title="Generuj hasło" aria-label="Generuj hasło"><svg viewBox="0 0 24 24"><path d="M20 7v5h-5"></path><path d="M20 12a8 8 0 1 1-2.34-5.66L20 7"></path></svg></button>';
$body .= '<button type="button" class="tool-btn" data-toggle-target="advanced-role-password" title="Pokaż lub ukryj hasło" aria-label="Pokaż lub ukryj hasło"><svg viewBox="0 0 24 24"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"></path><circle cx="12" cy="12" r="3"></circle></svg></button>';
$body .= '</span></div>';
$body .= '<p class="muted" style="margin-top:6px">W trybie zaawansowanym hasło jest wymagane.</p></div>';
$body .= '</div>';
if ($showRemoteHosts) {
$body .= '<div class="row">';
$body .= '<div><label>Dodatkowy host IPv4/CIDR dla tworzonej bazy (opcjonalnie)</label><input type="text" name="remote_host" placeholder="203.0.113.10 lub 203.0.113.0/24"></div>';
$body .= '<div><label>Komentarz hosta (opcjonalnie)</label><input type="text" name="remote_comment" placeholder="np. serwer aplikacji"></div>';
$body .= '</div>';
}
$body .= '<div><label>Uprawnienia początkowe</label><div class="privileges-group" data-privileges-group>' . $privCheckboxes . '</div></div>';
$body .= '</details>';
$body .= '<div class="actions" style="justify-content:flex-end"><button class="primary" type="submit">UTWÓRZ</button></div>';
$body .= '</form>';
$body .= '</section>';
if (!empty($overlayHtmlById)) {
$body .= implode('', array_values($overlayHtmlById));
}
$ctx->renderPage('Zarządzanie bazami danych PostgreSQL', $body, $ok, $errors);
};
+224
View File
@@ -0,0 +1,224 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
if (!$ctx->settings->allowRemoteHosts()) {
$ctx->renderFatal('Zarządzanie hostami zdalnymi jest wyłączone (`allow_remote_hosts=0`).', 403);
return;
}
$ok = [];
$errors = [];
$lastSyncOutput = '';
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$postgresPort = $ctx->pg->serverPort();
$dbNames = array_map(static fn(array $db): string => $db['name'], $ctx->pg->listDatabasesForUser($daUser));
if (empty($dbNames)) {
$ctx->renderPage('Hosty Zdalne Użytkownika', '<div class="panel"><p class="muted">Brak baz danych do zarządzania hostami.</p></div>', $ok, $errors);
return;
}
$selectedDb = $ctx->postString('db_name');
if ($selectedDb === '') {
$selectedDb = $ctx->queryString('db');
}
if ($selectedDb === '') {
$selectedDb = $dbNames[0];
}
try {
$selectedDb = NamePolicy::normalizeDatabaseName($selectedDb, $prefix);
} catch (Throwable $e) {
$selectedDb = $dbNames[0];
}
if (!in_array($selectedDb, $dbNames, true)) {
$selectedDb = $dbNames[0];
}
$roleNames = $ctx->pg->listDatabaseUsers($daUser, $selectedDb);
$selectedRole = $ctx->queryString('role');
if ($ctx->isPost()) {
$selectedRole = $ctx->postString('role_name');
try {
$ctx->requireCsrf('manage_hosts_submit');
$selectedDb = NamePolicy::normalizeDatabaseName($ctx->postString('db_name', $selectedDb), $prefix);
if (!in_array($selectedDb, $dbNames, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $selectedDb);
}
$roleNames = $ctx->pg->listDatabaseUsers($daUser, $selectedDb);
$selectedRole = NamePolicy::normalizeRoleName($selectedRole, $prefix);
if (!in_array($selectedRole, $roleNames, true)) {
throw new RuntimeException('Użytkownik PostgreSQL nie ma dostępu do wybranej bazy: ' . $selectedRole);
}
$currentEntries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole, $selectedDb);
$hostMap = [];
foreach ($currentEntries as $entry) {
$hostMap[$entry['host']] = $entry['note'];
}
$removeHosts = $ctx->postArray('remove_hosts');
$removeHosts = array_values(array_unique($removeHosts));
foreach ($removeHosts as $rawHost) {
$host = trim($rawHost);
if ($host === '') {
continue;
}
if (array_key_exists($host, $hostMap)) {
unset($hostMap[$host]);
}
}
$addHostRaw = trim($ctx->postString('add_host'));
if ($addHostRaw !== '') {
$addHost = NamePolicy::normalizeRemoteIpv4AccessPattern($addHostRaw);
$addComment = NamePolicy::normalizeHostComment($ctx->postString('add_comment'));
$hostMap[$addHost] = $addComment;
}
$allowAllHosts = $ctx->postString('allow_all_hosts') === '1';
if ($allowAllHosts) {
$allHostsComment = NamePolicy::normalizeHostComment($ctx->postString('allow_all_comment'));
$hostMap[NamePolicy::ALL_IPV4_HOST_PATTERN] = $allHostsComment !== '' ? $allHostsComment : 'Dostęp z każdego adresu IPv4';
} else {
unset($hostMap[NamePolicy::ALL_IPV4_HOST_PATTERN]);
}
if (count($hostMap) > $ctx->settings->maxHostsPerUser()) {
throw new RuntimeException('Przekroczono limit hostów dla wybranej bazy i użytkownika PostgreSQL.');
}
$finalEntries = [];
foreach ($hostMap as $host => $note) {
$finalEntries[] = ['host' => $host, 'note' => $note];
}
$ctx->pg->replaceRoleHostsWithNotes($daUser, $selectedRole, $finalEntries, $selectedDb);
$restartPostgresql = $ctx->postString('restart_postgresql') === '1';
$sync = $ctx->pg->syncHbaRules($restartPostgresql);
$lastSyncOutput = trim((string)($sync['output'] ?? ''));
if (!$sync['ok']) {
$errors[] = 'Hosty zapisane, ale synchronizacja konfiguracji hostów zdalnych nie powiodła się: ' . $sync['output'];
} elseif ($restartPostgresql) {
$ok[] = 'Synchronizacja konfiguracji PostgreSQL zakończona i usługa PostgreSQL została zrestartowana.';
} elseif (array_key_exists(NamePolicy::ALL_IPV4_HOST_PATTERN, $hostMap)) {
$ok[] = 'Synchronizacja PostgreSQL zakończona. Dla dostępu z każdego hosta port PostgreSQL do otwarcia w CSF: ' . $postgresPort . '.';
} else {
$ok[] = 'Synchronizacja konfiguracji PostgreSQL i CSF zakończona.';
}
$ok[] = 'Zaktualizowano hosty dostępu dla użytkownika ' . $selectedRole . '.';
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$roleNames = $ctx->pg->listDatabaseUsers($daUser, $selectedDb);
if ($selectedRole !== '') {
try {
$selectedRole = NamePolicy::normalizeRoleName($selectedRole, $prefix);
} catch (Throwable $e) {
$selectedRole = '';
}
}
if ($selectedRole === '' && !empty($roleNames)) {
$selectedRole = $roleNames[0];
}
$currentEntries = [];
if ($selectedRole !== '') {
$currentEntries = $ctx->pg->getRoleHostEntries($daUser, $selectedRole, $selectedDb);
}
$allHostsEnabled = false;
$allHostsComment = '';
foreach ($currentEntries as $entry) {
if ($entry['host'] === NamePolicy::ALL_IPV4_HOST_PATTERN) {
$allHostsEnabled = true;
$allHostsComment = $entry['note'];
break;
}
}
$dbOptions = '';
foreach ($dbNames as $dbName) {
$sel = ($dbName === $selectedDb) ? ' selected' : '';
$dbOptions .= '<option value="' . AppContext::e($dbName) . '"' . $sel . '>' . AppContext::e($dbName) . '</option>';
}
$roleOptions = '<option value="">-- wybierz użytkownika --</option>';
foreach ($roleNames as $roleName) {
$sel = ($roleName === $selectedRole) ? ' selected' : '';
$roleOptions .= '<option value="' . AppContext::e($roleName) . '"' . $sel . '>' . AppContext::e($roleName) . '</option>';
}
$hostRows = '';
if (empty($currentEntries)) {
$hostRows = '<tr><td colspan="3" class="muted">Brak zapisanych hostów.</td></tr>';
} else {
foreach ($currentEntries as $entry) {
$host = $entry['host'];
$note = $entry['note'];
$hostRows .= '<tr>';
$hostRows .= '<td>' . AppContext::e($host) . '</td>';
$hostRows .= '<td>' . AppContext::e($note) . '</td>';
$hostRows .= '<td><label class="inline"><input type="checkbox" name="remove_hosts[]" value="' . AppContext::e($host) . '"> usuń</label></td>';
$hostRows .= '</tr>';
}
}
$body = '';
if ($allHostsEnabled) {
$body .= '<div class="alert alert-err" style="border:1px solid #b42318;background:#fff1f0;color:#7a1b13;margin-bottom:12px">';
$body .= '<strong>Uwaga:</strong> dla bazy <code>' . AppContext::e($selectedDb) . '</code> i użytkownika <code>' . AppContext::e($selectedRole) . '</code> włączono dostęp z każdego adresu IPv4. Plugin nie otwiera portu PostgreSQL w CSF dla tego trybu. Port PostgreSQL do otwarcia w CSF: <code>' . AppContext::e((string)$postgresPort) . '</code>.';
$body .= '</div>';
}
$body .= '<div class="panel">';
$body .= '<h3>Status konfiguracji hostów zdalnych</h3>';
$body .= '<p class="muted">Hosty zdalne są włączone przez <code>allow_remote_hosts=1</code>. Zapis hostów uruchamia synchronizację <code>pg_hba</code>, <code>postgresql.conf</code> oraz CSF.</p>';
if ($lastSyncOutput !== '') {
$body .= '<p><strong>Ostatni wynik synchronizacji:</strong></p>';
$body .= '<pre class="muted">' . AppContext::e($lastSyncOutput) . '</pre>';
} else {
$body .= '<p class="muted">Ostatni wynik synchronizacji będzie widoczny po zapisaniu zmian hostów.</p>';
}
$body .= '</div>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('manage_hosts_submit');
$body .= '<div class="row">';
$body .= '<div><label>Baza danych</label><select name="db_name" required>' . $dbOptions . '</select></div>';
$body .= '<div><label>Użytkownik PostgreSQL</label><select name="role_name" required>' . $roleOptions . '</select></div>';
$body .= '</div>';
$body .= '<div class="row">';
$body .= '<div><label>Dodaj IPv4/CIDR</label><input type="text" name="add_host" placeholder="198.51.100.12 lub 198.51.100.0/24"></div>';
$body .= '<div><label>Komentarz do nowego hosta</label><input type="text" name="add_comment" placeholder="np. serwer aplikacyjny PROD"></div>';
$body .= '</div>';
$body .= '<div class="panel" style="margin-top:12px;border-color:#f0b4a7;background:#fff8f6">';
$body .= '<div class="actions" style="justify-content:space-between;align-items:center;margin-top:0">';
$body .= '<label class="inline" style="margin:0"><input type="checkbox" name="allow_all_hosts" value="1"' . ($allHostsEnabled ? ' checked' : '') . '> Dostęp dla wszystkich hostów IPv4 (<code>0.0.0.0/0</code>)</label>';
$body .= '<button class="danger" type="submit" name="restart_postgresql" value="1">Zapisz i restartuj PostgreSQL</button>';
$body .= '</div>';
$body .= '<p class="muted">Ta opcja dotyczy tylko wybranej bazy i użytkownika. Plugin skonfiguruje PostgreSQL, ale nie otworzy portu PostgreSQL w CSF. Port PostgreSQL do otwarcia w CSF: <code>' . AppContext::e((string)$postgresPort) . '</code>.</p>';
$body .= '<label>Komentarz dla trybu wszystkich hostów</label><input type="text" name="allow_all_comment" value="' . AppContext::e($allHostsComment) . '" placeholder="np. VPS klienta - port ' . AppContext::e((string)$postgresPort) . ' otwierany ręcznie w CSF">';
$body .= '</div>';
$body .= '<div class="panel"><h3>Aktualne hosty dla wybranej bazy</h3>';
$body .= '<table><thead><tr><th>Host/CIDR</th><th>Komentarz</th><th>Akcja</th></tr></thead><tbody>' . $hostRows . '</tbody></table>';
$body .= '</div>';
$body .= '<div class="actions"><button class="primary" type="submit">Zapisz hosty</button></div>';
$body .= '</form>';
$ctx->renderPage('Hosty Zdalne Użytkownika', $body, $ok, $errors);
};
+216
View File
@@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$ok = [];
$errors = [];
$daUser = $ctx->daUser->username();
$prefix = $ctx->daUser->prefix();
$dbNames = array_map(static fn(array $db): string => $db['name'], $ctx->pg->listDatabasesForUser($daUser));
$roleNames = array_map(static fn(array $r): string => $r['name'], $ctx->pg->listRolesForUser($daUser));
if (empty($dbNames)) {
$ctx->renderPage('Zarządzaj bazą danych', '<div class="panel"><p class="muted">Brak baz danych do zarządzania.</p></div>', $ok, $errors);
return;
}
$selectedDb = $ctx->postString('db_name');
if ($selectedDb === '') {
$selectedDb = $ctx->queryString('db');
}
if ($selectedDb === '') {
$selectedDb = $dbNames[0];
}
try {
$selectedDb = NamePolicy::normalizeDatabaseName($selectedDb, $prefix);
} catch (Throwable $e) {
$selectedDb = $dbNames[0];
}
if (!in_array($selectedDb, $dbNames, true)) {
$selectedDb = $dbNames[0];
}
$selectedRole = $ctx->postString('role_name');
if ($selectedRole === '') {
$selectedRole = $ctx->queryString('role');
}
if ($ctx->isPost()) {
try {
$ctx->requireCsrf('modify_privileges_submit');
$syncHbaAfterChange = false;
$selectedDb = NamePolicy::normalizeDatabaseName($ctx->postString('db_name', $selectedDb), $prefix);
if (!in_array($selectedDb, $dbNames, true)) {
throw new RuntimeException('Baza nie należy do konta DA: ' . $selectedDb);
}
$action = $ctx->postString('form_action', 'save_privileges');
if ($action === 'revoke_user') {
$role = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if (!in_array($role, $roleNames, true)) {
throw new RuntimeException('Nieprawidłowy użytkownik PostgreSQL: ' . $role);
}
$ctx->pg->revokePrivileges($selectedDb, $role);
$ctx->pg->deleteRoleHosts($daUser, $role, $selectedDb);
$syncHbaAfterChange = true;
$selectedRole = $role;
$ok[] = 'Odebrano dostęp użytkownikowi ' . $role . ' do bazy ' . $selectedDb . '.';
} elseif ($action === 'grant_user') {
$role = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if (!in_array($role, $roleNames, true)) {
throw new RuntimeException('Nieprawidłowy użytkownik PostgreSQL: ' . $role);
}
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('grant_privileges'));
if (empty($privileges)) {
$privileges = NamePolicy::defaultPrivileges();
}
$ctx->pg->grantPrivileges($selectedDb, $role, $privileges);
$syncHbaAfterChange = true;
$selectedRole = $role;
$ok[] = 'Przyznano dostęp użytkownikowi ' . $role . ' do bazy ' . $selectedDb . '.';
} else {
$role = NamePolicy::normalizeRoleName($ctx->postString('role_name'), $prefix);
if (!in_array($role, $roleNames, true)) {
throw new RuntimeException('Nieprawidłowy użytkownik PostgreSQL: ' . $role);
}
$privileges = NamePolicy::sanitizePrivileges($ctx->postArray('privileges'));
if (empty($privileges)) {
throw new RuntimeException('Wybierz przynajmniej jedno uprawnienie.');
}
$ctx->pg->grantPrivileges($selectedDb, $role, $privileges);
$syncHbaAfterChange = true;
$selectedRole = $role;
$ok[] = 'Zaktualizowano uprawnienia użytkownika ' . $role . '.';
}
if ($syncHbaAfterChange) {
$sync = $ctx->pg->syncHbaRules();
if (!$sync['ok']) {
$errors[] = 'Uprawnienia zapisane, ale synchronizacja pg_hba nie powiodła się: ' . $sync['output'];
} else {
$ok[] = 'Synchronizacja pg_hba zakończona.';
}
}
} catch (Throwable $e) {
$errors[] = $e->getMessage();
}
}
$dbInfo = $ctx->pg->getDatabaseInfo($selectedDb);
$dbStats = $ctx->pg->getDatabaseObjectStats($selectedDb);
$dbUsers = $ctx->pg->listDatabaseUsers($daUser, $selectedDb);
if ($selectedRole !== '') {
try {
$selectedRole = NamePolicy::normalizeRoleName($selectedRole, $prefix);
} catch (Throwable $e) {
$selectedRole = '';
}
}
if ($selectedRole === '' && !empty($dbUsers)) {
$selectedRole = $dbUsers[0];
}
$currentProfile = [];
if ($selectedRole !== '') {
$currentProfile = $ctx->pg->getGrantProfile($selectedDb, $selectedRole);
if (empty($currentProfile) && in_array($selectedRole, $dbUsers, true)) {
$currentProfile = ['ALL'];
}
}
$dbOptions = '';
foreach ($dbNames as $dbName) {
$sel = ($dbName === $selectedDb) ? ' selected' : '';
$dbOptions .= '<option value="' . AppContext::e($dbName) . '"' . $sel . '>' . AppContext::e($dbName) . '</option>';
}
$assignableRoles = array_values(array_diff($roleNames, $dbUsers));
$assignOptions = '<option value="">-- wybierz nowego użytkownika --</option>';
foreach ($assignableRoles as $role) {
$assignOptions .= '<option value="' . AppContext::e($role) . '">' . AppContext::e($role) . '</option>';
}
$usersRows = '';
foreach ($dbUsers as $role) {
$profile = $ctx->pg->getGrantProfile($selectedDb, $role);
$label = (empty($profile) || in_array('ALL', $profile, true))
? 'Pełny dostęp'
: implode(', ', $profile);
$usersRows .= '<tr>';
$usersRows .= '<td>' . AppContext::e($role) . '</td>';
$usersRows .= '<td><span class="badge">' . AppContext::e($label) . '</span></td>';
$usersRows .= '<td class="actions">';
$usersRows .= '<a class="btn" href="' . AppContext::e($ctx->url('change_password.html', ['role' => $role])) . '">Zarządzaj</a>';
$usersRows .= '<a class="btn" href="' . AppContext::e($ctx->url('modify_privileges.html', ['db' => $selectedDb, 'role' => $role])) . '#przywileje">Przywileje</a>';
$usersRows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
$usersRows .= $ctx->csrfField('modify_privileges_submit');
$usersRows .= '<input type="hidden" name="db_name" value="' . AppContext::e($selectedDb) . '">';
$usersRows .= '<input type="hidden" name="role_name" value="' . AppContext::e($role) . '">';
$usersRows .= '<input type="hidden" name="form_action" value="revoke_user">';
$usersRows .= '<button class="btn danger" type="submit">Odbierz dostęp</button>';
$usersRows .= '</form>';
$usersRows .= '</td>';
$usersRows .= '</tr>';
}
if ($usersRows === '') {
$usersRows = '<tr><td colspan="3" class="muted">Brak użytkowników z dostępem do bazy.</td></tr>';
}
$privCheckboxes = '';
foreach (NamePolicy::PRIVILEGE_LABELS as $code => $label) {
$checked = in_array($code, $currentProfile, true) ? ' checked' : '';
$privCheckboxes .= '<label class="inline"><input type="checkbox" name="privileges[]" value="' . AppContext::e($code) . '"' . $checked . '> ' . AppContext::e($label) . '</label>';
}
$body = '';
$body .= '<section class="panel">';
$body .= '<h3>Zarządzaj bazą danych</h3>';
$body .= '<form method="get" action="' . AppContext::e($ctx->url('modify_privileges.html')) . '" class="actions" style="justify-content:flex-end">';
$body .= '<label style="margin:0">Baza: <select name="db" style="width:auto;display:inline-block;margin-left:8px">' . $dbOptions . '</select></label>';
$body .= '<button class="btn" type="submit">Przełącz</button>';
$body .= '</form>';
$body .= '<p class="section-text">Szczegółowe informacje o bazie danych.</p>';
$body .= '<table><tbody>';
$body .= '<tr><td><strong>Nazwa bazy danych</strong><br>' . AppContext::e($dbInfo['name']) . '</td><td><strong>Domyślne kodowanie znaków</strong><br>' . AppContext::e($dbInfo['encoding']) . '</td><td><strong>Domyślne porównywanie znaków</strong><br>' . AppContext::e($dbInfo['collation']) . '</td></tr>';
$body .= '<tr><td><strong>Rozmiar</strong><br>' . AppContext::e($dbInfo['size']) . '</td><td><strong>Użytkownicy</strong><br>' . AppContext::e((string)count($dbUsers)) . '</td><td><strong>Liczba Tabel</strong><br>' . AppContext::e((string)$dbStats['tables']) . '</td></tr>';
$body .= '<tr><td><strong>Widoki</strong><br>' . AppContext::e((string)$dbStats['views']) . '</td><td><strong>Wyzwalacze</strong><br>' . AppContext::e((string)$dbStats['triggers']) . '</td><td><strong>Rutyny i procedury</strong><br>' . AppContext::e((string)$dbStats['routines']) . '</td></tr>';
$body .= '</tbody></table>';
$body .= '</section>';
$body .= '<section class="panel" style="margin-top:14px">';
$body .= '<h3>Dostęp użytkownika</h3>';
$body .= '<p class="section-text">Lista użytkowników, którzy mają dostęp do tej bazy danych wraz z podsumowaniem uprawnień.</p>';
$body .= '<table><thead><tr><th>Nazwa użytkownika</th><th>Przywileje</th><th>Akcje</th></tr></thead><tbody>' . $usersRows . '</tbody></table>';
$body .= '<form method="post" style="margin-top:10px">';
$body .= $ctx->csrfField('modify_privileges_submit');
$body .= '<input type="hidden" name="db_name" value="' . AppContext::e($selectedDb) . '">';
$body .= '<input type="hidden" name="form_action" value="grant_user">';
$body .= '<label>Przyznaj dostęp kolejnemu użytkownikowi</label>';
$body .= '<select name="role_name">' . $assignOptions . '</select>';
$body .= '<input type="hidden" name="grant_privileges[]" value="ALL">';
$body .= '<div class="actions"><button class="btn" type="submit">+ Przyznaj pełny dostęp</button></div>';
$body .= '</form>';
$body .= '</section>';
if ($selectedRole !== '') {
$body .= '<section class="panel" id="przywileje" style="margin-top:14px">';
$body .= '<h3>Przywileje użytkownika: ' . AppContext::e($selectedRole) . '</h3>';
$body .= '<form method="post">';
$body .= $ctx->csrfField('modify_privileges_submit');
$body .= '<input type="hidden" name="db_name" value="' . AppContext::e($selectedDb) . '">';
$body .= '<input type="hidden" name="role_name" value="' . AppContext::e($selectedRole) . '">';
$body .= '<input type="hidden" name="form_action" value="save_privileges">';
$body .= '<div class="privileges-group" data-privileges-group>' . $privCheckboxes . '</div>';
$body .= '<div class="actions"><button class="primary" type="submit">Zapisz zmiany</button></div>';
$body .= '</form>';
$body .= '</section>';
}
$ctx->renderPage('Zarządzaj bazą danych', $body, $ok, $errors);
};
+82
View File
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
return static function (AppContext $ctx): void {
$rawMode = defined('DA_POSTGRESQL_RAW_MODE') && DA_POSTGRESQL_RAW_MODE === true;
if (!Http::isPost()) {
$message = 'Nieprawidłowa metoda żądania.';
if ($rawMode) {
echo "HTTP/1.1 405 Method Not Allowed\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo $message;
} else {
http_response_code(405);
header('Content-Type: text/plain; charset=UTF-8');
echo $message;
}
exit;
}
if (!$ctx->settings->enableAdminer()) {
$message = 'Integracja Adminera jest wyłączona na tym serwerze.';
if ($rawMode) {
echo "HTTP/1.1 403 Forbidden\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo $message;
} else {
http_response_code(403);
header('Content-Type: text/plain; charset=UTF-8');
echo $message;
}
exit;
}
try {
$ctx->requireCsrf('open_adminer_submit');
$requestedDb = trim($ctx->postString('adminer_db'));
if ($requestedDb === '') {
$requestedDb = null;
}
$sso = new AdminerSso($ctx->settings, $ctx->daUser, $ctx->pg);
$target = $sso->issueLoginUrl($requestedDb);
$safeTarget = htmlspecialchars($target, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
if ($rawMode) {
echo "HTTP/1.1 200 OK\r\n";
echo "Content-Type: text/html; charset=UTF-8\r\n";
echo "Cache-Control: no-store, no-cache, must-revalidate\r\n";
echo "Pragma: no-cache\r\n\r\n";
echo '<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=' . $safeTarget . '">';
echo '<script>(function(){var u=' . json_encode($target, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';try{if(window.top&&window.top!==window){window.top.location.replace(u);return;}}catch(e){}window.location.replace(u);}());</script>';
echo '</head><body>Przekierowanie do Adminera... <a href="' . $safeTarget . '">Kliknij tutaj</a></body></html>';
} else {
header('Location: ' . $target, true, 302);
}
exit;
} catch (Throwable $e) {
$message = 'Nie można otworzyć Adminera: ' . $e->getMessage();
@error_log(
sprintf(
"[%s] ADMINER_SSO_FAIL user=%s remote=%s message=%s\n",
date('c'),
$ctx->daUser->username(),
Http::server('REMOTE_ADDR'),
$e->getMessage()
),
3,
PLUGIN_ROOT . '/error.log'
);
if ($rawMode) {
echo "HTTP/1.1 400 Bad Request\r\n";
echo "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
echo $message;
} else {
http_response_code(400);
header('Content-Type: text/plain; charset=UTF-8');
echo $message;
}
exit;
}
};
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
final class AdminerRuntimeConfig
{
private const FILE_NAME = 'config.json';
public static function sync(Settings $settings, string $pluginErrorLog = ''): void
{
$runtimeDir = rtrim($settings->adminerSsoDir(), '/');
if ($runtimeDir === '' || !is_dir($runtimeDir)) {
return;
}
if (!is_writable($runtimeDir)) {
self::log($pluginErrorLog, 'Adminer runtime dir is not writable: ' . $runtimeDir);
return;
}
$payload = [
'version' => 1,
'adminer_public' => ($settings->enableAdminer() && $settings->adminerPublic()),
'updated_at' => time(),
];
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
if (!is_string($encoded) || $encoded === '') {
self::log($pluginErrorLog, 'Failed to encode Adminer runtime config JSON.');
return;
}
$targetPath = $runtimeDir . '/' . self::FILE_NAME;
$tempPath = $runtimeDir . '/.' . self::FILE_NAME . '.' . getmypid() . '.tmp';
$written = @file_put_contents($tempPath, $encoded, LOCK_EX);
if ($written === false) {
self::log($pluginErrorLog, 'Failed to write temporary Adminer runtime config: ' . $tempPath);
return;
}
@chmod($tempPath, 0644);
if (!@rename($tempPath, $targetPath)) {
@unlink($tempPath);
self::log($pluginErrorLog, 'Failed to replace Adminer runtime config: ' . $targetPath);
return;
}
@chmod($targetPath, 0644);
}
private static function log(string $pluginErrorLog, string $message): void
{
if ($pluginErrorLog === '') {
return;
}
$line = sprintf("[%s] ADMINER_RUNTIME_CONFIG %s\n", date('c'), $message);
@error_log($line, 3, $pluginErrorLog);
}
}
+326
View File
@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
final class AdminerSso
{
private const TMP_ROLE_PREFIX = 'da_tmp_adminer_';
private Settings $settings;
private DirectAdminUser $daUser;
private PostgresService $pg;
private string $runtimeDir;
private string $ticketsDir;
public function __construct(Settings $settings, DirectAdminUser $daUser, PostgresService $pg)
{
$this->settings = $settings;
$this->daUser = $daUser;
$this->pg = $pg;
$this->runtimeDir = rtrim($settings->adminerSsoDir(), '/');
$this->ticketsDir = $this->runtimeDir . '/tickets';
}
public function issueLoginUrl(?string $requestedDatabase = null): string
{
if (!$this->settings->enableAdminer()) {
throw new RuntimeException('Integracja Adminera jest wyłączona.');
}
$this->assertRuntimeReady();
$now = time();
$this->cleanupTicketFiles($now);
$this->pg->cleanupExpiredAdminerRoles(self::TMP_ROLE_PREFIX);
$databaseRows = $this->pg->listDatabasesForUser($this->daUser->username(), false);
if (empty($databaseRows)) {
throw new RuntimeException('Użytkownik nie ma żadnej bazy PostgreSQL do otwarcia w Adminerze.');
}
$databaseNames = [];
foreach ($databaseRows as $row) {
$name = (string)($row['name'] ?? '');
if ($name !== '') {
$databaseNames[] = $name;
}
}
$databaseNames = array_values(array_unique($databaseNames));
if (empty($databaseNames)) {
throw new RuntimeException('Nie znaleziono poprawnych nazw baz danych dla użytkownika.');
}
$ticketTtl = $this->settings->adminerTicketTtl();
$sessionTtl = $this->settings->adminerSessionTtl();
$roleTtl = max($this->settings->adminerRoleTtl(), $sessionTtl + 120);
$ticketExpiresAt = $now + $ticketTtl;
$sessionExpiresAt = $now + $sessionTtl;
$roleExpiresAt = $now + $roleTtl;
$role = $this->generateTemporaryRoleName();
$password = $this->generateSecret(36);
$endpoint = $this->pg->connectionEndpoint();
$targetDatabase = '';
if ($requestedDatabase !== null) {
$requestedDatabase = trim($requestedDatabase);
if ($requestedDatabase !== '') {
if (!in_array($requestedDatabase, $databaseNames, true)) {
throw new RuntimeException('Wskazana baza danych nie należy do zalogowanego użytkownika.');
}
$targetDatabase = $requestedDatabase;
}
}
$createdRole = false;
try {
$this->pg->createTemporaryRole($role, $password, $roleExpiresAt);
$createdRole = true;
foreach ($databaseNames as $dbName) {
$this->pg->grantTemporaryAdminerAccess($dbName, $role);
}
$ticketId = $this->generateTicketId();
$payload = [
'v' => 1,
'da_user' => $this->daUser->username(),
'host' => (string)($endpoint['host'] ?? 'localhost'),
'port' => (int)($endpoint['port'] ?? 5432),
'role' => $role,
'password' => $password,
'database' => $targetDatabase,
'issued_at' => $now,
'expires_at' => $ticketExpiresAt,
'session_expires_at' => $sessionExpiresAt,
'role_expires_at' => $roleExpiresAt,
'user_binding' => $this->buildUserBinding(),
];
$this->writeTicket($ticketId, $payload);
$targetBase = $this->buildAdminerBaseUrl();
$separator = str_contains($targetBase, '?') ? '&' : '?';
return $targetBase . $separator . 'ticket=' . rawurlencode($ticketId);
} catch (Throwable $e) {
if ($createdRole) {
try {
$this->pg->dropTemporaryRole($role);
} catch (Throwable $cleanupError) {
// Ignorujemy błąd cleanupu roli, aby zwrócić pierwotny wyjątek.
}
}
throw new RuntimeException('Nie udało się przygotować sesji Adminera: ' . $e->getMessage(), 0, $e);
}
}
private function assertRuntimeReady(): void
{
if (!is_dir($this->runtimeDir)) {
throw new RuntimeException('Brak katalogu runtime Adminera: ' . $this->runtimeDir . '. Uruchom scripts/setup/adminer_install.sh');
}
if (!is_dir($this->ticketsDir)) {
throw new RuntimeException('Brak katalogu ticketów Adminera: ' . $this->ticketsDir . '. Uruchom scripts/setup/adminer_install.sh');
}
if (!is_readable($this->ticketsDir) || !is_writable($this->ticketsDir)) {
throw new RuntimeException('Brak uprawnień odczytu/zapisu do katalogu ticketów: ' . $this->ticketsDir);
}
}
private function cleanupTicketFiles(int $now): void
{
$files = glob($this->ticketsDir . '/*.json');
if ($files === false) {
return;
}
$hardTtl = max($this->settings->adminerRoleTtl(), 3600);
foreach ($files as $path) {
if (!is_file($path)) {
continue;
}
$remove = false;
$raw = @file_get_contents($path);
if (is_string($raw) && $raw !== '') {
$data = json_decode($raw, true);
if (is_array($data) && isset($data['expires_at']) && is_numeric($data['expires_at'])) {
$expiresAt = (int)$data['expires_at'];
if ($expiresAt < ($now - 60)) {
$remove = true;
}
}
}
if (!$remove) {
$mtime = @filemtime($path);
if ($mtime !== false && $mtime < ($now - $hardTtl)) {
$remove = true;
}
}
if ($remove) {
@unlink($path);
}
}
}
/**
* @param array<string,mixed> $payload
*/
private function writeTicket(string $ticketId, array $payload): void
{
$path = $this->ticketsDir . '/' . $ticketId . '.json';
$encoded = json_encode($payload, JSON_UNESCAPED_SLASHES);
if ($encoded === false) {
throw new RuntimeException('Nie udało się zakodować ticketu Adminera.');
}
$handle = @fopen($path, 'xb');
if (!is_resource($handle)) {
throw new RuntimeException('Nie udało się utworzyć ticketu Adminera: ' . $path);
}
$written = @fwrite($handle, $encoded);
@fclose($handle);
if ($written === false || $written < strlen($encoded)) {
@unlink($path);
throw new RuntimeException('Nie udało się zapisać ticketu Adminera.');
}
$this->alignTicketGroup($path);
@chmod($path, 0644);
}
private function alignTicketGroup(string $path): void
{
$dirGroupId = @filegroup($this->ticketsDir);
if (!is_int($dirGroupId) || $dirGroupId < 0) {
return;
}
$targetGroup = null;
if (function_exists('posix_getgrgid')) {
$groupInfo = @posix_getgrgid($dirGroupId);
if (is_array($groupInfo) && isset($groupInfo['name']) && is_string($groupInfo['name'])) {
$targetGroup = $groupInfo['name'];
}
}
if ($targetGroup === null || $targetGroup === '') {
$targetGroup = (string)$dirGroupId;
}
@chgrp($path, $targetGroup);
}
private function generateTemporaryRoleName(): string
{
$base = strtolower($this->daUser->username());
$base = preg_replace('/[^a-z0-9_]+/', '_', $base) ?? 'user';
$base = trim($base, '_');
if ($base === '') {
$base = 'user';
}
for ($i = 0; $i < 5; $i++) {
$suffix = bin2hex(random_bytes(6));
$maxBaseLen = NamePolicy::MAX_IDENTIFIER_LEN - strlen(self::TMP_ROLE_PREFIX) - 1 - strlen($suffix);
if ($maxBaseLen < 1) {
$maxBaseLen = 1;
}
$safeBase = substr($base, 0, $maxBaseLen);
$role = self::TMP_ROLE_PREFIX . $safeBase . '_' . $suffix;
if (!$this->pg->roleExists($role)) {
return $role;
}
}
throw new RuntimeException('Nie udało się wygenerować unikalnej tymczasowej roli dla Adminera.');
}
private function generateTicketId(): string
{
return rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
}
private function generateSecret(int $len): string
{
$value = rtrim(strtr(base64_encode(random_bytes(48)), '+/', 'AZ'), '=');
if (strlen($value) >= $len) {
return substr($value, 0, $len);
}
return str_pad($value, $len, 'x');
}
private function buildUserBinding(): string
{
$ua = Http::server('HTTP_USER_AGENT');
if ($ua === '') {
$raw = getenv('HTTP_USER_AGENT');
if ($raw !== false) {
$ua = trim((string)$raw);
}
}
return hash('sha256', $ua);
}
private function buildAdminerBaseUrl(): string
{
$path = $this->settings->adminerUrlPath();
$customBase = $this->settings->adminerPublicBaseUrl();
if ($customBase !== '') {
return $customBase . $path;
}
$host = trim(Http::server('HTTP_HOST'));
if ($host === '') {
$host = trim(Http::server('SERVER_NAME'));
}
if ($host === '') {
return $path;
}
$host = preg_replace('/:\d+$/', '', $host) ?? $host;
$scheme = $this->detectRequestScheme();
if ($scheme !== '') {
return $scheme . '://' . $host . $path;
}
// Fallback bezpieczny: odziedzicz protokół aktualnej strony (unika mixed content).
return '//' . $host . $path;
}
private function detectRequestScheme(): string
{
$forwardedProto = strtolower(trim(Http::server('HTTP_X_FORWARDED_PROTO')));
if ($forwardedProto === 'https' || $forwardedProto === 'http') {
return $forwardedProto;
}
$requestScheme = strtolower(trim(Http::server('REQUEST_SCHEME')));
if ($requestScheme === 'https' || $requestScheme === 'http') {
return $requestScheme;
}
$frontEndHttps = strtolower(trim(Http::server('HTTP_FRONT_END_HTTPS')));
if ($frontEndHttps === 'on' || $frontEndHttps === '1' || $frontEndHttps === 'https') {
return 'https';
}
$https = strtolower(trim(Http::server('HTTPS')));
if ($https === 'on' || $https === '1' || $https === 'https' || $https === 'true') {
return 'https';
}
$serverPort = trim(Http::server('SERVER_PORT'));
if ($serverPort === '443') {
return 'https';
}
if ($serverPort === '80') {
return 'http';
}
return '';
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+70
View File
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
final class CsrfGuard
{
private string $secret;
private string $username;
private string $sessionId;
public function __construct(string $secret, string $username, string $sessionId)
{
$this->secret = $secret;
$this->username = $username;
$this->sessionId = $sessionId !== '' ? $sessionId : 'no_session';
}
/**
* @return array{ts:int,token:string}
*/
public function issue(string $intent): array
{
$ts = time();
$sid = $this->sessionId;
return [
'ts' => $ts,
'sid' => $sid,
'token' => $this->buildToken($intent, $ts, $sid),
];
}
public function validate(string $intent, string $timestamp, string $token, int $ttl = 3600, string $sessionHint = ''): bool
{
if (!preg_match('/^[0-9]{1,20}$/', $timestamp)) {
return false;
}
$ts = (int)$timestamp;
$now = time();
if ($ts <= 0 || $ts > ($now + 30) || ($now - $ts) > $ttl) {
return false;
}
$token = trim($token);
$sids = [];
$hint = trim($sessionHint);
if ($hint !== '') {
$sids[] = $hint;
}
$sids[] = $this->sessionId;
$sids[] = 'no_session';
$sids[] = '';
$sids = array_values(array_unique($sids));
foreach ($sids as $sid) {
$expected = $this->buildToken($intent, $ts, $sid);
if (hash_equals($expected, $token)) {
return true;
}
}
return false;
}
private function buildToken(string $intent, int $timestamp, string $sid): string
{
$payload = implode('|', [$this->username, $sid !== '' ? $sid : 'no_session', $intent, (string)$timestamp]);
return hash_hmac('sha256', $payload, $this->secret);
}
}
+296
View File
@@ -0,0 +1,296 @@
<?php
declare(strict_types=1);
final class DirectAdminUser
{
private string $username;
private string $prefix;
/** @var array<string,string> */
private array $userConf;
/** @var array<string,string> */
private array $packageConf;
private Settings $settings;
private function __construct(string $username, array $userConf, array $packageConf, Settings $settings)
{
$this->username = $username;
$this->prefix = $username . '_';
$this->userConf = $userConf;
$this->packageConf = $packageConf;
$this->settings = $settings;
}
public static function fromEnvironment(Settings $settings): self
{
$username = Http::server('USER');
if ($username === '') {
$username = Http::server('USERNAME');
}
if ($username === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $username)) {
throw new RuntimeException('Nie można ustalić użytkownika DirectAdmin.');
}
$userConfPath = '/usr/local/directadmin/data/users/' . $username . '/user.conf';
$userConf = self::loadSimpleConf($userConfPath);
$packageConf = self::loadPackageConf($username, $userConf);
return new self($username, $userConf, $packageConf, $settings);
}
public function username(): string
{
return $this->username;
}
public function prefix(): string
{
return $this->prefix;
}
public function language(): string
{
$lang = strtolower(trim((string)($this->userConf['language'] ?? $this->userConf['lang'] ?? 'en')));
if ($lang === '' || strpos($lang, 'en') === 0) {
return 'en';
}
if (strpos($lang, 'pl') === 0) {
return 'pl';
}
return 'en';
}
public function skin(): string
{
$skin = strtolower(trim((string)($this->userConf['skin'] ?? 'enhanced')));
if (strpos($skin, 'evolution') !== false) {
return 'evolution';
}
if (strpos($skin, 'enhanced') !== false) {
return 'enhanced';
}
return 'enhanced';
}
public function hasPluginAccess(): bool
{
return $this->isPluginEnabledByCustomItem() && $this->isDatabaseQuotaEnabled() && $this->isPluginAllowedByPluginRules();
}
public function pluginAccessErrorMessage(): string
{
if (!$this->isPluginEnabledByCustomItem()) {
return 'Plugin PostgreSQL jest wyłączony dla tego konta lub pakietu.';
}
if (!$this->isDatabaseQuotaEnabled()) {
return 'Plugin PostgreSQL jest wyłączony: limit baz danych PostgreSQL ustawiono na 0 i nie zaznaczono opcji Bez Ograniczeń.';
}
if (!$this->isPluginAllowedByPluginRules()) {
return 'Plugin PostgreSQL jest zablokowany przez reguły plugins_allow/plugins_deny.';
}
return 'Brak dostępu do pluginu PostgreSQL.';
}
public function maxDatabases(): int
{
if ($this->isUnlimitedDatabasesByCustomItem()) {
return 0;
}
$rawValue = $this->rawDatabasesLimitValue();
if ($rawValue === null || trim($rawValue) === '') {
$default = $this->settings->defaultDatabasesLimit();
return $default < 0 ? 0 : $default;
}
$raw = trim($rawValue);
$normalized = strtolower(trim($raw));
if ($normalized === 'unlimited') {
return 0;
}
if (!preg_match('/^[0-9]+$/', trim($raw))) {
$default = $this->settings->defaultDatabasesLimit();
return $default < 0 ? 0 : $default;
}
$limit = (int)trim($raw);
return $limit < 0 ? 0 : $limit;
}
private function isPluginEnabledByCustomItem(): bool
{
$raw = $this->readScopedValue('postgresql_enabled');
if ($raw === null) {
return false;
}
return Settings::toBool($raw, false);
}
private function isUnlimitedDatabasesByCustomItem(): bool
{
$unlimitedFlags = [
$this->readScopedValue('upostgresql'),
$this->readScopedValue('upostgresql_max_databases'),
$this->readScopedValue('postgresql_unlimited'), // backward compatibility
];
foreach ($unlimitedFlags as $raw) {
if ($raw !== null && Settings::toBool($raw, false)) {
return true;
}
}
$rawLimit = $this->rawDatabasesLimitValue();
if ($rawLimit !== null && strtolower(trim($rawLimit)) === 'unlimited') {
return true;
}
return false;
}
private function isDatabaseQuotaEnabled(): bool
{
if ($this->isUnlimitedDatabasesByCustomItem()) {
return true;
}
return $this->maxDatabases() > 0;
}
private function rawDatabasesLimitValue(): ?string
{
$raw = $this->readScopedValue('postgresql_max_databases');
if ($raw === null || trim($raw) === '') {
$raw = $this->readScopedValue('postgresql');
}
return $raw;
}
private function isPluginAllowedByPluginRules(): bool
{
$pluginId = 'da-postgresql';
foreach ([$this->packageConf, $this->userConf] as $conf) {
if (!empty($conf['plugins_allow'])) {
$allow = self::parsePluginsList($conf['plugins_allow']);
return in_array($pluginId, $allow, true);
}
}
foreach ([$this->packageConf, $this->userConf] as $conf) {
if (!empty($conf['plugins_deny'])) {
$deny = self::parsePluginsList($conf['plugins_deny']);
return !in_array($pluginId, $deny, true);
}
}
return true;
}
private function readScopedValue(string $key): ?string
{
if (array_key_exists($key, $this->userConf)) {
return $this->userConf[$key];
}
if (array_key_exists($key, $this->packageConf)) {
return $this->packageConf[$key];
}
return null;
}
/**
* @return array<string,string>
*/
private static function loadSimpleConf(string $path): array
{
$data = [];
if (!is_readable($path)) {
return $data;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $data;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!preg_match('/^([A-Za-z0-9_]+)=(.*)$/', $line, $m)) {
continue;
}
$data[strtolower($m[1])] = trim((string)$m[2]);
}
return $data;
}
/**
* @param array<string,string> $userConf
* @return array<string,string>
*/
private static function loadPackageConf(string $username, array $userConf): array
{
$package = trim((string)($userConf['package'] ?? $userConf['user_package'] ?? ''));
if ($package === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $package)) {
return [];
}
$candidates = [];
$owners = [];
foreach (['creator', 'owner', 'reseller', 'username'] as $k) {
if (!empty($userConf[$k]) && preg_match('/^[A-Za-z0-9._-]+$/', $userConf[$k])) {
$owners[] = $userConf[$k];
}
}
$owners[] = $username;
$owners[] = 'admin';
$owners = array_values(array_unique($owners));
foreach ($owners as $owner) {
$candidates[] = '/usr/local/directadmin/data/users/' . $owner . '/packages/' . $package;
$candidates[] = '/usr/local/directadmin/data/users/' . $owner . '/packages/' . $package . '.conf';
}
foreach ($candidates as $path) {
$conf = self::loadSimpleConf($path);
if (!empty($conf)) {
return $conf;
}
}
return [];
}
/**
* @return string[]
*/
private static function parsePluginsList(string $value): array
{
$value = trim($value);
if ($value === '') {
return [];
}
$items = [];
foreach (explode(':', strtolower($value)) as $item) {
$item = trim($item);
if ($item !== '') {
$items[] = $item;
}
}
return array_values(array_unique($items));
}
}
+536
View File
@@ -0,0 +1,536 @@
<?php
declare(strict_types=1);
final class FilePicker
{
public static function styles(): string
{
return <<<'CSS'
.br-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.br-modal {
width: min(560px, 92vw);
background: #ffffff;
border-radius: 12px;
padding: 16px;
border: 2px solid #d8dde6;
box-shadow: 0 20px 40px rgba(15, 23, 42, 0.25);
}
.br-modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
.br-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.br-modal-title {
font-weight: 700;
font-size: 16px;
}
.br-modal-body {
max-height: 60vh;
overflow: auto;
}
.br-muted { color: #64748b; }
.br-alert {
border-radius: 8px;
padding: 10px 12px;
margin: 8px 0;
border: 1px solid transparent;
}
.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; }
.br-picker-path {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.br-picker-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #d8dde6;
border-radius: 10px;
max-height: 45vh;
overflow: auto;
}
.br-picker-create {
margin-top: 10px;
display: flex;
gap: 8px;
}
.br-picker-create input {
flex: 1;
padding: 6px 8px;
border: 1px solid #d8dde6;
border-radius: 8px;
}
.br-picker-list li {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #d8dde6;
}
.br-picker-list li:last-child {
border-bottom: none;
}
.br-picker-list li:hover {
background: #f8fafc;
}
.br-picker-list li.active {
background: #e0f2fe;
}
.br-btn {
background: #b77200;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 14px;
cursor: pointer;
font-weight: 600;
}
.br-btn:hover { background: #9a6200; }
.br-btn-ghost {
background: transparent;
color: #1f2d3d;
border: 1px solid #d8dde6;
}
.br-btn-ghost:hover { background: #eef2f7; }
.br-btn-small {
padding: 6px 10px;
font-size: 12px;
}
.br-icon-btn {
border: 1px solid #d8dde6;
background: #fff;
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
font-size: 16px;
line-height: 1;
}
.br-icon-btn:hover {
background: #f8fafc;
}
CSS;
}
public static function scripts(): string
{
return <<<'JS'
var pickerTarget = null;
var pickerPath = '';
var pickerBase = '';
var pickerMode = 'dir';
var pickerSelected = '';
var pickerUrl = '';
var pickerRoot = '';
function openPicker(inputId, mode) {
var modal = document.getElementById('br-picker');
if (!modal) { return; }
var target = document.getElementById(inputId);
if (!target) { return; }
pickerTarget = target;
pickerMode = mode === 'file' ? 'file' : 'dir';
pickerSelected = '';
pickerUrl = modal.getAttribute('data-picker-url') || window.location.href;
pickerRoot = modal.getAttribute('data-picker-root') || '';
var initial = (target.value || '').trim();
if (initial === '') {
initial = pickerRoot || '';
}
var createRow = document.getElementById('br-picker-create');
if (createRow) {
createRow.style.display = (pickerMode === 'dir') ? 'flex' : 'none';
}
loadPicker(initial);
modal.style.display = 'flex';
}
function closePicker() {
var modal = document.getElementById('br-picker');
if (modal) { modal.style.display = 'none'; }
}
function pickerSetError(msg) {
var el = document.getElementById('br-picker-error');
if (!el) { return; }
if (msg) {
el.textContent = msg;
el.style.display = 'block';
} else {
el.textContent = '';
el.style.display = 'none';
}
}
function renderPickerList(items) {
var list = document.getElementById('br-picker-list');
if (!list) { return; }
list.innerHTML = '';
if (!items || !items.length) {
var empty = document.createElement('li');
empty.textContent = pickerMode === 'file' ? tr('Brak plików .sql/.gz') : tr('Brak katalogów');
empty.className = 'br-muted';
list.appendChild(empty);
return;
}
items.forEach(function (entry) {
var li = document.createElement('li');
if (entry.type === 'dir') {
li.textContent = '📁 ' + entry.name + '/';
li.addEventListener('click', function () {
pickerSelected = '';
loadPicker(pickerPath.replace(/\/+$/, '') + '/' + entry.name);
});
} else {
var full = pickerPath.replace(/\/+$/, '') + '/' + entry.name;
li.textContent = '📄 ' + entry.name;
li.addEventListener('click', function () {
pickerSelected = full;
var active = list.querySelectorAll('li.active');
for (var i = 0; i < active.length; i += 1) {
active[i].classList.remove('active');
}
li.classList.add('active');
});
if (pickerSelected === full) {
li.classList.add('active');
}
}
list.appendChild(li);
});
}
function extractPickerJson(text) {
var marker = '__DA_POSTGRESQL_PICKER_JSON__';
var start = text.indexOf(marker);
if (start === -1) { return null; }
start += marker.length;
var end = text.indexOf(marker, start);
if (end === -1) { return null; }
var jsonText = text.substring(start, end).trim();
try { return JSON.parse(jsonText); } catch (e) { return null; }
}
function loadPicker(path) {
pickerSetError('');
var url = pickerUrl + '?action=dir_list&mode=' + encodeURIComponent(pickerMode) + '&path=' + encodeURIComponent(path || '');
fetch(url, { credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var data = extractPickerJson(text || '');
if (!data || !data.ok) {
pickerSetError(tr('Nie udało się wczytać katalogów.'));
return;
}
pickerPath = data.path || '';
pickerBase = data.base || '';
if (data.selected) {
pickerSelected = data.selected;
}
var current = document.getElementById('br-picker-current');
if (current) { current.textContent = pickerPath; }
renderPickerList(data.items || []);
})
.catch(function () {
pickerSetError(tr('Nie udało się wczytać katalogów.'));
});
}
function pickerGoUp() {
if (!pickerPath) { return; }
if (pickerBase && pickerPath === pickerBase) { return; }
var up = pickerPath.replace(/\/+$/, '');
up = up.substring(0, up.lastIndexOf('/')) || '/';
loadPicker(up);
}
function createPickerDir() {
var input = document.getElementById('br-picker-new');
if (!input) { return; }
var name = (input.value || '').trim();
if (name === '') {
pickerSetError(tr('Podaj nazwę katalogu.'));
return;
}
pickerSetError('');
var url = pickerUrl + '?action=dir_create&path=' + encodeURIComponent(pickerPath || '') + '&name=' + encodeURIComponent(name);
fetch(url, { credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var data = extractPickerJson(text || '');
if (!data || !data.ok) {
pickerSetError(tr('Nie udało się utworzyć katalogu.'));
return;
}
input.value = '';
loadPicker(pickerPath);
})
.catch(function () {
pickerSetError(tr('Nie udało się utworzyć katalogu.'));
});
}
function selectPickerPath() {
if (!pickerTarget) { closePicker(); return; }
if (pickerMode === 'file' && pickerSelected) {
pickerTarget.value = pickerSelected || '';
} else if (pickerMode === 'dir') {
pickerTarget.value = pickerPath || '';
}
try {
pickerTarget.dispatchEvent(new Event('input', { bubbles: true }));
pickerTarget.dispatchEvent(new Event('change', { bubbles: true }));
} catch (e) {
// ignore dispatch errors on older browsers
}
if (typeof window.__restoreFileSync === 'function') {
window.__restoreFileSync();
}
closePicker();
}
function bindFilePicker() {
window.openPicker = openPicker;
window.closePicker = closePicker;
window.pickerGoUp = pickerGoUp;
window.createPickerDir = createPickerDir;
window.selectPickerPath = selectPickerPath;
}
JS;
}
public static function renderOverlay(AppContext $ctx, string $overlayId, string $rootPath, string $listUrl, string $inputId = 'source-file-path'): string
{
$safeRoot = AppContext::e($rootPath);
$safeUrl = AppContext::e($listUrl);
$title = $ctx->t('Wybierz plik');
$close = $ctx->t('Zamknij');
$up = $ctx->t('W górę');
$newDir = $ctx->t('Nowy katalog');
$create = $ctx->t('Utwórz');
$cancel = $ctx->t('Anuluj');
$choose = $ctx->t('Wybierz');
$html = '';
$html .= '<div id="br-picker" class="br-overlay" style="display:none;" data-picker-url="' . $safeUrl . '" data-picker-root="' . $safeRoot . '">';
$html .= '<div class="br-modal">';
$html .= '<div class="br-modal-header"><div class="br-modal-title">' . AppContext::e($title) . '</div><button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="closePicker();">' . AppContext::e($close) . '</button></div>';
$html .= '<div class="br-modal-body">';
$html .= '<div class="br-picker-path"><button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="pickerGoUp();">⟵ ' . AppContext::e($up) . '</button><span id="br-picker-current" class="br-muted"></span></div>';
$html .= '<div id="br-picker-error" class="br-alert br-alert-error" style="display:none;"></div>';
$html .= '<ul id="br-picker-list" class="br-picker-list"></ul>';
$html .= '<div id="br-picker-create" class="br-picker-create" style="display:none;">';
$html .= '<input id="br-picker-new" type="text" placeholder="' . AppContext::e($newDir) . '" />';
$html .= '<button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="createPickerDir();">' . AppContext::e($create) . '</button>';
$html .= '</div>';
$html .= '</div>';
$html .= '<div class="br-modal-actions">';
$html .= '<button class="br-btn br-btn-ghost" type="button" onclick="closePicker();">' . AppContext::e($cancel) . '</button>';
$html .= '<button class="br-btn" type="button" onclick="selectPickerPath();">' . AppContext::e($choose) . '</button>';
$html .= '</div>';
$html .= '</div></div>';
return $html;
}
public static function handleDirList(AppContext $ctx, BackupQueueService $queue, string $rootPath): bool
{
$action = strtolower($ctx->queryString('action'));
if ($action !== 'dir_list') {
return false;
}
while (ob_get_level() > 0) {
@ob_end_clean();
}
$base = self::normalizeBase($rootPath);
$mode = strtolower($ctx->queryString('mode', 'dir')) === 'file' ? 'file' : 'dir';
$requested = trim($ctx->queryString('path', $base));
if ($requested === '') {
$requested = $base;
}
if ($requested !== '' && $requested[0] !== '/') {
$requested = '/' . $requested;
}
$selected = '';
if ($mode === 'file' && $requested !== '') {
$realReq = @realpath($requested);
if ($realReq !== false && is_file($realReq)) {
$selected = $realReq;
$requested = dirname($realReq);
}
}
[$resolved, $base, $ok] = self::resolvePickerPath($requested, $base);
if (!$ok) {
self::pickerOutput(['ok' => false, 'error' => 'Nieprawidłowa ścieżka.']);
exit;
}
if ($base !== '/' && $selected !== '') {
if (strpos($selected, $base . '/') !== 0 && $selected !== $base) {
$selected = '';
}
}
$dirItems = [];
$fileItems = [];
$entries = @scandir($resolved);
if (is_array($entries)) {
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$full = $resolved . '/' . $entry;
if (is_dir($full)) {
$dirItems[] = $entry;
} elseif ($mode === 'file' && is_file($full) && $queue->isAllowedSqlFile($full)) {
$fileItems[] = $entry;
}
}
}
natcasesort($dirItems);
natcasesort($fileItems);
$items = [];
foreach ($dirItems as $name) {
$items[] = ['name' => $name, 'type' => 'dir'];
}
foreach ($fileItems as $name) {
$items[] = ['name' => $name, 'type' => 'file'];
}
self::pickerOutput([
'ok' => true,
'path' => $resolved,
'base' => $base,
'items' => $items,
'selected' => $selected,
]);
exit;
}
public static function handleDirCreate(AppContext $ctx, string $rootPath, string $daUser): bool
{
$action = strtolower($ctx->queryString('action'));
if ($action !== 'dir_create') {
return false;
}
while (ob_get_level() > 0) {
@ob_end_clean();
}
$base = self::normalizeBase($rootPath);
$parentReq = trim($ctx->queryString('path', $base));
if ($parentReq !== '' && $parentReq[0] !== '/') {
$parentReq = '/' . $parentReq;
}
[$parent, $base, $ok] = self::resolvePickerPath($parentReq, $base);
$name = trim($ctx->queryString('name'));
if ($name === '' || $name === '.' || $name === '..' || strpos($name, '/') !== false || strpos($name, '\\') !== false) {
self::pickerOutput(['ok' => false, 'error' => 'invalid_name']);
exit;
}
if (!$ok || !is_dir($parent)) {
self::pickerOutput(['ok' => false, 'error' => 'invalid_parent']);
exit;
}
$newPath = rtrim($parent, '/') . '/' . $name;
if ($base !== '/' && strpos($newPath, $base . '/') !== 0 && $newPath !== $base) {
self::pickerOutput(['ok' => false, 'error' => 'outside_base']);
exit;
}
if (file_exists($newPath)) {
self::pickerOutput(['ok' => false, 'error' => 'exists']);
exit;
}
if (!@mkdir($newPath, 0700, true)) {
self::pickerOutput(['ok' => false, 'error' => 'mkdir_failed']);
exit;
}
@chown($newPath, $daUser);
@chgrp($newPath, $daUser);
self::pickerOutput(['ok' => true, 'path' => $parent, 'base' => $base, 'created' => $newPath]);
exit;
}
/**
* @param array<string,mixed> $payload
*/
private static function pickerOutput(array $payload): void
{
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
if ($json === false) {
$json = json_encode(['ok' => false, 'error' => 'json_encode_failed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
$marker = '__DA_POSTGRESQL_PICKER_JSON__';
header('Content-Type: text/plain; charset=UTF-8');
echo $marker . "\n" . $json . "\n" . $marker;
}
private static function normalizeBase(string $base): string
{
$base = trim($base);
if ($base === '') {
return '/';
}
if ($base[0] !== '/') {
$base = '/' . $base;
}
if ($base !== '/' && substr($base, -1) === '/') {
$base = rtrim($base, '/');
}
return $base === '' ? '/' : $base;
}
/**
* @return array{0:string,1:string,2:bool}
*/
private static function resolvePickerPath(string $requested, string $base): array
{
$base = self::normalizeBase($base);
$candidate = trim($requested);
if ($candidate === '') {
$candidate = $base;
}
if ($candidate[0] !== '/') {
$candidate = '/' . $candidate;
}
$resolved = @realpath($candidate);
if ($resolved === false || !is_dir($resolved)) {
$resolved = @realpath($base);
}
if ($resolved === false || !is_dir($resolved)) {
return [$base, $base, false];
}
if ($base !== '/' && strpos($resolved, $base . '/') !== 0 && $resolved !== $base) {
$resolved = @realpath($base);
}
if ($resolved === false || !is_dir($resolved)) {
return [$base, $base, false];
}
return [$resolved, $base, true];
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
final class Http
{
public static function bootstrapGlobals(): void
{
if (PHP_SAPI !== 'cli') {
return;
}
$query = getenv('QUERY_STRING') ?: '';
$method = strtoupper((string)(getenv('REQUEST_METHOD') ?: 'GET'));
$_GET = [];
if ($query !== '') {
parse_str($query, $_GET);
}
$queryAction = strtolower(trim((string)($_GET['action'] ?? '')));
$_POST = [];
if ($method === 'POST') {
$rawPost = (string)(getenv('POST') ?: '');
if ($rawPost === '') {
$rawPost = (string)(getenv('HTTP_POST') ?: '');
}
$contentType = strtolower((string)(getenv('CONTENT_TYPE') ?: ''));
$isUrlEncoded = ($contentType === '' || str_contains($contentType, 'application/x-www-form-urlencoded'));
if ($rawPost === '' && $isUrlEncoded) {
if ($queryAction !== 'upload_blob') {
$stdin = @file_get_contents('php://stdin');
if (is_string($stdin) && $stdin !== '') {
$rawPost = $stdin;
}
}
}
if ($rawPost !== '') {
parse_str($rawPost, $_POST);
}
}
$_REQUEST = array_merge($_GET, $_POST);
$_SERVER['REQUEST_METHOD'] = $method;
$_SERVER['QUERY_STRING'] = $query;
$knownServerVars = [
'USER',
'USERNAME',
'HOME',
'LANGUAGE',
'SESSION_ID',
'REQUEST_URI',
'HTTP_HOST',
'HTTPS',
'HTTP_USER_AGENT',
'HTTP_X_FORWARDED_PROTO',
'HTTP_FRONT_END_HTTPS',
'REQUEST_SCHEME',
'SERVER_NAME',
'SERVER_PORT',
'REMOTE_ADDR',
];
foreach ($knownServerVars as $key) {
if (!isset($_SERVER[$key])) {
$value = getenv($key);
if ($value !== false) {
$_SERVER[$key] = $value;
}
}
}
}
public static function method(): string
{
return strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
}
public static function isPost(): bool
{
return self::method() === 'POST';
}
public static function getString(array $source, string $key, string $default = ''): string
{
if (!isset($source[$key])) {
return $default;
}
$value = $source[$key];
if (is_array($value)) {
return $default;
}
return trim((string)$value);
}
public static function getArray(array $source, string $key): array
{
if (!isset($source[$key])) {
return [];
}
$value = $source[$key];
if (!is_array($value)) {
return [];
}
return $value;
}
public static function server(string $key, string $default = ''): string
{
$value = $_SERVER[$key] ?? null;
if ($value === null) {
return $default;
}
return trim((string)$value);
}
public static function redirect(string $url): void
{
header('Location: ' . $url, true, 302);
exit;
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
final class Lang
{
/** @var array<string,string> */
private array $map;
/**
* @param array<string,string> $map
*/
private function __construct(array $map)
{
$this->map = $map;
}
public static function load(string $code): self
{
$code = strtolower(trim($code));
if ($code !== 'pl' && $code !== 'en') {
$code = 'en';
}
$path = PLUGIN_ROOT . '/lang/' . $code . '.php';
if (!is_file($path)) {
$path = PLUGIN_ROOT . '/lang/en.php';
}
$map = [];
if (is_file($path)) {
$data = require $path;
if (is_array($data)) {
$map = $data;
}
}
return new self($map);
}
/**
* @param array<string,string|int|float> $vars
*/
public function t(string $key, array $vars = []): string
{
$text = $this->map[$key] ?? $key;
foreach ($vars as $var => $value) {
$text = str_replace('{' . $var . '}', (string)$value, $text);
}
return $text;
}
public function translateHtml(string $html): string
{
if ($html === '' || empty($this->map)) {
return $html;
}
$keys = array_keys($this->map);
usort($keys, static function (string $a, string $b): int {
return strlen($b) <=> strlen($a);
});
$values = [];
foreach ($keys as $key) {
$values[] = $this->map[$key];
}
return str_replace($keys, $values, $html);
}
/**
* @return string[]
*/
public function keys(): array
{
return array_keys($this->map);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
final class LocalizedException extends RuntimeException
{
/** @var array<string,string|int|float> */
private array $vars;
/**
* @param array<string,string|int|float> $vars
*/
public function __construct(string $key, array $vars = [], int $code = 0, ?Throwable $previous = null)
{
parent::__construct($key, $code, $previous);
$this->vars = $vars;
}
public function key(): string
{
return $this->getMessage();
}
/**
* @return array<string,string|int|float>
*/
public function vars(): array
{
return $this->vars;
}
}
+229
View File
@@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
final class NamePolicy
{
public const MAX_IDENTIFIER_LEN = 63;
public const MAX_HOST_COMMENT_LEN = 180;
public const ALL_IPV4_HOST_PATTERN = '0.0.0.0/0';
/**
* @var array<string,string>
*/
public const PRIVILEGE_LABELS = [
'ALL' => 'Wszystkie uprawnienia',
'CONNECT' => 'CONNECT',
'CREATE' => 'CREATE (baza/schemat)',
'TEMPORARY' => 'TEMPORARY',
'SCHEMA_USAGE' => 'USAGE (schema public)',
'TABLE_SELECT' => 'SELECT (tabele)',
'TABLE_INSERT' => 'INSERT (tabele)',
'TABLE_UPDATE' => 'UPDATE (tabele)',
'TABLE_DELETE' => 'DELETE (tabele)',
'TABLE_TRUNCATE' => 'TRUNCATE (tabele)',
'TABLE_REFERENCES' => 'REFERENCES (tabele)',
'TABLE_TRIGGER' => 'TRIGGER (tabele)',
'SEQUENCE_USAGE' => 'USAGE (sekwencje)',
'SEQUENCE_SELECT' => 'SELECT (sekwencje)',
'SEQUENCE_UPDATE' => 'UPDATE (sekwencje)',
'FUNCTION_EXECUTE' => 'EXECUTE (funkcje)',
];
public static function defaultPrivileges(): array
{
return ['ALL'];
}
public static function normalizeDatabaseName(string $input, string $prefix): string
{
return self::normalizeIdentifier($input, $prefix, 'Nazwa bazy');
}
public static function normalizeRoleName(string $input, string $prefix): string
{
return self::normalizeIdentifier($input, $prefix, 'Nazwa użytkownika');
}
public static function assertBelongsToUser(string $name, string $prefix, string $entityLabel = 'Obiekt'): void
{
if (strpos($name, $prefix) !== 0) {
throw new InvalidArgumentException($entityLabel . ' musi zaczynać się od prefiksu: ' . $prefix);
}
}
public static function normalizeIdentifier(string $input, string $prefix, string $label): string
{
$value = strtolower(trim($input));
if ($value === '') {
throw new InvalidArgumentException($label . ' jest wymagana.');
}
if (!preg_match('/^[a-z0-9_]+$/', $value)) {
throw new InvalidArgumentException($label . ' może zawierać wyłącznie znaki a-z, 0-9 oraz _.');
}
if (strpos($value, $prefix) !== 0) {
$value = $prefix . $value;
}
if (strpos($value, $prefix) !== 0) {
throw new InvalidArgumentException($label . ' musi zaczynać się od prefiksu: ' . $prefix);
}
if (strlen($value) > self::MAX_IDENTIFIER_LEN) {
throw new InvalidArgumentException($label . ' przekracza limit 63 znaków PostgreSQL.');
}
if ($value === $prefix) {
throw new InvalidArgumentException($label . ' nie może być równa samemu prefiksowi.');
}
return $value;
}
/**
* @return string[]
*/
public static function parseHosts(string $raw): array
{
$raw = str_replace(["\r\n", "\r", ';'], ["\n", "\n", "\n"], $raw);
$raw = str_replace(',', "\n", $raw);
$parts = explode("\n", $raw);
$hosts = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
$hosts[] = $part;
}
return array_values(array_unique($hosts));
}
public static function normalizeHost(string $input, bool $allowWildcard): string
{
$host = strtolower(trim($input));
if ($host === '') {
throw new InvalidArgumentException('Pusty host.');
}
if (in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
return $host;
}
if (self::isStrictIpv4($host)) {
return $host;
}
throw new InvalidArgumentException('Nieobsługiwany format hosta: ' . $input);
}
public static function normalizeRemoteIpv4Host(string $input): string
{
$host = trim($input);
if ($host === '') {
throw new InvalidArgumentException('Adres IPv4 jest wymagany.');
}
if (!self::isStrictIpv4($host)) {
throw new InvalidArgumentException('Dozwolone są tylko pojedyncze adresy IPv4, np. 203.0.113.10.');
}
return $host;
}
public static function normalizeRemoteIpv4AccessPattern(string $input): string
{
$host = trim(strtolower($input));
if ($host === '') {
throw new InvalidArgumentException('Adres IPv4 albo CIDR jest wymagany.');
}
if (self::isStrictIpv4($host)) {
return $host;
}
if (preg_match('#^([^/]+)/([0-9]{1,2})$#', $host, $m)) {
$ip = $m[1];
$prefix = (int)$m[2];
if (!self::isStrictIpv4($ip) || $prefix < 0 || $prefix > 32) {
throw new InvalidArgumentException('Nieprawidłowy CIDR IPv4: ' . $input);
}
return self::normalizeIpv4Cidr($ip, $prefix);
}
throw new InvalidArgumentException('Dozwolone są pojedyncze adresy IPv4 albo CIDR, np. 203.0.113.10 lub 203.0.113.0/24.');
}
public static function normalizeHostComment(string $input): string
{
$comment = trim(preg_replace('/[\r\n\t]+/', ' ', $input) ?? '');
$comment = preg_replace('/\s+/', ' ', $comment) ?? '';
$comment = trim($comment);
if (strlen($comment) > self::MAX_HOST_COMMENT_LEN) {
throw new InvalidArgumentException('Komentarz hosta przekracza limit ' . self::MAX_HOST_COMMENT_LEN . ' znaków.');
}
return $comment;
}
/**
* @param array<int,string> $input
* @return string[]
*/
public static function sanitizePrivileges(array $input): array
{
$allowed = array_keys(self::PRIVILEGE_LABELS);
$result = [];
foreach ($input as $priv) {
$priv = strtoupper(trim((string)$priv));
if ($priv === '') {
continue;
}
if (!in_array($priv, $allowed, true)) {
continue;
}
$result[] = $priv;
}
$result = array_values(array_unique($result));
if (in_array('ALL', $result, true)) {
return ['ALL'];
}
return $result;
}
private static function isStrictIpv4(string $value): bool
{
if (!preg_match('/^(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/', $value)) {
return false;
}
return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
}
private static function normalizeIpv4Cidr(string $ip, int $prefix): string
{
$long = ip2long($ip);
if ($long === false) {
throw new InvalidArgumentException('Nieprawidłowy adres IPv4: ' . $ip);
}
$unsigned = (int)sprintf('%u', $long);
$mask = $prefix === 0 ? 0 : ((0xFFFFFFFF << (32 - $prefix)) & 0xFFFFFFFF);
$network = $unsigned & $mask;
$networkIp = long2ip($network);
if ($networkIp === false) {
throw new InvalidArgumentException('Nieprawidłowy CIDR IPv4: ' . $ip . '/' . $prefix);
}
return $networkIp . '/' . $prefix;
}
}
File diff suppressed because it is too large Load Diff
+365
View File
@@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
final class Settings
{
/** @var array<string,string> */
private array $values;
private function __construct(array $values)
{
$this->values = $values;
}
public static function load(string $path): self
{
$values = [];
if (is_readable($path)) {
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines !== false) {
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!preg_match('/^([A-Za-z0-9_]+)=(.*)$/', $line, $m)) {
continue;
}
$values[$m[1]] = self::parseShValue((string)$m[2]);
}
}
}
return new self($values);
}
public static function loadPostgresqlCredentials(string $path): array
{
if (!is_readable($path)) {
throw new RuntimeException('Brak pliku z danymi PostgreSQL: ' . $path);
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
throw new RuntimeException('Nie można odczytać pliku: ' . $path);
}
$kv = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (strpos($line, '=') !== false) {
[$k, $v] = array_map('trim', explode('=', $line, 2));
if ($k !== '') {
$kv[strtoupper($k)] = self::parseShValue($v);
}
continue;
}
$parts = explode(':', $line);
if (count($parts) >= 5) {
return [
'host' => trim($parts[0]) !== '' ? trim($parts[0]) : 'localhost',
'port' => (int)(trim($parts[1]) !== '' ? trim($parts[1]) : '5432'),
'database' => trim($parts[2]) !== '' ? trim($parts[2]) : 'postgres',
'user' => trim($parts[3]),
'password' => trim(implode(':', array_slice($parts, 4))),
];
}
}
if (!empty($kv['PG_HOST']) || !empty($kv['PG_USER']) || !empty($kv['PG_PASSWORD'])) {
return [
'host' => $kv['PG_HOST'] ?? 'localhost',
'port' => (int)($kv['PG_PORT'] ?? '5432'),
'database' => $kv['PG_DATABASE'] ?? ($kv['PG_DBNAME'] ?? 'postgres'),
'user' => $kv['PG_USER'] ?? 'postgres',
'password' => $kv['PG_PASSWORD'] ?? '',
];
}
throw new RuntimeException('Niepoprawny format pliku danych PostgreSQL: ' . $path);
}
public function getString(string $key, string $default = ''): string
{
return isset($this->values[$key]) ? trim($this->values[$key]) : $default;
}
public function getInt(string $key, int $default = 0): int
{
if (!isset($this->values[$key])) {
return $default;
}
$value = trim($this->values[$key]);
if ($value === '' || !preg_match('/^-?[0-9]+$/', $value)) {
return $default;
}
return (int)$value;
}
public function getBool(string $key, bool $default = false): bool
{
if (!isset($this->values[$key])) {
return $default;
}
return self::toBool($this->values[$key], $default);
}
public function defaultDatabasesLimit(): int
{
$limit = $this->getInt('PG_PLUGIN_DEFAULT_DB_LIMIT', 5);
return $limit < 0 ? 0 : $limit;
}
public function maxHostsPerUser(): int
{
$limit = $this->getInt('PG_PLUGIN_MAX_HOSTS_PER_USER', 30);
return $limit < 1 ? 1 : $limit;
}
public function allowRemoteHosts(): bool
{
if (array_key_exists('allow_remote_hosts', $this->values)) {
return self::toBool($this->values['allow_remote_hosts'], true);
}
return $this->getBool('PG_PLUGIN_ALLOW_REMOTE_HOSTS', true);
}
public function hbaSyncScript(): string
{
$path = $this->getString('PG_PLUGIN_SUDO_SYNC_HBA', '/usr/local/directadmin/plugins/da-postgresql/scripts/setup/pg_hba_sync.sh');
return $path !== '' ? $path : '/usr/local/directadmin/plugins/da-postgresql/scripts/setup/pg_hba_sync.sh';
}
public function enableBackup(): bool
{
return $this->getBool('ENABLE_BACKUP', true);
}
public function enableUploadBackup(): bool
{
return $this->getBool('ENABLE_UPLOAD_BACKUP', true);
}
public function enableAdminer(): bool
{
if (!$this->getBool('ENABLE_ADMINER', false)) {
return false;
}
return $this->isAdminerInstalled();
}
public function adminerUrlPath(): string
{
$path = trim($this->getString('ADMINER_URL_PATH', '/adminer'));
if ($path === '') {
return '/adminer';
}
if ($path[0] !== '/') {
$path = '/' . $path;
}
$path = rtrim($path, '/');
return $path !== '' ? $path : '/adminer';
}
public function adminerPublicBaseUrl(): string
{
$url = trim($this->getString('ADMINER_PUBLIC_BASE_URL', ''));
if ($url === '') {
return '';
}
$url = rtrim($url, '/');
if (!preg_match('#^https?://#i', $url)) {
return '';
}
return $url;
}
public function adminerPublic(): bool
{
return $this->getBool('ADMINER_PUBLIC', false);
}
public function adminerSsoDir(): string
{
$path = trim($this->getString('ADMINER_SSO_DIR', '/var/www/html/adminer/runtime'));
if ($path === '') {
$path = '/var/www/html/adminer/runtime';
}
return rtrim($path, '/');
}
public function adminerTicketTtl(): int
{
$ttl = $this->getInt('ADMINER_TICKET_TTL', 120);
if ($ttl < 10) {
return 10;
}
if ($ttl > 300) {
return 300;
}
return $ttl;
}
public function adminerSessionTtl(): int
{
$ttl = $this->getInt('ADMINER_SESSION_TTL', 900);
if ($ttl < 60) {
return 60;
}
if ($ttl > 3600) {
return 3600;
}
return $ttl;
}
public function adminerRoleTtl(): int
{
$ttl = $this->getInt('ADMINER_ROLE_TTL', 1200);
if ($ttl < 120) {
return 120;
}
if ($ttl > 7200) {
return 7200;
}
return $ttl;
}
private function isAdminerInstalled(): bool
{
$indexFile = '/var/www/html/adminer/index.php';
$coreFile = '/var/www/html/adminer/adminer-core.php';
return is_file($indexFile) && is_readable($indexFile)
&& is_file($coreFile) && is_readable($coreFile);
}
public function allowRestoreToOtherDatabase(): bool
{
return $this->getBool('ALLOW_RESTORE_TO_OTHER_DATABASE', true);
}
public function enableTableCount(): bool
{
return $this->getBool('ENABLE_TABLE_COUNT', true);
}
public function countDatabaseSize(): bool
{
return $this->getBool('COUNT_DATABASE_SIZE', true);
}
public function compressBackup(): bool
{
return $this->getBool('COMPRESS_BACKUP', true);
}
public function hitmeBackupLocation(string $daUser = ''): string
{
$path = $this->getString('HITME_BACKUP_LOCATION', '/home/admin/postgres_backup');
if ($path === '') {
$path = '/home/admin/postgres_backup';
}
if ($daUser !== '') {
$path = str_replace(['DA_USER', 'da_user'], $daUser, $path);
}
return rtrim($path, '/');
}
public function userBaseBackupDir(string $daUser = ''): string
{
$path = $this->getString('USER_BASE_BACKUP_DIR', '/home/DA_USER/postgres_backup');
if ($path === '') {
$path = '/home/DA_USER/postgres_backup';
}
if ($daUser !== '') {
$path = str_replace(['DA_USER', 'da_user'], $daUser, $path);
}
return rtrim($path, '/');
}
public function userBackupDateFormat(): string
{
$format = $this->getString('USER_BACKUP_DATE_FORMAT', 'd.m.Y-H.i');
return $format !== '' ? $format : 'd.m.Y-H.i';
}
public function loadOrCreateSecret(string $path): string
{
if (is_readable($path)) {
$value = trim((string)file_get_contents($path));
if ($value !== '') {
return $value;
}
}
$secret = bin2hex(random_bytes(32));
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
if (@file_put_contents($path, $secret) !== false) {
@chmod($path, 0600);
return $secret;
}
return hash('sha256', __FILE__ . '|' . php_uname('n') . '|' . $path . '|da-postgresql');
}
public static function toBool(string $value, bool $default = false): bool
{
$v = strtolower(trim($value));
if ($v === '') {
return $default;
}
if (in_array($v, ['1', 'true', 'yes', 'y', 'on', 'checked'], true)) {
return true;
}
if (in_array($v, ['0', 'false', 'no', 'n', 'off', 'unchecked'], true)) {
return false;
}
return $default;
}
private static function parseShValue(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
if ($value[0] === '\'' && substr($value, -1) === '\'') {
return substr($value, 1, -1);
}
if ($value[0] === '"' && substr($value, -1) === '"') {
return stripcslashes(substr($value, 1, -1));
}
$value = preg_replace('/\s+#.*$/', '', $value);
return trim((string)$value);
}
}
+1
View File
@@ -0,0 +1 @@
<a href="/CMD_PLUGINS/da-postgresql/index.html" target="_top"><img src="/CMD_PLUGINS/da-postgresql/images/user_icon.svg" width="16" height="16" alt="PostgreSQL"/></a>
+1
View File
@@ -0,0 +1 @@
<a href="/CMD_PLUGINS/da-postgresql/index.html" target="_top">Bazy danych PostgreSQL</a>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 4167 4167" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Layer2">
<g transform="matrix(7.71871,0,0,7.71871,1094.66,1656.5)">
<path d="M89.153,145.785C82.627,138.975 79.663,129.503 81.02,119.795C82.92,106.203 82.219,94.365 81.842,88.005C81.789,87.115 81.742,86.335 81.715,85.72C84.788,82.995 99.029,75.365 109.185,77.692C113.819,78.753 116.643,81.909 117.817,87.337C123.893,115.44 118.621,127.153 114.385,136.566C113.512,138.505 112.687,140.338 111.983,142.234L111.437,143.7C110.055,147.406 108.769,150.852 107.972,154.124C101.034,154.104 94.285,151.14 89.153,145.784L89.153,145.785ZM108.076,81.525C105.396,81.152 102.969,81.497 101.741,82.427C101.051,82.95 100.837,83.556 100.779,83.973C100.625,85.078 101.399,86.3 101.875,86.93C103.221,88.714 105.187,89.94 107.133,90.21C107.415,90.249 107.696,90.269 107.975,90.268C111.22,90.268 114.171,87.741 114.431,85.876C114.756,83.54 111.365,81.983 108.076,81.526L108.076,81.525ZM66.674,175.778C60.774,182.872 56.7,181.512 55.36,181.066C46.63,178.154 36.5,159.702 27.569,130.442C19.841,105.124 15.325,79.665 14.967,72.526C13.839,49.948 19.312,34.213 31.235,25.757C50.639,11.997 82.541,20.233 95.36,24.41C95.176,24.592 94.984,24.762 94.802,24.947C73.766,46.191 74.265,82.487 74.317,84.706C74.315,85.562 74.387,86.774 74.485,88.441C74.847,94.546 75.521,105.908 73.721,118.775C72.049,130.732 75.735,142.435 83.832,150.884C84.663,151.75 85.536,152.573 86.449,153.352C82.845,157.212 75.012,165.748 66.674,175.778ZM90.218,183.684C88.192,183.178 86.37,182.299 85.301,181.57C86.194,181.15 87.783,180.578 90.539,180.01C103.876,177.265 105.936,175.327 110.434,169.616C111.465,168.306 112.634,166.822 114.253,165.014L114.255,165.012C116.666,162.312 117.769,162.77 119.769,163.6C121.39,164.27 122.969,166.302 123.609,168.538C123.912,169.594 124.252,171.598 123.139,173.158C113.743,186.314 100.051,186.145 90.218,183.684ZM228.121,170.701C223.861,172.678 216.732,174.161 209.96,174.48C202.48,174.83 198.672,173.642 197.776,172.911C197.356,164.267 200.573,163.364 203.978,162.408C204.513,162.258 205.035,162.111 205.539,161.935C205.852,162.19 206.196,162.442 206.571,162.691C212.583,166.659 223.306,167.087 238.445,163.962L238.611,163.929C236.569,165.838 233.075,168.4 228.121,170.701ZM186.052,14.758C207.386,15.229 224.262,23.21 236.21,38.478C245.374,50.189 235.283,103.476 206.07,149.447C205.776,149.073 205.481,148.701 205.184,148.33L204.814,147.868C212.363,135.401 210.887,123.066 209.573,112.13C209.033,107.642 208.523,103.403 208.653,99.421C208.787,95.201 209.345,91.581 209.885,88.081C210.548,83.768 211.223,79.305 211.037,74.044C211.176,73.492 211.232,72.84 211.159,72.066C210.684,67.021 204.924,51.922 193.184,38.256C186.762,30.781 177.397,22.416 164.61,16.774C170.11,15.634 177.631,14.571 186.052,14.758ZM203.696,76.545C203.635,80.192 203.133,83.503 202.601,86.959C202.028,90.676 201.436,94.519 201.287,99.184C201.14,103.724 201.707,108.444 202.255,113.009C203.363,122.229 204.5,131.721 200.099,141.087C199.369,139.791 198.718,138.452 198.149,137.078C197.602,135.752 196.414,133.622 194.769,130.674C188.37,119.198 173.385,92.324 181.056,81.358C183.341,78.094 189.14,74.738 203.696,76.545ZM196.86,81.599C196.604,79.768 193.346,79.246 190.254,79.676C187.166,80.106 184.172,81.5 184.422,83.335C184.622,84.762 187.199,87.198 190.249,87.198C190.507,87.198 190.767,87.18 191.029,87.144C193.065,86.862 194.559,85.569 195.269,84.824C196.349,83.688 196.975,82.422 196.86,81.599ZM160.017,248.636C143.701,252.132 137.924,243.807 134.117,234.29C131.66,228.146 130.452,200.44 131.309,169.843C131.32,169.436 131.262,169.043 131.15,168.673C131.05,167.942 130.898,167.22 130.694,166.511C129.42,162.059 126.315,158.335 122.59,156.791C121.11,156.178 118.394,155.053 115.13,155.888C115.826,153.02 117.033,149.781 118.342,146.274L118.891,144.799C119.509,143.136 120.285,141.413 121.105,139.589C125.538,129.741 131.609,116.252 125.02,85.779C122.552,74.365 114.31,68.791 101.816,70.086C94.326,70.861 87.473,73.883 84.055,75.616C83.32,75.988 82.648,76.348 82.02,76.698C82.974,65.198 86.578,43.706 100.06,30.108C108.549,21.548 119.854,17.32 133.628,17.548C160.768,17.992 178.172,31.92 187.994,43.527C196.458,53.528 201.041,63.603 202.87,69.037C189.115,67.638 179.76,70.353 175.018,77.133C164.701,91.881 180.662,120.505 188.333,134.262C189.74,136.783 190.954,138.962 191.336,139.888C193.834,145.942 197.068,149.984 199.429,152.934C200.153,153.838 200.855,154.715 201.389,155.481C197.223,156.682 189.74,159.457 190.422,173.328C189.872,180.288 185.961,212.874 183.974,224.387C181.351,239.597 175.754,245.262 160.017,248.637L160.017,248.636Z" style="fill:rgb(90,93,157);"/>
</g>
<g transform="matrix(184.22,0,0,144.123,-127.311,-71.8256)">
<path d="M17.389,14.125C17.652,13.993 17.896,13.851 18.118,13.7C18.497,13.442 18.809,13.165 19.026,12.864C19.22,12.595 19.335,12.309 19.335,12C19.335,11.685 19.543,11.43 19.8,11.43L20.2,11.43C20.457,11.43 20.665,11.685 20.665,12L20.665,18C20.665,18.028 20.664,18.056 20.663,18.085L20.665,18.083L20.665,24.083C20.665,24.141 20.658,24.197 20.645,24.249C20.528,25.234 19.89,26.186 18.795,26.933C17.262,27.978 14.791,28.677 12,28.677C9.209,28.677 6.738,27.978 5.205,26.933C4.11,26.186 3.472,25.234 3.356,24.249C3.342,24.197 3.335,24.141 3.335,24.083L3.335,6C3.335,4.89 3.987,3.805 5.205,2.974C6.738,1.929 9.209,1.23 12,1.23C14.791,1.23 17.262,1.929 18.795,2.974C20.013,3.805 20.665,4.89 20.665,6C20.665,7.11 20.013,8.195 18.795,9.026C18.073,9.518 17.143,9.934 16.069,10.235L16.069,8.652C16.866,8.402 17.561,8.079 18.118,7.7C18.497,7.442 18.809,7.165 19.026,6.864C19.22,6.595 19.335,6.309 19.335,6C19.335,5.691 19.22,5.405 19.026,5.136C18.809,4.835 18.497,4.558 18.118,4.3C16.739,3.36 14.51,2.77 12,2.77C9.49,2.77 7.261,3.36 5.882,4.3C5.503,4.558 5.191,4.835 4.974,5.136C4.78,5.405 4.665,5.691 4.665,6C4.665,6.309 4.78,6.595 4.974,6.864C5.191,7.165 5.503,7.442 5.882,7.7C6.612,8.198 7.581,8.598 8.704,8.864L8.704,10.427C7.302,10.123 6.095,9.633 5.205,9.026C5.01,8.893 4.83,8.754 4.665,8.61L4.665,12C4.665,12.309 4.78,12.595 4.974,12.864C5.191,13.165 5.503,13.442 5.882,13.7C6.136,13.873 6.418,14.034 6.725,14.181L6.725,15.825C6.152,15.592 5.641,15.323 5.205,15.026C5.01,14.893 4.83,14.754 4.665,14.61L4.665,18C4.665,18.309 4.78,18.595 4.974,18.864C5.191,19.165 5.503,19.442 5.882,19.7C6.136,19.873 6.418,20.034 6.725,20.181L6.725,21.825C6.152,21.592 5.641,21.323 5.205,21.026C5.01,20.893 4.83,20.754 4.665,20.61L4.665,23.907C4.665,24.216 4.78,24.502 4.974,24.771C5.191,25.072 5.503,25.349 5.882,25.607C7.261,26.547 9.49,27.138 12,27.138C14.51,27.138 16.739,26.547 18.118,25.607C18.497,25.349 18.809,25.072 19.026,24.771C19.22,24.502 19.335,24.216 19.335,23.907L19.335,20.61C19.17,20.754 18.99,20.893 18.795,21.026C18.388,21.303 17.916,21.556 17.389,21.778L17.389,20.125C17.652,19.993 17.896,19.851 18.118,19.7C18.497,19.442 18.809,19.165 19.026,18.864C19.22,18.595 19.335,18.309 19.335,18L19.335,14.61C19.17,14.754 18.99,14.893 18.795,15.026C18.388,15.303 17.916,15.556 17.389,15.778L17.389,14.125Z" style="fill:rgb(97,189,246);"/>
</g>
<g transform="matrix(1.21985,0,0,1.21985,-370.358,-97.3027)">
<path d="M1649.35,1278.68L1649.35,765.363L1870.92,765.363C1909.02,765.363 1942.35,772.882 1970.93,787.921C1999.5,802.959 2021.72,824.097 2037.6,851.333C2053.47,878.57 2061.41,910.402 2061.41,946.828C2061.41,983.589 2053.26,1015.42 2036.97,1042.32C2020.68,1069.23 1997.87,1089.95 1968.55,1104.48C1939.22,1119.02 1905.01,1126.29 1865.91,1126.29L1733.57,1126.29L1733.57,1018.01L1837.84,1018.01C1854.21,1018.01 1868.2,1015.13 1879.82,1009.36C1891.43,1003.6 1900.37,995.37 1906.64,984.675C1912.9,973.981 1916.04,961.366 1916.04,946.828C1916.04,932.124 1912.9,919.55 1906.64,909.107C1900.37,898.663 1891.43,890.643 1879.82,885.045C1868.2,879.447 1854.21,876.648 1837.84,876.648L1788.71,876.648L1788.71,1278.68L1649.35,1278.68Z" style="fill:rgb(255,160,8);fill-rule:nonzero;"/>
<path d="M2292,1431.07C2253.4,1431.07 2220.36,1425.6 2192.87,1414.65C2165.38,1403.71 2144.08,1388.67 2128.96,1369.54C2113.84,1350.4 2105.36,1328.47 2103.52,1303.74L2236.86,1303.74C2238.2,1311.93 2241.54,1318.53 2246.89,1323.55C2252.23,1328.56 2259.12,1332.19 2267.56,1334.45C2276,1336.7 2285.48,1337.83 2296.01,1337.83C2313.89,1337.83 2328.72,1333.49 2340.5,1324.8C2352.28,1316.11 2358.17,1300.4 2358.17,1277.68L2358.17,1212.51L2354.16,1212.51C2349.15,1225.54 2341.46,1236.61 2331.1,1245.72C2320.74,1254.83 2308.29,1261.76 2293.76,1266.52C2279.22,1271.29 2263.26,1273.67 2245.88,1273.67C2218.48,1273.67 2193.29,1267.32 2170.31,1254.62C2147.34,1241.92 2128.96,1221.78 2115.17,1194.21C2101.39,1166.64 2094.49,1130.63 2094.49,1086.19C2094.49,1039.73 2101.72,1001.97 2116.17,972.895C2130.63,943.821 2149.34,922.516 2172.32,908.981C2195.29,895.447 2219.48,888.679 2244.88,888.679C2263.93,888.679 2280.64,891.979 2295.01,898.58C2309.38,905.18 2321.41,913.952 2331.1,924.897C2340.79,935.842 2348.14,947.831 2353.16,960.864L2356.16,960.864L2356.16,893.692L2494.52,893.692L2494.52,1277.68C2494.52,1310.26 2486.12,1337.96 2469.33,1360.77C2452.54,1383.57 2428.93,1400.99 2398.52,1413.02C2368.11,1425.06 2332.61,1431.07 2292,1431.07ZM2297.01,1175.41C2309.71,1175.41 2320.62,1171.86 2329.72,1164.76C2338.83,1157.66 2345.85,1147.47 2350.78,1134.18C2355.7,1120.9 2358.17,1104.9 2358.17,1086.19C2358.17,1067.14 2355.7,1050.8 2350.78,1037.18C2345.85,1023.57 2338.83,1013.12 2329.72,1005.86C2320.62,998.586 2309.71,994.952 2297.01,994.952C2284.31,994.952 2273.49,998.586 2264.55,1005.86C2255.62,1013.12 2248.76,1023.57 2244,1037.18C2239.24,1050.8 2236.86,1067.14 2236.86,1086.19C2236.86,1105.24 2239.24,1121.4 2244,1134.68C2248.76,1147.97 2255.62,1158.08 2264.55,1165.01C2273.49,1171.95 2284.31,1175.41 2297.01,1175.41Z" style="fill:rgb(255,160,8);fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 4167 4167" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Layer2">
<g transform="matrix(7.71871,0,0,7.71871,1094.66,1656.5)">
<path d="M89.153,145.785C82.627,138.975 79.663,129.503 81.02,119.795C82.92,106.203 82.219,94.365 81.842,88.005C81.789,87.115 81.742,86.335 81.715,85.72C84.788,82.995 99.029,75.365 109.185,77.692C113.819,78.753 116.643,81.909 117.817,87.337C123.893,115.44 118.621,127.153 114.385,136.566C113.512,138.505 112.687,140.338 111.983,142.234L111.437,143.7C110.055,147.406 108.769,150.852 107.972,154.124C101.034,154.104 94.285,151.14 89.153,145.784L89.153,145.785ZM108.076,81.525C105.396,81.152 102.969,81.497 101.741,82.427C101.051,82.95 100.837,83.556 100.779,83.973C100.625,85.078 101.399,86.3 101.875,86.93C103.221,88.714 105.187,89.94 107.133,90.21C107.415,90.249 107.696,90.269 107.975,90.268C111.22,90.268 114.171,87.741 114.431,85.876C114.756,83.54 111.365,81.983 108.076,81.526L108.076,81.525ZM66.674,175.778C60.774,182.872 56.7,181.512 55.36,181.066C46.63,178.154 36.5,159.702 27.569,130.442C19.841,105.124 15.325,79.665 14.967,72.526C13.839,49.948 19.312,34.213 31.235,25.757C50.639,11.997 82.541,20.233 95.36,24.41C95.176,24.592 94.984,24.762 94.802,24.947C73.766,46.191 74.265,82.487 74.317,84.706C74.315,85.562 74.387,86.774 74.485,88.441C74.847,94.546 75.521,105.908 73.721,118.775C72.049,130.732 75.735,142.435 83.832,150.884C84.663,151.75 85.536,152.573 86.449,153.352C82.845,157.212 75.012,165.748 66.674,175.778ZM90.218,183.684C88.192,183.178 86.37,182.299 85.301,181.57C86.194,181.15 87.783,180.578 90.539,180.01C103.876,177.265 105.936,175.327 110.434,169.616C111.465,168.306 112.634,166.822 114.253,165.014L114.255,165.012C116.666,162.312 117.769,162.77 119.769,163.6C121.39,164.27 122.969,166.302 123.609,168.538C123.912,169.594 124.252,171.598 123.139,173.158C113.743,186.314 100.051,186.145 90.218,183.684ZM228.121,170.701C223.861,172.678 216.732,174.161 209.96,174.48C202.48,174.83 198.672,173.642 197.776,172.911C197.356,164.267 200.573,163.364 203.978,162.408C204.513,162.258 205.035,162.111 205.539,161.935C205.852,162.19 206.196,162.442 206.571,162.691C212.583,166.659 223.306,167.087 238.445,163.962L238.611,163.929C236.569,165.838 233.075,168.4 228.121,170.701ZM186.052,14.758C207.386,15.229 224.262,23.21 236.21,38.478C245.374,50.189 235.283,103.476 206.07,149.447C205.776,149.073 205.481,148.701 205.184,148.33L204.814,147.868C212.363,135.401 210.887,123.066 209.573,112.13C209.033,107.642 208.523,103.403 208.653,99.421C208.787,95.201 209.345,91.581 209.885,88.081C210.548,83.768 211.223,79.305 211.037,74.044C211.176,73.492 211.232,72.84 211.159,72.066C210.684,67.021 204.924,51.922 193.184,38.256C186.762,30.781 177.397,22.416 164.61,16.774C170.11,15.634 177.631,14.571 186.052,14.758ZM203.696,76.545C203.635,80.192 203.133,83.503 202.601,86.959C202.028,90.676 201.436,94.519 201.287,99.184C201.14,103.724 201.707,108.444 202.255,113.009C203.363,122.229 204.5,131.721 200.099,141.087C199.369,139.791 198.718,138.452 198.149,137.078C197.602,135.752 196.414,133.622 194.769,130.674C188.37,119.198 173.385,92.324 181.056,81.358C183.341,78.094 189.14,74.738 203.696,76.545ZM196.86,81.599C196.604,79.768 193.346,79.246 190.254,79.676C187.166,80.106 184.172,81.5 184.422,83.335C184.622,84.762 187.199,87.198 190.249,87.198C190.507,87.198 190.767,87.18 191.029,87.144C193.065,86.862 194.559,85.569 195.269,84.824C196.349,83.688 196.975,82.422 196.86,81.599ZM160.017,248.636C143.701,252.132 137.924,243.807 134.117,234.29C131.66,228.146 130.452,200.44 131.309,169.843C131.32,169.436 131.262,169.043 131.15,168.673C131.05,167.942 130.898,167.22 130.694,166.511C129.42,162.059 126.315,158.335 122.59,156.791C121.11,156.178 118.394,155.053 115.13,155.888C115.826,153.02 117.033,149.781 118.342,146.274L118.891,144.799C119.509,143.136 120.285,141.413 121.105,139.589C125.538,129.741 131.609,116.252 125.02,85.779C122.552,74.365 114.31,68.791 101.816,70.086C94.326,70.861 87.473,73.883 84.055,75.616C83.32,75.988 82.648,76.348 82.02,76.698C82.974,65.198 86.578,43.706 100.06,30.108C108.549,21.548 119.854,17.32 133.628,17.548C160.768,17.992 178.172,31.92 187.994,43.527C196.458,53.528 201.041,63.603 202.87,69.037C189.115,67.638 179.76,70.353 175.018,77.133C164.701,91.881 180.662,120.505 188.333,134.262C189.74,136.783 190.954,138.962 191.336,139.888C193.834,145.942 197.068,149.984 199.429,152.934C200.153,153.838 200.855,154.715 201.389,155.481C197.223,156.682 189.74,159.457 190.422,173.328C189.872,180.288 185.961,212.874 183.974,224.387C181.351,239.597 175.754,245.262 160.017,248.637L160.017,248.636Z" style="fill:rgb(90,93,157);"/>
</g>
<g transform="matrix(184.22,0,0,144.123,-127.311,-71.8256)">
<path d="M17.389,14.125C17.652,13.993 17.896,13.851 18.118,13.7C18.497,13.442 18.809,13.165 19.026,12.864C19.22,12.595 19.335,12.309 19.335,12C19.335,11.685 19.543,11.43 19.8,11.43L20.2,11.43C20.457,11.43 20.665,11.685 20.665,12L20.665,18C20.665,18.028 20.664,18.056 20.663,18.085L20.665,18.083L20.665,24.083C20.665,24.141 20.658,24.197 20.645,24.249C20.528,25.234 19.89,26.186 18.795,26.933C17.262,27.978 14.791,28.677 12,28.677C9.209,28.677 6.738,27.978 5.205,26.933C4.11,26.186 3.472,25.234 3.356,24.249C3.342,24.197 3.335,24.141 3.335,24.083L3.335,6C3.335,4.89 3.987,3.805 5.205,2.974C6.738,1.929 9.209,1.23 12,1.23C14.791,1.23 17.262,1.929 18.795,2.974C20.013,3.805 20.665,4.89 20.665,6C20.665,7.11 20.013,8.195 18.795,9.026C18.073,9.518 17.143,9.934 16.069,10.235L16.069,8.652C16.866,8.402 17.561,8.079 18.118,7.7C18.497,7.442 18.809,7.165 19.026,6.864C19.22,6.595 19.335,6.309 19.335,6C19.335,5.691 19.22,5.405 19.026,5.136C18.809,4.835 18.497,4.558 18.118,4.3C16.739,3.36 14.51,2.77 12,2.77C9.49,2.77 7.261,3.36 5.882,4.3C5.503,4.558 5.191,4.835 4.974,5.136C4.78,5.405 4.665,5.691 4.665,6C4.665,6.309 4.78,6.595 4.974,6.864C5.191,7.165 5.503,7.442 5.882,7.7C6.612,8.198 7.581,8.598 8.704,8.864L8.704,10.427C7.302,10.123 6.095,9.633 5.205,9.026C5.01,8.893 4.83,8.754 4.665,8.61L4.665,12C4.665,12.309 4.78,12.595 4.974,12.864C5.191,13.165 5.503,13.442 5.882,13.7C6.136,13.873 6.418,14.034 6.725,14.181L6.725,15.825C6.152,15.592 5.641,15.323 5.205,15.026C5.01,14.893 4.83,14.754 4.665,14.61L4.665,18C4.665,18.309 4.78,18.595 4.974,18.864C5.191,19.165 5.503,19.442 5.882,19.7C6.136,19.873 6.418,20.034 6.725,20.181L6.725,21.825C6.152,21.592 5.641,21.323 5.205,21.026C5.01,20.893 4.83,20.754 4.665,20.61L4.665,23.907C4.665,24.216 4.78,24.502 4.974,24.771C5.191,25.072 5.503,25.349 5.882,25.607C7.261,26.547 9.49,27.138 12,27.138C14.51,27.138 16.739,26.547 18.118,25.607C18.497,25.349 18.809,25.072 19.026,24.771C19.22,24.502 19.335,24.216 19.335,23.907L19.335,20.61C19.17,20.754 18.99,20.893 18.795,21.026C18.388,21.303 17.916,21.556 17.389,21.778L17.389,20.125C17.652,19.993 17.896,19.851 18.118,19.7C18.497,19.442 18.809,19.165 19.026,18.864C19.22,18.595 19.335,18.309 19.335,18L19.335,14.61C19.17,14.754 18.99,14.893 18.795,15.026C18.388,15.303 17.916,15.556 17.389,15.778L17.389,14.125Z" style="fill:rgb(97,189,246);"/>
</g>
<g transform="matrix(1.21985,0,0,1.21985,-370.358,-97.3027)">
<path d="M1649.35,1278.68L1649.35,765.363L1870.92,765.363C1909.02,765.363 1942.35,772.882 1970.93,787.921C1999.5,802.959 2021.72,824.097 2037.6,851.333C2053.47,878.57 2061.41,910.402 2061.41,946.828C2061.41,983.589 2053.26,1015.42 2036.97,1042.32C2020.68,1069.23 1997.87,1089.95 1968.55,1104.48C1939.22,1119.02 1905.01,1126.29 1865.91,1126.29L1733.57,1126.29L1733.57,1018.01L1837.84,1018.01C1854.21,1018.01 1868.2,1015.13 1879.82,1009.36C1891.43,1003.6 1900.37,995.37 1906.64,984.675C1912.9,973.981 1916.04,961.366 1916.04,946.828C1916.04,932.124 1912.9,919.55 1906.64,909.107C1900.37,898.663 1891.43,890.643 1879.82,885.045C1868.2,879.447 1854.21,876.648 1837.84,876.648L1788.71,876.648L1788.71,1278.68L1649.35,1278.68Z" style="fill:rgb(255,160,8);fill-rule:nonzero;"/>
<path d="M2292,1431.07C2253.4,1431.07 2220.36,1425.6 2192.87,1414.65C2165.38,1403.71 2144.08,1388.67 2128.96,1369.54C2113.84,1350.4 2105.36,1328.47 2103.52,1303.74L2236.86,1303.74C2238.2,1311.93 2241.54,1318.53 2246.89,1323.55C2252.23,1328.56 2259.12,1332.19 2267.56,1334.45C2276,1336.7 2285.48,1337.83 2296.01,1337.83C2313.89,1337.83 2328.72,1333.49 2340.5,1324.8C2352.28,1316.11 2358.17,1300.4 2358.17,1277.68L2358.17,1212.51L2354.16,1212.51C2349.15,1225.54 2341.46,1236.61 2331.1,1245.72C2320.74,1254.83 2308.29,1261.76 2293.76,1266.52C2279.22,1271.29 2263.26,1273.67 2245.88,1273.67C2218.48,1273.67 2193.29,1267.32 2170.31,1254.62C2147.34,1241.92 2128.96,1221.78 2115.17,1194.21C2101.39,1166.64 2094.49,1130.63 2094.49,1086.19C2094.49,1039.73 2101.72,1001.97 2116.17,972.895C2130.63,943.821 2149.34,922.516 2172.32,908.981C2195.29,895.447 2219.48,888.679 2244.88,888.679C2263.93,888.679 2280.64,891.979 2295.01,898.58C2309.38,905.18 2321.41,913.952 2331.1,924.897C2340.79,935.842 2348.14,947.831 2353.16,960.864L2356.16,960.864L2356.16,893.692L2494.52,893.692L2494.52,1277.68C2494.52,1310.26 2486.12,1337.96 2469.33,1360.77C2452.54,1383.57 2428.93,1400.99 2398.52,1413.02C2368.11,1425.06 2332.61,1431.07 2292,1431.07ZM2297.01,1175.41C2309.71,1175.41 2320.62,1171.86 2329.72,1164.76C2338.83,1157.66 2345.85,1147.47 2350.78,1134.18C2355.7,1120.9 2358.17,1104.9 2358.17,1086.19C2358.17,1067.14 2355.7,1050.8 2350.78,1037.18C2345.85,1023.57 2338.83,1013.12 2329.72,1005.86C2320.62,998.586 2309.71,994.952 2297.01,994.952C2284.31,994.952 2273.49,998.586 2264.55,1005.86C2255.62,1013.12 2248.76,1023.57 2244,1037.18C2239.24,1050.8 2236.86,1067.14 2236.86,1086.19C2236.86,1105.24 2239.24,1121.4 2244,1134.68C2248.76,1147.97 2255.62,1158.08 2264.55,1165.01C2273.49,1171.95 2284.31,1175.41 2297.01,1175.41Z" style="fill:rgb(255,160,8);fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

+462
View File
@@ -0,0 +1,462 @@
<?php
return [
'Pulpit' => 'Dashboard',
'Bazy danych' => 'Databases',
'Wersja serwera PostgreSQL' => 'PostgreSQL server version',
'Powrót' => 'Back',
'Błąd' => 'Error',
'Dostęp zablokowany' => 'Access blocked',
'Włącz plugin przez custom package items: <code>postgresql_enabled</code> oraz ustaw limity.' => 'Enable the plugin via custom package items: <code>postgresql_enabled</code> and set limits.',
'Plugin PostgreSQL jest wyłączony dla tego konta lub pakietu.' => 'PostgreSQL plugin is disabled for this account or package.',
'Plugin PostgreSQL jest wyłączony: limit baz danych PostgreSQL ustawiono na 0 i nie zaznaczono opcji Bez Ograniczeń.'
=> 'PostgreSQL plugin is disabled: PostgreSQL databases limit is set to 0 and Unlimited option is not enabled.',
'Plugin PostgreSQL jest zablokowany przez reguły plugins_allow/plugins_deny.' => 'PostgreSQL plugin is blocked by plugins_allow/plugins_deny rules.',
'Brak dostępu do pluginu PostgreSQL.' => 'No access to PostgreSQL plugin.',
'Bazy danych PostgreSQL nie są dostępne dla Twojego konta - skontaktuj się z pomocą techniczną aby uzyskać pomoc i więcej informacji'
=> 'PostgreSQL databases are not available for your account. Contact technical support for help and more information.',
'Nie można ustalić użytkownika DirectAdmin.' => 'Cannot determine DirectAdmin user.',
'ZARZĄDZAJ BAZAMI DANYCH' => 'MANAGE DATABASES',
'ZARZĄDZAJ UŻYTKOWNIKAMI' => 'MANAGE USERS',
'WGRAJ BACKUP' => 'UPLOAD BACKUP',
'Użytkownicy baz danych' => 'Database users',
'Backupy Baz danych' => 'Database backups',
'PHPPGADMIN' => 'PHPPGADMIN',
'ADMINER' => 'ADMINER',
'Zarządzanie bazami danych PostgreSQL' => 'PostgreSQL database management',
'Lista baz danych' => 'Database list',
'Utwórz bazę danych' => 'Create database',
'Zarządzaj bazą danych' => 'Manage database',
'Zarządzaj użytkownikami' => 'Manage users',
'Brak baz danych do zarządzania.' => 'No databases to manage.',
'Zarządzanie hasłami' => 'Password management',
'Dostęp do baz danych' => 'Database access',
'Dostęp użytkownika' => 'User access',
'Przywileje' => 'Privileges',
'Akcje' => 'Actions',
'Akcja' => 'Action',
'Baza' => 'Database',
'Baza danych' => 'Database',
'Nazwa bazy danych' => 'Database name',
'Nazwa użytkownika' => 'Username',
'Użytkownik' => 'User',
'Użytkownik PostgreSQL' => 'PostgreSQL user',
'Rozmiar' => 'Size',
'Użytkownicy' => 'Users',
'Liczba Tabel' => 'Table count',
'Dozwolone hosty' => 'Allowed hosts',
'wyłączone' => 'disabled',
'Host' => 'Host',
'Komentarz' => 'Comment',
'Komentarz hosta' => 'Host comment',
'Komentarz hosta (opcjonalnie)' => 'Host comment (optional)',
'Komentarz do nowego hosta' => 'Comment for new host',
'Wybierz' => 'Select',
'Wybierz plik' => 'Choose file',
'Zamknij' => 'Close',
'W górę' => 'Up',
'Nowy katalog' => 'New folder',
'Utwórz' => 'Create',
'Anuluj' => 'Cancel',
'Przełącz' => 'Switch',
'Wykonaj Backup' => 'Create backup',
'Pobierz Backup' => 'Download backup',
'Usuń Backup' => 'Delete backup',
'Usuń logi' => 'Delete logs',
'Usuń bazę' => 'Delete database',
'Usuń' => 'Delete',
'Zmień hasło' => 'Change password',
'Zapisz zmiany' => 'Save changes',
'Zapisz hosty' => 'Save hosts',
'Przywróć' => 'Restore',
'Pokaż' => 'Show',
'Pokaż logi' => 'Show logs',
'Pobierz plik backupu' => 'Download backup file',
'Pobieranie...' => 'Downloading...',
'Nieudana odpowiedź serwera ({code})' => 'Server returned an error ({code})',
'Nieudana odpowiedź serwera (HTML zamiast pliku, URL: {url})' => 'Server returned HTML instead of file (URL: {url})',
'Serwer zwrócił pusty plik.' => 'Server returned an empty file.',
'Nie można pobrać pliku backupu.' => 'Cannot download backup file.',
'Wysyłanie pliku...' => 'Uploading file...',
'Nie udało się wysłać pliku backupu.' => 'Failed to upload backup file.',
'Wybierz plik .sql/.gz/.sql.gz lub wskaż ścieżkę pliku na serwerze.' => 'Select a .sql/.gz/.sql.gz file or provide a server file path.',
'Wskaż ścieżkę do pliku SQL lub SQL.GZ.' => 'Provide a path to a SQL or SQL.GZ file.',
'Wybierz plik .sql/.gz/.sql.gz do przywrócenia.' => 'Select a .sql/.gz/.sql.gz file to restore.',
'Nie udało się przygotować pliku do przywrócenia. Wybierz plik ponownie.' => 'Failed to prepare file for restore. Select the file again.',
'Przywróć Backup' => 'Restore Backup',
'Lokalizacja pliku' => 'File location',
'Przesyłanie pliku z komputera' => 'Upload file from computer',
'Wskaż plik na koncie użytkownika' => 'Select file in user account',
'Po dodaniu pliku zostanie on przesłany na serwer i dodany do kolejki przywracania.' => 'After selecting a file, it will be uploaded to the server and queued for restore.',
'Dozwolone są tylko ścieżki z katalogu /home/{user}.' => 'Only paths from /home/{user} are allowed.',
'Przeciągnij plik backupu tutaj' => 'Drag and drop the backup file here',
'lub kliknij, aby wybrać plik' => 'or click to choose a file',
'Plik' => 'File',
'Rozmiar' => 'Size',
'Usuń plik' => 'Remove file',
'Backupy HITME.PL' => 'HITME.PL backups',
'Backupy użytkownika' => 'User backups',
'Backupy lokalne' => 'Local backups',
'Backupy z pliku' => 'Backups from file',
'Źródło: {source} (backupy z postgres_backup.sh)' => 'Source: {source} (postgres_backup.sh backups)',
'Data:' => 'Date:',
'Data i godzina' => 'Date and time',
'Baza źródłowa' => 'Source database',
'Plik' => 'File',
'Baza docelowa' => 'Target database',
'Kolejka zadań i logi' => 'Jobs queue and logs',
'ID zadania' => 'Job ID',
'Typ' => 'Type',
'Rodzaj operacji' => 'Operation type',
'Typ przywracania' => 'Restore type',
'Tworzenie kopii zapasowej' => 'Backup creation',
'Przywracanie kopii zapasowej' => 'Backup restore',
'Przywracanie z nadpisaniem' => 'Restore with overwrite',
'Przywracanie do nowej bazy' => 'Restore to new database',
'Nie dotyczy' => 'Not applicable',
'Status' => 'Status',
'Start' => 'Start',
'Aktualizacja' => 'Updated',
'Podgląd logu' => 'View log',
'Log zadania: {id}' => 'Job log: {id}',
'Log zdarzenia' => 'Event log',
'Data rozpoczęcia' => 'Start date',
'Nazwa bazy' => 'Database name',
'Akcja' => 'Action',
'Log zadania' => 'Job log',
'Kopiuj' => 'Copy',
'Skopiowano' => 'Copied',
'Oczekuje' => 'Pending',
'W toku' => 'In progress',
'Zakończone OK' => 'Completed OK',
'Błąd' => 'Error',
'Anulowane' => 'Canceled',
'Szczegóły wykonywania backupu' => 'Backup details',
'Szczegóły zadania backupu' => 'Backup job details',
'Na tej bazie trwa już operacja backup/restore' => 'A backup/restore operation is already running for this database',
'Akcja:' => 'Action:',
'Status:' => 'Status:',
'Czas rozpoczęcia:' => 'Start time:',
'Ostatnia aktualizacja:' => 'Last update:',
'Logi zadania' => 'Job logs',
'Log jest niedostępny dla tego zadania.' => 'Log is not available for this job.',
'Szczegóły' => 'Details',
'Brak plików .sql/.gz' => 'No .sql/.gz files',
'Brak katalogów' => 'No folders',
'Nie udało się wczytać katalogów.' => 'Failed to load directories.',
'Podaj nazwę katalogu.' => 'Enter folder name.',
'Nie udało się utworzyć katalogu.' => 'Failed to create folder.',
'Obsługa przywracania backupów jest wyłączona (`ENABLE_UPLOAD_BACKUP=false`).' => 'Backup restore is disabled (`ENABLE_UPLOAD_BACKUP=false`).',
'Dodano zadanie przywracania backupu lokalnego (ID: {id}).' => 'Local backup restore queued (ID: {id}).',
'Dodano zadanie przywracania backupu z pliku (ID: {id}).' => 'File restore queued (ID: {id}).',
'Brak backupów lokalnych dla wybranej daty.' => 'No local backups for the selected date.',
'Brak baz danych do wyboru.' => 'No databases available for selection.',
'Wybierz bazę docelową przy wybranym backupie.' => 'Select the target database for the chosen backup.',
'Tryb przywracania' => 'Restore mode',
'Nadpisanie istniejącej bazy danych (obecne działanie)' => 'Overwrite existing database (current behavior)',
'Przywrócenie wskazanej bazy do nowej bazy danych' => 'Restore to a new database',
'Nazwa docelowej bazy danych' => 'Target database name',
'Wskaż docelową bazę danych.' => 'Provide a target database.',
'Docelowa baza danych musi być inna niż źródłowa baza z backupu.' => 'Target database must be different from the source backup database.',
'Wskazana docelowa baza danych nie istnieje. Utwórz ją ręcznie w DirectAdmin.' => 'The target database does not exist. Create it manually in DirectAdmin.',
'Docelowa baza danych nie jest pusta. Usuń dane przed kontynuowaniem.' => 'Target database is not empty. Remove data before continuing.',
'Nie można ustalić bazy z nazwy pliku.' => 'Cannot determine database name from the file name.',
'Nie można ustalić docelowej bazy dla backupu lokalnego.' => 'Cannot determine target database for local backup.',
'Wskaż plik `.sql`, `.gz` lub `.sql.gz` z komputera albo podaj ścieżkę pliku na serwerze (w katalogu `/home/{user}`).'
=> 'Select a `.sql`, `.gz` or `.sql.gz` file from your computer or provide a server path (in `/home/{user}`).',
'Plik backupu (.sql, .gz lub .sql.gz)' => 'Backup file (.sql, .gz or .sql.gz)',
'Ścieżka pliku SQL / SQL.GZ (opcjonalnie)' => 'SQL/SQL.GZ file path (optional)',
'Ścieżka pliku SQL / SQL.GZ' => 'SQL/SQL.GZ file path',
'Dodaj przywracanie do kolejki' => 'Queue restore job',
'Brak zadań backup/restore do wyświetlenia.' => 'No backup/restore jobs to display.',
'Brak backupów HITME.PL dla wybranej daty.' => 'No HITME.PL backups for the selected date.',
'Brak backupów użytkownika dla wybranej daty.' => 'No user backups for the selected date.',
'Kalendarz backupów' => 'Backup calendar',
'Wybierz aktywną datę w kalendarzu.' => 'Select an active date in the calendar.',
'Wybierz godzinę backupu' => 'Select backup time',
'Usunięto log zadania backupu.' => 'Backup job log deleted.',
'Usunięto plik backupu: {path}' => 'Backup file deleted: {path}',
'Czy na pewno chcesz usunąć backup bazy danych {db} z dn. {date}?'
=> 'Are you sure you want to delete the backup of database {db} from {date}?',
'Poprzedni miesiąc' => 'Previous month',
'Następny miesiąc' => 'Next month',
'Potwierdzenie operacji' => 'Confirm operation',
'Tak, kontynuuj' => 'Yes, continue',
'Nie' => 'No',
'nadpisując obecne dane' => 'overwriting current data',
'Czy na pewno chcesz przywrócić kopię zapasową bazy danych {source} z dn. {date} {overwrite} znajdujące się w bazie danych {target}?'
=> 'Are you sure you want to restore the backup of database {source} from {date}, {overwrite}, in database {target}?',
'Czy na pewno chcesz przywrócić dane z kopii zapasowej bazy danych {source} z dn. {date} do innej bazy danych o nazwie {target}?'
=> 'Are you sure you want to restore data from the backup of database {source} from {date} into another database named {target}?',
'Czy na pewno chcesz przywrócić dane z kopii zapasowej bazy danych {source} z pliku {file} do innej bazy danych o nazwie {target}?'
=> 'Are you sure you want to restore data from the backup of database {source} from file {file} into another database named {target}?',
'Uwaga: dane znajdujące się obecnie w bazie danych {target} zostaną usunięte i nadpisane danymi z kopii zapasowej dla bazy {source}.'
=> 'Warning: the data currently in database {target} will be deleted and overwritten with the backup data for database {source}.',
'Czy na pewno chcesz przywrócić plik kopii zapasowej {file} do bazy danych {target}?'
=> 'Are you sure you want to restore backup file {file} to database {target}?',
'Uwaga: jeśli baza danych {target} zawiera dane, zostaną one nadpisane.'
=> 'Warning: if database {target} already contains data, it will be overwritten.',
'Zadanie przywrócenia kopii zapasowej {file} do bazy danych {db} zostało dodane do kolejki'
=> 'Restore job for backup file {file} to database {db} has been added to the queue.',
'Szczegółowe informacje o bazie danych.' => 'Detailed database information.',
'Szczegółowe informacje o użytkowniku.' => 'Detailed user information.',
'Domyślne kodowanie znaków' => 'Default character encoding',
'Domyślne porównywanie znaków' => 'Default collation',
'Widoki' => 'Views',
'Wyzwalacze' => 'Triggers',
'Rutyny i procedury' => 'Routines and procedures',
'Lista użytkowników, którzy mają dostęp do tej bazy danych wraz z podsumowaniem uprawnień.'
=> 'List of users with access to this database and their privileges summary.',
'Przyznaj dostęp kolejnemu użytkownikowi' => 'Grant access to another user',
'+ Przyznaj pełny dostęp' => '+ Grant full access',
'Przywileje użytkownika:' => 'User privileges:',
'Brak użytkowników z dostępem do bazy.' => 'No users with access to this database.',
'Baza:' => 'Database:',
'Użytkownik:' => 'User:',
'Nazwa użytkownika:' => 'Username:',
'Nowe hasło' => 'New password',
'Wpisz nowe hasło' => 'Enter new password',
'Generuj hasło' => 'Generate password',
'Pokaż lub ukryj hasło' => 'Show or hide password',
'Dostęp do baz danych' => 'Database access',
'Przyznaj dostęp do kolejnej bazy danych' => 'Grant access to another database',
'Przyznaj pełny dostęp' => 'Grant full access',
'Dodaj użytkownika' => 'Add user',
'Utwórz nowego użytkownika PostgreSQL' => 'Create a new PostgreSQL user',
'Utwórz użytkownika' => 'Create user',
'Przypisz do baz danych (opcjonalnie)' => 'Assign to databases (optional)',
'Możesz pozostawić użytkownika bez przypisania do baz i nadać dostęp później.'
=> 'You can leave the user without database assignments and grant access later.',
'Brak użytkowników PostgreSQL. Dodaj pierwszego użytkownika.'
=> 'No PostgreSQL users. Add the first user.',
'Aby zarządzać użytkownikami najpierw utwórz użytkownika PostgreSQL.'
=> 'To manage users, first create a PostgreSQL user.',
'Dopuszczone hosty' => 'Allowed hosts',
'Zezwalaj na dostęp z' => 'Allow access from',
'Dodaj IPv4' => 'Add IPv4',
'Dodatkowy host IPv4' => 'Additional IPv4 host',
'Dodatkowy host IPv4 (opcjonalnie)' => 'Additional IPv4 host (optional)',
'Komentarz hosta (opcjonalnie)' => 'Host comment (optional)',
'Komentarz' => 'Comment',
'+ Dodaj host' => '+ Add host',
'host stały' => 'fixed host',
'Brak dozwolonych hostów.' => 'No allowed hosts.',
'Brak przypisanych baz danych.' => 'No assigned databases.',
'Uprawnienia' => 'Privileges',
'Uprawnienia początkowe' => 'Initial privileges',
'Uprawnienia dla przypisywanych baz' => 'Privileges for assigned databases',
'Domyślny dostęp' => 'Default access',
'`localhost` jest dodawany automatycznie.' => '`localhost` is added automatically.',
'Brak baz do przypisania.' => 'No databases to assign.',
'Hosty zdalne są wyłączone w konfiguracji pluginu (`allow_remote_hosts=0`).' => 'Remote hosts are disabled in plugin configuration (`allow_remote_hosts=0`).',
'Brak przypisań' => 'No assignments',
'Tryb prosty tworzy użytkownika o tej samej nazwie co baza i automatycznie generuje hasło.'
=> 'Simple mode creates a user with the same name as the database and generates a password automatically.',
'Użytkownik o tej samej nazwie i bezpiecznym haśle zostanie wygenerowany automatycznie.'
=> 'A user with the same name and a secure password will be generated automatically.',
'Tryb zaawansowany' => 'Advanced mode',
'Nowy użytkownik' => 'New user',
'Istniejący użytkownik' => 'Existing user',
'Istniejący użytkownik' => 'Existing user',
'Nazwa użytkownika PostgreSQL' => 'PostgreSQL username',
'Hasło użytkownika' => 'User password',
'W trybie zaawansowanym hasło jest wymagane.' => 'Password is required in advanced mode.',
'UTWÓRZ' => 'CREATE',
'Dane dostępowe do bazy danych' => 'Database access credentials',
'Dane dostępowe użytkownika PostgreSQL' => 'PostgreSQL user access credentials',
'Nazwa hosta:' => 'Host name:',
'Baza danych:' => 'Database:',
'Nazwa użytkownika:' => 'Username:',
'Hasło:' => 'Password:',
'Przypisane bazy danych:' => 'Assigned databases:',
'Wykorzystano limit baz danych (' => 'Database limit reached (',
'Bez ograniczeń' => 'Unlimited',
'Nie znaleziono baz danych...' => 'No databases found...',
'Ilość baz danych' => 'Database count',
'Lista wszystkich baz danych wraz z liczbą użytkowników' => 'List of all databases with user count',
' i rozmiarem bazy' => ' and database size',
' oraz liczbą tabel' => ' and table count',
'Backupy wykonywane przez użytkownika zapisywane są w: ' => 'User backups are stored in: ',
'Czy na pewno chcesz usunąć backup bazy danych - ' => 'Are you sure you want to delete the database backup - ',
'Czy na pewno chcesz usunąć użytkownika ' => 'Are you sure you want to delete user ',
'Potwierdź usunięcie backupu' => 'Confirm backup deletion',
'Czy na pewno chcesz usunąć następującą bazę danych - ' => 'Are you sure you want to delete the following database - ',
'Do bazy danych ' => 'For database ',
'przypisani są użytkownicy, z poniższej listy wybierz użytkowników, których chcesz usunąć.'
=> 'there are users assigned. Select which users you want to delete from the list below.',
'Wybierz' => 'Select',
'Przypisane Bazy danych' => 'Assigned databases',
'Do wskazanej bazy nie są przypisani użytkownicy PostgreSQL.' => 'No PostgreSQL users are assigned to the selected database.',
'Potwierdź usunięcie bazy' => 'Confirm database deletion',
'Uwaga: wskazany użytkownik jest przypisany do wielu baz danych i jego usunięcie spowoduje brak możliwości dostępu do innych baz danych przy użyciu danych logowania użytkownika '
=> 'Warning: the selected user is assigned to multiple databases and deleting this user will remove access to other databases using the credentials of user ',
'Uwaga użytkownik ' => 'Warning: user ',
' jest przypisany do następującej bazy danych ' => ' is assigned to the following database ',
' jest przypisany do następujących baz danych:' => ' is assigned to the following databases:',
'Usuwanie Użytkowników' => 'Delete users',
'Brak użytkowników do usunięcia.' => 'No users to delete.',
'Przypisane bazy' => 'Assigned databases',
'Bazy, których jest właścicielem' => 'Databases owned',
'Usuń zaznaczonych użytkowników' => 'Delete selected users',
'Usuń użytkownika' => 'Delete user',
'Potwierdzenie Usuwania Użytkowników' => 'Confirm user deletion',
'Brak poprawnych użytkowników do usunięcia.' => 'No valid users to delete.',
'Potwierdź usunięcie zaznaczonych użytkowników PostgreSQL. Operacja jest nieodwracalna.'
=> 'Confirm deletion of selected PostgreSQL users. This action is irreversible.',
'Potwierdź usunięcie' => 'Confirm deletion',
'Potwierdź usunięcie użytkownika' => 'Confirm user deletion',
'Usuwanie Baz Danych' => 'Delete databases',
'Brak baz do usunięcia.' => 'No databases to delete.',
'Przypisani użytkownicy' => 'Assigned users',
'Usuń zaznaczone bazy' => 'Delete selected databases',
'Potwierdzenie Usuwania Baz Danych' => 'Confirm database deletion',
'Brak poprawnych baz do usunięcia.' => 'No valid databases to delete.',
'Potwierdź usunięcie baz danych. Operacja jest nieodwracalna.' => 'Confirm deletion of databases. This action is irreversible.',
'Usuń także użytkowników przypisanych do usuwanych baz (jeżeli po usunięciu baz nie mają już innych przypisań)'
=> 'Also delete users assigned to deleted databases (if they have no other assignments after deletion)',
'Przypisywanie Bazy do Użytkownika' => 'Assign database to user',
'-- wybierz bazę --' => '-- select database --',
'-- wybierz użytkownika --' => '-- select user --',
'-- wybierz nowego użytkownika --' => '-- select new user --',
'-- wybierz istniejącego użytkownika --' => '-- select existing user --',
'Przypisz bazę' => 'Assign database',
'Hosty Zdalne Użytkownika' => 'User remote hosts',
'Zarządzanie hostami zdalnymi jest wyłączone (`allow_remote_hosts=0`).'
=> 'Remote host management is disabled (`allow_remote_hosts=0`).',
'Informacja' => 'Information',
'Dozwolone są wyłącznie pojedyncze adresy IPv4, bez CIDR i bez nazw hostów.'
=> 'Only single IPv4 addresses are allowed, without CIDR and without hostnames.',
'Aktualne hosty' => 'Current hosts',
'Brak zapisanych hostów.' => 'No hosts saved.',
'usuń' => 'remove',
'Nieprawidłowy lub wygasły token CSRF.' => 'Invalid or expired CSRF token.',
'Wskaż ścieżkę do pliku SQL lub SQL.GZ.' => 'Provide a path to SQL or SQL.GZ file.',
'Nie można odczytać pliku: ' => 'Cannot read file: ',
'Ścieżka pliku musi znajdować się w katalogu /home/' => 'File path must be under /home/',
'Dozwolone są wyłącznie pliki .sql, .gz lub .sql.gz.' => 'Only .sql, .gz or .sql.gz files are allowed.',
'Nie można utworzyć katalogu: ' => 'Cannot create directory: ',
'Nie można utworzyć katalogu upload: ' => 'Cannot create upload directory: ',
'Nie udało się skopiować pliku do kolejki restore.' => 'Failed to copy file to restore queue.',
'Nie udało się zapisać przesłanego pliku do kolejki restore.' => 'Failed to save uploaded file to restore queue.',
'Nie udało się przesłać pliku backupu (kod błędu: ' => 'Upload failed (error code: ',
'Nie można odczytać przesłanego pliku backupu.' => 'Cannot read uploaded backup file.',
'Wybierz plik .sql, .gz lub .sql.gz do przywrócenia.' => 'Select a .sql, .gz or .sql.gz file to restore.',
'Tryb przesyłania został zaktualizowany. Odśwież stronę i spróbuj ponownie.' => 'Upload mode has been updated. Refresh the page and try again.',
'Nie znaleziono zadania lub brak dostępu.' => 'Job not found or access denied.',
'Nieprawidłowy identyfikator zadania.' => 'Invalid job ID.',
'Nie znaleziono zadania.' => 'Job not found.',
'Nie znaleziono logu.' => 'Log not found.',
'Nie udało się połączyć z PostgreSQL.' => 'Failed to connect to PostgreSQL.',
'Nie znaleziono bazy danych: ' => 'Database not found: ',
'Brak uprawnień do nadania.' => 'No privileges to grant.',
'Nieprawidłowy identyfikator PostgreSQL: ' => 'Invalid PostgreSQL identifier: ',
'Identyfikator PostgreSQL przekracza 63 znaki: ' => 'PostgreSQL identifier exceeds 63 characters: ',
'Nie udało się bezpiecznie zacytować wartości SQL.' => 'Failed to safely quote SQL value.',
'Niepoprawny format pliku danych PostgreSQL: ' => 'Invalid PostgreSQL credentials file format: ',
'Brak pliku z danymi PostgreSQL: ' => 'Missing PostgreSQL credentials file: ',
'Brak identyfikatora zadania backupu.' => 'Missing backup job ID.',
'Plik backupu nie istnieje na serwerze.' => 'Backup file does not exist on the server.',
'Brak uprawnień odczytu pliku backupu.' => 'No read permission for backup file.',
'Nie można pobrać backupu (nagłówki zostały już wysłane).' => 'Cannot download backup (headers already sent).',
'Nie można odczytać pliku backupu do pobrania.' => 'Cannot read backup file for download.',
'Tworzenie backupu jest wyłączone w konfiguracji pluginu.' => 'Backup creation is disabled in plugin configuration.',
'Wskazana baza danych nie należy do konta: ' => 'Selected database does not belong to this account: ',
'Osiągnięto limit baz danych dla konta (' => 'Database limit reached (',
'Baza danych już istnieje: ' => 'Database already exists: ',
'Wybrany użytkownik PostgreSQL nie istnieje: ' => 'Selected PostgreSQL user does not exist: ',
'Użytkownik PostgreSQL już istnieje: ' => 'PostgreSQL user already exists: ',
'Hasło użytkownika jest wymagane w trybie zaawansowanym.' => 'Password is required in advanced mode.',
'Hasło użytkownika musi mieć co najmniej 12 znaków.' => 'Password must be at least 12 characters.',
'Nowe hasło musi mieć minimum 12 znaków.' => 'New password must be at least 12 characters.',
'Przekroczono limit hostów dla użytkownika PostgreSQL.' => 'Host limit exceeded for PostgreSQL user.',
'Host lokalny nie może zostać usunięty.' => 'Local host cannot be removed.',
'Baza i uprawnienia zapisane, ale synchronizacja pg_hba nie powiodła się: '
=> 'Database and privileges saved, but pg_hba sync failed: ',
'Baza danych została utworzona.' => 'Database has been created.',
'Kopia zapasowa bazy danych ' => 'Backup of database ',
' została dodana do kolejki' => ' has been added to the queue',
'Nie wskazano bazy danych do usunięcia.' => 'No database selected for deletion.',
'Usuwanie wykonane, ale synchronizacja pg_hba nie powiodła się: '
=> 'Deletion completed, but pg_hba sync failed: ',
'Usunięto bazę danych ' => 'Deleted database ',
' i użytkownika ' => ' and user ',
' i następujących użytkowników przypisanych do bazy danych '
=> ' and the following users assigned to database ',
'Usunięto bazę danych: ' => 'Database deleted: ',
'Usunięto użytkowników: ' => 'Users deleted: ',
'Nie udało się usunąć części użytkowników: ' => 'Failed to delete some users: ',
'Nie można odczytać statusów zadań backup/restore: ' => 'Cannot read backup/restore job statuses: ',
'Wskazane zadanie nie jest zadaniem backupu.' => 'Selected job is not a backup job.',
'Backup nie został zakończony powodzeniem.' => 'Backup did not complete successfully.',
'nieznana baza' => 'unknown database',
'Usunięto plik backupu: ' => 'Deleted backup file: ',
'Nie można pobrać backupu: ' => 'Cannot download backup: ',
'Brak dostępu do pluginu.' => 'No access to plugin.',
'Brak parametru job.' => 'Missing job parameter.',
'Plik backupu nie istnieje lub brak odczytu.' => 'Backup file does not exist or is not readable.',
'Nagłówki zostały już wysłane.' => 'Headers have already been sent.',
'Baza nie należy do konta DA: ' => 'Database does not belong to the DirectAdmin account: ',
'Baza nie należy do bieżącego użytkownika DA: ' => 'Database does not belong to current DirectAdmin user: ',
'Użytkownik PostgreSQL nie należy do konta DA: ' => 'PostgreSQL user does not belong to the DirectAdmin account: ',
'Użytkownik PostgreSQL nie należy do bieżącego konta DA: ' => 'PostgreSQL user does not belong to current DirectAdmin account: ',
'Nieprawidłowy użytkownik PostgreSQL: ' => 'Invalid PostgreSQL user: ',
'Wybierz przynajmniej jedno uprawnienie.' => 'Select at least one privilege.',
'Odebrano dostęp użytkownikowi ' => 'Revoked access for user ',
'Przyznano dostęp użytkownikowi ' => 'Granted access to user ',
'Zaktualizowano uprawnienia użytkownika ' => 'Updated privileges for user ',
'Przypisanie wykonane, ale synchronizacja pg_hba nie powiodła się: '
=> 'Assignment completed, but pg_hba sync failed: ',
'Przypisano bazę ' => 'Assigned database ',
'Utworzono użytkownika ' => 'Created user ',
' bez przypisanych baz danych.' => ' without assigned databases.',
'Usunięto użytkownika ' => 'Deleted user ',
'Nie można usunąć użytkownika, ponieważ jest właścicielem baz: '
=> 'Cannot delete user because the user owns databases: ',
'Użytkownik został utworzony, ale synchronizacja pg_hba nie powiodła się: '
=> 'User was created, but pg_hba sync failed: ',
'Brak użytkowników PostgreSQL do zarządzania.' => 'No PostgreSQL users to manage.',
'Przyznano dostęp do bazy ' => 'Granted access to database ',
'Odebrano dostęp do bazy ' => 'Revoked access to database ',
'Dodano host ' => 'Added host ',
'Usunięto host ' => 'Removed host ',
'Host dodany, ale synchronizacja pg_hba nie powiodła się: ' => 'Host added, but pg_hba sync failed: ',
'Host usunięty, ale synchronizacja pg_hba nie powiodła się: ' => 'Host removed, but pg_hba sync failed: ',
'Nie wybrano użytkowników do usunięcia.' => 'No users selected for deletion.',
'Nie usunięto: ' => 'Not deleted: ',
'Nie wybrano baz danych do usunięcia.' => 'No databases selected for deletion.',
'Pominięto bazę spoza konta: ' => 'Skipped database outside account: ',
'Operacja wykonana, ale synchronizacja pg_hba nie powiodła się: '
=> 'Operation completed, but pg_hba sync failed: ',
'Usunięto bazy: ' => 'Databases deleted: ',
'Usunięto użytkowników powiązanych z usuniętymi bazami: '
=> 'Users linked to deleted databases were removed: ',
'Użytkownicy pozostawieni: ' => 'Users kept: ',
'Zarządzanie hostami zdalnymi jest wyłączone (`allow_remote_hosts=0`).' => 'Remote host management is disabled (`allow_remote_hosts=0`).',
'Hosty zapisane, ale synchronizacja pg_hba nie powiodła się: ' => 'Hosts saved, but pg_hba sync failed: ',
'Zaktualizowano hosty dostępu dla użytkownika ' => 'Updated access hosts for user ',
];
+50
View File
@@ -0,0 +1,50 @@
<?php
$en = include __DIR__ . '/en.php';
function pl_normalize_text(string $text): string
{
static $replacements = null;
if ($replacements === null) {
$replacements = [
'Pulpit' => 'Pulpit',
'Bazy danych' => 'Bazy danych',
'Przywróć Backup' => 'Przywróć Backup',
'Przeciągnij plik backupu tutaj' => 'Przeciągnij plik backupu tutaj',
'lub kliknij, aby wybrać plik' => 'lub kliknij, aby wybrać plik',
'Plik' => 'Plik',
'Rozmiar' => 'Rozmiar',
'Usuń plik' => 'Usuń plik',
'Wersja serwera PostgreSQL' => 'Wersja serwera PostgreSQL',
'Powrót' => 'Powrót',
'Błąd' => 'Błąd',
'ZARZĄDZAJ BAZAMI DANYCH' => 'ZARZĄDZAJ BAZAMI DANYCH',
'ZARZĄDZAJ UŻYTKOWNIKAMI' => 'ZARZĄDZAJ UŻYTKOWNIKAMI',
'WGRAJ BACKUP' => 'WGRAJ BACKUP',
'PHPPGADMIN' => 'PHPPGADMIN',
'Pobieranie...' => 'Pobieranie...',
'Nieudana odpowiedź serwera ({code})' => 'Nieudana odpowiedź serwera ({code})',
'Nieudana odpowiedź serwera (HTML zamiast pliku, URL: {url})' => 'Nieudana odpowiedź serwera (HTML zamiast pliku, URL: {url})',
'Serwer zwrócił pusty plik.' => 'Serwer zwrócił pusty plik.',
'Nie można pobrać pliku backupu.' => 'Nie można pobrać pliku backupu.',
'Pobierz plik backupu' => 'Pobierz plik backupu',
'Brak plików .sql/.gz' => 'Brak plików .sql/.gz',
'Brak katalogów' => 'Brak katalogów',
'Nie udało się wczytać katalogów.' => 'Nie udało się wczytać katalogów.',
'Podaj nazwę katalogu.' => 'Podaj nazwę katalogu.',
'Nie udało się utworzyć katalogu.' => 'Nie udało się utworzyć katalogu.',
];
}
if (isset($replacements[$text])) {
return $replacements[$text];
}
return $text;
}
$pl = [];
foreach ($en as $key => $_value) {
$pl[$key] = pl_normalize_text((string)$key);
}
return $pl;
+10
View File
@@ -0,0 +1,10 @@
allow_url_fopen=Off
allow_url_include=Off
display_errors=Off
log_errors=On
error_reporting=E_ALL
session.use_strict_mode=1
file_uploads=On
upload_max_filesize=4096M
post_max_size=4096M
max_file_uploads=20
+50
View File
@@ -0,0 +1,50 @@
# Ustawienia pluginu PostgreSQL Manager.
# PG_PLUGIN_MAX_HOSTS_PER_USER - maksymalna liczba hostów zdalnych przypisanych do jednego użytkownika PostgreSQL.
PG_PLUGIN_MAX_HOSTS_PER_USER=30
# allow_remote_hosts - czy pokazywać UI i obsługę hostów zdalnych (1=tak, 0=nie).
# Gdy 1, synchronizacja hostów aktualizuje pg_hba, postgresql.conf oraz CSF.
allow_remote_hosts=1
# Aliasy kompatybilności wstecznej.
PG_PLUGIN_ALLOW_REMOTE_HOSTS=1
# PG_PLUGIN_SUDO_SYNC_HBA - ścieżka do skryptu synchronizacji pg_hba.conf.
PG_PLUGIN_SUDO_SYNC_HBA=/usr/local/directadmin/plugins/da-postgresql/scripts/setup/pg_hba_sync.sh
# PG_PLUGIN_DEFAULT_DB_LIMIT - domyślny limit baz dla użytkownika, gdy brak wartości w pakiecie/użytkowniku.
PG_PLUGIN_DEFAULT_DB_LIMIT=5
# ENABLE_BACKUP - czy pokazywać i umożliwiać tworzenie backupu baz danych (true/false).
ENABLE_BACKUP=true
# ENABLE_UPLOAD_BACKUP - czy pokazywać UI i obsługę przywracania backupu (true/false).
ENABLE_UPLOAD_BACKUP=true
# ENABLE_ADMINER - czy pokazywać integrację Adminera w panelu pluginu (true/false).
ENABLE_ADMINER=false
# ADMINER_PUBLIC - czy pozwalać na bezpośredni dostęp do /adminer bez SSO (true/false).
# Gdy true: dostęp publiczny do formularza logowania Adminera (bez autologowania), tylko PostgreSQL.
# Gdy false: wejście bez ticketu SSO pokaże placeholder informujący o dostępie przez DirectAdmin.
# Zmiana zaczyna działać po następnym odświeżeniu pluginu (synchronizacja runtime/config.json).
ADMINER_PUBLIC=false
# ADMINER_PUBLIC_BASE_URL - opcjonalny publiczny adres WWW (bez końcowego /), np. https://panel.example.com.
# Gdy pusty, plugin automatycznie użyje aktualnego hosta bez portu DirectAdmin (np. bez :2222).
ADMINER_PUBLIC_BASE_URL=
# ADMINER_URL_PATH - ścieżka URL aliasu Adminera.
ADMINER_URL_PATH=/adminer
# ADMINER_SSO_DIR - katalog runtime ticketów SSO dla Adminera.
ADMINER_SSO_DIR=/var/www/html/adminer/runtime
# ADMINER_TICKET_TTL - ważność ticketu SSO (sekundy).
ADMINER_TICKET_TTL=120
# ADMINER_SESSION_TTL - maksymalny czas sesji Adminera otwartej z pluginu (sekundy).
ADMINER_SESSION_TTL=900
# ADMINER_ROLE_TTL - ważność tymczasowej roli PostgreSQL dla sesji Adminera (sekundy).
ADMINER_ROLE_TTL=1200
# ALLOW_RESTORE_TO_OTHER_DATABASE - czy pozwalać na przywracanie lokalnych backupów do innej bazy (true/false).
ALLOW_RESTORE_TO_OTHER_DATABASE=true
# ENABLE_TABLE_COUNT - czy zliczać tabele w bazach i pokazywać kolumnę Liczba Tabel (true/false).
ENABLE_TABLE_COUNT=true
# COUNT_DATABASE_SIZE - czy zliczać rozmiar baz i pokazywać kolumnę Rozmiar (true/false).
COUNT_DATABASE_SIZE=true
# COMPRESS_BACKUP - czy backupy tworzone przez plugin kompresować do .sql.gz (true/false).
COMPRESS_BACKUP=true
# HITME_BACKUP_LOCATION - główny katalog lokalnych backupów PostgreSQL.
HITME_BACKUP_LOCATION=/home/admin/postgres_backup
# USER_BASE_BACKUP_DIR - katalog bazowy backupów wykonywanych przez użytkownika (DA_USER podmieniany automatycznie).
USER_BASE_BACKUP_DIR=/home/DA_USER/postgres_backup
# USER_BACKUP_DATE_FORMAT - format katalogu daty backupu użytkownika (PHP date()).
USER_BACKUP_DATE_FORMAT=d.m.Y-H.i
+8
View File
@@ -0,0 +1,8 @@
name=da-postgresql
id=da-postgresql
type=user
author=HITME.PL
version=1.5.41
active=no
installed=no
user_run_as=root
+343
View File
@@ -0,0 +1,343 @@
#!/bin/bash
set -Eeuo pipefail
ERROR_MSG=""
BACKUP_OK=0
DA_USER=""
DB_NAME=""
JOB_ID=""
LOG_FILE=""
TMP_DB_USER=""
TMP_DB_PASS=""
TMP_DB_ACTIVE=0
JOB_FILE="${1:-}"
if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then
echo "da-postgresql: missing job file" >&2
exit 1
fi
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf"
LOG_DIR="${PLUGIN_DIR}/data/logs"
mkdir -p "$LOG_DIR"
urlencode() {
local s="$1"
local out=""
local i c
for ((i=0; i<${#s}; i++)); do
c="${s:i:1}"
case "$c" in
[a-zA-Z0-9.~_-]) out+="$c" ;;
' ') out+='%20' ;;
*) printf -v c '%%%02X' "'$c"; out+="$c" ;;
esac
done
printf '%s' "$out"
}
notify_user() {
local code="$1"
if [ -z "${DA_USER:-}" ]; then
return 0
fi
local subject message
if [ "$code" -eq 0 ] && [ "$BACKUP_OK" -eq 1 ]; then
subject="Backup bazy PostgreSQL zakończony pomyślnie"
message="Backup bazy ${DB_NAME} został wykonany. Plik: ${TARGET_FILE:-nieznany}."
else
subject="Błąd wykonywania backupu bazy PostgreSQL"
if [ -n "$ERROR_MSG" ]; then
message="Backup bazy ${DB_NAME} nie powiódł się: ${ERROR_MSG}"
else
message="Backup bazy ${DB_NAME} nie powiódł się. Sprawdź log zadania."
fi
fi
local task_queue="/usr/local/directadmin/data/task.queue"
local users="select1%3D$(urlencode "$DA_USER")"
local line="action=notify&value=users&subject=$(urlencode "$subject")&message=$(urlencode "$message")&users=${users}"
printf "%s\n" "$line" >> "$task_queue"
}
fail() {
local msg="$1"
ERROR_MSG="$msg"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${msg}" >> "$LOG_FILE"
fi
}
log_unexpected_error() {
local line_no="$1"
local command="$2"
local code="$3"
if [ -z "${LOG_FILE:-}" ]; then
return 0
fi
if [ -n "${ERROR_MSG:-}" ]; then
return 0
fi
ERROR_MSG="Nieoczekiwany błąd wykonywania backupu (kod ${code}, linia ${line_no})."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG} CMD: ${command}" >> "$LOG_FILE"
}
load_plugin_settings() {
if [ ! -f "$CONFIG_FILE" ]; then
fail "Brak pliku konfiguracyjnego pluginu: $CONFIG_FILE"
exit 1
fi
# shellcheck disable=SC1090
source "$CONFIG_FILE"
}
quote_sh_value() {
local value="$1"
value="${value//\'/\'\\\'\'}"
printf "'%s'" "$value"
}
set_job_var() {
local key="$1"
local value="$2"
local tmp
local owner_group=""
if [ -f "$JOB_FILE" ]; then
owner_group="$(stat -c '%u:%g' "$JOB_FILE" 2>/dev/null || stat -f '%u:%g' "$JOB_FILE" 2>/dev/null || true)"
fi
tmp="$(mktemp)"
grep -v "^${key}=" "$JOB_FILE" > "$tmp" 2>/dev/null || true
printf "%s=%s\n" "$key" "$(quote_sh_value "$value")" >> "$tmp"
mv "$tmp" "$JOB_FILE"
chmod 600 "$JOB_FILE" 2>/dev/null || true
if [ "$(id -u)" -eq 0 ] && [ -n "$owner_group" ]; then
chown "$owner_group" "$JOB_FILE" 2>/dev/null || true
fi
}
load_pg_credentials() {
local creds_file="/usr/local/directadmin/conf/postgresql.conf"
PGHOST_VAL=""
PGPORT_VAL=""
PGUSER_VAL=""
PGPASSWORD_VAL=""
if [ ! -r "$creds_file" ]; then
fail "Brak pliku poświadczeń PostgreSQL: $creds_file"
exit 1
fi
local line
line="$(grep -Ev '^[[:space:]]*($|#)' "$creds_file" | head -n1 || true)"
if [ -z "$line" ]; then
fail "Pusty plik poświadczeń PostgreSQL: $creds_file"
exit 1
fi
if [[ "$line" == *=* ]]; then
while IFS='=' read -r key value; do
key="$(echo "$key" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')"
value="$(echo "$value" | sed -E 's/[[:space:]]+#.*$//' | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
case "$key" in
PG_HOST) PGHOST_VAL="$value" ;;
PG_PORT) PGPORT_VAL="$value" ;;
PG_USER) PGUSER_VAL="$value" ;;
PG_PASSWORD) PGPASSWORD_VAL="$value" ;;
esac
done < <(grep -Ev '^[[:space:]]*($|#)' "$creds_file")
else
local rest
PGHOST_VAL="${line%%:*}"
rest="${line#*:}"
PGPORT_VAL="${rest%%:*}"
rest="${rest#*:}"
rest="${rest#*:}"
PGUSER_VAL="${rest%%:*}"
PGPASSWORD_VAL="${rest#*:}"
fi
[ -n "$PGHOST_VAL" ] || PGHOST_VAL="localhost"
[ -n "$PGPORT_VAL" ] || PGPORT_VAL="5432"
[ -n "$PGUSER_VAL" ] || PGUSER_VAL="postgres"
if [ -z "$PGPASSWORD_VAL" ]; then
fail "Brak hasła PostgreSQL w /usr/local/directadmin/conf/postgresql.conf"
exit 1
fi
}
resolve_bins() {
PSQL_BIN="$(command -v psql 2>/dev/null || true)"
PG_DUMP_BIN="$(command -v pg_dump 2>/dev/null || true)"
if [ -z "$PSQL_BIN" ] || [ -z "$PG_DUMP_BIN" ]; then
fail "Brak wymaganych binariów PostgreSQL (psql/pg_dump)."
exit 1
fi
}
sql_escape_literal() {
printf '%s' "$1" | sed "s/'/''/g"
}
psql_admin() {
local database="$1"
shift
PGPASSWORD="$PGPASSWORD_VAL" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$PGUSER_VAL" -d "$database" -v ON_ERROR_STOP=1 "$@"
}
cleanup_tmp_user() {
if [ "$TMP_DB_ACTIVE" -ne 1 ] || [ -z "${TMP_DB_USER:-}" ]; then
return 0
fi
psql_admin postgres -c "DROP ROLE IF EXISTS \"$TMP_DB_USER\";" >/dev/null 2>&1 || true
TMP_DB_ACTIVE=0
}
on_exit() {
local code="$1"
if [ "$code" -ne 0 ] && [ -z "${ERROR_MSG:-}" ] && [ -n "${LOG_FILE:-}" ]; then
ERROR_MSG="Zadanie backupu zakończyło się błędem (kod ${code}) bez zarejestrowanego szczegółu."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG}" >> "$LOG_FILE"
fi
cleanup_tmp_user
notify_user "$code"
}
trap 'log_unexpected_error "$LINENO" "$BASH_COMMAND" "$?"' ERR
trap 'on_exit $?' EXIT
random_tmp_password() {
local value
value="$(od -An -N48 -tx1 /dev/urandom 2>/dev/null | tr -d '[:space:]')"
if [ -z "$value" ]; then
value="$(date +%s%N)${RANDOM}${RANDOM}"
fi
printf '%s' "${value:0:32}"
}
# shellcheck disable=SC1090
source "$JOB_FILE"
JOB_ID="${JOB_ID:-$(basename "$JOB_FILE" .env)}"
DA_USER="${DA_USER:-}"
DB_NAME="${DB_NAME:-}"
DATE_DIR="${DATE_DIR:-$(date +%Y-%m-%d)}"
COMPRESS_BACKUP="${COMPRESS_BACKUP:-1}"
USER_BASE_BACKUP_DIR="${USER_BASE_BACKUP_DIR:-/home/${DA_USER}/postgres_backup}"
JOB_COMPRESS_BACKUP="${COMPRESS_BACKUP}"
JOB_USER_BASE_BACKUP_DIR="${USER_BASE_BACKUP_DIR}"
LOG_FILE="${LOG_DIR}/${JOB_ID}.log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Start backup job ${JOB_ID}" > "$LOG_FILE"
if [ -z "$DA_USER" ] || [ -z "$DB_NAME" ]; then
fail "Brak DA_USER lub DB_NAME w pliku zadania."
exit 1
fi
if [[ "$DB_NAME" != "${DA_USER}_"* ]]; then
fail "Baza ${DB_NAME} nie należy do użytkownika ${DA_USER}."
exit 1
fi
load_plugin_settings
COMPRESS_BACKUP="${JOB_COMPRESS_BACKUP:-$COMPRESS_BACKUP}"
USER_BASE_BACKUP_DIR="${JOB_USER_BASE_BACKUP_DIR:-$USER_BASE_BACKUP_DIR}"
load_pg_credentials
resolve_bins
if [[ "$COMPRESS_BACKUP" =~ ^(true|yes|on)$ ]]; then
COMPRESS_BACKUP="1"
fi
if [[ "$COMPRESS_BACKUP" =~ ^(false|no|off)$ ]]; then
COMPRESS_BACKUP="0"
fi
TARGET_DIR="${USER_BASE_BACKUP_DIR%/}/${DATE_DIR}"
if [[ "$DATE_DIR" == *".."* || "$DATE_DIR" == */* ]]; then
fail "Nieprawidłowa nazwa katalogu backupu."
exit 1
fi
if ! mkdir -p "$TARGET_DIR" 2>>"$LOG_FILE"; then
fail "Brak dostępu do katalogu backupu: ${TARGET_DIR}. Uruchom worker jako root lub popraw uprawnienia USER_BASE_BACKUP_DIR."
exit 1
fi
if ! chmod 755 "$TARGET_DIR" >/dev/null 2>&1; then
fail "Nie można ustawić uprawnień katalogu backupu: ${TARGET_DIR}."
exit 1
fi
if [ "$(id -u)" -eq 0 ] && [ -n "$DA_USER" ]; then
chown "$DA_USER:$DA_USER" "$TARGET_DIR" 2>/dev/null || true
fi
TARGET_FILE="${TARGET_DIR}/${DB_NAME}.sql"
if [ "$COMPRESS_BACKUP" = "1" ]; then
TARGET_FILE="${TARGET_FILE}.gz"
fi
set_job_var "TARGET_FILE" "$TARGET_FILE"
DB_ESC="$(sql_escape_literal "$DB_NAME")"
DB_EXISTS="$(psql_admin postgres -At -c "SELECT 1 FROM pg_database WHERE datname='${DB_ESC}'" 2>/dev/null || true)"
if [ -z "$DB_EXISTS" ]; then
fail "Baza danych ${DB_NAME} nie istnieje."
exit 1
fi
TMP_DB_USER="pgtmp_bkp_$(printf '%s' "$JOB_ID" | tr -cd 'A-Za-z0-9' | tr '[:upper:]' '[:lower:]' | head -c 18)"
[ -n "$TMP_DB_USER" ] || TMP_DB_USER="pgtmp_bkp_$(date +%s)"
TMP_DB_PASS="$(random_tmp_password)"
PASS_ESC="$(sql_escape_literal "$TMP_DB_PASS")"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tworzenie tymczasowego użytkownika backupu ${TMP_DB_USER}" >> "$LOG_FILE"
psql_admin postgres -c "SET password_encryption='scram-sha-256'; DROP ROLE IF EXISTS \"$TMP_DB_USER\"; CREATE ROLE \"$TMP_DB_USER\" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION PASSWORD '${PASS_ESC}';" >> "$LOG_FILE" 2>&1
TMP_DB_ACTIVE=1
psql_admin postgres -c "GRANT CONNECT, TEMPORARY ON DATABASE \"$DB_NAME\" TO \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1
psql_admin "$DB_NAME" -c "GRANT USAGE ON SCHEMA public TO \"$TMP_DB_USER\"; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"$TMP_DB_USER\"; GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO \"$TMP_DB_USER\"; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tworzenie backupu bazy ${DB_NAME}" >> "$LOG_FILE"
if [ "$COMPRESS_BACKUP" = "1" ]; then
set +e
PGPASSWORD="$TMP_DB_PASS" "$PG_DUMP_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$TMP_DB_USER" -d "$DB_NAME" --no-owner --no-privileges --format=plain 2>>"$LOG_FILE" | gzip -9 > "$TARGET_FILE"
RC=$?
set -e
else
set +e
PGPASSWORD="$TMP_DB_PASS" "$PG_DUMP_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$TMP_DB_USER" -d "$DB_NAME" --no-owner --no-privileges --format=plain > "$TARGET_FILE" 2>>"$LOG_FILE"
RC=$?
set -e
fi
if [ "$RC" -ne 0 ]; then
fail "Nie udało się utworzyć backupu bazy ${DB_NAME}."
rm -f "$TARGET_FILE" >/dev/null 2>&1 || true
exit 1
fi
if [ "$(id -u)" -eq 0 ] && [ -n "$DA_USER" ]; then
chown "$DA_USER:$DA_USER" "$TARGET_FILE" 2>/dev/null || true
fi
chmod 644 "$TARGET_FILE" 2>/dev/null || true
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup zapisany: ${TARGET_FILE}" >> "$LOG_FILE"
CURRENT_EXT=".sql"
if [ "$COMPRESS_BACKUP" = "1" ]; then
CURRENT_EXT="${CURRENT_EXT}.gz"
fi
CURRENT_FILE="${TARGET_DIR}/${DB_NAME}-current${CURRENT_EXT}"
if [ -n "$USER_BASE_BACKUP_DIR" ] && [ -d "$USER_BASE_BACKUP_DIR" ]; then
find "$USER_BASE_BACKUP_DIR" -maxdepth 3 -type f -name "${DB_NAME}-current.sql*" -exec rm -f {} + >/dev/null 2>&1 || true
fi
ln -sfn "$TARGET_FILE" "$CURRENT_FILE" 2>/dev/null || cp -f "$TARGET_FILE" "$CURRENT_FILE" >/dev/null 2>&1 || true
if [ "$(id -u)" -eq 0 ] && [ -n "$DA_USER" ]; then
chown "$DA_USER:$DA_USER" "$CURRENT_FILE" 2>/dev/null || true
fi
chmod 644 "$CURRENT_FILE" 2>/dev/null || true
BACKUP_OK=1
exit 0
+156
View File
@@ -0,0 +1,156 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CPIF="/usr/local/directadmin/data/admin/custom_package_items.conf"
SUDOERS_FILE="/etc/sudoers.d/directadmin-postgresql-plugin"
CRON_FILE="/etc/cron.d/da-postgresql-jobs"
IS_ROOT=0
if [ "$(id -u)" -eq 0 ]; then
IS_ROOT=1
fi
set_permissions() {
local owner="$1"
local mode="$2"
local path="$3"
if [ -e "$path" ]; then
chown "$owner" "$path" 2>/dev/null || true
chmod "$mode" "$path" 2>/dev/null || true
fi
}
set_mode_if_exists() {
local mode="$1"
local path="$2"
if [ -e "$path" ]; then
chmod "$mode" "$path" 2>/dev/null || true
fi
}
bootstrap_custom_package_items() {
if [ "$IS_ROOT" -ne 1 ]; then
echo "postgresql: warning: uruchom jako root, aby zaktualizować $CPIF" >&2
return 0
fi
touch "$CPIF"
sed -i '/^postgresql_enabled=/d' "$CPIF" || true
sed -i '/^postgresql=/d' "$CPIF" || true
sed -i '/^upostgresql=/d' "$CPIF" || true
sed -i '/^postgresql_max_databases=/d' "$CPIF" || true
sed -i '/^postgresql_unlimited=/d' "$CPIF" || true
sed -i '/^upostgresql_max_databases=/d' "$CPIF" || true
cat >> "$CPIF" <<'EOF'
postgresql_enabled=type=checkbox&string=Enable da-postgresql&desc=Zezwól na dostęp do baz danych PostgreSQL&default=no
postgresql=type=text&string=Bazy PostgreSQL&desc=&default=5
upostgresql=type=checkbox&string=Bez ograniczeń&desc=&default=no&custom=autofocus%20onfocus%3D%22%28function%28c%29%7Bif%28c.dataset.pgInit%3D%3D%3D%271%27%29%7Breturn%3B%7Dc.dataset.pgInit%3D%271%27%3Bvar%20l%3Ddocument.querySelector%28%22input%5Bname%3D%27postgresql%27%5D%22%29%3Bif%28%21l%29%7Breturn%3B%7Dvar%20r%3Dc.closest%28%27tr%27%29%3Bvar%20td%3Dl.closest%28%27td%27%29%3Bif%28r%26%26td%29%7Bvar%20w%3Ddocument.getElementById%28%27da-pg-unlim-wrap%27%29%3Bif%28%21w%29%7Bw%3Ddocument.createElement%28%27label%27%29%3Bw.id%3D%27da-pg-unlim-wrap%27%3Bw.style.display%3D%27inline-flex%27%3Bw.style.alignItems%3D%27center%27%3Bw.style.gap%3D%278px%27%3Bw.style.marginLeft%3D%2712px%27%3Bw.appendChild%28c%29%3Bw.appendChild%28document.createTextNode%28%27Bez%20ogranicze%C5%84%27%29%29%3Btd.style.display%3D%27flex%27%3Btd.style.alignItems%3D%27center%27%3Btd.style.justifyContent%3D%27space-between%27%3Btd.appendChild%28w%29%3B%7Dr.style.display%3D%27none%27%3B%7Dvar%20s%3Dfunction%28%29%7Bl.disabled%3D%21%21c.checked%3B%7D%3Bc.addEventListener%28%27change%27%2Cs%29%3Bs%28%29%3B%7D%29%28this%29%22%20onchange%3D%22var%20l%3Ddocument.querySelector%28%22input%5Bname%3D%27postgresql%27%5D%22%29%3Bif%28l%29%7Bl.disabled%3D%21%21this.checked%3B%7D%22
EOF
set_permissions diradmin:diradmin 640 "$CPIF"
}
bootstrap_sudoers() {
if [ "$IS_ROOT" -ne 1 ]; then
echo "postgresql: warning: uruchom jako root, aby dodać sudoers dla synchronizacji pg_hba" >&2
return 0
fi
cat > "$SUDOERS_FILE" <<SUDO
# DirectAdmin PostgreSQL plugin
diradmin ALL=(root) NOPASSWD: /usr/local/directadmin/plugins/da-postgresql/scripts/setup/pg_hba_sync.sh
SUDO
chmod 440 "$SUDOERS_FILE"
}
bootstrap_worker_cron() {
if [ "$IS_ROOT" -ne 1 ]; then
echo "postgresql: warning: uruchom jako root, aby dodać cron workera backup/restore" >&2
return 0
fi
touch "$CRON_FILE"
if ! grep -Fq '/usr/local/directadmin/plugins/da-postgresql/scripts/worker.sh' "$CRON_FILE"; then
echo '* * * * * root /usr/local/directadmin/plugins/da-postgresql/scripts/worker.sh >/dev/null 2>&1' >> "$CRON_FILE"
fi
chmod 644 "$CRON_FILE" 2>/dev/null || true
}
bootstrap_adminer() {
local adminer_setup="$PLUGIN_DIR/scripts/setup/adminer_install.sh"
if [ "$IS_ROOT" -ne 1 ]; then
echo "postgresql: warning: uruchom jako root, aby zainstalować Adminera." >&2
return 0
fi
if [ ! -f "$adminer_setup" ]; then
echo "postgresql: error: brak skryptu instalacji Adminera: $adminer_setup" >&2
return 1
fi
bash "$adminer_setup"
}
activate_plugin() {
local conf="$PLUGIN_DIR/plugin.conf"
if [ -f "$conf" ]; then
sed -i 's/^active=.*/active=yes/' "$conf" || true
sed -i 's/^installed=.*/installed=yes/' "$conf" || true
fi
}
mkdir -p "$PLUGIN_DIR/data"
mkdir -p "$PLUGIN_DIR/exec" "$PLUGIN_DIR/exec/lib" "$PLUGIN_DIR/exec/handlers"
mkdir -p "$PLUGIN_DIR/data/jobs/pending" "$PLUGIN_DIR/data/jobs/processing" "$PLUGIN_DIR/data/jobs/done"
mkdir -p "$PLUGIN_DIR/data/logs" "$PLUGIN_DIR/data/lock" "$PLUGIN_DIR/data/pids" "$PLUGIN_DIR/data/cancel"
mkdir -p "$PLUGIN_DIR/data/uploads"
if [ "$IS_ROOT" -eq 1 ]; then
chown -R diradmin:diradmin "$PLUGIN_DIR" 2>/dev/null || true
else
echo "postgresql: warning: uruchom jako root, aby ustawić właściciela plików pluginu (w przeciwnym razie może wystąpić biała strona)." >&2
fi
find "$PLUGIN_DIR" -type d -exec chmod 755 {} + 2>/dev/null || true
set_mode_if_exists 700 "$PLUGIN_DIR/data"
set_mode_if_exists 700 "$PLUGIN_DIR/data/jobs"
set_mode_if_exists 700 "$PLUGIN_DIR/data/jobs/pending"
set_mode_if_exists 700 "$PLUGIN_DIR/data/jobs/processing"
set_mode_if_exists 700 "$PLUGIN_DIR/data/jobs/done"
set_mode_if_exists 700 "$PLUGIN_DIR/data/logs"
set_mode_if_exists 700 "$PLUGIN_DIR/data/lock"
set_mode_if_exists 700 "$PLUGIN_DIR/data/pids"
set_mode_if_exists 700 "$PLUGIN_DIR/data/cancel"
set_mode_if_exists 700 "$PLUGIN_DIR/data/uploads"
find "$PLUGIN_DIR" -type f -name '*.html' -exec chmod 755 {} + 2>/dev/null || true
find "$PLUGIN_DIR" -type f -name '*.raw' -exec chmod 755 {} + 2>/dev/null || true
find "$PLUGIN_DIR" -type f -name '*.css' -exec chmod 644 {} + 2>/dev/null || true
find "$PLUGIN_DIR/scripts" -type f -name '*.sh' -exec chmod 755 {} + 2>/dev/null || true
find "$PLUGIN_DIR" -type f -name '*.php' -exec chmod 640 {} + 2>/dev/null || true
set_mode_if_exists 644 "$PLUGIN_DIR/plugin.conf"
set_mode_if_exists 644 "$PLUGIN_DIR/plugin-settings.conf"
set_mode_if_exists 644 "$PLUGIN_DIR/php.ini"
if [ ! -f "$PLUGIN_DIR/data/secret.key" ]; then
tr -dc 'a-f0-9' </dev/urandom | head -c 64 > "$PLUGIN_DIR/data/secret.key" || true
fi
set_mode_if_exists 600 "$PLUGIN_DIR/data/secret.key"
touch "$PLUGIN_DIR/error.log" 2>/dev/null || true
if [ "$IS_ROOT" -eq 1 ]; then
chown diradmin:diradmin "$PLUGIN_DIR/error.log" 2>/dev/null || true
fi
set_mode_if_exists 640 "$PLUGIN_DIR/error.log"
bootstrap_custom_package_items
bootstrap_sudoers
bootstrap_worker_cron
bootstrap_adminer
activate_plugin
echo "da-postgresql: instalacja zakończona."
echo "Przypomnienie: zapisz pakiety i user.conf w DirectAdmin, aby odświeżyć custom package items."
+578
View File
@@ -0,0 +1,578 @@
#!/bin/bash
set -Eeuo pipefail
ERROR_MSG=""
RESTORE_OK=0
DA_USER=""
DB_NAME=""
JOB_ID=""
LOG_FILE=""
RESTORE_MODE=""
SOURCE_DB_NAME=""
USE_OWNER_RESTORE=0
USE_SANITIZE=0
PRE_CLEAN=0
TARGET_DB_EMPTY=0
TMP_DB_USER=""
TMP_DB_PASS=""
TMP_DB_ACTIVE=0
ORIG_DB_OWNER=""
DB_OWNER_SWITCHED=0
JOB_FILE="${1:-}"
if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then
echo "da-postgresql: missing job file" >&2
exit 1
fi
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf"
LOG_DIR="${PLUGIN_DIR}/data/logs"
mkdir -p "$LOG_DIR"
urlencode() {
local s="$1"
local out=""
local i c
for ((i=0; i<${#s}; i++)); do
c="${s:i:1}"
case "$c" in
[a-zA-Z0-9.~_-]) out+="$c" ;;
' ') out+='%20' ;;
*) printf -v c '%%%02X' "'$c"; out+="$c" ;;
esac
done
printf '%s' "$out"
}
notify_user() {
local code="$1"
if [ -z "${DA_USER:-}" ]; then
return 0
fi
local subject message
if [ "$code" -eq 0 ] && [ "$RESTORE_OK" -eq 1 ]; then
subject="Przywracanie bazy PostgreSQL zakończone pomyślnie"
message="Przywracanie bazy ${DB_NAME} z pliku ${SOURCE_FILE:-nieznany} zakończone."
else
subject="Błąd przywracania bazy PostgreSQL"
if [ -n "$ERROR_MSG" ]; then
message="Przywracanie bazy ${DB_NAME} nie powiodło się: ${ERROR_MSG}"
else
message="Przywracanie bazy ${DB_NAME} nie powiodło się. Sprawdź log zadania."
fi
fi
local task_queue="/usr/local/directadmin/data/task.queue"
local users="select1%3D$(urlencode "$DA_USER")"
local line="action=notify&value=users&subject=$(urlencode "$subject")&message=$(urlencode "$message")&users=${users}"
printf "%s\n" "$line" >> "$task_queue"
}
fail() {
local msg="$1"
ERROR_MSG="$msg"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${msg}" >> "$LOG_FILE"
fi
}
log_unexpected_error() {
local line_no="$1"
local command="$2"
local code="$3"
if [ -z "${LOG_FILE:-}" ]; then
return 0
fi
if [ -n "${ERROR_MSG:-}" ]; then
return 0
fi
ERROR_MSG="Nieoczekiwany błąd wykonywania przywracania (kod ${code}, linia ${line_no})."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG} CMD: ${command}" >> "$LOG_FILE"
}
load_plugin_settings() {
if [ ! -f "$CONFIG_FILE" ]; then
fail "Brak pliku konfiguracyjnego pluginu: $CONFIG_FILE"
exit 1
fi
# shellcheck disable=SC1090
source "$CONFIG_FILE"
}
load_pg_credentials() {
local creds_file="/usr/local/directadmin/conf/postgresql.conf"
PGHOST_VAL=""
PGPORT_VAL=""
PGUSER_VAL=""
PGPASSWORD_VAL=""
if [ ! -r "$creds_file" ]; then
fail "Brak pliku poświadczeń PostgreSQL: $creds_file"
exit 1
fi
local line
line="$(grep -Ev '^[[:space:]]*($|#)' "$creds_file" | head -n1 || true)"
if [ -z "$line" ]; then
fail "Pusty plik poświadczeń PostgreSQL: $creds_file"
exit 1
fi
if [[ "$line" == *=* ]]; then
while IFS='=' read -r key value; do
key="$(echo "$key" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')"
value="$(echo "$value" | sed -E 's/[[:space:]]+#.*$//' | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')"
case "$key" in
PG_HOST) PGHOST_VAL="$value" ;;
PG_PORT) PGPORT_VAL="$value" ;;
PG_USER) PGUSER_VAL="$value" ;;
PG_PASSWORD) PGPASSWORD_VAL="$value" ;;
esac
done < <(grep -Ev '^[[:space:]]*($|#)' "$creds_file")
else
local rest
PGHOST_VAL="${line%%:*}"
rest="${line#*:}"
PGPORT_VAL="${rest%%:*}"
rest="${rest#*:}"
rest="${rest#*:}"
PGUSER_VAL="${rest%%:*}"
PGPASSWORD_VAL="${rest#*:}"
fi
[ -n "$PGHOST_VAL" ] || PGHOST_VAL="localhost"
[ -n "$PGPORT_VAL" ] || PGPORT_VAL="5432"
[ -n "$PGUSER_VAL" ] || PGUSER_VAL="postgres"
if [ -z "$PGPASSWORD_VAL" ]; then
fail "Brak hasła PostgreSQL w /usr/local/directadmin/conf/postgresql.conf"
exit 1
fi
}
resolve_bins() {
PSQL_BIN="$(command -v psql 2>/dev/null || true)"
if [ -z "$PSQL_BIN" ]; then
fail "Brak binarium psql."
exit 1
fi
}
sql_escape_literal() {
printf '%s' "$1" | sed "s/'/''/g"
}
psql_admin() {
local database="$1"
shift
PGPASSWORD="$PGPASSWORD_VAL" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$PGUSER_VAL" -d "$database" -v ON_ERROR_STOP=1 "$@"
}
role_exists() {
local role="$1"
if [ -z "$role" ]; then
return 1
fi
local esc
esc="$(sql_escape_literal "$role")"
local ok
ok="$(psql_admin postgres -At -c "SELECT 1 FROM pg_roles WHERE rolname='${esc}'" 2>/dev/null || true)"
[ -n "$ok" ]
}
resolve_db_owner() {
local owner
owner="$(psql_admin postgres -At -c "SELECT pg_get_userbyid(datdba) FROM pg_database WHERE datname='${DB_ESC}'" 2>/dev/null | head -n1 | tr -d '\r' || true)"
if role_exists "$owner"; then
printf '%s' "$owner"
return 0
fi
if role_exists "$DA_USER"; then
printf '%s' "$DA_USER"
return 0
fi
if role_exists "$PGUSER_VAL"; then
printf '%s' "$PGUSER_VAL"
return 0
fi
printf '%s' ""
return 0
}
database_is_empty() {
local db_name="$1"
local has_objects
has_objects="$(
psql_admin "$db_name" -At -c "
SELECT 1
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname NOT LIKE 'pg_%'
AND n.nspname <> 'information_schema'
LIMIT 1;
" 2>/dev/null || true
)"
[ -z "$has_objects" ]
}
run_restore_as_owner() {
local owner="$1"
if [ -z "$owner" ]; then
fail "Nie można ustalić właściciela bazy ${DB_NAME}."
return 1
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Przywracanie jako właściciel ${owner}" >> "$LOG_FILE"
if [[ "$SOURCE_FILE" == *.gz ]]; then
set +e
{ printf 'SET ROLE "%s";\n' "$owner"; gunzip -c "$SOURCE_FILE" | sanitize_restore_compat; } | psql_admin "$DB_NAME" >> "$LOG_FILE" 2>&1
RC=$?
set -e
else
set +e
{ printf 'SET ROLE "%s";\n' "$owner"; sanitize_restore_compat < "$SOURCE_FILE"; } | psql_admin "$DB_NAME" >> "$LOG_FILE" 2>&1
RC=$?
set -e
fi
return "$RC"
}
sanitize_pg_dump() {
local src_db="$1"
local dst_db="$2"
awk -v src="$src_db" -v dst="$dst_db" '
function lower(s) { return tolower(s) }
{
line = $0
l = lower(line)
if (l ~ /^[[:space:]]*\\connect/ || l ~ /^[[:space:]]*\\c[[:space:]]/) { next }
if (l ~ /^[[:space:]]*create[[:space:]]+database[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*drop[[:space:]]+database[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*alter[[:space:]]+database[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*comment[[:space:]]+on[[:space:]]+database[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*revoke[[:space:]].*[[:space:]]on[[:space:]]+database[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*grant[[:space:]].*[[:space:]]on[[:space:]]+database[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*set[[:space:]]+role[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*set[[:space:]]+session[[:space:]]+authorization/) { next }
if (l ~ /^[[:space:]]*reset[[:space:]]+role/) { next }
if (l ~ /^[[:space:]]*reset[[:space:]]+session[[:space:]]+authorization/) { next }
if (l ~ /^[[:space:]]*alter[[:space:]].*[[:space:]]owner[[:space:]]+to[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*comment[[:space:]]+on[[:space:]]+schema[[:space:]]+public[[:space:]]+is[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*owner[[:space:]]+to[[:space:]]+/) { next }
if (l ~ /^[[:space:]]*alter[[:space:]]+default[[:space:]]+privileges/) { next }
if (l ~ /^[[:space:]]*reassign[[:space:]]+owned/) { next }
if (l ~ /^[[:space:]]*drop[[:space:]]+owned/) { next }
print line
}
'
}
sanitize_restore_compat() {
awk '
function lower(s) { return tolower(s) }
{
line = $0
l = lower(line)
# PostgreSQL < 17: parameter transaction_timeout does not exist.
if (l ~ /^[[:space:]]*set[[:space:]]+"?transaction_timeout"?[[:space:]]*=/) { next }
if (l ~ /^[[:space:]]*reset[[:space:]]+"?transaction_timeout"?[[:space:]]*;/) { next }
print line
}
'
}
preclean_database() {
local owner="$1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Czyszczenie bazy ${DB_NAME} przed przywróceniem" >> "$LOG_FILE"
local schemas
schemas="$(psql_admin "$DB_NAME" -At -c "SELECT nspname FROM pg_namespace WHERE nspname NOT LIKE 'pg_%' AND nspname <> 'information_schema'" 2>>"$LOG_FILE" || true)"
if [ -n "$schemas" ]; then
while IFS= read -r schema; do
schema="$(printf '%s' "$schema" | tr -d '\r')"
[ -z "$schema" ] && continue
psql_admin "$DB_NAME" -c "DROP SCHEMA IF EXISTS \"$schema\" CASCADE;" >> "$LOG_FILE" 2>&1 || true
done <<< "$schemas"
fi
if [ -n "$owner" ]; then
psql_admin "$DB_NAME" -c "CREATE SCHEMA IF NOT EXISTS public AUTHORIZATION \"$owner\";" >> "$LOG_FILE" 2>&1 || true
psql_admin "$DB_NAME" -c "GRANT ALL ON SCHEMA public TO \"$owner\";" >> "$LOG_FILE" 2>&1 || true
fi
}
restore_db_owner() {
if [ "$DB_OWNER_SWITCHED" -ne 1 ] || [ -z "$ORIG_DB_OWNER" ] || [ -z "${TMP_DB_USER:-}" ]; then
return 0
fi
if ! role_exists "$ORIG_DB_OWNER"; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Oryginalny właściciel bazy ${ORIG_DB_OWNER} nie istnieje, pomijam przywrócenie właściciela." >> "$LOG_FILE"
DB_OWNER_SWITCHED=0
return 0
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Przywracanie właściciela bazy ${DB_NAME} na ${ORIG_DB_OWNER}" >> "$LOG_FILE"
set +e
psql_admin "$DB_NAME" -c "REASSIGN OWNED BY \"$TMP_DB_USER\" TO \"$ORIG_DB_OWNER\";" >> "$LOG_FILE" 2>&1
local rc1=$?
psql_admin "$DB_NAME" -c "DROP OWNED BY \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1
local rc2=$?
psql_admin postgres -c "ALTER DATABASE \"$DB_NAME\" OWNER TO \"$ORIG_DB_OWNER\";" >> "$LOG_FILE" 2>&1
local rc3=$?
set -e
if [ "$rc1" -ne 0 ] || [ "$rc3" -ne 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Nie udało się w pełni przywrócić właściciela bazy ${DB_NAME}." >> "$LOG_FILE"
return 1
fi
DB_OWNER_SWITCHED=0
return 0
}
cleanup_tmp_user() {
if [ "$TMP_DB_ACTIVE" -ne 1 ] || [ -z "${TMP_DB_USER:-}" ]; then
return 0
fi
psql_admin postgres -c "DROP ROLE IF EXISTS \"$TMP_DB_USER\";" >/dev/null 2>&1 || true
TMP_DB_ACTIVE=0
}
on_exit() {
local code="$1"
if [ "$code" -ne 0 ] && [ -z "${ERROR_MSG:-}" ] && [ -n "${LOG_FILE:-}" ]; then
ERROR_MSG="Zadanie przywracania zakończyło się błędem (kod ${code}) bez zarejestrowanego szczegółu."
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: ${ERROR_MSG}" >> "$LOG_FILE"
fi
restore_db_owner || true
cleanup_tmp_user
notify_user "$code"
}
trap 'log_unexpected_error "$LINENO" "$BASH_COMMAND" "$?"' ERR
trap 'on_exit $?' EXIT
random_tmp_password() {
local value
value="$(od -An -N48 -tx1 /dev/urandom 2>/dev/null | tr -d '[:space:]')"
if [ -z "$value" ]; then
value="$(date +%s%N)${RANDOM}${RANDOM}"
fi
printf '%s' "${value:0:32}"
}
path_in_dir() {
local path="$1"
local dir="$2"
local real_path real_dir
real_path="$(realpath "$path" 2>/dev/null || true)"
real_dir="$(realpath "$dir" 2>/dev/null || true)"
if [ -z "$real_path" ] || [ -z "$real_dir" ]; then
return 1
fi
if [ "$real_path" = "$real_dir" ]; then
return 0
fi
case "$real_path" in
"$real_dir"/*) return 0 ;;
esac
return 1
}
# shellcheck disable=SC1090
source "$JOB_FILE"
JOB_ID="${JOB_ID:-$(basename "$JOB_FILE" .env)}"
DA_USER="${DA_USER:-}"
DB_NAME="${DB_NAME:-}"
SOURCE_FILE="${SOURCE_FILE:-}"
SOURCE_KIND="${SOURCE_KIND:-local}"
RESTORE_MODE="${RESTORE_MODE:-overwrite}"
SOURCE_DB_NAME="${SOURCE_DB_NAME:-}"
RESTORE_MODE="$(printf '%s' "$RESTORE_MODE" | tr '[:upper:]' '[:lower:]')"
if [ "$RESTORE_MODE" != "new_db" ]; then
RESTORE_MODE="overwrite"
fi
LOG_FILE="${LOG_DIR}/${JOB_ID}.log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Start restore job ${JOB_ID}" > "$LOG_FILE"
if [ -z "$DA_USER" ] || [ -z "$DB_NAME" ] || [ -z "$SOURCE_FILE" ]; then
fail "Brak DA_USER, DB_NAME lub SOURCE_FILE w pliku zadania."
exit 1
fi
if [[ "$DB_NAME" != "${DA_USER}_"* ]]; then
fail "Baza ${DB_NAME} nie należy do użytkownika ${DA_USER}."
exit 1
fi
load_plugin_settings
load_pg_credentials
resolve_bins
if [ ! -f "$SOURCE_FILE" ] || [ ! -r "$SOURCE_FILE" ]; then
fail "Nie można odczytać pliku backupu: ${SOURCE_FILE}"
exit 1
fi
case "$SOURCE_FILE" in
*.sql|*.sql.gz|*.gz) ;;
*)
fail "Dozwolone są wyłącznie pliki .sql, .gz i .sql.gz."
exit 1
;;
esac
USER_BACKUP_ROOT="${HITME_BACKUP_LOCATION:-/home/admin/postgres_backup}"
UPLOAD_ROOT="${PLUGIN_DIR}/data/uploads/${DA_USER}"
if [ "$SOURCE_KIND" = "local" ]; then
if ! path_in_dir "$SOURCE_FILE" "$USER_BACKUP_ROOT"; then
fail "Plik backupu lokalnego znajduje się poza dozwolonym katalogiem użytkownika."
exit 1
fi
else
if ! path_in_dir "$SOURCE_FILE" "$UPLOAD_ROOT"; then
fail "Plik do przywrócenia znajduje się poza katalogiem upload."
exit 1
fi
fi
DB_ESC="$(sql_escape_literal "$DB_NAME")"
DB_EXISTS="$(psql_admin postgres -At -c "SELECT 1 FROM pg_database WHERE datname='${DB_ESC}'" 2>/dev/null || true)"
if [ -z "$DB_EXISTS" ]; then
fail "Docelowa baza danych ${DB_NAME} nie istnieje."
exit 1
fi
if [ "$RESTORE_MODE" = "overwrite" ] && [ -n "$SOURCE_DB_NAME" ] && [ "$SOURCE_DB_NAME" = "$DB_NAME" ]; then
USE_OWNER_RESTORE=1
PRE_CLEAN=1
fi
if [ "$RESTORE_MODE" = "new_db" ]; then
PRE_CLEAN=1
fi
if [ "$SOURCE_KIND" = "local" ]; then
USE_SANITIZE=1
fi
if [ "$SOURCE_KIND" = "file" ]; then
SOURCE_DB_NAME=""
USE_SANITIZE=1
fi
if [ "$RESTORE_MODE" = "new_db" ] && [ -n "$SOURCE_DB_NAME" ] && [ "$SOURCE_DB_NAME" != "$DB_NAME" ]; then
USE_SANITIZE=1
fi
ORIG_DB_OWNER="$(resolve_db_owner)"
if [ -z "$ORIG_DB_OWNER" ]; then
fail "Nie można ustalić właściciela bazy ${DB_NAME}."
exit 1
fi
if [ "$RESTORE_MODE" = "new_db" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Typ przywracania: Przywracanie do nowej bazy" >> "$LOG_FILE"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Typ przywracania: Przywracanie z nadpisaniem" >> "$LOG_FILE"
fi
if [ -n "$SOURCE_DB_NAME" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Baza źródłowa: ${SOURCE_DB_NAME}" >> "$LOG_FILE"
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Baza docelowa: ${DB_NAME}" >> "$LOG_FILE"
if [ "$PRE_CLEAN" -eq 1 ]; then
preclean_database "$ORIG_DB_OWNER"
fi
# Jeżeli restore pochodzi z zewnętrznego pliku i baza docelowa jest pusta,
# użyj użytkownika przypisanego do bazy (właściciela), bez użytkownika tymczasowego.
if database_is_empty "$DB_NAME"; then
TARGET_DB_EMPTY=1
fi
if [ "$SOURCE_KIND" = "file" ] && [ "$TARGET_DB_EMPTY" -eq 1 ] && [ "$USE_OWNER_RESTORE" -eq 0 ]; then
USE_OWNER_RESTORE=1
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Wykryto pustą bazę docelową i restore z pliku: używam użytkownika przypisanego do bazy (${ORIG_DB_OWNER})." >> "$LOG_FILE"
fi
if [ "$USE_OWNER_RESTORE" -eq 0 ]; then
TMP_DB_USER="pgtmp_rst_$(printf '%s' "$JOB_ID" | tr -cd 'A-Za-z0-9' | tr '[:upper:]' '[:lower:]' | head -c 18)"
[ -n "$TMP_DB_USER" ] || TMP_DB_USER="pgtmp_rst_$(date +%s)"
TMP_DB_PASS="$(random_tmp_password)"
PASS_ESC="$(sql_escape_literal "$TMP_DB_PASS")"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tworzenie tymczasowego użytkownika restore ${TMP_DB_USER}" >> "$LOG_FILE"
psql_admin postgres -c "SET password_encryption='scram-sha-256'; DROP ROLE IF EXISTS \"$TMP_DB_USER\"; CREATE ROLE \"$TMP_DB_USER\" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION PASSWORD '${PASS_ESC}';" >> "$LOG_FILE" 2>&1
TMP_DB_ACTIVE=1
psql_admin postgres -c "GRANT CONNECT, CREATE, TEMPORARY ON DATABASE \"$DB_NAME\" TO \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1
psql_admin "$DB_NAME" -c "GRANT USAGE, CREATE ON SCHEMA public TO \"$TMP_DB_USER\"; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"$TMP_DB_USER\"; GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"$TMP_DB_USER\"; GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1
if [ -n "$ORIG_DB_OWNER" ] && [ "$ORIG_DB_OWNER" != "$TMP_DB_USER" ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tymczasowe przejęcie właściciela bazy ${DB_NAME} przez ${TMP_DB_USER}" >> "$LOG_FILE"
psql_admin postgres -c "ALTER DATABASE \"$DB_NAME\" OWNER TO \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1
DB_OWNER_SWITCHED=1
fi
# Restore z plików dump może zawierać operacje wymagające bycia właścicielem
# schematu public (np. COMMENT ON SCHEMA public). Ustaw właściciela na użytkownika
# tymczasowego, a po restore owner wróci przez restore_db_owner().
psql_admin "$DB_NAME" -c "ALTER SCHEMA public OWNER TO \"$TMP_DB_USER\";" >> "$LOG_FILE" 2>&1 || true
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Przywracanie bazy ${DB_NAME} z pliku ${SOURCE_FILE}" >> "$LOG_FILE"
if [ "$USE_OWNER_RESTORE" -eq 1 ]; then
set +e
if [ "$USE_SANITIZE" -eq 1 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Sanityzacja dumpa: ${SOURCE_DB_NAME} -> ${DB_NAME}" >> "$LOG_FILE"
if [[ "$SOURCE_FILE" == *.gz ]]; then
{ printf 'SET ROLE "%s";\n' "$ORIG_DB_OWNER"; gunzip -c "$SOURCE_FILE" | sanitize_pg_dump "$SOURCE_DB_NAME" "$DB_NAME" | sanitize_restore_compat; } | psql_admin "$DB_NAME" >> "$LOG_FILE" 2>&1
RC=$?
else
{ printf 'SET ROLE "%s";\n' "$ORIG_DB_OWNER"; sanitize_pg_dump "$SOURCE_DB_NAME" "$DB_NAME" < "$SOURCE_FILE" | sanitize_restore_compat; } | psql_admin "$DB_NAME" >> "$LOG_FILE" 2>&1
RC=$?
fi
else
run_restore_as_owner "$ORIG_DB_OWNER"
RC=$?
fi
set -e
else
if [[ "$SOURCE_FILE" == *.gz ]]; then
set +e
if [ "$USE_SANITIZE" -eq 1 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Sanityzacja dumpa: ${SOURCE_DB_NAME} -> ${DB_NAME}" >> "$LOG_FILE"
gunzip -c "$SOURCE_FILE" | sanitize_pg_dump "$SOURCE_DB_NAME" "$DB_NAME" | sanitize_restore_compat | PGPASSWORD="$TMP_DB_PASS" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$TMP_DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 >> "$LOG_FILE" 2>&1
RC=$?
else
gunzip -c "$SOURCE_FILE" | sanitize_restore_compat | PGPASSWORD="$TMP_DB_PASS" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$TMP_DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 >> "$LOG_FILE" 2>&1
RC=$?
fi
set -e
else
set +e
if [ "$USE_SANITIZE" -eq 1 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Sanityzacja dumpa: ${SOURCE_DB_NAME} -> ${DB_NAME}" >> "$LOG_FILE"
sanitize_pg_dump "$SOURCE_DB_NAME" "$DB_NAME" < "$SOURCE_FILE" | sanitize_restore_compat | PGPASSWORD="$TMP_DB_PASS" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$TMP_DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 >> "$LOG_FILE" 2>&1
RC=$?
else
sanitize_restore_compat < "$SOURCE_FILE" | PGPASSWORD="$TMP_DB_PASS" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$TMP_DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 >> "$LOG_FILE" 2>&1
RC=$?
fi
set -e
fi
fi
if [ "$RC" -ne 0 ]; then
fail "Nie udało się przywrócić bazy ${DB_NAME}."
exit 1
fi
if [ "$USE_OWNER_RESTORE" -eq 0 ]; then
if ! restore_db_owner; then
fail "Nie udało się przywrócić właściciela bazy ${DB_NAME}."
exit 1
fi
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Przywracanie zakończone powodzeniem" >> "$LOG_FILE"
RESTORE_OK=1
exit 0
+704
View File
@@ -0,0 +1,704 @@
#!/bin/bash
# =============================================================================
# adminer_install.sh
# Instalacja i integracja Adminera z pluginem da-postgresql (DirectAdmin)
#
# Założenia bezpieczeństwa:
# - Adminer dostępny przez alias /adminer
# - Tryb public/private odczytywany runtime z /var/www/html/adminer/runtime/config.json
# - Tryb publiczny (ADMINER_PUBLIC=true) pozwala na ręczne logowanie do PostgreSQL
# - Tryb prywatny (ADMINER_PUBLIC=false) wymaga ticketu SSO z pluginu
# - Ticket SSO ma krótki TTL i jest przypięty do User-Agent
# - Logowanie odbywa się tymczasową rolą PostgreSQL tworzoną przez plugin
# =============================================================================
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
sekcja() { echo -e "\n${BOLD}${CYAN}=== $* ===${NC}\n"; }
die() { echo -e "${RED}[BŁĄD]${NC} $*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Skrypt musi być uruchomiony jako root."
LOGFILE="/var/log/adminer_install_$(date +%Y%m%d_%H%M%S).log"
exec 1> >(tee >(sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' >> "$LOGFILE")) 2>&1
info "Pełny log zapisany w: $LOGFILE"
ADMINER_URL_PATH="/adminer"
ADMINER_VERSION="5.4.2"
ADMINER_FLAVOR="pgsql"
ADMINER_INSTALL_DIR="/var/www/html/adminer"
ADMINER_RUNTIME_DIR="${ADMINER_INSTALL_DIR}/runtime"
ADMINER_TICKETS_DIR="${ADMINER_RUNTIME_DIR}/tickets"
ADMINER_RUNTIME_CONFIG_FILE="${ADMINER_RUNTIME_DIR}/config.json"
ADMINER_CORE_FILE="${ADMINER_INSTALL_DIR}/adminer-core.php"
ADMINER_EXTENSION_FILE="${ADMINER_INSTALL_DIR}/adminer-extension.php"
ADMINER_INDEX_FILE="${ADMINER_INSTALL_DIR}/index.php"
PLUGIN_BUNDLED_ADMINER="/usr/local/directadmin/plugins/da-postgresql/assets/adminer/adminer-${ADMINER_VERSION}-${ADMINER_FLAVOR}.php"
PLUGIN_BUNDLED_ADMINER_SHA256="059505abc2b56487d78bbfbeb9485ed8c6a10fe357f4ddec9fc69b1043bff4af"
PLUGIN_BUNDLED_EXTENSION="/usr/local/directadmin/plugins/da-postgresql/assets/adminer/adminer-extension.php"
PLUGIN_BUNDLED_EXTENSION_SHA256="6e2f098b172838ccb3736506867396dc0954a6383fdd7b7c5e7739ab21baafeb"
CB_DIR="/usr/local/directadmin/custombuild"
CB_BUILD="${CB_DIR}/build"
APACHE_ALIAS_CUSTOM="${CB_DIR}/custom/ap2/conf/extra/httpd-alias.conf"
APACHE_ALIAS_SOURCE="/etc/httpd/conf/extra/httpd-alias.conf"
APACHE_ALIAS_CONFIGURE="${CB_DIR}/configure/ap2/conf/extra/httpd-alias.conf"
PLUGIN_SETTINGS="/usr/local/directadmin/plugins/da-postgresql/plugin-settings.conf"
PLUGIN_OWNER="diradmin:diradmin"
ADMINER_PUBLIC_DEFAULT="false"
ADMINER_PUBLIC_MODE="0"
detect_apache_group() {
local group_name=""
if [[ -f /etc/httpd/conf/httpd.conf ]]; then
group_name="$(awk 'tolower($1)=="group" {print $2; exit}' /etc/httpd/conf/httpd.conf | tr -d '[:space:]')"
fi
if [[ -z "$group_name" && -f /etc/apache2/apache2.conf ]]; then
group_name="$(awk 'tolower($1)=="group" {print $2; exit}' /etc/apache2/apache2.conf | tr -d '[:space:]')"
fi
if [[ -z "$group_name" ]]; then
for candidate in apache www-data nobody; do
if getent group "$candidate" >/dev/null 2>&1; then
group_name="$candidate"
break
fi
done
fi
[[ -n "$group_name" ]] || die "Nie można wykryć grupy użytkownika Apache."
echo "$group_name"
}
set_setting() {
local key="$1"
local value="$2"
local file="$3"
[[ -f "$file" ]] || return 0
sed -i "/^${key}=/d" "$file" || true
echo "${key}=${value}" >> "$file"
}
set_setting_if_missing() {
local key="$1"
local value="$2"
local file="$3"
[[ -f "$file" ]] || return 0
if grep -q "^${key}=" "$file"; then
return 0
fi
echo "${key}=${value}" >> "$file"
}
bool_is_true() {
local v
v="$(echo "${1:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
case "$v" in
1|true|yes|y|on|tak|t) return 0 ;;
*) return 1 ;;
esac
}
read_adminer_public_mode() {
local raw="$ADMINER_PUBLIC_DEFAULT"
if [[ -f "$PLUGIN_SETTINGS" ]]; then
local found
found="$(awk -F= '/^ADMINER_PUBLIC=/{print $2; exit}' "$PLUGIN_SETTINGS" | tr -d '[:space:]')"
if [[ -n "$found" ]]; then
raw="$found"
fi
fi
if bool_is_true "$raw"; then
ADMINER_PUBLIC_MODE="1"
else
ADMINER_PUBLIC_MODE="0"
fi
}
write_runtime_public_config() {
local public_json="false"
if [[ "$ADMINER_PUBLIC_MODE" == "1" ]]; then
public_json="true"
fi
cat > "$ADMINER_RUNTIME_CONFIG_FILE" <<EOF
{"version":1,"adminer_public":${public_json},"updated_at":$(date +%s)}
EOF
}
sha256_file() {
local path="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$path" | awk '{print $1}'
return 0
fi
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$path" | awk '{print $1}'
return 0
fi
if command -v openssl >/dev/null 2>&1; then
openssl dgst -sha256 "$path" | awk '{print $NF}'
return 0
fi
return 1
}
render_adminer_wrapper() {
cat > "$ADMINER_INDEX_FILE" <<'PHPEOF'
<?php
declare(strict_types=1);
const DA_ADMINER_RUNTIME_DIR = '/var/www/html/adminer/runtime';
const DA_ADMINER_TICKETS_DIR = DA_ADMINER_RUNTIME_DIR . '/tickets';
const DA_ADMINER_CONFIG_FILE = DA_ADMINER_RUNTIME_DIR . '/config.json';
const DA_ADMINER_CORE_FILE = __DIR__ . '/adminer-core.php';
const DA_ADMINER_EXTENSION_FILE = __DIR__ . '/adminer-extension.php';
const DA_ADMINER_SESSION_KEY = 'da_adminer_auth';
const DA_ADMINER_SESSION_NAME = 'DA_POSTGRESQL_ADMINER';
const DA_ADMINER_PUBLIC_DEFAULT = __ADMINER_PUBLIC_MODE__;
header('X-Frame-Options: SAMEORIGIN');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
session_name(DA_ADMINER_SESSION_NAME);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/adminer',
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
$now = time();
$adminerPublic = load_public_mode();
if (isset($_GET['logout'])) {
clear_session();
if (!$adminerPublic) {
render_placeholder('Sesja Adminera zostala zamknieta. Otworz Adminera ponownie z panelu DirectAdmin.');
}
}
$auth = null;
$ticketId = ticket_id_from_request();
if ($ticketId !== '') {
$ticketAccepted = false;
$ticketRejectReason = '';
$ticketBinding = '';
$ticket = load_ticket($ticketId);
if ($ticket !== null) {
$ticketBinding = isset($ticket['user_binding']) && is_string($ticket['user_binding']) ? $ticket['user_binding'] : '';
}
if ($ticket === null) {
$ticketRejectReason = 'ticket_missing_or_unreadable';
} elseif (validate_ticket($ticket, $now, $adminerPublic, $ticketRejectReason)) {
$auth = [
'host' => 'localhost',
'port' => (int)($ticket['port'] ?? 5432),
'user' => (string)($ticket['role'] ?? ''),
'password' => (string)($ticket['password'] ?? ''),
'database' => (string)($ticket['database'] ?? ''),
'expires_at' => (int)($ticket['session_expires_at'] ?? ($now + 900)),
];
$_SESSION[DA_ADMINER_SESSION_KEY] = $auth;
@unlink(ticket_path($ticketId));
clear_ticket_from_request();
$ticketAccepted = true;
}
debug_ticket_event($ticketId, $ticketAccepted, $ticketRejectReason, $ticketBinding, current_user_binding());
if (!$ticketAccepted && !$adminerPublic) {
render_placeholder('Nieprawidlowy lub wygasly link logowania. Otworz Adminera z poziomu pluginu.');
}
}
if ($auth === null) {
$auth = $_SESSION[DA_ADMINER_SESSION_KEY] ?? null;
}
if (!is_array($auth) || empty($auth['user']) || empty($auth['password'])) {
$auth = null;
if (!$adminerPublic) {
render_placeholder();
}
}
if ($auth !== null) {
$expiresAt = (int)($auth['expires_at'] ?? 0);
if ($expiresAt <= 0 || $expiresAt < $now) {
clear_session();
$auth = null;
if (!$adminerPublic) {
render_placeholder('Sesja Adminera wygasla. Otworz Adminera ponownie z poziomu pluginu.');
}
}
}
if (has_forbidden_driver_params()) {
clear_session();
http_response_code(403);
render_placeholder('Dozwolony jest tylko sterownik PostgreSQL.');
}
$_GET['pgsql'] = 'localhost';
if ($auth !== null) {
bootstrap_adminer_session($auth);
unset($_POST['auth']);
} else {
normalize_post_auth();
}
$GLOBALS['da_adminer_sso_auth'] = $auth;
require DA_ADMINER_EXTENSION_FILE;
require DA_ADMINER_CORE_FILE;
exit;
function ticket_id_from_request(): string
{
$ticket = $_GET['ticket'] ?? '';
if (!is_string($ticket)) {
return '';
}
$ticket = trim($ticket);
if ($ticket === '' || !preg_match('/^[A-Za-z0-9_-]{20,128}$/', $ticket)) {
return '';
}
return $ticket;
}
function ticket_path(string $ticketId): string
{
return DA_ADMINER_TICKETS_DIR . '/' . $ticketId . '.json';
}
function clear_ticket_from_request(): void
{
unset($_GET['ticket']);
$uri = $_SERVER['REQUEST_URI'] ?? '';
if (!is_string($uri) || $uri === '') {
return;
}
$parts = parse_url($uri);
if (!is_array($parts)) {
return;
}
$path = isset($parts['path']) && is_string($parts['path']) ? $parts['path'] : '/adminer';
$query = [];
if (isset($parts['query']) && is_string($parts['query']) && $parts['query'] !== '') {
parse_str($parts['query'], $query);
if (is_array($query)) {
unset($query['ticket']);
} else {
$query = [];
}
}
$newQuery = http_build_query($query);
$_SERVER['REQUEST_URI'] = $path . ($newQuery !== '' ? ('?' . $newQuery) : '');
}
function load_ticket(string $ticketId): ?array
{
$path = ticket_path($ticketId);
if (!is_file($path) || !is_readable($path)) {
return null;
}
$raw = @file_get_contents($path);
if (!is_string($raw) || $raw === '') {
return null;
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : null;
}
function load_public_mode(): bool
{
$default = (DA_ADMINER_PUBLIC_DEFAULT === 1);
if (!is_file(DA_ADMINER_CONFIG_FILE) || !is_readable(DA_ADMINER_CONFIG_FILE)) {
return $default;
}
$raw = @file_get_contents(DA_ADMINER_CONFIG_FILE);
if (!is_string($raw) || $raw === '') {
return $default;
}
$decoded = json_decode($raw, true);
if (!is_array($decoded) || !array_key_exists('adminer_public', $decoded)) {
return $default;
}
return parse_bool_value($decoded['adminer_public'], $default);
}
function parse_bool_value($value, bool $default = false): bool
{
if (is_bool($value)) {
return $value;
}
if (is_int($value) || is_float($value)) {
return ((int)$value) !== 0;
}
if (!is_string($value)) {
return $default;
}
$normalized = strtolower(trim($value));
if (in_array($normalized, ['1', 'true', 'yes', 'y', 'on', 'tak', 't'], true)) {
return true;
}
if (in_array($normalized, ['0', 'false', 'no', 'n', 'off', 'nie'], true)) {
return false;
}
return $default;
}
function validate_ticket(array $ticket, int $now, bool $adminerPublic, string &$reason = ''): bool
{
$required = ['role', 'password', 'database', 'host', 'port', 'expires_at', 'session_expires_at', 'user_binding'];
foreach ($required as $key) {
if (!array_key_exists($key, $ticket)) {
$reason = 'missing_key:' . $key;
return false;
}
}
$expiresAt = is_numeric($ticket['expires_at']) ? (int)$ticket['expires_at'] : 0;
if ($expiresAt < $now) {
$reason = 'ticket_expired';
return false;
}
$sessionExpiresAt = is_numeric($ticket['session_expires_at']) ? (int)$ticket['session_expires_at'] : 0;
if ($sessionExpiresAt < $now) {
$reason = 'session_expired';
return false;
}
$role = (string)$ticket['role'];
if ($role === '' || !preg_match('/^[a-z0-9_]{1,63}$/', $role)) {
$reason = 'invalid_role';
return false;
}
$binding = (string)$ticket['user_binding'];
if (!is_binding_valid($binding, current_user_binding(), $adminerPublic)) {
$reason = 'binding_mismatch';
return false;
}
$reason = 'ok';
return true;
}
function current_user_binding(): string
{
$ua = (string)($_SERVER['HTTP_USER_AGENT'] ?? '');
return hash('sha256', $ua);
}
function is_binding_valid(string $ticketBinding, string $currentBinding, bool $adminerPublic): bool
{
if ($adminerPublic) {
return true;
}
if ($ticketBinding === '') {
return false;
}
if (hash_equals($ticketBinding, $currentBinding)) {
return true;
}
// Kompatybilność: starsze ticket-y mogły być wystawione bez HTTP_USER_AGENT po stronie DA.
$legacyNoUaBinding = hash('sha256', '');
if (hash_equals($ticketBinding, $legacyNoUaBinding)) {
return true;
}
return false;
}
function debug_ticket_event(
string $ticketId,
bool $accepted,
string $reason,
string $ticketBinding,
string $currentBinding
): void {
$line = sprintf(
"[%s] ticket=%s accepted=%s reason=%s ticket_binding=%s current_binding=%s remote=%s ua=%s\n",
date('c'),
$ticketId,
$accepted ? '1' : '0',
$reason !== '' ? $reason : 'n/a',
$ticketBinding !== '' ? $ticketBinding : '-',
$currentBinding !== '' ? $currentBinding : '-',
(string)($_SERVER['REMOTE_ADDR'] ?? '-'),
(string)($_SERVER['HTTP_USER_AGENT'] ?? '-')
);
@error_log($line, 3, '/tmp/da_adminer_wrapper_debug.log');
}
function has_forbidden_driver_params(): bool
{
$forbidden = ['server', 'sqlite', 'mongo', 'mssql', 'oracle', 'elastic', 'clickhouse'];
foreach ($forbidden as $key) {
if (isset($_GET[$key])) {
return true;
}
}
return false;
}
function bootstrap_adminer_session(array $auth): void
{
$driver = 'pgsql';
$server = 'localhost';
$user = trim((string)($auth['user'] ?? ''));
$password = (string)($auth['password'] ?? '');
$database = trim((string)($auth['database'] ?? ''));
if ($user === '' || $password === '') {
return;
}
$_GET[$driver] = $server;
$_GET['username'] = $user;
if ($database !== '' && (!isset($_GET['db']) || !is_string($_GET['db']) || $_GET['db'] === '')) {
$_GET['db'] = $database;
}
$_SESSION['pwds'][$driver][$server][$user] = $password;
if ($database !== '') {
$_SESSION['db'][$driver][$server][$user][$database] = true;
}
}
function normalize_post_auth(): void
{
if (!isset($_POST['auth']) || !is_array($_POST['auth'])) {
return;
}
$_POST['auth']['driver'] = 'pgsql';
$_POST['auth']['server'] = 'localhost';
}
function clear_session(): void
{
$_SESSION = [];
if (session_status() === PHP_SESSION_ACTIVE) {
session_unset();
session_destroy();
}
}
function render_placeholder(string $extra = ''): void
{
http_response_code(200);
$message = 'Dostep do Adminera jest mozliwy wylacznie z poziomu panelu DirectAdmin.';
echo '<!doctype html><html lang="pl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">';
echo '<title>Adminer - DirectAdmin</title>';
echo '<style>body{margin:0;background:#f5f7fb;color:#1f2937;font:16px/1.5 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}';
echo '.wrap{max-width:760px;margin:10vh auto;padding:24px}';
echo '.card{background:#fff;border:1px solid #d7dfeb;border-radius:12px;padding:26px;box-shadow:0 6px 22px rgba(18,38,63,.08)}';
echo 'h1{margin:0 0 12px;font-size:24px}p{margin:0 0 10px}.muted{color:#52607a;font-size:14px}</style></head><body>';
echo '<div class="wrap"><div class="card"><h1>Adminer</h1><p>' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '</p>';
if ($extra !== '') {
echo '<p class="muted">' . htmlspecialchars($extra, ENT_QUOTES, 'UTF-8') . '</p>';
}
echo '<p class="muted">W panelu pluginu PostgreSQL kliknij przycisk "Adminer", aby otworzyc sesje SSO.</p></div></div></body></html>';
exit;
}
PHPEOF
sed -i "s/__ADMINER_PUBLIC_MODE__/${ADMINER_PUBLIC_MODE}/g" "$ADMINER_INDEX_FILE"
}
configure_apache_alias() {
[[ -d "$CB_DIR" ]] || die "Brak katalogu CustomBuild: ${CB_DIR}"
mkdir -p "$(dirname "$APACHE_ALIAS_CUSTOM")"
if [[ ! -f "$APACHE_ALIAS_CUSTOM" ]]; then
if [[ -f "$APACHE_ALIAS_SOURCE" ]]; then
cp -p "$APACHE_ALIAS_SOURCE" "$APACHE_ALIAS_CUSTOM"
elif [[ -f "$APACHE_ALIAS_CONFIGURE" ]]; then
cp -p "$APACHE_ALIAS_CONFIGURE" "$APACHE_ALIAS_CUSTOM"
else
warn "Nie znaleziono źródłowego httpd-alias.conf tworzę pusty custom template."
: > "$APACHE_ALIAS_CUSTOM"
fi
fi
local tmp_file
tmp_file="$(mktemp)"
awk '
BEGIN { skip = 0 }
/^# BEGIN DA_POSTGRESQL_ADMINER$/ { skip = 1; next }
/^# END DA_POSTGRESQL_ADMINER$/ { skip = 0; next }
skip == 0 { print }
' "$APACHE_ALIAS_CUSTOM" > "$tmp_file"
cat >> "$tmp_file" <<EOF
# BEGIN DA_POSTGRESQL_ADMINER
Alias ${ADMINER_URL_PATH} ${ADMINER_INSTALL_DIR}/index.php
Alias ${ADMINER_URL_PATH}/ ${ADMINER_INSTALL_DIR}/index.php
<Directory "${ADMINER_INSTALL_DIR}">
AllowOverride None
Options -Indexes
Require all granted
<FilesMatch "^adminer-(core|extension)\.php$">
Require all denied
</FilesMatch>
</Directory>
<Directory "${ADMINER_RUNTIME_DIR}">
AllowOverride None
Options -Indexes
Require all denied
</Directory>
# END DA_POSTGRESQL_ADMINER
EOF
mv "$tmp_file" "$APACHE_ALIAS_CUSTOM"
chmod 644 "$APACHE_ALIAS_CUSTOM"
}
sekcja "Weryfikacja środowiska"
command -v php >/dev/null 2>&1 || die "Brak polecenia php."
id diradmin >/dev/null 2>&1 || die "Brak użytkownika diradmin."
APACHE_GROUP="$(detect_apache_group)"
info "Wykryta grupa Apache: ${APACHE_GROUP}"
read_adminer_public_mode
if [[ "$ADMINER_PUBLIC_MODE" == "1" ]]; then
info "Tryb ADMINER_PUBLIC: true (bezpośredni dostęp do /adminer bez SSO)."
else
info "Tryb ADMINER_PUBLIC: false (dostęp wyłącznie przez SSO z pluginu)."
fi
sekcja "Instalacja z bundla Adminera ${ADMINER_VERSION}-${ADMINER_FLAVOR}"
mkdir -p "$ADMINER_INSTALL_DIR"
[[ -f "$PLUGIN_BUNDLED_ADMINER" ]] || die "Brak zbundlowanego pliku Adminera: ${PLUGIN_BUNDLED_ADMINER}"
[[ -f "$PLUGIN_BUNDLED_EXTENSION" ]] || die "Brak zbundlowanego rozszerzenia Adminera: ${PLUGIN_BUNDLED_EXTENSION}"
EXPECTED_SUM="$PLUGIN_BUNDLED_ADMINER_SHA256"
ACTUAL_SUM="$(sha256_file "$PLUGIN_BUNDLED_ADMINER" || true)"
[[ -n "$ACTUAL_SUM" ]] || die "Nie można policzyć SHA256 dla ${PLUGIN_BUNDLED_ADMINER}."
[[ "$ACTUAL_SUM" == "$EXPECTED_SUM" ]] \
|| die "Niezgodna suma SHA256 dla zbundlowanego Adminera. Oczekiwano: ${EXPECTED_SUM}, otrzymano: ${ACTUAL_SUM}"
cp -f "$PLUGIN_BUNDLED_ADMINER" "$ADMINER_CORE_FILE"
info "Adminer ${ADMINER_VERSION}-${ADMINER_FLAVOR} skopiowany do: ${ADMINER_CORE_FILE}"
EXPECTED_EXTENSION_SUM="$PLUGIN_BUNDLED_EXTENSION_SHA256"
ACTUAL_EXTENSION_SUM="$(sha256_file "$PLUGIN_BUNDLED_EXTENSION" || true)"
[[ -n "$ACTUAL_EXTENSION_SUM" ]] || die "Nie można policzyć SHA256 dla ${PLUGIN_BUNDLED_EXTENSION}."
[[ "$ACTUAL_EXTENSION_SUM" == "$EXPECTED_EXTENSION_SUM" ]] \
|| die "Niezgodna suma SHA256 dla zbundlowanego rozszerzenia Adminera. Oczekiwano: ${EXPECTED_EXTENSION_SUM}, otrzymano: ${ACTUAL_EXTENSION_SUM}"
cp -f "$PLUGIN_BUNDLED_EXTENSION" "$ADMINER_EXTENSION_FILE"
info "Rozszerzenie Adminera skopiowane do: ${ADMINER_EXTENSION_FILE}"
sekcja "Konfiguracja wrappera bezpieczeństwa"
mkdir -p "$ADMINER_RUNTIME_DIR" "$ADMINER_TICKETS_DIR"
render_adminer_wrapper
chown root:root "$ADMINER_INSTALL_DIR"
chmod 755 "$ADMINER_INSTALL_DIR"
chown root:root "$ADMINER_CORE_FILE" "$ADMINER_EXTENSION_FILE" "$ADMINER_INDEX_FILE"
chmod 644 "$ADMINER_CORE_FILE" "$ADMINER_EXTENSION_FILE" "$ADMINER_INDEX_FILE"
chown diradmin:"$APACHE_GROUP" "$ADMINER_RUNTIME_DIR" "$ADMINER_TICKETS_DIR"
chmod 711 "$ADMINER_RUNTIME_DIR"
chmod 2733 "$ADMINER_TICKETS_DIR"
write_runtime_public_config
chown diradmin:"$APACHE_GROUP" "$ADMINER_RUNTIME_CONFIG_FILE"
chmod 644 "$ADMINER_RUNTIME_CONFIG_FILE"
if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then
warn "Wykryto SELinux: ustawiam kontekst zapisu runtime dla Apache."
if command -v chcon >/dev/null 2>&1; then
chcon -R -t httpd_sys_rw_content_t "$ADMINER_RUNTIME_DIR" || warn "Nie udało się ustawić kontekstu SELinux dla runtime."
chcon -t httpd_sys_content_t "$ADMINER_CORE_FILE" "$ADMINER_EXTENSION_FILE" "$ADMINER_INDEX_FILE" || warn "Nie udało się ustawić kontekstu SELinux dla plików PHP."
else
warn "Brak chcon - ustaw kontekst SELinux ręcznie."
fi
fi
sekcja "Konfiguracja aliasu /adminer (DirectAdmin CustomBuild)"
configure_apache_alias
if [[ -x "$CB_BUILD" ]]; then
(cd "$CB_DIR" && ./build rewrite_confs) \
|| die "rewrite_confs zakończył się błędem."
info "rewrite_confs wykonane pomyślnie."
else
warn "Brak ${CB_BUILD} - uruchom ręcznie: cd ${CB_DIR} && ./build rewrite_confs"
fi
if systemctl is-active --quiet httpd 2>/dev/null; then
systemctl reload httpd || warn "Nie udało się przeładować httpd."
fi
sekcja "Aktualizacja ustawień pluginu"
if [[ -f "$PLUGIN_SETTINGS" ]]; then
set_setting "ENABLE_ADMINER" "true" "$PLUGIN_SETTINGS"
set_setting_if_missing "ADMINER_PUBLIC" "false" "$PLUGIN_SETTINGS"
set_setting_if_missing "ADMINER_PUBLIC_BASE_URL" "" "$PLUGIN_SETTINGS"
set_setting "ADMINER_URL_PATH" "$ADMINER_URL_PATH" "$PLUGIN_SETTINGS"
set_setting "ADMINER_SSO_DIR" "$ADMINER_RUNTIME_DIR" "$PLUGIN_SETTINGS"
set_setting "ADMINER_TICKET_TTL" "120" "$PLUGIN_SETTINGS"
set_setting "ADMINER_SESSION_TTL" "900" "$PLUGIN_SETTINGS"
set_setting "ADMINER_ROLE_TTL" "1200" "$PLUGIN_SETTINGS"
chown "$PLUGIN_OWNER" "$PLUGIN_SETTINGS" 2>/dev/null || true
chmod 640 "$PLUGIN_SETTINGS" 2>/dev/null || true
info "Zaktualizowano: ${PLUGIN_SETTINGS}"
else
warn "Nie znaleziono pliku ${PLUGIN_SETTINGS} - ustawienia pluginu pominięte."
fi
sekcja "Instalacja zakończona"
echo "Adminer URL: ${ADMINER_URL_PATH}"
echo "Katalog: ${ADMINER_INSTALL_DIR}"
echo "Runtime ticketów: ${ADMINER_TICKETS_DIR}"
echo "Runtime config: ${ADMINER_RUNTIME_CONFIG_FILE}"
echo "Alias template: ${APACHE_ALIAS_CUSTOM}"
echo "Log: ${LOGFILE}"
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# =============================================================================
# dbrestore.sh
# Przywracanie pojedynczej bazy PostgreSQL z pliku .sql lub .sql.gz
# wykonanego przez postgres_backup.sh
# =============================================================================
CREDENTIALS_FILE="/usr/local/directadmin/conf/postgresql.conf"
# Kolory tylko na terminalu
if [[ -t 1 ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; NC=''
fi
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
info() { echo -e " $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
die() { echo -e "${RED}[BŁĄD]${NC} $*" >&2; exit 1; }
# ---------------------------------------------------------------------------
# Sposób użycia
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
Użycie:
$(basename "$0") <plik.sql|plik.sql.gz>
Opis:
Przywraca bazę danych z pliku wykonanego przez postgres_backup.sh.
Obsługuje pliki nieskompresowane (.sql) i skompresowane (.sql.gz).
Jeśli docelowa baza istnieje, zostanie usunięta i odtworzona od nowa.
Przykłady:
$(basename "$0") /home/admin/postgres_backup/19-02-2026/sklep.sql
$(basename "$0") /home/admin/postgres_backup/19-02-2026/sklep.sql.gz
Uwaga:
Plik poświadczeń: ${CREDENTIALS_FILE}
Baza 'postgres' jest przywracana przez połączenie do 'template1',
pozostałe bazy przez połączenie do 'postgres'.
EOF
}
# ---------------------------------------------------------------------------
# Argumenty
# ---------------------------------------------------------------------------
if [[ $# -eq 0 ]]; then
usage
exit 0
fi
FILE="$1"
[[ -f "$FILE" ]] || die "Plik nie istnieje: ${FILE}"
case "$FILE" in
*.sql.gz|*.sql) ;;
*) die "Nieobsługiwany format pliku. Oczekiwany: .sql lub .sql.gz" ;;
esac
# ---------------------------------------------------------------------------
# Poświadczenia
# ---------------------------------------------------------------------------
[[ -f "$CREDENTIALS_FILE" ]] \
|| die "Brak pliku poświadczeń: ${CREDENTIALS_FILE}"
_perms=$(stat -c '%a' "$CREDENTIALS_FILE")
[[ "$_perms" == "600" || "$_perms" == "400" ]] \
|| die "Zbyt otwarte uprawnienia na ${CREDENTIALS_FILE} (${_perms}). Wymagane: 600 lub 400."
_line=$(head -1 "$CREDENTIALS_FILE")
PGHOST=$(echo "$_line" | cut -d: -f1)
PGPORT=$(echo "$_line" | cut -d: -f2)
PGUSER=$(echo "$_line" | cut -d: -f4)
[[ -n "$PGHOST" && -n "$PGPORT" && -n "$PGUSER" ]] \
|| die "Nieprawidłowy format pliku poświadczeń. Oczekiwany: host:port:database:user:password"
export PGPASSFILE="$CREDENTIALS_FILE"
# ---------------------------------------------------------------------------
# Ustal bazę inicjalną (nie można przywracać 'postgres' będąc do niej podłączonym)
# ---------------------------------------------------------------------------
DB_NAME=$(basename "$FILE" | sed 's/\.sql\.gz$//; s/\.sql$//')
if [[ "$DB_NAME" == "postgres" ]]; then
INIT_DB="template1"
else
INIT_DB="postgres"
fi
# ---------------------------------------------------------------------------
# Przywracanie
# ---------------------------------------------------------------------------
echo ""
ok "Plik: ${FILE}"
info "Baza: ${DB_NAME}"
info "Serwer: ${PGHOST}:${PGPORT} Użytkownik: ${PGUSER}"
echo ""
case "$FILE" in
*.sql.gz)
gunzip -c "$FILE" \
| psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password -d "$INIT_DB"
RC=${PIPESTATUS[1]}
;;
*.sql)
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password -d "$INIT_DB" -f "$FILE"
RC=$?
;;
esac
echo ""
if [[ $RC -eq 0 ]]; then
ok "Przywracanie bazy '${DB_NAME}' zakończone pomyślnie."
else
warn "Przywracanie bazy '${DB_NAME}' zakończone z ostrzeżeniami lub błędami (kod: ${RC})."
warn "Sprawdź powyższy output w celu identyfikacji problemów."
fi
exit $RC
+636
View File
@@ -0,0 +1,636 @@
#!/bin/bash
set -euo pipefail
CREDS_FILE="/usr/local/directadmin/conf/postgresql.conf"
PLUGIN_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
PLUGIN_SETTINGS_FILE="$PLUGIN_ROOT/plugin-settings.conf"
PGHOST_VAL=""
PGPORT_VAL="5432"
PGUSER_VAL="postgres"
PGPASSWORD_VAL=""
PG_VERSION_NUM=""
ALLOW_REMOTE_HOSTS="1"
RESTART_POSTGRESQL="0"
HBA_MANAGED_BLOCK_BEGIN="# DA_POSTGRESQL_PLUGIN_HBA_BEGIN"
HBA_MANAGED_BLOCK_END="# DA_POSTGRESQL_PLUGIN_HBA_END"
POSTGRESQL_CONF=""
HBA_FILE=""
CSF_ALLOW_FILE="/etc/csf/csf.allow"
CSF_INCLUDE_FILE="/etc/csf.allow.postgres"
PG_CONF_MARKER_BEGIN="# DA_POSTGRESQL_PLUGIN_BEGIN"
PG_CONF_MARKER_END="# DA_POSTGRESQL_PLUGIN_END"
declare -A REMOTE_IP_NOTES=()
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--restart-postgresql)
RESTART_POSTGRESQL="1"
;;
*)
echo "Nieznany argument: $1" >&2
exit 2
;;
esac
shift
done
}
find_psql() {
if command -v psql >/dev/null 2>&1; then
command -v psql
return
fi
local candidate
candidate="$(ls -1d /usr/pgsql-*/bin/psql 2>/dev/null | sort -V | tail -n1 || true)"
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
echo "$candidate"
return
fi
echo ""
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
strip_inline_comment() {
local value="$1"
value="${value%%#*}"
trim "$value"
}
unquote() {
local value
value="$(trim "$1")"
if [ "${#value}" -ge 2 ] && [ "${value:0:1}" = "'" ] && [ "${value: -1}" = "'" ]; then
value="${value:1:${#value}-2}"
elif [ "${#value}" -ge 2 ] && [ "${value:0:1}" = '"' ] && [ "${value: -1}" = '"' ]; then
value="${value:1:${#value}-2}"
fi
printf '%s' "$value"
}
bool_to_01() {
local raw
raw="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
raw="$(trim "$raw")"
case "$raw" in
1|true|yes|y|on|checked)
echo "1"
;;
*)
echo "0"
;;
esac
}
is_ipv4_strict() {
local ip="$1"
if [[ ! "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
return 1
fi
local IFS='.'
local o1 o2 o3 o4
read -r o1 o2 o3 o4 <<< "$ip"
for octet in "$o1" "$o2" "$o3" "$o4"; do
if [ "$octet" -gt 255 ]; then
return 1
fi
if [[ "$octet" =~ ^0[0-9]+$ ]]; then
return 1
fi
done
return 0
}
is_ipv4_cidr_strict() {
local cidr="$1"
if [[ ! "$cidr" =~ ^([^/]+)/([0-9]{1,2})$ ]]; then
return 1
fi
local ip="${BASH_REMATCH[1]}"
local prefix="${BASH_REMATCH[2]}"
is_ipv4_strict "$ip" || return 1
[ "$prefix" -ge 0 ] && [ "$prefix" -le 32 ]
}
skip_csf_remote_pattern() {
[ "$1" = "0.0.0.0/0" ]
}
sanitize_note() {
local note="$1"
note="${note//$'\r'/ }"
note="${note//$'\n'/ }"
note="${note//$'\t'/ }"
note="${note//#/ }"
note="$(echo "$note" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')"
note="${note:0:180}"
printf '%s' "$note"
}
load_plugin_settings() {
local plain_allow=""
local legacy_allow=""
if [ ! -r "$PLUGIN_SETTINGS_FILE" ]; then
ALLOW_REMOTE_HOSTS="1"
return
fi
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
raw_line="$(trim "$raw_line")"
[ -n "$raw_line" ] || continue
[[ "$raw_line" =~ ^# ]] && continue
[[ "$raw_line" == *"="* ]] || continue
local key="${raw_line%%=*}"
local value="${raw_line#*=}"
key="$(trim "$key")"
value="$(unquote "$(strip_inline_comment "$value")")"
case "$key" in
allow_remote_hosts)
plain_allow="$value"
;;
PG_PLUGIN_ALLOW_REMOTE_HOSTS)
legacy_allow="$value"
;;
esac
done < "$PLUGIN_SETTINGS_FILE"
if [ -n "$plain_allow" ]; then
ALLOW_REMOTE_HOSTS="$(bool_to_01 "$plain_allow")"
elif [ -n "$legacy_allow" ]; then
ALLOW_REMOTE_HOSTS="$(bool_to_01 "$legacy_allow")"
else
ALLOW_REMOTE_HOSTS="1"
fi
}
load_credentials() {
[ -r "$CREDS_FILE" ] || {
echo "Brak pliku poświadczeń: $CREDS_FILE" >&2
exit 1
}
local line
line="$(grep -Ev '^[[:space:]]*($|#)' "$CREDS_FILE" | head -n1 || true)"
[ -n "$line" ] || {
echo "Pusty plik poświadczeń: $CREDS_FILE" >&2
exit 1
}
if [[ "$line" == *=* ]]; then
while IFS='=' read -r key value; do
key="$(echo "$key" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')"
value="$(trim "$value")"
value="$(unquote "$value")"
case "$key" in
PG_HOST) PGHOST_VAL="$value" ;;
PG_PORT) PGPORT_VAL="$value" ;;
PG_USER) PGUSER_VAL="$value" ;;
PG_PASSWORD) PGPASSWORD_VAL="$value" ;;
esac
done < <(grep -Ev '^[[:space:]]*($|#)' "$CREDS_FILE")
else
local rest dbname
PGHOST_VAL="${line%%:*}"
rest="${line#*:}"
PGPORT_VAL="${rest%%:*}"
rest="${rest#*:}"
dbname="${rest%%:*}"
rest="${rest#*:}"
PGUSER_VAL="${rest%%:*}"
PGPASSWORD_VAL="${rest#*:}"
[ -n "$dbname" ] || dbname="postgres"
fi
[ -n "$PGHOST_VAL" ] || PGHOST_VAL="localhost"
[ -n "$PGPORT_VAL" ] || PGPORT_VAL="5432"
[ -n "$PGUSER_VAL" ] || PGUSER_VAL="postgres"
[ -n "$PGPASSWORD_VAL" ] || {
echo "Brak hasła PostgreSQL w $CREDS_FILE" >&2
exit 1
}
}
psql_exec() {
PGPASSWORD="$PGPASSWORD_VAL" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$PGUSER_VAL" -d postgres -v ON_ERROR_STOP=1 -At -F $'\t' "$@"
}
load_server_version_num() {
PG_VERSION_NUM="$(psql_exec -c 'SHOW server_version_num;' | tr -d '[:space:]')"
[[ "$PG_VERSION_NUM" =~ ^[0-9]+$ ]] || {
echo "Nieprawidłowy server_version_num: ${PG_VERSION_NUM}" >&2
exit 1
}
}
cleanup_hba_managed_entries() {
local hba_file="$1"
local tmp_file
tmp_file="$(mktemp)"
awk -v begin="$HBA_MANAGED_BLOCK_BEGIN" -v end="$HBA_MANAGED_BLOCK_END" '
$0 == begin {skip=1; next}
$0 == end {skip=0; next}
skip == 1 {next}
{
lower = tolower($0)
if (lower ~ /^[[:space:]]*include([[:space:]]|_if_exists)/ && lower ~ /pg_hba_da_plugin\.conf/) {
next
}
print
}
' "$hba_file" > "$tmp_file"
install -m 600 "$tmp_file" "$hba_file"
chown postgres:postgres "$hba_file" 2>/dev/null || true
rm -f "$tmp_file"
}
write_hba_inline_block() {
local hba_file="$1"
local include_file="$2"
local tmp_file
tmp_file="$(mktemp)"
cp -p "$hba_file" "${hba_file}.bak.$(date +%s)"
cat "$hba_file" > "$tmp_file"
{
echo ""
echo "$HBA_MANAGED_BLOCK_BEGIN"
echo "# Managed by DirectAdmin PostgreSQL plugin (inline mode)."
cat "$include_file"
echo "$HBA_MANAGED_BLOCK_END"
} >> "$tmp_file"
install -m 600 "$tmp_file" "$hba_file"
chown postgres:postgres "$hba_file" 2>/dev/null || true
rm -f "$tmp_file"
}
append_remote_ip_note() {
local ip="$1"
local note="$2"
if [ -z "${REMOTE_IP_NOTES[$ip]+x}" ]; then
REMOTE_IP_NOTES["$ip"]="$note"
return
fi
if [ -z "$note" ]; then
return
fi
local existing="${REMOTE_IP_NOTES[$ip]}"
if [ -z "$existing" ]; then
REMOTE_IP_NOTES["$ip"]="$note"
return
fi
if [[ "$existing" != *"$note"* ]]; then
REMOTE_IP_NOTES["$ip"]="$existing | $note"
fi
}
expand_hba_addresses() {
local host="$1"
case "$host" in
localhost)
echo "127.0.0.1/32"
echo "::1/128"
return
;;
127.0.0.1)
echo "127.0.0.1/32"
return
;;
::1)
echo "::1/128"
return
;;
esac
if is_ipv4_strict "$host"; then
echo "$host/32"
return
fi
if is_ipv4_cidr_strict "$host"; then
echo "$host"
fi
}
build_hba_include() {
local include_file="$1"
local tmp_file
tmp_file="$(mktemp)"
{
echo "# Auto-generated by DirectAdmin PostgreSQL plugin"
echo "# Generated at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
echo "# Method: scram-sha-256"
} > "$tmp_file"
local sql_query
sql_query="SELECT d.datname, h.role_name, h.host_pattern, COALESCE(h.note, '')
FROM da_plugin.access_hosts h
JOIN pg_roles r ON r.rolname = h.role_name
JOIN pg_database d
ON d.datistemplate = false
AND (
(COALESCE(h.db_name, '') <> '' AND d.datname = h.db_name)
OR
(COALESCE(h.db_name, '') = '' AND left(d.datname, length(h.da_user) + 1) = h.da_user || '_')
)
WHERE COALESCE(h.db_name, '') <> ''
OR has_database_privilege(h.role_name, d.datname, 'CONNECT')
ORDER BY d.datname, h.role_name, h.host_pattern;"
local query_output
query_output="$(psql_exec -c "$sql_query")" || {
local status=$?
rm -f "$tmp_file"
echo "Nie udało się odczytać rekordów hostów z da_plugin.access_hosts." >&2
return "$status"
}
local access_host_rows="0"
local generated_rules="0"
while IFS=$'\t' read -r db role host note; do
[ -n "$db" ] || continue
[ -n "$role" ] || continue
[ -n "$host" ] || continue
access_host_rows=$((access_host_rows + 1))
local host_note
host_note="$(sanitize_note "$note")"
local include_host="0"
local is_remote="0"
case "$host" in
localhost|127.0.0.1|::1)
include_host="1"
;;
*)
if [ "$ALLOW_REMOTE_HOSTS" = "1" ] && { is_ipv4_strict "$host" || is_ipv4_cidr_strict "$host"; }; then
include_host="1"
is_remote="1"
fi
;;
esac
[ "$include_host" = "1" ] || continue
if [ "$is_remote" = "1" ]; then
append_remote_ip_note "$host" "$host_note"
fi
local note_label="$host_note"
[ -n "$note_label" ] || note_label="(brak komentarza)"
printf '# role=%s db=%s host=%s comment=%s\n' "$role" "$db" "$host" "$note_label" >> "$tmp_file"
while IFS= read -r addr; do
[ -n "$addr" ] || continue
printf 'host\t%s\t%s\t%s\tscram-sha-256\n' "$db" "$role" "$addr" >> "$tmp_file"
generated_rules=$((generated_rules + 1))
done < <(expand_hba_addresses "$host")
done <<< "$query_output"
install -m 600 "$tmp_file" "$include_file"
chown postgres:postgres "$include_file" 2>/dev/null || true
rm -f "$tmp_file"
echo "Read access host rows: $access_host_rows"
echo "Generated pg_hba host rules: $generated_rules"
}
sorted_remote_ips() {
if [ "${#REMOTE_IP_NOTES[@]}" -eq 0 ]; then
return 0
fi
printf '%s\n' "${!REMOTE_IP_NOTES[@]}" | sort -V
}
write_postgresql_conf_block() {
local conf_file="$1"
local listen_value="localhost"
local has_remote_ips="0"
if [ "$ALLOW_REMOTE_HOSTS" = "1" ]; then
listen_value="*"
fi
if [ "$ALLOW_REMOTE_HOSTS" = "1" ] && [ "${#REMOTE_IP_NOTES[@]}" -gt 0 ]; then
has_remote_ips="1"
fi
local tmp_file
tmp_file="$(mktemp)"
awk -v begin="$PG_CONF_MARKER_BEGIN" -v end="$PG_CONF_MARKER_END" '
$0 == begin {skip=1; next}
$0 == end {skip=0; next}
skip != 1 {print}
' "$conf_file" > "$tmp_file"
{
echo ""
echo "$PG_CONF_MARKER_BEGIN"
echo "# Managed by DirectAdmin PostgreSQL plugin."
echo "listen_addresses = '$listen_value'"
echo "# Zdefiniowane hosty IPv4 z pluginu:"
if [ "$has_remote_ips" = "1" ]; then
while IFS= read -r ip; do
[ -n "$ip" ] || continue
local note="${REMOTE_IP_NOTES[$ip]}"
if [ -n "$note" ]; then
printf '# %s ; %s\n' "$ip" "$note"
else
printf '# %s\n' "$ip"
fi
done < <(sorted_remote_ips)
else
echo "# brak zdalnych hostów"
fi
echo "$PG_CONF_MARKER_END"
} >> "$tmp_file"
install -m 600 "$tmp_file" "$conf_file"
chown postgres:postgres "$conf_file" 2>/dev/null || true
rm -f "$tmp_file"
}
ensure_csf_include_directive() {
local allow_file="$1"
local include_file="$2"
[ -f "$allow_file" ] || return 1
local include_line="Include $include_file"
if ! grep -Fxq "$include_line" "$allow_file"; then
echo "$include_line" >> "$allow_file"
fi
return 0
}
write_csf_include_file() {
local include_file="$1"
local tmp_file
tmp_file="$(mktemp)"
{
echo "# Auto-generated by DirectAdmin PostgreSQL plugin"
echo "# Generated at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
} > "$tmp_file"
if [ "$ALLOW_REMOTE_HOSTS" = "1" ] && [ "${#REMOTE_IP_NOTES[@]}" -gt 0 ]; then
while IFS= read -r ip; do
[ -n "$ip" ] || continue
if skip_csf_remote_pattern "$ip"; then
continue
fi
local note="${REMOTE_IP_NOTES[$ip]}"
if [ -n "$note" ]; then
printf '%s # DA PostgreSQL: %s\n' "$ip" "$note" >> "$tmp_file"
else
printf '%s # DA PostgreSQL\n' "$ip" >> "$tmp_file"
fi
done < <(sorted_remote_ips)
fi
install -m 600 "$tmp_file" "$include_file"
chown root:root "$include_file" 2>/dev/null || true
rm -f "$tmp_file"
}
reload_postgres_conf() {
psql_exec -c 'SELECT pg_reload_conf();' >/dev/null
}
restart_postgresql_service() {
if ! command -v systemctl >/dev/null 2>&1; then
echo "Nie znaleziono systemctl, nie można zrestartować PostgreSQL." >&2
return 1
fi
local major=""
if [[ "$PG_VERSION_NUM" =~ ^[0-9]+$ ]] && [ "$PG_VERSION_NUM" -ge 100000 ]; then
major="$((PG_VERSION_NUM / 10000))"
fi
local candidates=()
if [ -n "$major" ]; then
candidates+=("postgresql-${major}" "postgresql@${major}-main")
fi
candidates+=("postgresql")
local service
for service in "${candidates[@]}"; do
if systemctl list-unit-files --no-legend "${service}.service" 2>/dev/null | grep -q . || systemctl status "$service" >/dev/null 2>&1; then
if systemctl restart "$service"; then
echo "Restarted PostgreSQL service: $service"
return 0
fi
fi
done
echo "Nie znaleziono usługi PostgreSQL do restartu. Próbowano: ${candidates[*]}" >&2
return 1
}
reload_csf_if_available() {
if command -v csf >/dev/null 2>&1; then
csf -r >/dev/null 2>&1 || true
fi
}
parse_args "$@"
load_plugin_settings
load_credentials
PSQL_BIN="$(find_psql)"
[ -n "$PSQL_BIN" ] || {
echo "Nie znaleziono binarki psql" >&2
exit 1
}
HBA_FILE="$(psql_exec -c 'SHOW hba_file;')"
[ -n "$HBA_FILE" ] || {
echo "Nie udało się odczytać ścieżki do pg_hba.conf" >&2
exit 1
}
POSTGRESQL_CONF="$(psql_exec -c 'SHOW config_file;')"
[ -n "$POSTGRESQL_CONF" ] || {
echo "Nie udało się odczytać ścieżki do postgresql.conf" >&2
exit 1
}
load_server_version_num
HBA_DIR="$(dirname "$HBA_FILE")"
HBA_INCLUDE_FILE="$HBA_DIR/pg_hba_da_plugin.conf"
build_hba_include "$HBA_INCLUDE_FILE"
cleanup_hba_managed_entries "$HBA_FILE"
write_hba_inline_block "$HBA_FILE" "$HBA_INCLUDE_FILE"
CSF_SYNCED="0"
CSF_SKIP_REASON=""
if [ "$ALLOW_REMOTE_HOSTS" = "1" ]; then
write_postgresql_conf_block "$POSTGRESQL_CONF"
if ensure_csf_include_directive "$CSF_ALLOW_FILE" "$CSF_INCLUDE_FILE"; then
write_csf_include_file "$CSF_INCLUDE_FILE"
reload_csf_if_available
CSF_SYNCED="1"
else
CSF_SKIP_REASON="CSF allow file not found: $CSF_ALLOW_FILE"
echo "Ostrzeżenie: nie znaleziono $CSF_ALLOW_FILE, pomijam aktualizację CSF." >&2
fi
fi
reload_postgres_conf
if [ "$RESTART_POSTGRESQL" = "1" ]; then
restart_postgresql_service
fi
echo "Synchronized pg_hba inline block in: $HBA_FILE"
echo "Generated pg_hba include file (diagnostic copy): $HBA_INCLUDE_FILE"
if [ "$ALLOW_REMOTE_HOSTS" = "1" ]; then
echo "Synchronized postgresql.conf: $POSTGRESQL_CONF"
echo "PostgreSQL restart required for listen_addresses changes"
if [ "$CSF_SYNCED" = "1" ]; then
echo "Synchronized CSF include: $CSF_INCLUDE_FILE"
else
echo "Skipped CSF synchronization: ${CSF_SKIP_REASON:-CSF allow file unavailable}"
fi
else
echo "Remote hosts disabled: skipped postgresql.conf and CSF synchronization"
fi
+226
View File
@@ -0,0 +1,226 @@
#!/bin/bash
# =============================================================================
# postgres_backup.sh
# Backup wszystkich baz PostgreSQL na serwerze
#
# Przeznaczenie: cron (nie interaktywny)
# Wymagania: pg_dump, psql, dostęp do pliku poświadczeń (format pgpass)
#
# Przywracanie pojedynczej bazy:
# psql -h localhost -U postgres -d postgres -f nazwa_bazy.sql
# gunzip -c nazwa_bazy.sql.gz | psql -h localhost -U postgres -d postgres
# =============================================================================
# =============================================================================
# KONFIGURACJA
# =============================================================================
BASE_DIR="/home/admin/postgres_backup"
# Format daty katalogu backupu (składnia date +FORMAT)
DATE_FORMAT="%d-%m-%Y"
# 1 = dodaj godzinę i minuty do nazwy katalogu (np. 19-02-2026_14-35)
ADD_TIME=0
# Liczba dni przechowywania backupów (0 = bez automatycznego usuwania)
RETENTION=7
# 1 = kompresuj do .sql.gz | 0 = zostawiaj jako .sql
ENABLE_COMPRESSION=1
# Plik poświadczeń w formacie pgpass: host:port:database:user:password
# Musi mieć uprawnienia 600 lub 400
CREDENTIALS_FILE="/usr/local/directadmin/conf/postgresql.conf"
# Bazy wykluczone z backupu (oddzielone spacją)
EXCLUDE_DBS="template0 template1"
# =============================================================================
# WEWNĘTRZNA KONFIGURACJA nie zmieniaj
# =============================================================================
LOCK_FILE="/var/run/postgres_backup.lock"
LOG_FILE="/var/log/postgres_backup.log"
# =============================================================================
# INICJALIZACJA
# =============================================================================
set -uo pipefail
# Kolory tylko na terminalu
if [[ -t 1 ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; NC=''
fi
ts() { date '+%Y-%m-%d %H:%M:%S'; }
log() { echo "[$(ts)] $*" | tee -a "$LOG_FILE"; }
ok() { echo -e "${GREEN}[OK]${NC} [$(ts)] $*" | tee -a "$LOG_FILE"; }
warn() { echo -e "${YELLOW}[WARN]${NC} [$(ts)] $*" | tee -a "$LOG_FILE"; }
die() { echo -e "${RED}[BŁĄD]${NC} [$(ts)] $*" | tee -a "$LOG_FILE" >&2; cleanup; exit 1; }
cleanup() { rm -f "$LOCK_FILE"; }
trap cleanup EXIT INT TERM
# ---------------------------------------------------------------------------
# Blokada przed równoległym uruchomieniem
# ---------------------------------------------------------------------------
if [[ -f "$LOCK_FILE" ]]; then
OLD_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
if [[ -n "$OLD_PID" ]] && kill -0 "$OLD_PID" 2>/dev/null; then
die "Backup jest już uruchomiony (PID: ${OLD_PID}). Przerywam."
fi
warn "Stały plik blokady (PID ${OLD_PID} nie istnieje) usuwam i kontynuuję."
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
# ---------------------------------------------------------------------------
# Weryfikacja pliku poświadczeń
# ---------------------------------------------------------------------------
[[ -f "$CREDENTIALS_FILE" ]] \
|| die "Brak pliku poświadczeń: ${CREDENTIALS_FILE}"
_perms=$(stat -c '%a' "$CREDENTIALS_FILE")
[[ "$_perms" == "600" || "$_perms" == "400" ]] \
|| die "Plik ${CREDENTIALS_FILE} ma zbyt otwarte uprawnienia (${_perms}). Wymagane: 600 lub 400."
# Parsowanie: host:port:database:user:password
_line=$(head -1 "$CREDENTIALS_FILE")
PGHOST=$(echo "$_line" | cut -d: -f1)
PGPORT=$(echo "$_line" | cut -d: -f2)
PGUSER=$(echo "$_line" | cut -d: -f4)
[[ -n "$PGHOST" && -n "$PGPORT" && -n "$PGUSER" ]] \
|| die "Nieprawidłowy format pliku poświadczeń. Oczekiwany: host:port:database:user:password"
# Hasło przekazywane przez PGPASSFILE nie pojawia się w środowisku ani linii poleceń
export PGPASSFILE="$CREDENTIALS_FILE"
# ---------------------------------------------------------------------------
# Katalog backupu
# ---------------------------------------------------------------------------
if [[ "$ADD_TIME" == "1" ]]; then
DATE_STR=$(date +"${DATE_FORMAT}_%H-%M")
else
DATE_STR=$(date +"${DATE_FORMAT}")
fi
BACKUP_DIR="${BASE_DIR}/${DATE_STR}"
mkdir -p "$BACKUP_DIR" || die "Nie można utworzyć katalogu backupu: ${BACKUP_DIR}"
# =============================================================================
# BACKUP
# =============================================================================
log "================================================================="
log "Rozpoczęcie backupu PostgreSQL"
log "Host: ${PGHOST}:${PGPORT} Użytkownik: ${PGUSER}"
log "Katalog: ${BACKUP_DIR}"
log "Kompresja: ${ENABLE_COMPRESSION} Retencja: ${RETENTION} dni"
log "================================================================="
# ---------------------------------------------------------------------------
# Globals: użytkownicy, role, hasła, przynależności (pg_dumpall --globals-only)
# Wymagane do pełnego disaster recovery przywróć jako pierwsze
# ---------------------------------------------------------------------------
ERRORS=0
BACKED_UP=0
GLOBALS_SUCCESS=true
if [[ "$ENABLE_COMPRESSION" == "1" ]]; then
GLOBALS_FILE="${BACKUP_DIR}/_globals.sql.gz"
pg_dumpall \
-h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password --globals-only --clean --if-exists \
2>>"$LOG_FILE" \
| gzip -9 > "$GLOBALS_FILE" \
|| GLOBALS_SUCCESS=false
else
GLOBALS_FILE="${BACKUP_DIR}/_globals.sql"
pg_dumpall \
-h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password --globals-only --clean --if-exists \
> "$GLOBALS_FILE" 2>>"$LOG_FILE" \
|| GLOBALS_SUCCESS=false
fi
if [[ "$GLOBALS_SUCCESS" == "true" ]]; then
SIZE=$(du -sh "$GLOBALS_FILE" 2>/dev/null | cut -f1)
ok "_globals → ${GLOBALS_FILE##*/} (${SIZE})"
else
warn "_globals: backup ról i użytkowników nie powiódł się plik usunięty."
rm -f "$GLOBALS_FILE"
ERRORS=$((ERRORS + 1))
fi
# Lista baz danych
DATABASES=$(
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password -t -A \
-c "SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname;" \
2>>"$LOG_FILE"
) || die "Nie można pobrać listy baz. Sprawdź połączenie i poświadczenia."
# Zbuduj wzorzec wykluczeń
EXCLUDE_PATTERN=$(echo "$EXCLUDE_DBS" | tr ' ' '|')
for DB in $DATABASES; do
# Pomiń wykluczone
if echo "$DB" | grep -qE "^(${EXCLUDE_PATTERN})$"; then
continue
fi
SUCCESS=true
if [[ "$ENABLE_COMPRESSION" == "1" ]]; then
OUT_FILE="${BACKUP_DIR}/${DB}.sql.gz"
pg_dump \
-h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password --create --clean --if-exists \
-F p "$DB" 2>>"$LOG_FILE" \
| gzip -9 > "$OUT_FILE" \
|| SUCCESS=false
else
OUT_FILE="${BACKUP_DIR}/${DB}.sql"
pg_dump \
-h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password --create --clean --if-exists \
-F p "$DB" > "$OUT_FILE" 2>>"$LOG_FILE" \
|| SUCCESS=false
fi
if [[ "$SUCCESS" == "true" ]]; then
SIZE=$(du -sh "$OUT_FILE" 2>/dev/null | cut -f1)
ok "${DB}${OUT_FILE##*/} (${SIZE})"
BACKED_UP=$((BACKED_UP + 1))
else
warn "${DB}: backup nie powiódł się plik usunięty."
rm -f "$OUT_FILE"
ERRORS=$((ERRORS + 1))
fi
done
log "Wynik: ${BACKED_UP} OK, ${ERRORS} błędów."
# =============================================================================
# RETENCJA
# =============================================================================
if [[ "$RETENTION" -gt 0 ]]; then
log "Usuwanie backupów starszych niż ${RETENTION} dni..."
find "$BASE_DIR" -maxdepth 1 -mindepth 1 -type d -mtime +"$RETENTION" \
-exec rm -rf {} + 2>/dev/null || true
fi
# =============================================================================
# ZAKOŃCZENIE
# =============================================================================
log "Backup zakończony."
[[ "$ERRORS" -eq 0 ]] || exit 1
exit 0
+786
View File
@@ -0,0 +1,786 @@
#!/bin/bash
# =============================================================================
# postgresql_install.sh
# Instalacja PostgreSQL na AlmaLinux 8/9, CloudLinux 8/9
# i systemach kompatybilnych z RHEL pod kontrolą DirectAdmin
#
# Polityka bezpieczeństwa:
# - Dostęp wyłącznie przez localhost (listen_addresses = 'localhost')
# - Uwierzytelnianie hasłem dla WSZYSTKICH połączeń (brak peer, brak trust)
# - Połączenia replikacji odrzucone
#
# Rozszerzenia PHP:
# - Podejście przez custom configure (zapewnia pgsql + pdo_pgsql)
# - Obsługa: apache, nginx, nginx_apache, litespeed, openlitespeed
# - Tryby PHP: php-fpm, fastcgi, lsphp
# =============================================================================
set -euo pipefail
# ---------------------------------------------------------------------------
# Kolory
# ---------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
sekcja() { echo -e "\n${BOLD}${CYAN}=== $* ===${NC}\n"; }
die() { echo -e "${RED}[BŁĄD]${NC} $*" >&2; exit 1; }
CB_PHP_WEBSERVER=""
CB_PHP_OPTIONS_CONF=""
DA_PG_RUNTIME_LIBDIR=""
DA_PG_RUNTIME_LIBPQ=""
declare -ag CB_PHP_VERSIONS=()
declare -ag CB_PHP_CODES=()
declare -ag CB_PHP_MODES=()
cb_pgsql_reset_php_targets() {
CB_PHP_VERSIONS=()
CB_PHP_CODES=()
CB_PHP_MODES=()
}
cb_pgsql_get_cb_option() {
local options_conf="$1"
local key="$2"
local value=""
[[ -f "$options_conf" ]] || return 1
value="$(awk -F= -v key="$key" '
$1 == key {
sub(/^[[:space:]]+/, "", $2)
sub(/[[:space:]]+$/, "", $2)
print $2
exit
}
' "$options_conf")"
[[ -n "$value" ]] || return 1
printf '%s\n' "$value"
}
cb_pgsql_php_code_from_version() {
printf '%s\n' "${1//./}"
}
cb_pgsql_php_version_from_code() {
local code="$1"
if [[ "$code" == *.* ]]; then
printf '%s\n' "$code"
elif [[ "$code" =~ ^[0-9]{2,3}$ ]]; then
printf '%s.%s\n' "${code:0:1}" "${code:1}"
else
printf '%s\n' "$code"
fi
}
cb_pgsql_add_php_target() {
local version="$1"
local mode="$2"
local code=""
local idx=""
[[ -n "$version" ]] || return 0
code="$(cb_pgsql_php_code_from_version "$version")"
for idx in "${!CB_PHP_CODES[@]}"; do
if [[ "${CB_PHP_CODES[$idx]}" == "$code" ]]; then
return 0
fi
done
CB_PHP_VERSIONS+=("$version")
CB_PHP_CODES+=("$code")
CB_PHP_MODES+=("${mode:-php-fpm}")
}
cb_pgsql_detect_webserver() {
local cb_dir="$1"
CB_PHP_OPTIONS_CONF="${cb_dir}/options.conf"
CB_PHP_WEBSERVER="apache"
if [[ -f "$CB_PHP_OPTIONS_CONF" ]]; then
CB_PHP_WEBSERVER="$(cb_pgsql_get_cb_option "$CB_PHP_OPTIONS_CONF" "webserver" || true)"
[[ -n "$CB_PHP_WEBSERVER" ]] || CB_PHP_WEBSERVER="apache"
fi
}
cb_pgsql_collect_php_targets() {
local cb_dir="$1"
local slot=""
local release=""
local mode=""
local default_mode=""
cb_pgsql_reset_php_targets
cb_pgsql_detect_webserver "$cb_dir"
# CloudLinux może używać lsphp także przy webserver=apache.
# Dlatego najpierw honorujemy phpN_mode/php1_mode z options.conf,
# a dopiero przy ich braku stosujemy domyślny tryb wynikający z webserwera.
case "$CB_PHP_WEBSERVER" in
litespeed|openlitespeed) default_mode="lsphp" ;;
*) default_mode="php-fpm" ;;
esac
for slot in 1 2 3 4 5 6 7 8 9; do
release="$(cb_pgsql_get_cb_option "$CB_PHP_OPTIONS_CONF" "php${slot}_release" || true)"
mode="$(cb_pgsql_get_cb_option "$CB_PHP_OPTIONS_CONF" "php${slot}_mode" || true)"
[[ -n "$mode" ]] || mode="$(cb_pgsql_get_cb_option "$CB_PHP_OPTIONS_CONF" "php1_mode" || true)"
[[ -n "$mode" ]] || mode="$default_mode"
case "${release,,}" in
""|no|off) continue ;;
esac
case "${mode,,}" in
no|off) continue ;;
esac
cb_pgsql_add_php_target "$release" "$mode"
done
}
cb_pgsql_build_file_candidates() {
local cb_dir="$1"
local mode="$2"
local code="$3"
local -n out_ref="$4"
out_ref=()
if [[ "$mode" == "lsphp" ]]; then
case "$CB_PHP_WEBSERVER" in
litespeed)
out_ref+=("${cb_dir}/configure/litespeed/configure.php${code}")
;;
openlitespeed)
out_ref+=("${cb_dir}/configure/openlitespeed/configure.php${code}")
;;
esac
out_ref+=("${cb_dir}/configure/lsphp/configure.lsphp${code}")
out_ref+=("${cb_dir}/configure/lsphp/configure.php${code}")
fi
out_ref+=("${cb_dir}/configure/php/configure.php${code}")
}
cb_pgsql_resolve_config_paths() {
local cb_dir="$1"
local version="$2"
local mode="$3"
local source_ref_name="$4"
local custom_ref_name="$5"
local code=""
local -a candidates=()
local candidate=""
code="$(cb_pgsql_php_code_from_version "$version")"
printf -v "$source_ref_name" '%s' ""
printf -v "$custom_ref_name" '%s' ""
cb_pgsql_build_file_candidates "$cb_dir" "$mode" "$code" candidates
for candidate in "${candidates[@]}"; do
if [[ -f "$candidate" ]]; then
printf -v "$source_ref_name" '%s' "$candidate"
printf -v "$custom_ref_name" '%s' "$(printf '%s\n' "$candidate" | sed "s#^${cb_dir}/configure/#${cb_dir}/custom/#")"
return 0
fi
done
for candidate in "${candidates[@]}"; do
candidate="$(printf '%s\n' "$candidate" | sed "s#^${cb_dir}/configure/#${cb_dir}/custom/#")"
if [[ -f "$candidate" ]]; then
printf -v "$source_ref_name" '%s' "$candidate"
printf -v "$custom_ref_name" '%s' "$candidate"
return 0
fi
done
return 1
}
cb_pgsql_insert_flags() {
local config_file="$1"
local pg_prefix="$2"
local tmp_file="${config_file}.tmp"
sed -i '/--with-pgsql=/d; /--with-pdo-pgsql=/d' "$config_file"
if grep -q -- '--with-pdo-mysql' "$config_file"; then
awk -v prefix="$pg_prefix" '
/--with-pdo-mysql/ {
print
print "\t--with-pgsql=" prefix " \\"
print "\t--with-pdo-pgsql=" prefix " \\"
next
}
{ print }
' "$config_file" > "$tmp_file"
else
awk -v prefix="$pg_prefix" '
/^[[:space:]]*--with/ { last = NR }
{ lines[NR] = $0 }
END {
if (last == 0) {
for (i = 1; i <= NR; i++) print lines[i]
exit
}
for (i = 1; i <= NR; i++) {
print lines[i]
if (i == last) {
print "\t--with-pgsql=" prefix " \\"
print "\t--with-pdo-pgsql=" prefix " \\"
}
}
}
' "$config_file" > "$tmp_file"
fi
mv "$tmp_file" "$config_file"
}
cb_pgsql_prepare_custom_config() {
local cb_dir="$1"
local version="$2"
local mode="$3"
local pg_prefix="$4"
local source_conf=""
local custom_conf=""
local ts=""
if ! cb_pgsql_resolve_config_paths "$cb_dir" "$version" "$mode" source_conf custom_conf; then
warn "Nie znaleziono configure dla PHP ${version} (tryb ${mode}, webserver ${CB_PHP_WEBSERVER})."
return 1
fi
mkdir -p "$(dirname "$custom_conf")"
ts="$(date +%s)"
if [[ "$source_conf" != "$custom_conf" && ! -f "$custom_conf" ]]; then
cp -p "$source_conf" "$custom_conf"
elif [[ -f "$custom_conf" ]]; then
cp -p "$custom_conf" "${custom_conf}.bak.${ts}"
fi
cb_pgsql_insert_flags "$custom_conf" "$pg_prefix"
chmod +x "$custom_conf" || true
info "Przygotowano configure dla PHP ${version}: ${custom_conf}"
return 0
}
cb_pgsql_configure_runtime_libpq() {
local pg_prefix="$1"
local pg_libdir="${pg_prefix}/lib"
local libpq_so="${pg_libdir}/libpq.so.5"
local ld_conf="/etc/ld.so.conf.d/00-da-postgresql-libpq.conf"
local old_ld_conf="/etc/ld.so.conf.d/da-postgresql-libpq.conf"
local preferred=""
[[ -d "$pg_libdir" ]] || die "Nie znaleziono katalogu bibliotek PostgreSQL: ${pg_libdir}"
[[ -r "$libpq_so" ]] || die "Nie znaleziono biblioteki libpq: ${libpq_so}"
printf '%s\n' "$pg_libdir" > "$ld_conf"
chmod 644 "$ld_conf"
[[ "$old_ld_conf" == "$ld_conf" ]] || rm -f "$old_ld_conf"
if command -v ldconfig >/dev/null 2>&1; then
ldconfig || die "Nie udało się odświeżyć cache bibliotek przez ldconfig."
preferred="$(ldconfig -p 2>/dev/null | awk '/libpq\.so\.5/{print $NF; exit}')"
if [[ -n "$preferred" && "$preferred" != "$libpq_so" ]]; then
warn "Domyślna libpq to ${preferred}; wymuszę właściwą bibliotekę przez LD_PRELOAD podczas kompilacji PHP."
fi
else
warn "Brak ldconfig. Kontynuuję z LD_LIBRARY_PATH."
fi
DA_PG_RUNTIME_LIBDIR="$pg_libdir"
DA_PG_RUNTIME_LIBPQ="$libpq_so"
info "Skonfigurowano bibliotekę runtime PostgreSQL: ${pg_libdir}"
}
cb_pgsql_find_php_bin_for_version() {
local version="$1"
local code=""
local candidate=""
code="$(cb_pgsql_php_code_from_version "$version")"
for candidate in \
"/usr/local/php${code}/bin/php" \
"/usr/local/php${code}/bin/lsphp" \
"/usr/local/lsphp${code}/bin/lsphp" \
"/usr/local/php${code}/bin/php-cgi"; do
if [[ -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
cb_pgsql_probe_php_extensions() {
local php_bin="$1"
local pgsql_ref_name="$2"
local pdo_ref_name="$3"
local modules=""
local modules_lc=""
local probe_pgsql_status="NIE"
local probe_pdo_status="NIE"
modules="$("$php_bin" -m 2>/dev/null || true)"
if [[ -z "$modules" ]]; then
probe_pgsql_status="BŁĄD"
probe_pdo_status="BŁĄD"
printf -v "$pgsql_ref_name" '%s' "$probe_pgsql_status"
printf -v "$pdo_ref_name" '%s' "$probe_pdo_status"
return 1
fi
modules_lc="$(printf '%s\n' "$modules" | tr '[:upper:]' '[:lower:]')"
if printf '%s\n' "$modules_lc" | grep -qx 'pgsql'; then
probe_pgsql_status="OK"
fi
if printf '%s\n' "$modules_lc" | grep -qx 'pdo_pgsql'; then
probe_pdo_status="OK"
fi
printf -v "$pgsql_ref_name" '%s' "$probe_pgsql_status"
printf -v "$pdo_ref_name" '%s' "$probe_pdo_status"
return 0
}
cb_pgsql_render_extensions_table() {
local found=0
local idx=""
local version=""
local php_bin=""
local pgsql_status=""
local pdo_pgsql_status=""
echo "+------------+-------+-----------+"
printf "| %-10s | %-5s | %-9s |\n" "Wersja PHP" "PGSQL" "PDO_PGSQL"
echo "+------------+-------+-----------+"
for idx in "${!CB_PHP_VERSIONS[@]}"; do
version="${CB_PHP_VERSIONS[$idx]}"
php_bin="$(cb_pgsql_find_php_bin_for_version "$version" || true)"
if [[ -z "$php_bin" ]]; then
printf "| %-10s | %-5s | %-9s |\n" "PHP ${version}" "BRAK" "BRAK"
found=1
continue
fi
cb_pgsql_probe_php_extensions "$php_bin" pgsql_status pdo_pgsql_status >/dev/null 2>&1 || true
printf "| %-10s | %-5s | %-9s |\n" "PHP ${version}" "$pgsql_status" "$pdo_pgsql_status"
found=1
done
if [[ $found -eq 1 ]]; then
echo "+------------+-------+-----------+"
else
warn "Nie znaleziono wersji PHP do raportu."
fi
}
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
POSTGRES_BACKUP_SRC="${SCRIPT_DIR}/postgres_backup.sh"
POSTGRES_BACKUP_DST="/opt/postgres_backup"
POSTGRES_BACKUP_CRON_LINE="0 2 * * * /opt/postgres_backup >/dev/null 2>&1"
# ---------------------------------------------------------------------------
# Wymagany root
# ---------------------------------------------------------------------------
[[ $EUID -eq 0 ]] || die "Skrypt musi być uruchomiony jako root."
# ---------------------------------------------------------------------------
# Plik logu terminal: kolorowy output / plik: czysty tekst (bez ANSI)
# ---------------------------------------------------------------------------
LOGFILE="/var/log/postgresql_install_$(date +%Y%m%d_%H%M%S).log"
exec 1> >(tee >(sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' >> "$LOGFILE")) 2>&1
info "Pełny log zapisany w: $LOGFILE"
# ---------------------------------------------------------------------------
# Wykrywanie systemu operacyjnego
# ---------------------------------------------------------------------------
sekcja "Wykrywanie systemu operacyjnego"
[[ -f /etc/os-release ]] || die "Nie można wykryć systemu: brak pliku /etc/os-release."
source /etc/os-release
OS_ID="${ID:-}"
OS_MAJOR="${VERSION_ID%%.*}"
case "$OS_ID" in
almalinux) OS_LABEL="AlmaLinux ${VERSION_ID}" ;;
cloudlinux) OS_LABEL="CloudLinux ${VERSION_ID}" ;;
centos|rhel|rocky|ol)
OS_LABEL="${NAME} ${VERSION_ID}"
warn "System '${OS_LABEL}' jest kompatybilny z RHEL, ale nie jest głównym celem skryptu."
;;
*)
die "Nieobsługiwany system: ${OS_ID}. Skrypt obsługuje AlmaLinux/CloudLinux 8 i 9."
;;
esac
[[ "$OS_MAJOR" == "8" || "$OS_MAJOR" == "9" ]] \
|| die "Nieobsługiwana wersja główna ${OS_MAJOR}. Obsługiwane są tylko wersje 8 i 9."
[[ "$(uname -m)" == "x86_64" ]] || die "Obsługiwana jest tylko architektura x86_64."
info "Wykryto: ${OS_LABEL} (EL${OS_MAJOR})"
# =============================================================================
# PYTANIA INTERAKTYWNE
# =============================================================================
# ---------------------------------------------------------------------------
# P1: Wersja PostgreSQL
# ---------------------------------------------------------------------------
sekcja "Wybór wersji PostgreSQL"
AVAILABLE_VERSIONS=(14 15 16 17 18)
echo "Dostępne wersje PostgreSQL:"
idx=1
for v in "${AVAILABLE_VERSIONS[@]}"; do
echo " [${idx}] PostgreSQL ${v}"
((idx++))
done
echo " [${idx}] Inna (podaj ręcznie numer wersji)"
echo ""
while true; do
read -rp "Wybierz opcję [1-${idx}, domyślnie 4]: " _ver
_ver="${_ver:-4}"
if [[ ! "$_ver" =~ ^[0-9]+$ ]] || (( _ver < 1 || _ver > idx )); then
warn "Podaj liczbę od 1 do ${idx}."
continue
fi
if (( _ver < idx )); then
PG_VERSION="${AVAILABLE_VERSIONS[$((_ver-1))]}"
break
fi
while true; do
read -rp "Podaj ręcznie numer wersji PostgreSQL (np. 16 lub 17): " PG_VERSION
if [[ ! "$PG_VERSION" =~ ^[0-9]+$ ]]; then
warn "Dozwolone są tylko liczby całkowite, np. 16 lub 17."
continue
fi
break
done
break
done
info "Wybrano: PostgreSQL ${PG_VERSION}"
PG_SERVICE="postgresql-${PG_VERSION}"
PG_BINDIR="/usr/pgsql-${PG_VERSION}/bin"
PG_DATADIR="/var/lib/pgsql/${PG_VERSION}/data"
PG_HBA="${PG_DATADIR}/pg_hba.conf"
PG_CONF="${PG_DATADIR}/postgresql.conf"
PG_AUTH="scram-sha-256"
# ---------------------------------------------------------------------------
# P2: Rozszerzenia PHP
# ---------------------------------------------------------------------------
CB_DIR="/usr/local/directadmin/custombuild"
INSTALL_PHP="nie"
if [[ -x "${CB_DIR}/build" ]]; then
while true; do
read -rp "Zainstalować rozszerzenia pgsql i pdo_pgsql dla PHP? [t/n, domyślnie t]: " _php
_php="${_php:-t}"
case "${_php,,}" in
t|tak) INSTALL_PHP="tak"; break ;;
n|nie) INSTALL_PHP="nie"; break ;;
*) warn "Podaj t lub n." ;;
esac
done
else
warn "Nie znaleziono DirectAdmin CustomBuild w ${CB_DIR}/build pomijam instalację rozszerzeń PHP."
fi
# =============================================================================
# INSTALACJA
# =============================================================================
genpasswd() { tr -dc 'A-Za-z0-9_' </dev/urandom | head -c "${1:-32}"; echo; }
# ---------------------------------------------------------------------------
# 1. Repozytorium PGDG
# ---------------------------------------------------------------------------
sekcja "Instalacja repozytorium PGDG (EL${OS_MAJOR})"
PGDG_RPM="https://download.postgresql.org/pub/repos/yum/reporpms/EL-${OS_MAJOR}-x86_64/pgdg-redhat-repo-latest.noarch.rpm"
if rpm -q pgdg-redhat-repo &>/dev/null; then
info "Usuwam istniejące repozytorium PGDG."
dnf remove -y pgdg-redhat-repo
fi
info "Instaluję: ${PGDG_RPM}"
dnf install -y "$PGDG_RPM" \
|| die "Nie udało się zainstalować repozytorium PGDG."
dnf -qy module disable postgresql 2>/dev/null || true
# ---------------------------------------------------------------------------
# 2. Instalacja pakietów PostgreSQL
# ---------------------------------------------------------------------------
sekcja "Instalacja pakietów PostgreSQL ${PG_VERSION}"
dnf install -y \
"postgresql${PG_VERSION}-server" \
"postgresql${PG_VERSION}-contrib" \
"postgresql${PG_VERSION}" \
|| die "Nie udało się zainstalować pakietów PostgreSQL."
# ---------------------------------------------------------------------------
# 3. Inicjalizacja klastra
# ---------------------------------------------------------------------------
sekcja "Inicjalizacja klastra PostgreSQL"
if [[ -f "${PG_DATADIR}/PG_VERSION" ]]; then
warn "Klaster jest już zainicjalizowany w ${PG_DATADIR}. Pomijam initdb."
else
"${PG_BINDIR}/postgresql-${PG_VERSION}-setup" initdb \
|| die "Inicjalizacja klastra nie powiodła się."
fi
# ---------------------------------------------------------------------------
# 4. Zabezpieczenie postgresql.conf
# ---------------------------------------------------------------------------
sekcja "Zabezpieczanie postgresql.conf"
cp -p "$PG_CONF" "${PG_CONF}.bak.$(date +%s)"
pg_param() {
local param="$1" value="$2"
sed -i "s|^\s*${param}\s*=.*|#&|" "$PG_CONF"
echo "${param} = ${value}" >> "$PG_CONF"
}
pg_param listen_addresses "'localhost'"
pg_param password_encryption "scram-sha-256"
pg_param max_connections 100
pg_param unix_socket_permissions 0700
pg_param log_connections on
pg_param log_disconnections on
pg_param log_hostname off
pg_param log_line_prefix "'%m [%p] %q%u@%d '"
info "postgresql.conf zaktualizowany."
# ---------------------------------------------------------------------------
# 5. Tymczasowy pg_hba.conf (tylko trust dla postgres przez socket)
# ---------------------------------------------------------------------------
cp -p "$PG_HBA" "${PG_HBA}.orig.$(date +%s)"
cat > "$PG_HBA" <<EOF
# TYMCZASOWY zastąpiony po ustawieniu hasła
local all postgres trust
host all all 127.0.0.1/32 reject
host all all ::1/128 reject
EOF
chown postgres:postgres "$PG_HBA"
chmod 600 "$PG_HBA"
# ---------------------------------------------------------------------------
# 6. Uruchomienie PostgreSQL
# ---------------------------------------------------------------------------
sekcja "Uruchamianie PostgreSQL ${PG_VERSION}"
systemctl enable "${PG_SERVICE}"
systemctl start "${PG_SERVICE}" \
|| die "Nie udało się uruchomić ${PG_SERVICE}. Sprawdź: journalctl -xe"
sleep 2
systemctl is-active --quiet "${PG_SERVICE}" \
|| die "Serwis ${PG_SERVICE} nie jest aktywny po uruchomieniu."
info "Serwis ${PG_SERVICE} działa."
# ---------------------------------------------------------------------------
# 7. Ustawienie hasła superużytkownika postgres
# ---------------------------------------------------------------------------
sekcja "Ustawianie hasła superużytkownika postgres"
POSTGRES_PASS=$(genpasswd 32)
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '${POSTGRES_PASS}';" \
|| die "Nie udało się ustawić hasła dla użytkownika postgres."
info "Hasło użytkownika postgres ustawione."
# Zapis poświadczeń w formacie pgpass (host:port:db:user:password)
CRED_FILE="/usr/local/directadmin/conf/postgresql.conf"
mkdir -p "$(dirname "$CRED_FILE")"
printf 'localhost:5432:*:postgres:%s\n' "$POSTGRES_PASS" > "$CRED_FILE"
chmod 600 "$CRED_FILE"
if id diradmin &>/dev/null; then
chown diradmin:diradmin "$CRED_FILE"
fi
# ---------------------------------------------------------------------------
# 8. Finalny pg_hba.conf
# ---------------------------------------------------------------------------
cat > "$PG_HBA" <<EOF
# pg_hba.conf zarządzany przez postgresql_install.sh
# Wygenerowano: $(date)
#
# Polityka bezpieczeństwa:
# - Uwierzytelnianie hasłem dla WSZYSTKICH połączeń (brak peer, brak trust)
# - Wyłącznie localhost (127.0.0.1/32 i ::1/128)
# - Połączenia zdalne zablokowane przez listen_addresses = 'localhost'
# - Replikacja jawnie odrzucona
#
# RODZAJ BAZA UŻYTKOWNIK ADRES METODA
local all all ${PG_AUTH}
host all all 127.0.0.1/32 ${PG_AUTH}
host all all ::1/128 ${PG_AUTH}
local replication all reject
host replication all 127.0.0.1/32 reject
host replication all ::1/128 reject
EOF
chown postgres:postgres "$PG_HBA"
chmod 600 "$PG_HBA"
# ---------------------------------------------------------------------------
# 9. Przeładowanie konfiguracji + weryfikacja połączenia
# ---------------------------------------------------------------------------
sekcja "Przeładowanie konfiguracji PostgreSQL"
systemctl reload "${PG_SERVICE}" \
|| die "Nie udało się przeładować ${PG_SERVICE}."
info "Konfiguracja przeładowana."
sleep 1
info "Test uwierzytelniania hasłem..."
if PGPASSWORD="$POSTGRES_PASS" \
psql -U postgres -h 127.0.0.1 -p 5432 -d postgres -c "SELECT version();" &>/dev/null; then
info "Test połączenia: OK"
else
warn "Test połączenia nie powiódł się. Sprawdź pg_hba.conf i password_encryption."
fi
# ---------------------------------------------------------------------------
# 10. Rozszerzenia PHP pgsql i pdo_pgsql przez CustomBuild
# ---------------------------------------------------------------------------
if [[ "$INSTALL_PHP" == "tak" ]]; then
sekcja "Rozszerzenia PHP pgsql i pdo_pgsql DirectAdmin CustomBuild"
cb_pgsql_collect_php_targets "$CB_DIR"
info "Web serwer: ${CB_PHP_WEBSERVER}"
if [[ ${#CB_PHP_VERSIONS[@]} -gt 0 ]]; then
info "Wykryte wersje PHP: $(IFS=', '; echo "${CB_PHP_VERSIONS[*]}")"
else
warn "Nie wykryto wersji PHP w DirectAdmin CustomBuild."
fi
info "Instaluję postgresql${PG_VERSION}-devel..."
dnf install -y "postgresql${PG_VERSION}-devel" \
"postgresql${PG_VERSION}-libs" \
|| die "Nie udało się zainstalować postgresql${PG_VERSION}-devel/libs."
PG_PREFIX="/usr/pgsql-${PG_VERSION}"
PG_CONFIG_SRC="${PG_BINDIR}/pg_config"
PG_CONFIG_LINK="/usr/local/bin/pg_config"
if [[ -x "$PG_CONFIG_SRC" ]]; then
if [[ -e "$PG_CONFIG_LINK" && ! -L "$PG_CONFIG_LINK" ]]; then
warn "${PG_CONFIG_LINK} istnieje i nie jest dowiązaniem pomijam symlink."
else
ln -sf "$PG_CONFIG_SRC" "$PG_CONFIG_LINK"
fi
else
die "Nie znaleziono pg_config w ${PG_CONFIG_SRC}."
fi
cb_pgsql_configure_runtime_libpq "$PG_PREFIX"
PHP_PREPARED=0
for idx in "${!CB_PHP_VERSIONS[@]}"; do
if cb_pgsql_prepare_custom_config "$CB_DIR" "${CB_PHP_VERSIONS[$idx]}" "${CB_PHP_MODES[$idx]}" "$PG_PREFIX"; then
PHP_PREPARED=1
fi
done
if [[ "$PHP_PREPARED" -eq 1 ]]; then
cd "$CB_DIR"
LD_LIBRARY_PATH="${DA_PG_RUNTIME_LIBDIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" \
LD_PRELOAD="${DA_PG_RUNTIME_LIBPQ}${LD_PRELOAD:+:${LD_PRELOAD}}" \
LIBRARY_PATH="${DA_PG_RUNTIME_LIBDIR}${LIBRARY_PATH:+:${LIBRARY_PATH}}" \
PKG_CONFIG_PATH="${DA_PG_RUNTIME_LIBDIR}/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" \
LDFLAGS="-L${DA_PG_RUNTIME_LIBDIR} ${LDFLAGS:-}" \
CPPFLAGS="-I${PG_PREFIX}/include ${CPPFLAGS:-}" \
./build php \
|| die "Rekompilacja PHP nie powiodła się. Sprawdź logi w ${CB_DIR}/."
./build rewrite_confs \
|| warn "rewrite_confs zakończył się błędem sprawdź konfigurację ręcznie."
cd - > /dev/null
info "Rekompilacja PHP zakończona."
else
warn "Nie przygotowano żadnych plików configure dla PHP pomijam rekompilację."
fi
fi
# ---------------------------------------------------------------------------
# 11. Instalacja narzędzia backupu w /opt + cron root (02:00)
# ---------------------------------------------------------------------------
sekcja "Instalacja narzędzia backupu i konfiguracja crona"
[[ -f "$POSTGRES_BACKUP_SRC" ]] || die "Brak pliku: ${POSTGRES_BACKUP_SRC}"
cp -f "$POSTGRES_BACKUP_SRC" "$POSTGRES_BACKUP_DST"
chown root:root "$POSTGRES_BACKUP_DST"
chmod 700 "$POSTGRES_BACKUP_DST"
CURRENT_CRONTAB="$(crontab -l 2>/dev/null || true)"
if printf '%s\n' "$CURRENT_CRONTAB" | grep -Fqx "$POSTGRES_BACKUP_CRON_LINE"; then
info "Wpis cron już istnieje w crontab roota: /opt/postgres_backup"
else
TMP_CRON="$(mktemp)"
if [[ -n "$CURRENT_CRONTAB" ]]; then
printf '%s\n' "$CURRENT_CRONTAB" > "$TMP_CRON"
fi
printf '%s\n' "$POSTGRES_BACKUP_CRON_LINE" >> "$TMP_CRON"
crontab "$TMP_CRON"
rm -f "$TMP_CRON"
info "Dodano wpis do crontab roota (02:00): /opt/postgres_backup"
fi
info "Skopiowano: ${POSTGRES_BACKUP_DST}"
# =============================================================================
# PODSUMOWANIE
# =============================================================================
sekcja "Instalacja zakończona"
# ---------------------------------------------------------------------------
# Tabela rozszerzeń PHP
# ---------------------------------------------------------------------------
if [[ "$INSTALL_PHP" == "tak" ]]; then
echo -e "${BOLD}Rozszerzenia PHP:${NC}"
echo ""
cb_pgsql_collect_php_targets "$CB_DIR"
cb_pgsql_render_extensions_table
echo ""
fi
info "Pełny log: ${LOGFILE}"
echo -e "${BOLD}Podsumowanie instalacji:${NC}"
printf "Host:\t%s\n" "localhost"
printf "Login:\t%s\n" "postgres"
printf "Hasło:\t%s\n" "${POSTGRES_PASS}"
printf "Port:\t%s\n" "5432"
printf "Plik danych dostępowych:\t%s\n" "${CRED_FILE}"
echo ""
exit 0
+1371
View File
File diff suppressed because it is too large Load Diff
+520
View File
@@ -0,0 +1,520 @@
#!/bin/bash
# =============================================================================
# recompile_php.sh
# Weryfikacja i rekompilacja wersji PHP w DirectAdmin CustomBuild
# tak, aby każda aktywna wersja miała rozszerzenia pgsql i pdo_pgsql.
# =============================================================================
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
sekcja() { echo -e "\n${BOLD}${CYAN}=== $* ===${NC}\n"; }
die() { echo -e "${RED}[BŁĄD]${NC} $*" >&2; exit 1; }
CB_PHP_WEBSERVER=""
CB_PHP_OPTIONS_CONF=""
CB_PG_PREFIX=""
CB_PG_VERSION=""
CB_PG_CONFIG=""
DA_PG_RUNTIME_LIBDIR=""
DA_PG_RUNTIME_LIBPQ=""
declare -ag CB_PHP_VERSIONS=()
declare -ag CB_PHP_CODES=()
declare -ag CB_PHP_MODES=()
cb_pgsql_reset_php_targets() {
CB_PHP_VERSIONS=()
CB_PHP_CODES=()
CB_PHP_MODES=()
}
cb_pgsql_get_cb_option() {
local options_conf="$1"
local key="$2"
local value=""
[[ -f "$options_conf" ]] || return 1
value="$(awk -F= -v key="$key" '
$1 == key {
sub(/^[[:space:]]+/, "", $2)
sub(/[[:space:]]+$/, "", $2)
print $2
exit
}
' "$options_conf")"
[[ -n "$value" ]] || return 1
printf '%s\n' "$value"
}
cb_pgsql_php_code_from_version() {
printf '%s\n' "${1//./}"
}
cb_pgsql_php_version_from_code() {
local code="$1"
if [[ "$code" == *.* ]]; then
printf '%s\n' "$code"
elif [[ "$code" =~ ^[0-9]{2,3}$ ]]; then
printf '%s.%s\n' "${code:0:1}" "${code:1}"
else
printf '%s\n' "$code"
fi
}
cb_pgsql_add_php_target() {
local version="$1"
local mode="$2"
local code=""
local idx=""
[[ -n "$version" ]] || return 0
code="$(cb_pgsql_php_code_from_version "$version")"
for idx in "${!CB_PHP_CODES[@]}"; do
if [[ "${CB_PHP_CODES[$idx]}" == "$code" ]]; then
return 0
fi
done
CB_PHP_VERSIONS+=("$version")
CB_PHP_CODES+=("$code")
CB_PHP_MODES+=("${mode:-php-fpm}")
}
cb_pgsql_detect_webserver() {
local cb_dir="$1"
CB_PHP_OPTIONS_CONF="${cb_dir}/options.conf"
CB_PHP_WEBSERVER="apache"
if [[ -f "$CB_PHP_OPTIONS_CONF" ]]; then
CB_PHP_WEBSERVER="$(cb_pgsql_get_cb_option "$CB_PHP_OPTIONS_CONF" "webserver" || true)"
[[ -n "$CB_PHP_WEBSERVER" ]] || CB_PHP_WEBSERVER="apache"
fi
}
cb_pgsql_collect_php_targets() {
local cb_dir="$1"
local slot=""
local release=""
local mode=""
local default_mode=""
cb_pgsql_reset_php_targets
cb_pgsql_detect_webserver "$cb_dir"
# CloudLinux może używać lsphp także przy webserver=apache.
# Dlatego najpierw honorujemy phpN_mode/php1_mode z options.conf,
# a dopiero przy ich braku stosujemy domyślny tryb wynikający z webserwera.
case "$CB_PHP_WEBSERVER" in
litespeed|openlitespeed) default_mode="lsphp" ;;
*) default_mode="php-fpm" ;;
esac
for slot in 1 2 3 4 5 6 7 8 9; do
release="$(cb_pgsql_get_cb_option "$CB_PHP_OPTIONS_CONF" "php${slot}_release" || true)"
mode="$(cb_pgsql_get_cb_option "$CB_PHP_OPTIONS_CONF" "php${slot}_mode" || true)"
[[ -n "$mode" ]] || mode="$(cb_pgsql_get_cb_option "$CB_PHP_OPTIONS_CONF" "php1_mode" || true)"
[[ -n "$mode" ]] || mode="$default_mode"
case "${release,,}" in
""|no|off) continue ;;
esac
case "${mode,,}" in
no|off) continue ;;
esac
cb_pgsql_add_php_target "$release" "$mode"
done
}
cb_pgsql_build_file_candidates() {
local cb_dir="$1"
local mode="$2"
local code="$3"
local -n out_ref="$4"
out_ref=()
if [[ "$mode" == "lsphp" ]]; then
case "$CB_PHP_WEBSERVER" in
litespeed)
out_ref+=("${cb_dir}/configure/litespeed/configure.php${code}")
;;
openlitespeed)
out_ref+=("${cb_dir}/configure/openlitespeed/configure.php${code}")
;;
esac
out_ref+=("${cb_dir}/configure/lsphp/configure.lsphp${code}")
out_ref+=("${cb_dir}/configure/lsphp/configure.php${code}")
fi
out_ref+=("${cb_dir}/configure/php/configure.php${code}")
}
cb_pgsql_resolve_config_paths() {
local cb_dir="$1"
local version="$2"
local mode="$3"
local source_ref_name="$4"
local custom_ref_name="$5"
local code=""
local -a candidates=()
local candidate=""
code="$(cb_pgsql_php_code_from_version "$version")"
printf -v "$source_ref_name" '%s' ""
printf -v "$custom_ref_name" '%s' ""
cb_pgsql_build_file_candidates "$cb_dir" "$mode" "$code" candidates
for candidate in "${candidates[@]}"; do
if [[ -f "$candidate" ]]; then
printf -v "$source_ref_name" '%s' "$candidate"
printf -v "$custom_ref_name" '%s' "$(printf '%s\n' "$candidate" | sed "s#^${cb_dir}/configure/#${cb_dir}/custom/#")"
return 0
fi
done
for candidate in "${candidates[@]}"; do
candidate="$(printf '%s\n' "$candidate" | sed "s#^${cb_dir}/configure/#${cb_dir}/custom/#")"
if [[ -f "$candidate" ]]; then
printf -v "$source_ref_name" '%s' "$candidate"
printf -v "$custom_ref_name" '%s' "$candidate"
return 0
fi
done
return 1
}
cb_pgsql_insert_flags() {
local config_file="$1"
local pg_prefix="$2"
local tmp_file="${config_file}.tmp"
sed -i '/--with-pgsql=/d; /--with-pdo-pgsql=/d' "$config_file"
if grep -q -- '--with-pdo-mysql' "$config_file"; then
awk -v prefix="$pg_prefix" '
/--with-pdo-mysql/ {
print
print "\t--with-pgsql=" prefix " \\"
print "\t--with-pdo-pgsql=" prefix " \\"
next
}
{ print }
' "$config_file" > "$tmp_file"
else
awk -v prefix="$pg_prefix" '
/^[[:space:]]*--with/ { last = NR }
{ lines[NR] = $0 }
END {
if (last == 0) {
for (i = 1; i <= NR; i++) print lines[i]
exit
}
for (i = 1; i <= NR; i++) {
print lines[i]
if (i == last) {
print "\t--with-pgsql=" prefix " \\"
print "\t--with-pdo-pgsql=" prefix " \\"
}
}
}
' "$config_file" > "$tmp_file"
fi
mv "$tmp_file" "$config_file"
}
cb_pgsql_prepare_custom_config() {
local cb_dir="$1"
local version="$2"
local mode="$3"
local pg_prefix="$4"
local source_conf=""
local custom_conf=""
local ts=""
if ! cb_pgsql_resolve_config_paths "$cb_dir" "$version" "$mode" source_conf custom_conf; then
warn "Nie znaleziono configure dla PHP ${version} (tryb ${mode}, webserver ${CB_PHP_WEBSERVER})."
return 1
fi
mkdir -p "$(dirname "$custom_conf")"
ts="$(date +%s)"
if [[ "$source_conf" != "$custom_conf" && ! -f "$custom_conf" ]]; then
cp -p "$source_conf" "$custom_conf"
elif [[ -f "$custom_conf" ]]; then
cp -p "$custom_conf" "${custom_conf}.bak.${ts}"
fi
cb_pgsql_insert_flags "$custom_conf" "$pg_prefix"
chmod +x "$custom_conf" || true
info "Przygotowano configure dla PHP ${version}: ${custom_conf}"
return 0
}
cb_pgsql_configure_runtime_libpq() {
local pg_prefix="$1"
local pg_libdir="${pg_prefix}/lib"
local libpq_so="${pg_libdir}/libpq.so.5"
local ld_conf="/etc/ld.so.conf.d/00-da-postgresql-libpq.conf"
local old_ld_conf="/etc/ld.so.conf.d/da-postgresql-libpq.conf"
local preferred=""
[[ -d "$pg_libdir" ]] || die "Nie znaleziono katalogu bibliotek PostgreSQL: ${pg_libdir}"
[[ -r "$libpq_so" ]] || die "Nie znaleziono biblioteki libpq: ${libpq_so}"
printf '%s\n' "$pg_libdir" > "$ld_conf"
chmod 644 "$ld_conf"
[[ "$old_ld_conf" == "$ld_conf" ]] || rm -f "$old_ld_conf"
if command -v ldconfig >/dev/null 2>&1; then
ldconfig || die "Nie udało się odświeżyć cache bibliotek przez ldconfig."
preferred="$(ldconfig -p 2>/dev/null | awk '/libpq\.so\.5/{print $NF; exit}')"
if [[ -n "$preferred" && "$preferred" != "$libpq_so" ]]; then
warn "Domyślna libpq to ${preferred}; wymuszę właściwą bibliotekę przez LD_PRELOAD podczas kompilacji PHP."
fi
else
warn "Brak ldconfig. Kontynuuję z LD_LIBRARY_PATH."
fi
DA_PG_RUNTIME_LIBDIR="$pg_libdir"
DA_PG_RUNTIME_LIBPQ="$libpq_so"
info "Skonfigurowano bibliotekę runtime PostgreSQL: ${pg_libdir}"
}
cb_pgsql_detect_pg_prefix() {
local candidate=""
CB_PG_PREFIX=""
CB_PG_VERSION=""
CB_PG_CONFIG=""
if [[ -x /usr/local/bin/pg_config ]]; then
candidate="$(readlink -f /usr/local/bin/pg_config 2>/dev/null || true)"
if [[ -x "$candidate" ]]; then
CB_PG_CONFIG="$candidate"
fi
fi
if [[ -z "$CB_PG_CONFIG" ]]; then
candidate="$(ls -1 /usr/pgsql-*/bin/pg_config 2>/dev/null | sort -V | tail -n1 || true)"
if [[ -n "$candidate" && -x "$candidate" ]]; then
CB_PG_CONFIG="$candidate"
fi
fi
[[ -n "$CB_PG_CONFIG" ]] || return 1
CB_PG_PREFIX="$(cd "$(dirname "$CB_PG_CONFIG")/.." && pwd)"
if [[ "$CB_PG_PREFIX" =~ /usr/pgsql-([0-9]+)$ ]]; then
CB_PG_VERSION="${BASH_REMATCH[1]}"
fi
[[ -n "$CB_PG_PREFIX" ]] || return 1
return 0
}
cb_pgsql_find_php_bin_for_version() {
local version="$1"
local code=""
local candidate=""
code="$(cb_pgsql_php_code_from_version "$version")"
for candidate in \
"/usr/local/php${code}/bin/php" \
"/usr/local/php${code}/bin/lsphp" \
"/usr/local/lsphp${code}/bin/lsphp" \
"/usr/local/php${code}/bin/php-cgi"; do
if [[ -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
cb_pgsql_probe_php_extensions() {
local php_bin="$1"
local pgsql_ref_name="$2"
local pdo_ref_name="$3"
local modules=""
local modules_lc=""
local probe_pgsql_status="NIE"
local probe_pdo_status="NIE"
modules="$("$php_bin" -m 2>/dev/null || true)"
if [[ -z "$modules" ]]; then
probe_pgsql_status="BŁĄD"
probe_pdo_status="BŁĄD"
printf -v "$pgsql_ref_name" '%s' "$probe_pgsql_status"
printf -v "$pdo_ref_name" '%s' "$probe_pdo_status"
return 1
fi
modules_lc="$(printf '%s\n' "$modules" | tr '[:upper:]' '[:lower:]')"
if printf '%s\n' "$modules_lc" | grep -qx 'pgsql'; then
probe_pgsql_status="OK"
fi
if printf '%s\n' "$modules_lc" | grep -qx 'pdo_pgsql'; then
probe_pdo_status="OK"
fi
printf -v "$pgsql_ref_name" '%s' "$probe_pgsql_status"
printf -v "$pdo_ref_name" '%s' "$probe_pdo_status"
return 0
}
cb_pgsql_render_extensions_table() {
local found=0
local idx=""
local version=""
local php_bin=""
local pgsql_status=""
local pdo_pgsql_status=""
echo "+------------+-------+-----------+"
printf "| %-10s | %-5s | %-9s |\n" "Wersja PHP" "PGSQL" "PDO_PGSQL"
echo "+------------+-------+-----------+"
for idx in "${!CB_PHP_VERSIONS[@]}"; do
version="${CB_PHP_VERSIONS[$idx]}"
php_bin="$(cb_pgsql_find_php_bin_for_version "$version" || true)"
if [[ -z "$php_bin" ]]; then
printf "| %-10s | %-5s | %-9s |\n" "PHP ${version}" "BRAK" "BRAK"
found=1
continue
fi
cb_pgsql_probe_php_extensions "$php_bin" pgsql_status pdo_pgsql_status >/dev/null 2>&1 || true
printf "| %-10s | %-5s | %-9s |\n" "PHP ${version}" "$pgsql_status" "$pdo_pgsql_status"
found=1
done
if [[ $found -eq 1 ]]; then
echo "+------------+-------+-----------+"
else
warn "Nie znaleziono wersji PHP do raportu."
fi
}
[[ $EUID -eq 0 ]] || die "Skrypt musi być uruchomiony jako root."
LOGFILE="/var/log/recompile_php_$(date +%Y%m%d_%H%M%S).log"
exec 1> >(tee >(sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' >> "$LOGFILE")) 2>&1
info "Pełny log zapisany w: $LOGFILE"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CB_DIR="/usr/local/directadmin/custombuild"
[[ -x "${CB_DIR}/build" ]] || die "Nie znaleziono DirectAdmin CustomBuild: ${CB_DIR}/build"
sekcja "Wykrywanie środowiska DirectAdmin"
cb_pgsql_collect_php_targets "$CB_DIR"
[[ ${#CB_PHP_VERSIONS[@]} -gt 0 ]] || die "Nie wykryto aktywnych wersji PHP w DirectAdmin CustomBuild."
info "Web serwer: ${CB_PHP_WEBSERVER}"
info "Wykryte wersje PHP: $(IFS=', '; echo "${CB_PHP_VERSIONS[*]}")"
sekcja "Wykrywanie PostgreSQL dla kompilacji PHP"
cb_pgsql_detect_pg_prefix || die "Nie znaleziono pg_config. Zainstaluj PostgreSQL lub ustaw /usr/local/bin/pg_config."
[[ -n "$CB_PG_VERSION" ]] || die "Nie udało się ustalić wersji PostgreSQL z ${CB_PG_CONFIG}."
info "Wykryty prefix PostgreSQL: ${CB_PG_PREFIX}"
info "Wykryta wersja PostgreSQL: ${CB_PG_VERSION}"
dnf install -y "postgresql${CB_PG_VERSION}-devel" \
"postgresql${CB_PG_VERSION}-libs" \
|| die "Nie udało się zainstalować postgresql${CB_PG_VERSION}-devel/libs."
if [[ -x "$CB_PG_CONFIG" ]]; then
if [[ -e /usr/local/bin/pg_config && ! -L /usr/local/bin/pg_config ]]; then
warn "/usr/local/bin/pg_config istnieje i nie jest dowiązaniem pomijam aktualizację symlinku."
else
ln -sf "$CB_PG_CONFIG" /usr/local/bin/pg_config
info "Zaktualizowano symlink: /usr/local/bin/pg_config -> ${CB_PG_CONFIG}"
fi
fi
cb_pgsql_configure_runtime_libpq "$CB_PG_PREFIX"
sekcja "Analiza rozszerzeń PHP"
declare -a REBUILD_VERSIONS=()
declare -a REBUILD_MODES=()
for idx in "${!CB_PHP_VERSIONS[@]}"; do
php_version="${CB_PHP_VERSIONS[$idx]}"
php_mode="${CB_PHP_MODES[$idx]}"
php_bin="$(cb_pgsql_find_php_bin_for_version "$php_version" || true)"
pgsql_status="BRAK"
pdo_pgsql_status="BRAK"
if [[ -z "$php_bin" ]]; then
warn "Nie znaleziono binarki PHP dla wersji ${php_version}. Dodaję do rekompilacji."
REBUILD_VERSIONS+=("$php_version")
REBUILD_MODES+=("$php_mode")
continue
fi
cb_pgsql_probe_php_extensions "$php_bin" pgsql_status pdo_pgsql_status >/dev/null 2>&1 || true
if [[ "$pgsql_status" != "OK" || "$pdo_pgsql_status" != "OK" ]]; then
info "PHP ${php_version}: brakujące rozszerzenia (PGSQL=${pgsql_status}, PDO_PGSQL=${pdo_pgsql_status})"
REBUILD_VERSIONS+=("$php_version")
REBUILD_MODES+=("$php_mode")
else
info "PHP ${php_version}: rozszerzenia pgsql i pdo_pgsql są już dostępne."
fi
done
if [[ ${#REBUILD_VERSIONS[@]} -eq 0 ]]; then
sekcja "Tabela rozszerzeń PHP"
cb_pgsql_render_extensions_table
info "Pełny log: ${LOGFILE}"
exit 0
fi
sekcja "Przygotowanie configure.phpXX"
prepared_any=0
for idx in "${!REBUILD_VERSIONS[@]}"; do
if cb_pgsql_prepare_custom_config "$CB_DIR" "${REBUILD_VERSIONS[$idx]}" "${REBUILD_MODES[$idx]}" "$CB_PG_PREFIX"; then
prepared_any=1
fi
done
[[ "$prepared_any" -eq 1 ]] || die "Nie udało się przygotować żadnego pliku configure dla rekompilacji PHP."
sekcja "Rekompilacja PHP"
cd "$CB_DIR"
for php_version in "${REBUILD_VERSIONS[@]}"; do
info "Uruchamiam: ./build php ${php_version}"
LD_LIBRARY_PATH="${DA_PG_RUNTIME_LIBDIR}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" \
LD_PRELOAD="${DA_PG_RUNTIME_LIBPQ}${LD_PRELOAD:+:${LD_PRELOAD}}" \
LIBRARY_PATH="${DA_PG_RUNTIME_LIBDIR}${LIBRARY_PATH:+:${LIBRARY_PATH}}" \
PKG_CONFIG_PATH="${DA_PG_RUNTIME_LIBDIR}/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" \
LDFLAGS="-L${DA_PG_RUNTIME_LIBDIR} ${LDFLAGS:-}" \
CPPFLAGS="-I${CB_PG_PREFIX}/include ${CPPFLAGS:-}" \
./build php "$php_version" \
|| die "Rekompilacja PHP ${php_version} nie powiodła się."
done
./build rewrite_confs \
|| warn "rewrite_confs zakończył się błędem sprawdź konfigurację ręcznie."
cd - >/dev/null
sekcja "Tabela rozszerzeń PHP po rekompilacji"
cb_pgsql_collect_php_targets "$CB_DIR"
cb_pgsql_render_extensions_table
info "Pełny log: ${LOGFILE}"
exit 0
+211
View File
@@ -0,0 +1,211 @@
#!/bin/bash
# =============================================================================
# restore_all.sh
# Przywracanie wszystkich baz PostgreSQL z katalogu backupu
# wykonanego przez postgres_backup.sh
# =============================================================================
CREDENTIALS_FILE="/usr/local/directadmin/conf/postgresql.conf"
# Kolory tylko na terminalu
if [[ -t 1 ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BOLD='\033[1m'; CYAN='\033[0;36m'; NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; BOLD=''; CYAN=''; NC=''
fi
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
info() { echo -e " $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
die() { echo -e "${RED}[BŁĄD]${NC} $*" >&2; exit 1; }
sekcja() { echo -e "\n${BOLD}${CYAN}--- $* ---${NC}\n"; }
# ---------------------------------------------------------------------------
# Sposób użycia
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
Użycie:
$(basename "$0") <katalog_backupu> [--restore-users]
Opis:
Przywraca wszystkie bazy danych z podanego katalogu backupu.
Pliki w katalogu powinny być wykonane przez postgres_backup.sh.
Flagi:
--restore-users Przed przywracaniem baz odtworzy użytkowników, role
i ich hasła z pliku _globals.sql[.gz] (jeśli istnieje).
WYMAGANE do pełnego disaster recovery.
Kolejność przywracania:
1. _globals.sql[.gz] użytkownicy i role (tylko z --restore-users)
2. *.sql[.gz] bazy danych (alfabetycznie, z pominięciem _globals)
Przykłady:
$(basename "$0") /home/admin/postgres_backup/19-02-2026
$(basename "$0") /home/admin/postgres_backup/19-02-2026 --restore-users
Uwaga:
Każda przywracana baza zostanie usunięta i odtworzona od nowa.
Plik poświadczeń: ${CREDENTIALS_FILE}
EOF
}
# ---------------------------------------------------------------------------
# Parsowanie argumentów
# ---------------------------------------------------------------------------
BACKUP_DIR=""
RESTORE_USERS=false
for ARG in "$@"; do
case "$ARG" in
--restore-users) RESTORE_USERS=true ;;
--help|-h) usage; exit 0 ;;
-*) die "Nieznana flaga: ${ARG}. Użyj --help aby zobaczyć pomoc." ;;
*)
[[ -z "$BACKUP_DIR" ]] || die "Podano więcej niż jeden katalog. Użyj --help aby zobaczyć pomoc."
BACKUP_DIR="$ARG"
;;
esac
done
if [[ -z "$BACKUP_DIR" ]]; then
usage
exit 0
fi
[[ -d "$BACKUP_DIR" ]] || die "Katalog nie istnieje: ${BACKUP_DIR}"
# ---------------------------------------------------------------------------
# Poświadczenia
# ---------------------------------------------------------------------------
[[ -f "$CREDENTIALS_FILE" ]] \
|| die "Brak pliku poświadczeń: ${CREDENTIALS_FILE}"
_perms=$(stat -c '%a' "$CREDENTIALS_FILE")
[[ "$_perms" == "600" || "$_perms" == "400" ]] \
|| die "Zbyt otwarte uprawnienia na ${CREDENTIALS_FILE} (${_perms}). Wymagane: 600 lub 400."
_line=$(head -1 "$CREDENTIALS_FILE")
PGHOST=$(echo "$_line" | cut -d: -f1)
PGPORT=$(echo "$_line" | cut -d: -f2)
PGUSER=$(echo "$_line" | cut -d: -f4)
[[ -n "$PGHOST" && -n "$PGPORT" && -n "$PGUSER" ]] \
|| die "Nieprawidłowy format pliku poświadczeń. Oczekiwany: host:port:database:user:password"
export PGPASSFILE="$CREDENTIALS_FILE"
# ---------------------------------------------------------------------------
# Funkcja przywracająca jeden plik
# ---------------------------------------------------------------------------
restore_file() {
local file="$1"
local init_db="$2"
case "$file" in
*.sql.gz)
gunzip -c "$file" \
| psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password -d "$init_db"
return ${PIPESTATUS[1]}
;;
*.sql)
psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" \
--no-password -d "$init_db" -f "$file"
return $?
;;
esac
}
# ---------------------------------------------------------------------------
# Start
# ---------------------------------------------------------------------------
echo ""
info "Katalog: ${BACKUP_DIR}"
info "Serwer: ${PGHOST}:${PGPORT} Użytkownik: ${PGUSER}"
info "Restore users: ${RESTORE_USERS}"
echo ""
ERRORS=0
RESTORED=0
# ---------------------------------------------------------------------------
# 1. Globals (użytkownicy, role) tylko z flagą --restore-users
# ---------------------------------------------------------------------------
if [[ "$RESTORE_USERS" == "true" ]]; then
sekcja "Przywracanie użytkowników i ról (_globals)"
GLOBALS_FILE=""
for _f in "${BACKUP_DIR}/_globals.sql.gz" "${BACKUP_DIR}/_globals.sql"; do
[[ -f "$_f" ]] && GLOBALS_FILE="$_f" && break
done
if [[ -z "$GLOBALS_FILE" ]]; then
warn "Nie znaleziono pliku _globals.sql[.gz] w ${BACKUP_DIR} pomijam użytkowników."
else
info "Plik: ${GLOBALS_FILE##*/}"
# Globals przywracamy do postgres; DROP/CREATE ROLE może generować ostrzeżenia
# jeśli role już istnieją to normalne, kontynuujemy
if restore_file "$GLOBALS_FILE" "postgres"; then
ok "Użytkownicy i role przywrócone."
RESTORED=$((RESTORED + 1))
else
warn "Przywracanie globals zakończone z ostrzeżeniami sprawdź output powyżej."
ERRORS=$((ERRORS + 1))
fi
fi
fi
# ---------------------------------------------------------------------------
# 2. Bazy danych (pliki *.sql i *.sql.gz z pominięciem _globals.*)
# ---------------------------------------------------------------------------
sekcja "Przywracanie baz danych"
# Zbierz pliki, posortuj alfabetycznie
mapfile -t DB_FILES < <(
find "$BACKUP_DIR" -maxdepth 1 -type f \( -name '*.sql' -o -name '*.sql.gz' \) \
! -name '_globals.*' \
| sort
)
if [[ ${#DB_FILES[@]} -eq 0 ]]; then
warn "Nie znaleziono plików .sql ani .sql.gz w katalogu ${BACKUP_DIR} (poza _globals)."
exit 0
fi
for FILE in "${DB_FILES[@]}"; do
DB_NAME=$(basename "$FILE" | sed 's/\.sql\.gz$//; s/\.sql$//')
# Baza 'postgres' nie może być przywracana przez siebie (DROP DATABASE blokuje aktywne połączenie)
if [[ "$DB_NAME" == "postgres" ]]; then
INIT_DB="template1"
else
INIT_DB="postgres"
fi
info "${DB_NAME} (${FILE##*/})"
if restore_file "$FILE" "$INIT_DB"; then
ok "${DB_NAME}: przywrócona."
RESTORED=$((RESTORED + 1))
else
warn "${DB_NAME}: przywracanie zakończone z błędem lub ostrzeżeniami."
ERRORS=$((ERRORS + 1))
fi
echo ""
done
# ---------------------------------------------------------------------------
# Podsumowanie
# ---------------------------------------------------------------------------
sekcja "Podsumowanie"
info "Przywrócono: ${RESTORED} | Błędy: ${ERRORS}"
echo ""
[[ "$ERRORS" -eq 0 ]] || exit 1
exit 0
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CPIF="/usr/local/directadmin/data/admin/custom_package_items.conf"
SUDOERS_FILE="/etc/sudoers.d/directadmin-postgresql-plugin"
if [ "$(id -u)" -eq 0 ]; then
if [ -f "$CPIF" ]; then
sed -i '/^postgresql_enabled=/d' "$CPIF" || true
sed -i '/^postgresql=/d' "$CPIF" || true
sed -i '/^upostgresql=/d' "$CPIF" || true
sed -i '/^postgresql_max_databases=/d' "$CPIF" || true
sed -i '/^postgresql_unlimited=/d' "$CPIF" || true
sed -i '/^upostgresql_max_databases=/d' "$CPIF" || true
chown diradmin:diradmin "$CPIF" 2>/dev/null || true
chmod 640 "$CPIF" 2>/dev/null || true
fi
rm -f "$SUDOERS_FILE" || true
else
echo "postgresql: warning: uruchom jako root, aby usunąć wpisy z custom_package_items i sudoers" >&2
fi
if [ -f "$PLUGIN_DIR/plugin.conf" ]; then
sed -i 's/^active=.*/active=no/' "$PLUGIN_DIR/plugin.conf" || true
sed -i 's/^installed=.*/installed=no/' "$PLUGIN_DIR/plugin.conf" || true
fi
echo "da-postgresql: deaktywowany."
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
set -euo pipefail
"$(cd "$(dirname "$0")" && pwd)/install.sh"
+104
View File
@@ -0,0 +1,104 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
DATA_DIR="$PLUGIN_DIR/data"
JOB_DIR="$DATA_DIR/jobs/pending"
PROCESSING_DIR="$DATA_DIR/jobs/processing"
DONE_DIR="$DATA_DIR/jobs/done"
WORKER_LOCK_DIR="$DATA_DIR/worker.lock"
DB_LOCK_DIR="$DATA_DIR/lock"
PID_DIR="$DATA_DIR/pids"
CANCEL_DIR="$DATA_DIR/cancel"
mkdir -p "$JOB_DIR" "$PROCESSING_DIR" "$DONE_DIR" "$PID_DIR" "$CANCEL_DIR" "$DB_LOCK_DIR"
if ! mkdir "$WORKER_LOCK_DIR" 2>/dev/null; then
exit 0
fi
cleanup() {
rmdir "$WORKER_LOCK_DIR" 2>/dev/null || true
}
trap cleanup EXIT
db_lock_path_for_db() {
local db_name="$1"
local safe
safe="$(printf '%s' "$db_name" | tr -c 'A-Za-z0-9_.-' '_')"
printf '%s/%s.lock' "$DB_LOCK_DIR" "$safe"
}
release_db_lock_later() {
local lock_path="$1"
if [ -z "$lock_path" ]; then
return 0
fi
(
sleep 60
rm -f "$lock_path" 2>/dev/null || true
) >/dev/null 2>&1 &
}
while true; do
JOB_FILE="$(ls -1 "$JOB_DIR"/*.env 2>/dev/null | head -n 1 || true)"
if [ -z "${JOB_FILE:-}" ]; then
break
fi
BASE_NAME="$(basename "$JOB_FILE")"
RUN_FILE="$PROCESSING_DIR/$BASE_NAME"
mv "$JOB_FILE" "$RUN_FILE"
JOB_ID="${BASE_NAME%.env}"
PID_FILE="$PID_DIR/${JOB_ID}.pid"
JOB_TYPE="restore"
DB_NAME=""
DB_LOCK_FILE=""
# shellcheck disable=SC1090
source "$RUN_FILE" || true
JOB_TYPE="${JOB_TYPE:-restore}"
DB_NAME="${DB_NAME:-}"
DB_LOCK_FILE="${DB_LOCK_FILE:-}"
LOCK_PATH=""
if [ -n "$DB_LOCK_FILE" ]; then
case "$DB_LOCK_FILE" in
"$DB_LOCK_DIR"/*.lock) LOCK_PATH="$DB_LOCK_FILE" ;;
*) LOCK_PATH="" ;;
esac
fi
if [ -z "$LOCK_PATH" ] && [ -n "$DB_NAME" ]; then
LOCK_PATH="$(db_lock_path_for_db "$DB_NAME")"
fi
RUNNER="$PLUGIN_DIR/scripts/restore_job.sh"
if [ "$JOB_TYPE" = "backup" ]; then
RUNNER="$PLUGIN_DIR/scripts/backup_job.sh"
fi
set +e
"$RUNNER" "$RUN_FILE" &
RUN_PID=$!
echo "PID=${RUN_PID}" >> "$RUN_FILE"
echo "${RUN_PID}" > "$PID_FILE"
wait "$RUN_PID"
EXIT_CODE=$?
set -e
rm -f "$PID_FILE"
CANCEL_FLAG="${CANCEL_DIR}/${JOB_ID}.flag"
if [ -f "$CANCEL_FLAG" ]; then
rm -f "$CANCEL_FLAG"
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.cancel"
elif [ "$EXIT_CODE" -eq 0 ]; then
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.ok"
else
mv "$RUN_FILE" "$DONE_DIR/${JOB_ID}.fail"
fi
release_db_lock_later "$LOCK_PATH"
done
exit 0
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', 'download');
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', 'download');
require dirname(__DIR__) . '/exec/bootstrap.php';
+7
View File
@@ -0,0 +1,7 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('DA_POSTGRESQL_RAW_MODE', true);
define('PLUGIN_ACTION', 'download');
require dirname(__DIR__) . '/exec/bootstrap.php';
+483
View File
@@ -0,0 +1,483 @@
:root {
--bg: #f5f8ff;
--panel: #ffffff;
--text: #1f2a37;
--muted: #5f6a7b;
--border: #d8dde6;
--amber: #b77200;
--amber-strong: #9a6200;
--danger: #d4491f;
--ok: #127b3b;
--ok-bg: #e8f7ec;
--ok-left: #147f3e;
--warn-bg: #fff4eb;
--shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
}
* { box-sizing: border-box; }
html {
overflow-y: scroll;
scrollbar-gutter: stable;
}
body {
margin: 0;
font-family: "IBM Plex Sans", "Segoe UI", Arial, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 16px;
overflow-y: scroll;
}
#content,
#content .container,
#content .container-fluid,
#content .main-content,
#content .page-content,
.main-content,
.page-content {
max-width: none !important;
width: 100% !important;
}
.wrap { width: 100%; margin: 0 auto; padding: 16px 20px; }
.breadcrumbs { color: #7f8896; font-size: 13px; margin-bottom: 8px; }
.breadcrumbs span { margin: 0 6px; }
header { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; border-bottom: 1px solid var(--border); padding-bottom: 8px; margin-bottom: 10px; }
header h1 { margin: 0; font-size: 38px; font-weight: 600; letter-spacing: -0.2px; }
header p { margin: 0; color: var(--amber-strong); font-size: 15px; }
.header-center { justify-content: center; text-align: center; }
.header-center h1 { width: 100%; text-align: center; }
.top-actions { display: flex; gap: 8px; justify-content: flex-end; margin-bottom: 12px; flex-wrap: wrap; }
.top-actions .btn { text-transform: uppercase; }
.dropzone {
border: 2px dashed #cfd6e3;
border-radius: 12px;
padding: 16px;
text-align: center;
background: #f8fafc;
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
}
.dropzone.is-dragover {
border-color: var(--amber-strong);
background: #fff7e6;
}
.dropzone.is-filled .dropzone-placeholder { display: none; }
.dropzone .dropzone-info { display: none; }
.dropzone.is-filled .dropzone-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
text-align: left;
}
.dropzone-title { font-weight: 700; color: #1f2d3d; }
.dropzone-subtitle { color: #6b7280; font-size: 14px; margin-top: 4px; }
.dropzone-input { display: none; }
.dropzone-meta { display: grid; gap: 4px; }
.main-section { display: block; }
.logs-section { margin-top: 16px; }
.logs-section:empty { display: none; }
.alert { padding: 12px 14px; border-radius: 10px; margin: 8px 0 12px; border: 1px solid; font-size: 15px; white-space: pre-line; }
.alert-ok { background: var(--ok-bg); border-color: #a8ddb8; color: #12562b; }
.alert-err { background: #fff0ed; border-color: #f3c0b4; color: #8b2b18; }
.grid { display: grid; gap: 12px; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.panel { border: 1px solid var(--border); border-radius: 10px; padding: 14px; background: var(--panel); box-shadow: var(--shadow); margin-bottom: 14px; }
.panel h3 { margin: 0 0 10px; font-size: 22px; font-weight: 700; letter-spacing: -0.1px; }
.muted { color: var(--muted); }
form { margin: 0; }
label { display: block; margin-bottom: 6px; font-weight: 600; font-size: 15px; }
input[type="text"], input[type="password"], select, textarea {
width: 100%; border: 1px solid #cdd4e0; border-radius: 6px; padding: 10px 12px; font-size: 15px; background: #fff;
}
.input-with-tools { position: relative; }
.input-with-tools input[type="text"], .input-with-tools input[type="password"] { padding-right: 94px; }
.field-tools { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: inline-flex; gap: 4px; }
.tool-btn {
border: 1px solid #cbd5e4;
background: #fff;
color: #4a5566;
width: 32px;
height: 28px;
border-radius: 6px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.tool-btn:hover { background: #f6f8fb; border-color: #b9c7db; }
.tool-btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
textarea { min-height: 90px; resize: vertical; }
.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; margin-bottom: 12px; }
.file-source-mode { margin: 0 0 12px; }
.file-source-options { display: flex; flex-wrap: wrap; gap: 8px; }
.file-source-option {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0;
border: 1px solid var(--border);
border-radius: 999px;
padding: 8px 12px;
background: #fff;
cursor: pointer;
font-weight: 600;
font-size: 14px;
}
.file-source-option:hover { border-color: var(--amber); background: #fff7ec; }
.file-source-option input { margin: 0; accent-color: var(--amber); }
.file-source-panel { margin-bottom: 12px; }
.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; align-items: center; }
button, .btn {
border: 1px solid #b7c6da; background: #fff; color: #1f2d3d; border-radius: 999px; padding: 8px 14px; cursor: pointer; text-decoration: none; font-size: 13px; line-height: 1; display: inline-flex; align-items: center; gap: 6px;
}
button.primary, .btn.primary { background: var(--amber); border-color: var(--amber); color: #fff; }
button.primary:hover, .btn.primary:hover { background: var(--amber-strong); border-color: var(--amber-strong); }
.btn.amber, button.amber { background: var(--amber); border-color: var(--amber); color: #fff; }
.btn.amber:hover, button.amber:hover { background: var(--amber-strong); border-color: var(--amber-strong); }
button.danger, .btn.danger { border-color: var(--danger); color: var(--danger); background: #fff; }
.btn.danger-fill, button.danger-fill { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn.warn, button.warn { background: #f2c94c; border-color: #f2c94c; color: #000; }
.btn.success, button.success { background: var(--ok); border-color: var(--ok); color: #fff; }
.btn.success:hover, button.success:hover { background: #0f6a33; border-color: #0f6a33; }
.btn.adminer-login, button.adminer-login { background: #0b3c82; border-color: #0b3c82; color: #fff; }
.btn.adminer-login:hover, button.adminer-login:hover { background: #082d63; border-color: #082d63; }
.btn.disabled, button:disabled { opacity: 0.55; cursor: default; pointer-events: none; }
button:hover, .btn:hover { filter: brightness(0.99); }
table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 15px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: #fff; }
th, td { border-bottom: 1px solid var(--border); padding: 10px 12px; text-align: left; vertical-align: top; }
tbody tr:last-child td { border-bottom: 0; }
th { background: #f6f8fb; color: #4a5566; font-weight: 600; }
.badge { display: inline-block; background: #fce2b7; color: #7b4b00; border: 1px solid #efc57f; border-radius: 5px; padding: 2px 8px; font-size: 13px; }
.inline { display: inline-flex; align-items: center; gap: 6px; margin-right: 10px; margin-bottom: 8px; font-weight: 500; }
.list-clean { margin: 0; padding-left: 18px; }
.section-title { font-size: 33px; margin: 14px 0 10px; font-weight: 700; }
.section-title-center { text-align: center; }
.section-text { color: var(--muted); margin: 0 0 12px; }
.success-credentials { border: 1px solid #bfdcc6; background: var(--ok-bg); border-radius: 12px; display: grid; grid-template-columns: 64px 1fr; overflow: hidden; margin-bottom: 14px; }
.success-credentials .left { background: var(--ok-left); display:flex; align-items:center; justify-content:center; color:#fff; font-size:26px; }
.success-credentials .right { padding: 14px 16px; }
.success-credentials h4 { margin: 0 0 8px; font-size: 24px; }
.kv { display: grid; grid-template-columns: 220px 1fr; gap: 6px 10px; font-size: 15px; }
.kv code { background: transparent; border: 0; padding: 0; font-size: 15px; color: #1d2530; }
.details-advanced { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 10px; }
.details-advanced summary { cursor: pointer; color: #374357; font-weight: 600; padding: 6px 0; }
.input-prefix { display: grid; grid-template-columns: auto 1fr; }
.input-prefix .prefix { border: 1px solid #cdd4e0; border-right: 0; padding: 10px 12px; border-radius: 6px 0 0 6px; background: #f8fafd; color: #566176; }
.input-prefix input { border-radius: 0 6px 6px 0; }
.db-choice-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px; margin-top: 6px; }
.db-choice-card {
border: 1px solid #cdd4e0;
border-radius: 10px;
padding: 8px 10px;
background: #f8fafc;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.db-choice-card:hover { border-color: #b7c6da; background: #fff; }
.db-choice-card:focus-within { border-color: var(--amber); box-shadow: 0 0 0 2px rgba(183, 114, 0, 0.15); background: #fff; }
.db-choice-card input { margin: 0; accent-color: var(--amber); }
.db-choice-card span { font-weight: 600; color: #2b3445; word-break: break-all; }
.db-choice-card input:disabled + span { color: #9aa3b2; }
.restore-target-per-row { min-width: 240px; }
td.actions form { display: inline-flex; }
.br-restore-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 16px;
align-items: start;
}
.br-restore-calendar {
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px;
background: #f8fafc;
}
.br-restore-calendar h4 {
margin: 0 0 10px;
font-size: 16px;
font-weight: 700;
}
.br-month-nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.br-month-btn {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
color: #3a4655;
cursor: pointer;
}
.br-month-btn:hover { border-color: var(--amber); }
.br-month-btn:disabled { opacity: 0.4; cursor: default; }
.br-month-label { font-weight: 600; color: #1f2a37; }
.br-calendar-grid-wrap { margin: 0; }
.br-calendar-grid {
width: 100%;
border-collapse: collapse;
font-size: 13px;
border: 0;
background: transparent;
border-radius: 0;
overflow: visible;
}
.br-calendar-grid th {
padding: 4px;
text-align: center;
border-bottom: 1px solid var(--border);
background: transparent;
color: var(--muted);
}
.br-calendar-grid td {
padding: 4px;
text-align: center;
border-bottom: 0;
}
.br-cal-day {
width: 30px;
height: 30px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
cursor: pointer;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.br-cal-day:hover {
border-color: var(--amber);
background: #fff7ec;
}
.br-cal-day.is-selected {
background: var(--amber);
color: #fff;
border-color: var(--amber);
}
.br-cal-day-disabled {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
color: #a0a7b4;
}
.br-cal-empty { background: transparent; }
.br-restore-picker { min-width: 0; }
.br-user-backup-picker { min-width: 0; }
.br-date-label {
font-weight: 600;
color: #1f2a37;
margin: 6px 0 10px;
}
.br-time-picker { margin: 8px 0 14px; }
.br-time-title { font-weight: 600; margin-bottom: 6px; color: var(--muted); }
.br-time-list { display: flex; flex-wrap: wrap; gap: 8px; }
.br-time-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 999px;
background: #fff;
cursor: pointer;
font-size: 13px;
}
.br-time-item input { margin: 0; accent-color: var(--amber); }
.privileges-group { display: flex; flex-wrap: wrap; gap: 4px 10px; }
body.overlay-open { overflow: hidden; }
.job-overlay { position: fixed; inset: 0; display: none; z-index: 9999; }
.job-overlay.open { display: block; }
.job-overlay-backdrop { position: absolute; inset: 0; background: rgba(24, 31, 44, 0.55); }
.job-overlay-card {
position: relative;
width: min(920px, calc(100vw - 32px));
max-height: calc(100vh - 50px);
overflow: auto;
margin: 24px auto;
background: #fff;
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px;
box-shadow: 0 16px 48px rgba(18, 28, 45, 0.35);
}
.job-overlay-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.job-overlay-head h3 { margin: 0; font-size: 22px; }
.job-overlay-head-actions { display: inline-flex; align-items: center; gap: 8px; }
.log-overlay .log-meta {
display: grid;
gap: 6px;
margin: 10px 0 12px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: #f8fafc;
font-size: 13px;
}
.log-overlay .log-content {
margin: 0;
padding: 12px;
border-radius: 10px;
border: 1px solid #1f2a44;
background: #0f172a;
color: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.4;
white-space: pre;
max-height: 50vh;
overflow: auto;
}
.br-text-danger { color: var(--danger); font-weight: 700; }
.br-modal-danger { border-color: var(--danger); }
.br-modal-danger h3 { color: var(--danger); }
.br-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.br-modal {
width: min(560px, 92vw);
background: #ffffff;
border-radius: 12px;
padding: 16px;
border: 2px solid var(--border);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
max-height: 90vh;
}
.br-modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
.br-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.br-modal-title {
font-weight: 700;
font-size: 16px;
}
.br-modal-body {
max-height: 60vh;
overflow: auto;
}
.br-muted { color: var(--muted); }
.br-alert {
border-radius: 8px;
padding: 10px 12px;
margin: 8px 0;
border: 1px solid transparent;
}
.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; }
.br-picker-path {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.br-picker-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid var(--border);
border-radius: 10px;
max-height: 45vh;
overflow: auto;
}
.br-picker-create {
margin-top: 10px;
display: flex;
gap: 8px;
}
.br-picker-create input {
flex: 1;
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 8px;
}
.br-picker-list li {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--border);
}
.br-picker-list li:last-child {
border-bottom: none;
}
.br-picker-list li:hover {
background: #f8fafc;
}
.br-picker-list li.active {
background: #d1fae5;
}
.br-btn {
background: var(--amber);
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 14px;
cursor: pointer;
font-weight: 600;
}
.br-btn:hover { background: var(--amber-strong); }
.br-btn-ghost {
background: transparent;
color: #1f2d3d;
border: 1px solid var(--border);
}
.br-btn-ghost:hover { background: #eef2f7; }
.br-btn-danger {
background: var(--danger);
border: 1px solid var(--danger);
color: #fff;
border-radius: 6px;
padding: 8px 14px;
cursor: pointer;
font-weight: 600;
}
.br-btn-danger:hover { background: #c03a15; border-color: #c03a15; }
.br-btn-small {
padding: 6px 10px;
font-size: 12px;
}
.br-icon-btn {
border: 1px solid var(--border);
background: #fff;
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
font-size: 16px;
line-height: 1;
}
.br-icon-btn:hover {
background: #f8fafc;
}
@media (max-width: 900px) {
.wrap { padding: 10px; }
header h1 { font-size: 32px; }
.section-title { font-size: 28px; }
.kv { grid-template-columns: 1fr; }
.br-restore-layout { grid-template-columns: 1fr; }
}
+483
View File
@@ -0,0 +1,483 @@
:root {
--bg: #f4f6f8;
--panel: #ffffff;
--text: #1f2a37;
--muted: #6b7280;
--border: #d9dee5;
--amber: #b77200;
--amber-strong: #9a6200;
--danger: #d4491f;
--ok: #127b3b;
--ok-bg: #e8f7ec;
--ok-left: #147f3e;
--warn-bg: #fff4eb;
--shadow: 0 8px 24px rgba(31, 41, 55, 0.12);
}
* { box-sizing: border-box; }
html {
overflow-y: scroll;
scrollbar-gutter: stable;
}
body {
margin: 0;
font-family: "Helvetica Neue", Arial, sans-serif;
background: var(--bg);
color: var(--text);
font-size: 16px;
overflow-y: scroll;
}
#content,
#content .container,
#content .container-fluid,
#content .main-content,
#content .page-content,
.main-content,
.page-content {
max-width: none !important;
width: 100% !important;
}
.wrap { width: 100%; margin: 0 auto; padding: 16px 20px; }
.breadcrumbs { color: #7f8896; font-size: 13px; margin-bottom: 8px; }
.breadcrumbs span { margin: 0 6px; }
header { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; border-bottom: 1px solid var(--border); padding-bottom: 8px; margin-bottom: 10px; }
header h1 { margin: 0; font-size: 38px; font-weight: 600; letter-spacing: -0.2px; }
header p { margin: 0; color: var(--amber-strong); font-size: 15px; }
.header-center { justify-content: center; text-align: center; }
.header-center h1 { width: 100%; text-align: center; }
.top-actions { display: flex; gap: 8px; justify-content: flex-end; margin-bottom: 12px; flex-wrap: wrap; }
.top-actions .btn { text-transform: uppercase; }
.dropzone {
border: 2px dashed #cfd6e3;
border-radius: 12px;
padding: 16px;
text-align: center;
background: #f8fafc;
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
}
.dropzone.is-dragover {
border-color: var(--amber-strong);
background: #fff7e6;
}
.dropzone.is-filled .dropzone-placeholder { display: none; }
.dropzone .dropzone-info { display: none; }
.dropzone.is-filled .dropzone-info {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
text-align: left;
}
.dropzone-title { font-weight: 700; color: #1f2d3d; }
.dropzone-subtitle { color: #6b7280; font-size: 14px; margin-top: 4px; }
.dropzone-input { display: none; }
.dropzone-meta { display: grid; gap: 4px; }
.main-section { display: block; }
.logs-section { margin-top: 16px; }
.logs-section:empty { display: none; }
.alert { padding: 12px 14px; border-radius: 10px; margin: 8px 0 12px; border: 1px solid; font-size: 15px; white-space: pre-line; }
.alert-ok { background: var(--ok-bg); border-color: #a8ddb8; color: #12562b; }
.alert-err { background: #fff0ed; border-color: #f3c0b4; color: #8b2b18; }
.grid { display: grid; gap: 12px; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
.panel { border: 1px solid var(--border); border-radius: 10px; padding: 14px; background: var(--panel); box-shadow: var(--shadow); margin-bottom: 14px; }
.panel h3 { margin: 0 0 10px; font-size: 22px; font-weight: 700; letter-spacing: -0.1px; }
.muted { color: var(--muted); }
form { margin: 0; }
label { display: block; margin-bottom: 6px; font-weight: 600; font-size: 15px; }
input[type="text"], input[type="password"], select, textarea {
width: 100%; border: 1px solid #cdd4e0; border-radius: 6px; padding: 10px 12px; font-size: 15px; background: #fff;
}
.input-with-tools { position: relative; }
.input-with-tools input[type="text"], .input-with-tools input[type="password"] { padding-right: 94px; }
.field-tools { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display: inline-flex; gap: 4px; }
.tool-btn {
border: 1px solid #cbd5e4;
background: #fff;
color: #4a5566;
width: 32px;
height: 28px;
border-radius: 6px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.tool-btn:hover { background: #f6f8fb; border-color: #b9c7db; }
.tool-btn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
textarea { min-height: 90px; resize: vertical; }
.row { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; margin-bottom: 12px; }
.file-source-mode { margin: 0 0 12px; }
.file-source-options { display: flex; flex-wrap: wrap; gap: 8px; }
.file-source-option {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0;
border: 1px solid var(--border);
border-radius: 999px;
padding: 8px 12px;
background: #fff;
cursor: pointer;
font-weight: 600;
font-size: 14px;
}
.file-source-option:hover { border-color: var(--amber); background: #fff7ec; }
.file-source-option input { margin: 0; accent-color: var(--amber); }
.file-source-panel { margin-bottom: 12px; }
.actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; align-items: center; }
button, .btn {
border: 1px solid #b7c6da; background: #fff; color: #1f2d3d; border-radius: 999px; padding: 8px 14px; cursor: pointer; text-decoration: none; font-size: 13px; line-height: 1; display: inline-flex; align-items: center; gap: 6px;
}
button.primary, .btn.primary { background: var(--amber); border-color: var(--amber); color: #fff; }
button.primary:hover, .btn.primary:hover { background: var(--amber-strong); border-color: var(--amber-strong); }
.btn.amber, button.amber { background: var(--amber); border-color: var(--amber); color: #fff; }
.btn.amber:hover, button.amber:hover { background: var(--amber-strong); border-color: var(--amber-strong); }
button.danger, .btn.danger { border-color: var(--danger); color: var(--danger); background: #fff; }
.btn.danger-fill, button.danger-fill { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn.warn, button.warn { background: #f2c94c; border-color: #f2c94c; color: #000; }
.btn.success, button.success { background: var(--ok); border-color: var(--ok); color: #fff; }
.btn.success:hover, button.success:hover { background: #0f6a33; border-color: #0f6a33; }
.btn.adminer-login, button.adminer-login { background: #0b3c82; border-color: #0b3c82; color: #fff; }
.btn.adminer-login:hover, button.adminer-login:hover { background: #082d63; border-color: #082d63; }
.btn.disabled, button:disabled { opacity: 0.55; cursor: default; pointer-events: none; }
button:hover, .btn:hover { filter: brightness(0.99); }
table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 15px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: #fff; }
th, td { border-bottom: 1px solid var(--border); padding: 10px 12px; text-align: left; vertical-align: top; }
tbody tr:last-child td { border-bottom: 0; }
th { background: #f6f8fb; color: #4a5566; font-weight: 600; }
.badge { display: inline-block; background: #fce2b7; color: #7b4b00; border: 1px solid #efc57f; border-radius: 5px; padding: 2px 8px; font-size: 13px; }
.inline { display: inline-flex; align-items: center; gap: 6px; margin-right: 10px; margin-bottom: 8px; font-weight: 500; }
.list-clean { margin: 0; padding-left: 18px; }
.section-title { font-size: 33px; margin: 14px 0 10px; font-weight: 700; }
.section-title-center { text-align: center; }
.section-text { color: var(--muted); margin: 0 0 12px; }
.success-credentials { border: 1px solid #bfdcc6; background: var(--ok-bg); border-radius: 12px; display: grid; grid-template-columns: 64px 1fr; overflow: hidden; margin-bottom: 14px; }
.success-credentials .left { background: var(--ok-left); display:flex; align-items:center; justify-content:center; color:#fff; font-size:26px; }
.success-credentials .right { padding: 14px 16px; }
.success-credentials h4 { margin: 0 0 8px; font-size: 24px; }
.kv { display: grid; grid-template-columns: 220px 1fr; gap: 6px 10px; font-size: 15px; }
.kv code { background: transparent; border: 0; padding: 0; font-size: 15px; color: #1d2530; }
.details-advanced { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 10px; }
.details-advanced summary { cursor: pointer; color: #374357; font-weight: 600; padding: 6px 0; }
.input-prefix { display: grid; grid-template-columns: auto 1fr; }
.input-prefix .prefix { border: 1px solid #cdd4e0; border-right: 0; padding: 10px 12px; border-radius: 6px 0 0 6px; background: #f8fafd; color: #566176; }
.input-prefix input { border-radius: 0 6px 6px 0; }
.db-choice-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 8px; margin-top: 6px; }
.db-choice-card {
border: 1px solid #cdd4e0;
border-radius: 10px;
padding: 8px 10px;
background: #f8fafc;
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
}
.db-choice-card:hover { border-color: #b7c6da; background: #fff; }
.db-choice-card:focus-within { border-color: var(--amber); box-shadow: 0 0 0 2px rgba(183, 114, 0, 0.15); background: #fff; }
.db-choice-card input { margin: 0; accent-color: var(--amber); }
.db-choice-card span { font-weight: 600; color: #2b3445; word-break: break-all; }
.db-choice-card input:disabled + span { color: #9aa3b2; }
.restore-target-per-row { min-width: 240px; }
td.actions form { display: inline-flex; }
.br-restore-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 16px;
align-items: start;
}
.br-restore-calendar {
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px;
background: #f8fafc;
}
.br-restore-calendar h4 {
margin: 0 0 10px;
font-size: 16px;
font-weight: 700;
}
.br-month-nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.br-month-btn {
width: 32px;
height: 32px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
color: #3a4655;
cursor: pointer;
}
.br-month-btn:hover { border-color: var(--amber); }
.br-month-btn:disabled { opacity: 0.4; cursor: default; }
.br-month-label { font-weight: 600; color: #1f2a37; }
.br-calendar-grid-wrap { margin: 0; }
.br-calendar-grid {
width: 100%;
border-collapse: collapse;
font-size: 13px;
border: 0;
background: transparent;
border-radius: 0;
overflow: visible;
}
.br-calendar-grid th {
padding: 4px;
text-align: center;
border-bottom: 1px solid var(--border);
background: transparent;
color: var(--muted);
}
.br-calendar-grid td {
padding: 4px;
text-align: center;
border-bottom: 0;
}
.br-cal-day {
width: 30px;
height: 30px;
border-radius: 8px;
border: 1px solid var(--border);
background: #fff;
cursor: pointer;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
.br-cal-day:hover {
border-color: var(--amber);
background: #fff7ec;
}
.br-cal-day.is-selected {
background: var(--amber);
color: #fff;
border-color: var(--amber);
}
.br-cal-day-disabled {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
color: #a0a7b4;
}
.br-cal-empty { background: transparent; }
.br-restore-picker { min-width: 0; }
.br-user-backup-picker { min-width: 0; }
.br-date-label {
font-weight: 600;
color: #1f2a37;
margin: 6px 0 10px;
}
.br-time-picker { margin: 8px 0 14px; }
.br-time-title { font-weight: 600; margin-bottom: 6px; color: var(--muted); }
.br-time-list { display: flex; flex-wrap: wrap; gap: 8px; }
.br-time-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 999px;
background: #fff;
cursor: pointer;
font-size: 13px;
}
.br-time-item input { margin: 0; accent-color: var(--amber); }
.privileges-group { display: flex; flex-wrap: wrap; gap: 4px 10px; }
body.overlay-open { overflow: hidden; }
.job-overlay { position: fixed; inset: 0; display: none; z-index: 9999; }
.job-overlay.open { display: block; }
.job-overlay-backdrop { position: absolute; inset: 0; background: rgba(24, 31, 44, 0.55); }
.job-overlay-card {
position: relative;
width: min(920px, calc(100vw - 32px));
max-height: calc(100vh - 50px);
overflow: auto;
margin: 24px auto;
background: #fff;
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px;
box-shadow: 0 16px 48px rgba(18, 28, 45, 0.35);
}
.job-overlay-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.job-overlay-head h3 { margin: 0; font-size: 22px; }
.job-overlay-head-actions { display: inline-flex; align-items: center; gap: 8px; }
.log-overlay .log-meta {
display: grid;
gap: 6px;
margin: 10px 0 12px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: #f8fafc;
font-size: 13px;
}
.log-overlay .log-content {
margin: 0;
padding: 12px;
border-radius: 10px;
border: 1px solid #1f2a44;
background: #0f172a;
color: #e2e8f0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.4;
white-space: pre;
max-height: 50vh;
overflow: auto;
}
.br-text-danger { color: var(--danger); font-weight: 700; }
.br-modal-danger { border-color: var(--danger); }
.br-modal-danger h3 { color: var(--danger); }
.br-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.br-modal {
width: min(560px, 92vw);
background: #ffffff;
border-radius: 12px;
padding: 16px;
border: 2px solid var(--border);
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
max-height: 90vh;
}
.br-modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
.br-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.br-modal-title {
font-weight: 700;
font-size: 16px;
}
.br-modal-body {
max-height: 60vh;
overflow: auto;
}
.br-muted { color: var(--muted); }
.br-alert {
border-radius: 8px;
padding: 10px 12px;
margin: 8px 0;
border: 1px solid transparent;
}
.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; }
.br-picker-path {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.br-picker-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid var(--border);
border-radius: 10px;
max-height: 45vh;
overflow: auto;
}
.br-picker-create {
margin-top: 10px;
display: flex;
gap: 8px;
}
.br-picker-create input {
flex: 1;
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 8px;
}
.br-picker-list li {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--border);
}
.br-picker-list li:last-child {
border-bottom: none;
}
.br-picker-list li:hover {
background: #f8fafc;
}
.br-picker-list li.active {
background: #e0f2fe;
}
.br-btn {
background: var(--amber);
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 14px;
cursor: pointer;
font-weight: 600;
}
.br-btn:hover { background: var(--amber-strong); }
.br-btn-ghost {
background: transparent;
color: #1f2d3d;
border: 1px solid var(--border);
}
.br-btn-ghost:hover { background: #eef2f7; }
.br-btn-danger {
background: var(--danger);
border: 1px solid var(--danger);
color: #fff;
border-radius: 6px;
padding: 8px 14px;
cursor: pointer;
font-weight: 600;
}
.br-btn-danger:hover { background: #c03a15; border-color: #c03a15; }
.br-btn-small {
padding: 6px 10px;
font-size: 12px;
}
.br-icon-btn {
border: 1px solid var(--border);
background: #fff;
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
font-size: 16px;
line-height: 1;
}
.br-icon-btn:hover {
background: #f8fafc;
}
@media (max-width: 900px) {
.wrap { padding: 10px; }
header h1 { font-size: 32px; }
.section-title { font-size: 28px; }
.kv { grid-template-columns: 1fr; }
.br-restore-layout { grid-template-columns: 1fr; }
}
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+7
View File
@@ -0,0 +1,7 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.raw'));
define('DA_POSTGRESQL_RAW_MODE', true);
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';
+7
View File
@@ -0,0 +1,7 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.raw'));
define('DA_POSTGRESQL_RAW_MODE', true);
require dirname(__DIR__) . '/exec/bootstrap.php';
+6
View File
@@ -0,0 +1,6 @@
#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/da-postgresql/php.ini
<?php
declare(strict_types=1);
define('IN_DA_PLUGIN', true);
define('PLUGIN_ACTION', basename(__FILE__, '.html'));
require dirname(__DIR__) . '/exec/bootstrap.php';