1209 lines
39 KiB
PHP
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>></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;
|
|
}
|
|
}
|