Files
da-postgresql/exec/lib/AppContext.php
T
2026-07-04 16:00:04 +02:00

1209 lines
39 KiB
PHP

<?php
declare(strict_types=1);
final class AppContext
{
public DirectAdminUser $daUser;
public Settings $settings;
public PostgresService $pg;
private CsrfGuard $csrf;
private Lang $lang;
public function __construct(DirectAdminUser $daUser, Settings $settings, PostgresService $pg, CsrfGuard $csrf, Lang $lang)
{
$this->daUser = $daUser;
$this->settings = $settings;
$this->pg = $pg;
$this->csrf = $csrf;
$this->lang = $lang;
}
public function action(): string
{
return (string)PLUGIN_ACTION;
}
public function baseUrl(): string
{
return '/CMD_PLUGINS/da-postgresql';
}
public function url(string $page, array $query = []): string
{
$path = $this->baseUrl() . '/' . ltrim($page, '/');
if (!empty($query)) {
$path .= '?' . http_build_query($query);
}
return $path;
}
public function isPost(): bool
{
return Http::isPost();
}
public function postString(string $key, string $default = ''): string
{
return Http::getString($_POST, $key, $default);
}
/**
* @return array<int,string>
*/
public function postArray(string $key): array
{
$raw = Http::getArray($_POST, $key);
$out = [];
foreach ($raw as $item) {
if (is_array($item)) {
continue;
}
$out[] = trim((string)$item);
}
return $out;
}
public function queryString(string $key, string $default = ''): string
{
return Http::getString($_GET, $key, $default);
}
/**
* @param array<string,string|int|float> $vars
*/
public function t(string $key, array $vars = []): string
{
return $this->lang->t($key, $vars);
}
public function formatException(Throwable $e): string
{
if ($e instanceof LocalizedException) {
return $this->t($e->key(), $e->vars());
}
return $this->t($e->getMessage());
}
public function csrfField(string $intent): string
{
$issued = $this->csrf->issue($intent);
$ts = (string)$issued['ts'];
$sid = htmlspecialchars((string)($issued['sid'] ?? ''), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$token = htmlspecialchars($issued['token'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
return '<input type="hidden" name="csrf_ts" value="' . $ts . '">' .
'<input type="hidden" name="csrf_sid" value="' . $sid . '">' .
'<input type="hidden" name="csrf_token" value="' . $token . '">';
}
public function requireCsrf(string $intent): void
{
$ts = $this->postString('csrf_ts');
$sid = $this->postString('csrf_sid');
$token = $this->postString('csrf_token');
if (!$this->csrf->validate($intent, $ts, $token, 3600, $sid)) {
$debug = sprintf(
'[%s] CSRF_FAIL intent=%s action=%s method=%s ts=%s sid=%s token_prefix=%s post_keys=%s',
date('c'),
$intent,
$this->action(),
Http::method(),
$ts,
$sid,
substr($token, 0, 12),
implode(',', array_keys($_POST))
);
@error_log($debug . "\n", 3, PLUGIN_ROOT . '/error.log');
throw new RuntimeException('Nieprawidłowy lub wygasły token CSRF.');
}
}
/**
* @param string[] $okMessages
* @param string[] $errorMessages
*/
public function renderPage(string $title, string $body, array $okMessages = [], array $errorMessages = [], string $logs = ''): void
{
$alerts = '';
foreach ($okMessages as $msg) {
$alerts .= '<div class="alert alert-ok">' . self::e($msg) . '</div>';
}
foreach ($errorMessages as $msg) {
$alerts .= '<div class="alert alert-err">' . self::e($msg) . '</div>';
}
$alerts = $this->lang->translateHtml($alerts);
$body = $this->lang->translateHtml($body);
$logs = $this->lang->translateHtml($logs);
$safeTitle = self::e($this->t($title));
$topActions = $this->renderTopActions();
$langCode = $this->daUser->language();
header('Content-Type: text/html; charset=UTF-8');
echo '<!doctype html><html lang="' . self::e($langCode) . '"><head><meta charset="utf-8">';
echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
echo '<title>' . $safeTitle . '</title>';
echo '<style>' . $this->styles() . '</style>';
echo '</head><body>';
echo '<div class="wrap">';
echo '<div class="breadcrumbs">' . self::e($this->t('Pulpit')) . ' <span>&gt;</span> ' . self::e($this->t('Bazy danych')) . '</div>';
$headerClass = in_array($this->action(), ['index', 'upload_backup'], true) ? ' class="header-center"' : '';
echo '<header' . $headerClass . '><h1>' . $safeTitle . '</h1></header>';
echo '<div class="top-actions">' . $topActions . '</div>';
echo '<section class="main-section">' . $alerts . $body . '</section>';
echo '<section class="logs-section">' . $logs . '</section>';
echo '</div>';
$jsConfig = 'window.DA_POSTGRESQL_BASE_URL=' . json_encode($this->baseUrl(), JSON_UNESCAPED_SLASHES) . ';';
$jsConfig .= 'window.DA_POSTGRESQL_INDEX_URL=' . json_encode($this->url('index.html'), JSON_UNESCAPED_SLASHES) . ';';
$jsConfig .= 'window.DA_POSTGRESQL_USER_URL=' . json_encode($this->url('user/index.html'), JSON_UNESCAPED_SLASHES) . ';';
$jsConfig .= 'window.DA_POSTGRESQL_UPLOAD_RAW_URL=' . json_encode($this->url('upload_backup.raw'), JSON_UNESCAPED_SLASHES) . ';';
$jsConfig .= 'window.DA_POSTGRESQL_I18N=' . json_encode($this->jsI18n(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ';';
echo '<script>' . $jsConfig . '</script>';
echo '<script>' . $this->scripts() . '</script>';
echo '</body></html>';
}
public function renderFatal(string $message, int $code = 400): void
{
http_response_code($code);
$body = '<p>' . self::e($this->t($message)) . '</p>';
$body .= '<p><a href="' . self::e($this->url('index.html')) . '">' . self::e($this->t('Powrót')) . '</a></p>';
$this->renderPage('Błąd', $body, [], []);
}
public static function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
private function renderTopActions(): string
{
$links = [];
$links[] = '<a class="btn amber' . ($this->action() === 'index' ? ' active' : '') . '" href="' . self::e($this->url('index.html')) . '">' . self::e($this->t('BAZY DANYCH')) . '</a>';
$links[] = '<a class="btn amber' . ($this->action() === 'change_password' ? ' active' : '') . '" href="' . self::e($this->url('change_password.html')) . '">' . self::e($this->t('UŻYTKOWNICY BAZ DANYCH')) . '</a>';
if ($this->settings->enableUploadBackup()) {
$links[] = '<a class="btn amber' . ($this->action() === 'upload_backup' ? ' active' : '') . '" href="' . self::e($this->url('upload_backup.html')) . '">' . self::e($this->t('BACKUPY BAZ DANYCH')) . '</a>';
}
if ($this->settings->enableAdminer()) {
$links[] = '<form method="post" target="_top" action="' . self::e($this->url('open_adminer.raw')) . '" style="display:inline-flex;align-items:center;">' .
$this->csrfField('open_adminer_submit') .
'<button class="btn amber' . ($this->action() === 'open_adminer' ? ' active' : '') . '" type="submit">' . self::e($this->t('ADMINER')) . '</button>' .
'</form>';
}
return implode('', $links);
}
private function styles(): string
{
$skin = $this->daUser->skin();
$path = PLUGIN_ROOT . '/user/' . $skin . '.css';
if (!is_file($path)) {
$path = PLUGIN_ROOT . '/user/enhanced.css';
}
$css = is_file($path) ? file_get_contents($path) : '';
if ($css === false) {
return '';
}
return $css;
}
/**
* @return array<string,string>
*/
private function jsI18n(): array
{
$keys = [
'Pobieranie...',
'Nieudana odpowiedź serwera ({code})',
'Nieudana odpowiedź serwera (HTML zamiast pliku, URL: {url})',
'Serwer zwrócił pusty plik.',
'Nie można pobrać pliku backupu.',
'Pobierz plik backupu',
'Brak plików .sql/.gz',
'Brak katalogów',
'Nie udało się wczytać katalogów.',
'Podaj nazwę katalogu.',
'Nie udało się utworzyć katalogu.',
'Wysyłanie pliku...',
'Nie udało się wysłać pliku backupu.',
'Wybierz plik .sql/.gz/.sql.gz lub wskaż ścieżkę pliku na serwerze.',
'Wskaż ścieżkę do pliku SQL lub SQL.GZ.',
'Wybierz plik .sql/.gz/.sql.gz do przywrócenia.',
];
$out = [];
foreach ($keys as $key) {
$out[$key] = $this->t($key);
}
return $out;
}
private function scripts(): string
{
$js = <<<'JS'
(function () {
var __i18n = (typeof window.DA_POSTGRESQL_I18N === 'object' && window.DA_POSTGRESQL_I18N) ? window.DA_POSTGRESQL_I18N : {};
function tr(key) {
return (__i18n && Object.prototype.hasOwnProperty.call(__i18n, key)) ? __i18n[key] : key;
}
function trf(key, vars) {
var out = tr(key);
if (!vars) {
return out;
}
for (var k in vars) {
if (!Object.prototype.hasOwnProperty.call(vars, k)) {
continue;
}
var val = String(vars[k]);
out = out.replace(new RegExp('\\{' + k + '\\}', 'g'), val);
}
return out;
}
function bindPrivilegesGroups() {
function bindSingleGroup(group) {
var allCheckbox = group.querySelector('input[type="checkbox"][value="ALL"]');
if (!allCheckbox) {
return;
}
var checkboxes = group.querySelectorAll('input[type="checkbox"]');
var others = [];
for (var ci = 0; ci < checkboxes.length; ci += 1) {
if (checkboxes[ci] !== allCheckbox) {
others.push(checkboxes[ci]);
}
}
var setOthers = function (state) {
for (var oi = 0; oi < others.length; oi += 1) {
others[oi].checked = state;
}
};
var refreshAll = function () {
if (others.length === 0) {
return;
}
var fullyChecked = true;
for (var oi = 0; oi < others.length; oi += 1) {
if (!others[oi].checked) {
fullyChecked = false;
break;
}
}
allCheckbox.checked = fullyChecked;
};
if (allCheckbox.checked) {
setOthers(true);
} else {
refreshAll();
}
allCheckbox.addEventListener('change', function () {
setOthers(allCheckbox.checked);
});
for (var oi = 0; oi < others.length; oi += 1) {
others[oi].addEventListener('change', function (event) {
if (!event.target.checked) {
allCheckbox.checked = false;
return;
}
refreshAll();
});
}
}
var groups = document.querySelectorAll('[data-privileges-group]');
for (var gi = 0; gi < groups.length; gi += 1) {
bindSingleGroup(groups[gi]);
}
}
function generatePassword(length) {
var chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*_-+=';
var out = '';
var max = chars.length;
var useCrypto = typeof window.crypto !== 'undefined' && typeof window.crypto.getRandomValues === 'function';
var rnd = new Uint32Array(length);
if (useCrypto) {
window.crypto.getRandomValues(rnd);
for (var i = 0; i < length; i += 1) {
out += chars.charAt(rnd[i] % max);
}
return out;
}
for (var j = 0; j < length; j += 1) {
out += chars.charAt(Math.floor(Math.random() * max));
}
return out;
}
function bindPasswordTools() {
var generateButtons = document.querySelectorAll('[data-generate-target]');
for (var i = 0; i < generateButtons.length; i += 1) {
generateButtons[i].addEventListener('click', function () {
var inputId = this.getAttribute('data-generate-target');
var input = document.getElementById(inputId);
if (!input) {
return;
}
input.value = generatePassword(24);
var ev;
if (typeof Event === 'function') {
ev = new Event('input', { bubbles: true });
} else {
ev = document.createEvent('Event');
ev.initEvent('input', true, true);
}
input.dispatchEvent(ev);
});
}
var toggleButtons = document.querySelectorAll('[data-toggle-target]');
for (var j = 0; j < toggleButtons.length; j += 1) {
toggleButtons[j].addEventListener('click', function () {
var inputId = this.getAttribute('data-toggle-target');
var input = document.getElementById(inputId);
if (!input) {
return;
}
input.type = (input.type === 'password') ? 'text' : 'password';
});
}
}
function bindSelectAllColumns() {
var headers = document.querySelectorAll('[data-select-all-for]');
for (var i = 0; i < headers.length; i += 1) {
headers[i].style.cursor = 'pointer';
headers[i].addEventListener('click', function () {
var className = this.getAttribute('data-select-all-for');
if (!className) {
return;
}
var table = this.closest('table');
var selector = 'input[type="checkbox"].' + className;
var boxes = table ? table.querySelectorAll(selector) : document.querySelectorAll(selector);
if (!boxes.length) {
return;
}
var shouldCheck = false;
for (var bi = 0; bi < boxes.length; bi += 1) {
if (!boxes[bi].checked) {
shouldCheck = true;
break;
}
}
for (var bj = 0; bj < boxes.length; bj += 1) {
boxes[bj].checked = shouldCheck;
}
});
}
}
function bindCreateDatabaseAdvancedMode() {
var details = document.getElementById('create-db-advanced');
var modeField = document.getElementById('create-db-mode');
if (!details || !modeField) {
return;
}
var roleNew = document.getElementById('role-mode-new');
var roleExisting = document.getElementById('role-mode-existing');
var roleInput = document.getElementById('advanced-role-name');
var passwordInput = document.getElementById('advanced-role-password');
var existingSelect = document.getElementById('existing-role-name');
var sync = function () {
var isAdvanced = !!details.open;
modeField.value = isAdvanced ? 'advanced' : 'simple';
if (roleNew && roleExisting) {
roleNew.disabled = !isAdvanced;
roleExisting.disabled = !isAdvanced;
if (!isAdvanced) {
roleNew.checked = true;
}
}
var useExisting = isAdvanced && roleExisting && roleExisting.checked;
if (roleInput) {
roleInput.disabled = !isAdvanced || useExisting;
}
if (passwordInput) {
var needsPassword = isAdvanced && !useExisting;
passwordInput.disabled = !isAdvanced || useExisting;
passwordInput.required = needsPassword;
}
if (existingSelect) {
existingSelect.disabled = !isAdvanced || !useExisting;
existingSelect.required = isAdvanced && useExisting;
}
};
details.addEventListener('toggle', sync);
if (roleNew) {
roleNew.addEventListener('change', sync);
}
if (roleExisting) {
roleExisting.addEventListener('change', sync);
}
sync();
}
function bindRestoreModeControls() {
var radios = document.querySelectorAll('input[name="restore_mode_global"]');
if (!radios.length) {
return;
}
var targetWrap = document.getElementById('restore-mode-target-wrap');
var modeFields = document.querySelectorAll('.js-restore-mode-field');
var perRowTargets = document.querySelectorAll('[data-restore-targets]');
var targetCols = document.querySelectorAll('[data-restore-target-col]');
function currentMode() {
var checked = document.querySelector('input[name="restore_mode_global"]:checked');
if (!checked) {
return 'overwrite';
}
return checked.value === 'new_db' ? 'new_db' : 'overwrite';
}
function sync() {
var mode = currentMode();
if (targetWrap) {
targetWrap.style.display = (mode === 'new_db') ? '' : 'none';
}
for (var i = 0; i < modeFields.length; i += 1) {
modeFields[i].value = mode;
}
for (var j = 0; j < perRowTargets.length; j += 1) {
var container = perRowTargets[j];
if (mode === 'new_db') {
container.style.display = '';
} else {
container.style.display = 'none';
}
var inputs = container.querySelectorAll('input[type="radio"]');
for (var k = 0; k < inputs.length; k += 1) {
inputs[k].disabled = (mode !== 'new_db');
}
}
for (var c = 0; c < targetCols.length; c += 1) {
targetCols[c].style.display = (mode === 'new_db') ? '' : 'none';
}
}
for (var r = 0; r < radios.length; r += 1) {
radios[r].addEventListener('change', sync);
}
sync();
}
function bindJobOverlays() {
var openButtons = document.querySelectorAll('[data-overlay-open]');
var closeButtons = document.querySelectorAll('[data-overlay-close]');
function replaceOverlayJobInUrl(jobId) {
if (typeof window.URL !== 'function') {
return;
}
try {
var url = new URL(window.location.href);
if (jobId) {
url.searchParams.set('overlay_job', jobId);
} else {
url.searchParams.delete('overlay_job');
}
window.history.replaceState({}, '', url.toString());
} catch (e) {
}
}
function hasAnyOpen() {
return document.querySelectorAll('.job-overlay.open').length > 0;
}
function setOpen(id, state, jobId) {
var el = document.getElementById(id);
if (!el) {
return;
}
if (state) {
el.classList.add('open');
el.setAttribute('aria-hidden', 'false');
document.body.classList.add('overlay-open');
replaceOverlayJobInUrl(jobId || el.getAttribute('data-overlay-job-id') || '');
} else {
el.classList.remove('open');
el.setAttribute('aria-hidden', 'true');
if (!hasAnyOpen()) {
document.body.classList.remove('overlay-open');
replaceOverlayJobInUrl('');
}
}
}
for (var i = 0; i < openButtons.length; i += 1) {
openButtons[i].addEventListener('click', function () {
var id = this.getAttribute('data-overlay-open');
if (!id) {
return;
}
var jobId = this.getAttribute('data-overlay-job') || '';
setOpen(id, true, jobId);
});
}
for (var j = 0; j < closeButtons.length; j += 1) {
closeButtons[j].addEventListener('click', function () {
var id = this.getAttribute('data-overlay-close');
if (!id) {
return;
}
setOpen(id, false, '');
});
}
if (hasAnyOpen()) {
document.body.classList.add('overlay-open');
}
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') {
return;
}
var overlays = document.querySelectorAll('.job-overlay.open');
for (var oi = 0; oi < overlays.length; oi += 1) {
overlays[oi].classList.remove('open');
overlays[oi].setAttribute('aria-hidden', 'true');
}
document.body.classList.remove('overlay-open');
replaceOverlayJobInUrl('');
});
}
function bindBackupAutoRefresh() {
var cfg = document.getElementById('backup-auto-refresh');
if (!cfg) {
return;
}
if (cfg.getAttribute('data-enabled') !== '1') {
return;
}
var seconds = parseInt(cfg.getAttribute('data-seconds') || '3', 10);
if (!seconds || seconds < 1) {
seconds = 3;
}
var statusEl = document.getElementById('job-status');
var statusUrl = cfg.getAttribute('data-status-url') || '';
if (!statusEl || !statusUrl || typeof window.fetch !== 'function') {
return;
}
var busy = false;
function refreshStatus() {
if (busy) { return; }
if (window.__backupDownloadInProgress === true) { return; }
if (document.querySelector('.job-overlay.open')) { return; }
busy = true;
fetch(statusUrl, { credentials: 'same-origin', cache: 'no-store' })
.then(function (res) {
if (!res.ok) { return ''; }
return res.text();
})
.then(function (html) {
if (html) {
statusEl.innerHTML = html;
bindJobOverlays();
bindLogCopyButtons();
}
})
.catch(function () {})
.finally(function () { busy = false; });
}
window.setInterval(refreshStatus, seconds * 1000);
}
function bindBackupSlotPicker() {
var radios = document.querySelectorAll('form.br-time-picker input[type="radio"][name="backup_slot"]');
if (!radios.length) {
return;
}
for (var i = 0; i < radios.length; i += 1) {
if (radios[i].dataset.bound === '1') { continue; }
radios[i].dataset.bound = '1';
radios[i].addEventListener('change', function () {
if (!this.checked) { return; }
var form = this.form;
if (!form) { return; }
if (typeof window.fetch !== 'function') {
form.submit();
return;
}
var container = document.querySelector('.br-restore-picker');
if (!container) {
form.submit();
return;
}
var params = new URLSearchParams(new FormData(form));
params.set('action', 'local_picker');
var url = window.location.pathname + '?' + params.toString();
fetch(url, { credentials: 'same-origin', cache: 'no-store' })
.then(function (res) {
if (!res.ok) { return ''; }
return res.text();
})
.then(function (html) {
if (!html) { return; }
container.innerHTML = html;
bindRestoreModeControls();
bindBackupSlotPicker();
})
.catch(function () {
form.submit();
});
});
}
}
function bindUserBackupSlotPicker() {
var radios = document.querySelectorAll('form.br-user-time-picker input[type="radio"][name="user_slot"]');
if (!radios.length) {
return;
}
for (var i = 0; i < radios.length; i += 1) {
if (radios[i].dataset.bound === '1') { continue; }
radios[i].dataset.bound = '1';
radios[i].addEventListener('change', function () {
if (!this.checked) { return; }
var form = this.form;
if (!form) { return; }
if (typeof window.fetch !== 'function') {
form.submit();
return;
}
var container = document.querySelector('.br-user-backup-picker');
if (!container) {
form.submit();
return;
}
var params = new URLSearchParams(new FormData(form));
params.set('action', 'user_picker');
var url = window.location.pathname + '?' + params.toString();
fetch(url, { credentials: 'same-origin', cache: 'no-store' })
.then(function (res) {
if (!res.ok) { return ''; }
return res.text();
})
.then(function (html) {
if (!html) { return; }
container.innerHTML = html;
bindUserBackupSlotPicker();
bindBackupDownloadForms();
bindJobOverlays();
bindLogCopyButtons();
})
.catch(function () {
form.submit();
});
});
}
}
function bindLogCopyButtons() {
var buttons = document.querySelectorAll('[data-log-copy]');
if (!buttons.length) {
return;
}
function copyText(text, onDone) {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
navigator.clipboard.writeText(text).then(function () {
if (typeof onDone === 'function') { onDone(true); }
}).catch(function () {
if (typeof onDone === 'function') { onDone(false); }
});
return;
}
try {
var ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', 'readonly');
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
if (typeof onDone === 'function') { onDone(true); }
} catch (e) {
if (typeof onDone === 'function') { onDone(false); }
}
}
for (var i = 0; i < buttons.length; i += 1) {
buttons[i].addEventListener('click', function () {
var targetId = this.getAttribute('data-log-copy');
var target = targetId ? document.getElementById(targetId) : null;
if (!target) {
return;
}
var text = target.textContent || '';
var btn = this;
var original = btn.textContent;
copyText(text, function (ok) {
if (ok) {
btn.textContent = tr('Skopiowano');
window.setTimeout(function () {
btn.textContent = original || tr('Kopiuj');
}, 1200);
}
});
});
}
}
function bindDropzones() {
var zones = document.querySelectorAll('[data-dropzone]');
if (!zones.length) {
return;
}
function formatBytes(bytes) {
if (!bytes || bytes <= 0) { return '0 B'; }
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
var value = bytes / Math.pow(k, i);
return value.toFixed(value >= 10 || i === 0 ? 0 : 1) + ' ' + sizes[i];
}
zones.forEach(function (zone) {
var inputId = zone.getAttribute('data-input');
if (!inputId) { return; }
var input = document.getElementById(inputId);
if (!input) { return; }
var pathInputId = zone.getAttribute('data-path-input');
var pathInput = pathInputId ? document.getElementById(pathInputId) : null;
var stagedInputId = zone.getAttribute('data-staged-input');
var stagedInput = stagedInputId ? document.getElementById(stagedInputId) : null;
var stagedNameInputId = zone.getAttribute('data-staged-name-input');
var stagedNameInput = stagedNameInputId ? document.getElementById(stagedNameInputId) : null;
var nameEl = zone.querySelector('.dropzone-name');
var sizeEl = zone.querySelector('.dropzone-size');
var clearBtn = zone.querySelector('.dropzone-clear');
function clearStagedPath() {
if (stagedInput) {
stagedInput.value = '';
try {
stagedInput.dispatchEvent(new Event('input', { bubbles: true }));
stagedInput.dispatchEvent(new Event('change', { bubbles: true }));
} catch (e) {
}
}
if (stagedNameInput) {
stagedNameInput.value = '';
try {
stagedNameInput.dispatchEvent(new Event('input', { bubbles: true }));
stagedNameInput.dispatchEvent(new Event('change', { bubbles: true }));
} catch (e) {
}
}
}
function updateView() {
var file = (input.files && input.files.length) ? input.files[0] : null;
if (file) {
zone.classList.add('is-filled');
if (nameEl) { nameEl.textContent = file.name || '-'; }
if (sizeEl) { sizeEl.textContent = formatBytes(file.size || 0); }
if (stagedNameInput) {
stagedNameInput.value = String(file.name || '');
try {
stagedNameInput.dispatchEvent(new Event('input', { bubbles: true }));
stagedNameInput.dispatchEvent(new Event('change', { bubbles: true }));
} catch (e) {
}
}
} else {
zone.classList.remove('is-filled');
if (nameEl) { nameEl.textContent = '-'; }
if (sizeEl) { sizeEl.textContent = '-'; }
}
if (typeof window.__restoreFileSync === 'function') {
window.__restoreFileSync();
}
}
function setFile(file) {
if (!file) { return; }
try {
var dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
} catch (e) {
// fallback: rely on input change
}
if (pathInput) {
pathInput.value = '';
}
clearStagedPath();
updateView();
}
zone.addEventListener('click', function (ev) {
var hasFile = input.files && input.files.length > 0;
if (!hasFile) {
input.click();
}
});
zone.addEventListener('keydown', function (ev) {
if (ev.key === 'Enter' || ev.key === ' ') {
ev.preventDefault();
var hasFile = input.files && input.files.length > 0;
if (!hasFile) {
input.click();
}
}
});
zone.addEventListener('dragover', function (ev) {
ev.preventDefault();
zone.classList.add('is-dragover');
});
zone.addEventListener('dragleave', function () {
zone.classList.remove('is-dragover');
});
zone.addEventListener('drop', function (ev) {
ev.preventDefault();
zone.classList.remove('is-dragover');
var files = ev.dataTransfer && ev.dataTransfer.files ? ev.dataTransfer.files : null;
if (!files || !files.length) {
return;
}
setFile(files[0]);
});
input.addEventListener('change', function () {
if (input.files && input.files.length && pathInput) {
pathInput.value = '';
}
clearStagedPath();
updateView();
});
if (clearBtn) {
clearBtn.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
input.value = '';
clearStagedPath();
updateView();
});
}
updateView();
});
}
function bindRestoreFileValidation() {
var form = document.querySelector('form[data-restore-file-form]');
if (!form) { return; }
var fileInput = form.querySelector('#source-file-upload');
var pathInput = form.querySelector('#source-file-path');
var stagedInput = form.querySelector('#uploaded-tmp-path');
var originalNameInput = form.querySelector('#uploaded-original-name');
var submitBtn = form.querySelector('button[type="submit"]');
var modeInputs = form.querySelectorAll('input[name="source_mode"]');
var localPanel = form.querySelector('[data-source-panel="local"]');
var serverPanel = form.querySelector('[data-source-panel="server"]');
function selectedMode() {
if (!modeInputs || !modeInputs.length) {
return 'local';
}
for (var i = 0; i < modeInputs.length; i += 1) {
if (modeInputs[i].checked) {
return String(modeInputs[i].value || 'local');
}
}
return String(modeInputs[0].value || 'local');
}
function syncPanels() {
var mode = selectedMode();
if (localPanel) {
localPanel.style.display = (mode === 'local') ? '' : 'none';
}
if (serverPanel) {
serverPanel.style.display = (mode === 'server') ? '' : 'none';
}
}
function hasSelectedFile() {
return !!(fileInput && fileInput.files && fileInput.files.length > 0);
}
function hasStagedPath() {
return !!(stagedInput && String(stagedInput.value || '').trim() !== '');
}
function hasServerPath() {
return !!(pathInput && String(pathInput.value || '').trim() !== '');
}
function sync() {
syncPanels();
var mode = selectedMode();
var ok = false;
if (mode === 'server') {
ok = hasServerPath();
} else {
ok = hasSelectedFile() || hasStagedPath();
}
if (submitBtn) {
submitBtn.disabled = !ok;
}
}
window.__restoreFileSync = sync;
function dispatchInputEvents(target) {
if (!target) {
return;
}
try {
target.dispatchEvent(new Event('input', { bubbles: true }));
target.dispatchEvent(new Event('change', { bubbles: true }));
} catch (e) {
}
}
form.addEventListener('submit', function (event) {
var hasPath = hasServerPath();
var hasFile = hasSelectedFile();
var hasStaged = hasStagedPath();
var mode = selectedMode();
if (mode === 'server') {
if (!hasPath) {
event.preventDefault();
window.alert(tr('Wskaż ścieżkę do pliku SQL lub SQL.GZ.'));
sync();
return;
}
return;
}
if (hasStaged && !hasFile) {
if (pathInput) {
pathInput.value = '';
dispatchInputEvents(pathInput);
}
return;
}
if (!hasFile && !hasStaged) {
event.preventDefault();
window.alert(tr('Wybierz plik .sql/.gz/.sql.gz do przywrócenia.'));
sync();
return;
}
if (hasFile && originalNameInput && fileInput && fileInput.files && fileInput.files[0]) {
originalNameInput.value = String(fileInput.files[0].name || '');
dispatchInputEvents(originalNameInput);
}
if (pathInput) {
pathInput.value = '';
dispatchInputEvents(pathInput);
}
});
if (fileInput) {
fileInput.addEventListener('change', sync);
}
if (pathInput) {
pathInput.addEventListener('input', sync);
pathInput.addEventListener('change', sync);
}
if (stagedInput) {
stagedInput.addEventListener('input', sync);
stagedInput.addEventListener('change', sync);
}
if (originalNameInput) {
originalNameInput.addEventListener('input', sync);
originalNameInput.addEventListener('change', sync);
}
if (modeInputs && modeInputs.length) {
for (var mi = 0; mi < modeInputs.length; mi += 1) {
modeInputs[mi].addEventListener('change', function () {
if (selectedMode() === 'server' && stagedInput) {
stagedInput.value = '';
dispatchInputEvents(stagedInput);
}
if (selectedMode() === 'server' && originalNameInput) {
originalNameInput.value = '';
dispatchInputEvents(originalNameInput);
}
if (selectedMode() === 'local' && pathInput) {
pathInput.value = '';
dispatchInputEvents(pathInput);
}
if (selectedMode() === 'server' && fileInput) {
fileInput.value = '';
dispatchInputEvents(fileInput);
}
sync();
});
}
}
sync();
}
function bindBackupDownloadForms() {
var forms = document.querySelectorAll('form.js-backup-download-form');
if (!forms.length || typeof window.fetch !== 'function' || typeof window.URL === 'undefined') {
return;
}
function parseFileName(contentDisposition) {
var value = String(contentDisposition || '');
var utfMatch = value.match(/filename\*=UTF-8''([^;]+)/i);
if (utfMatch && utfMatch[1]) {
try {
return decodeURIComponent(utfMatch[1].trim().replace(/(^\"|\"$)/g, ''));
} catch (e) {
}
}
var basicMatch = value.match(/filename=\"?([^\";]+)\"?/i);
if (basicMatch && basicMatch[1]) {
return basicMatch[1].trim();
}
return 'backup.sql';
}
for (var i = 0; i < forms.length; i += 1) {
forms[i].addEventListener('submit', function (event) {
event.preventDefault();
var form = event.currentTarget;
if (!form) {
return;
}
var submitButton = form.querySelector('button[type="submit"]');
var originalLabel = submitButton ? submitButton.textContent : '';
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = tr('Pobieranie...');
}
window.__backupDownloadInProgress = true;
var configuredUserUrl = (typeof window.DA_POSTGRESQL_USER_URL === 'string') ? window.DA_POSTGRESQL_USER_URL : '';
var configuredIndexUrl = (typeof window.DA_POSTGRESQL_INDEX_URL === 'string') ? window.DA_POSTGRESQL_INDEX_URL : '';
var configuredBaseUrl = (typeof window.DA_POSTGRESQL_BASE_URL === 'string') ? window.DA_POSTGRESQL_BASE_URL : '';
var actionUrl = form.getAttribute('action') || configuredUserUrl || configuredIndexUrl || configuredBaseUrl || window.location.href;
var body = new FormData(form);
window.fetch(actionUrl, {
method: 'POST',
body: body,
credentials: 'same-origin',
cache: 'no-store',
headers: {
'Accept': 'application/octet-stream'
}
}).then(function (response) {
if (!response.ok) {
throw new Error(trf('Nieudana odpowiedź serwera ({code})', { code: response.status }));
}
var disposition = String(response.headers.get('Content-Disposition') || '');
var contentType = String(response.headers.get('Content-Type') || '').toLowerCase();
if (disposition.toLowerCase().indexOf('attachment') === -1 && contentType.indexOf('text/html') !== -1) {
return response.text().then(function (text) {
var trimmed = String(text || '').replace(/\s+/g, ' ').trim();
var tail = trimmed ? (': ' + trimmed.slice(0, 220)) : '';
throw new Error(trf('Nieudana odpowiedź serwera (HTML zamiast pliku, URL: {url})', { url: actionUrl }) + tail);
});
}
return response.blob().then(function (blob) {
if (!blob || blob.size === 0) {
throw new Error(tr('Serwer zwrócił pusty plik.'));
}
var fileName = parseFileName(disposition);
var blobUrl = window.URL.createObjectURL(blob);
var link = document.createElement('a');
link.style.display = 'none';
link.href = blobUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
window.setTimeout(function () {
window.URL.revokeObjectURL(blobUrl);
if (link.parentNode) {
link.parentNode.removeChild(link);
}
}, 1000);
});
}).catch(function (err) {
var message = (err && err.message) ? err.message : tr('Nie można pobrać pliku backupu.');
window.alert(message);
}).finally(function () {
window.__backupDownloadInProgress = false;
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = originalLabel || tr('Pobierz plik backupu');
}
});
});
}
}
JS;
$js .= FilePicker::scripts();
$js .= <<<'JS'
bindPrivilegesGroups();
bindPasswordTools();
bindSelectAllColumns();
bindCreateDatabaseAdvancedMode();
bindRestoreModeControls();
bindJobOverlays();
bindBackupAutoRefresh();
bindBackupDownloadForms();
bindBackupSlotPicker();
bindUserBackupSlotPicker();
bindLogCopyButtons();
bindFilePicker();
bindDropzones();
bindRestoreFileValidation();
})();
JS;
return $js;
}
}