daUser = $daUser; $this->settings = $settings; $this->mysql = $mysql; $this->csrf = $csrf; $this->lang = $lang; } public function action(): string { return (string)PLUGIN_ACTION; } public function baseUrl(): string { return '/CMD_PLUGINS/alt-mysql'; } 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_MYSQL_BASE_URL=' . json_encode($this->baseUrl(), JSON_UNESCAPED_SLASHES) . ';'; $jsConfig .= 'window.DA_MYSQL_INDEX_URL=' . json_encode($this->url('index.html'), JSON_UNESCAPED_SLASHES) . ';'; $jsConfig .= 'window.DA_MYSQL_USER_URL=' . json_encode($this->url('user/index.html'), JSON_UNESCAPED_SLASHES) . ';'; $jsConfig .= 'window.DA_MYSQL_UPLOAD_RAW_URL=' . json_encode($this->url('upload_backup.raw'), JSON_UNESCAPED_SLASHES) . ';'; $jsConfig .= 'window.DA_MYSQL_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_phpmyadmin_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_MYSQL_I18N === 'object' && window.DA_MYSQL_I18N) ? window.DA_MYSQL_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; } out = out.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k])); } return out; } function generatePassword(len) { var size = len || 20; var alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*_-+='; var bytes = new Uint32Array(size); if (window.crypto && window.crypto.getRandomValues) { window.crypto.getRandomValues(bytes); } else { for (var i = 0; i < size; i += 1) { bytes[i] = Math.floor(Math.random() * 0xffffffff); } } var out = ''; for (var j = 0; j < size; j += 1) { out += alphabet.charAt(bytes[j] % alphabet.length); } return out; } function bindPasswordHelpers() { document.querySelectorAll('[data-generate-target]').forEach(function (btn) { btn.addEventListener('click', function () { var input = document.getElementById(btn.getAttribute('data-generate-target')); if (!input) { return; } input.value = generatePassword(20); input.dispatchEvent(new Event('input', { bubbles: true })); }); }); document.querySelectorAll('[data-toggle-target]').forEach(function (btn) { btn.addEventListener('click', function () { var input = document.getElementById(btn.getAttribute('data-toggle-target')); if (!input) { return; } input.type = input.type === 'password' ? 'text' : 'password'; }); }); } function bindDownloadForms() { document.querySelectorAll('form[data-backup-download]').forEach(function (form) { form.addEventListener('submit', function (ev) { var btn = form.querySelector('button[type="submit"],input[type="submit"]'); if (btn) { btn.disabled = true; btn.dataset.originalText = btn.textContent; btn.textContent = tr('Pobieranie...'); } window.setTimeout(function () { if (btn) { btn.disabled = false; if (btn.dataset.originalText) { btn.textContent = btn.dataset.originalText; } } }, 4000); }); }); } window.daMysqlI18n = { tr: tr, trf: trf }; bindPasswordHelpers(); bindDownloadForms(); }()); JS; return FilePicker::scripts() . "\n" . $js; } }