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 */ 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 $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 '' . '' . ''; } 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 .= '
' . self::e($msg) . '
'; } foreach ($errorMessages as $msg) { $alerts .= '
' . self::e($msg) . '
'; } $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 ''; echo ''; echo '' . $safeTitle . ''; echo ''; echo ''; echo '
'; echo ''; $headerClass = in_array($this->action(), ['index', 'upload_backup'], true) ? ' class="header-center"' : ''; echo '

' . $safeTitle . '

'; echo '
' . $topActions . '
'; echo '
' . $alerts . $body . '
'; echo '
' . $logs . '
'; echo '
'; $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 ''; echo ''; echo ''; } public function renderFatal(string $message, int $code = 400): void { http_response_code($code); $body = '

' . self::e($this->t($message)) . '

'; $body .= '

' . self::e($this->t('Powrót')) . '

'; $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[] = '' . self::e($this->t('BAZY DANYCH')) . ''; $links[] = '' . self::e($this->t('UŻYTKOWNICY BAZ DANYCH')) . ''; if ($this->settings->enableUploadBackup()) { $links[] = '' . self::e($this->t('BACKUPY BAZ DANYCH')) . ''; } if ($this->settings->enableAdminer()) { $links[] = '
' . $this->csrfField('open_adminer_submit') . '' . '
'; } 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 */ 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; } }