Import postgresql plugin
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
.DS_Store
|
||||||
|
._*
|
||||||
|
__MACOSX/
|
||||||
|
.claude/
|
||||||
|
error.log
|
||||||
|
data/*
|
||||||
Executable
+5
@@ -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;
|
||||||
@@ -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`
|
||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
@@ -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>';
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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'));
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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';
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<a href="/CMD_PLUGINS/da-postgresql/index.html" target="_top">Bazy danych PostgreSQL</a>
|
||||||
@@ -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 |
@@ -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
@@ -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
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+156
@@ -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."
|
||||||
@@ -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
|
||||||
Executable
+704
@@ -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}"
|
||||||
Executable
+127
@@ -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
|
||||||
Executable
+636
@@ -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
|
||||||
Executable
+226
@@ -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
|
||||||
Executable
+786
@@ -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
|
||||||
Executable
+1371
File diff suppressed because it is too large
Load Diff
Executable
+520
@@ -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
|
||||||
Executable
+211
@@ -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
|
||||||
Executable
+30
@@ -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."
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
"$(cd "$(dirname "$0")" && pwd)/install.sh"
|
||||||
@@ -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
|
||||||
Executable
+6
@@ -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';
|
||||||
Executable
+6
@@ -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';
|
||||||
Executable
+6
@@ -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';
|
||||||
Executable
+6
@@ -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';
|
||||||
Executable
+6
@@ -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';
|
||||||
Executable
+6
@@ -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';
|
||||||
@@ -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';
|
||||||
@@ -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';
|
||||||
@@ -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';
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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';
|
||||||
Executable
+6
@@ -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';
|
||||||
Executable
+6
@@ -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';
|
||||||
Executable
+6
@@ -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';
|
||||||
Executable
+7
@@ -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';
|
||||||
@@ -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';
|
||||||
Reference in New Issue
Block a user