1291 lines
66 KiB
PHP
1291 lines
66 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
return static function (AppContext $ctx): void {
|
||
$ok = [];
|
||
$errors = [];
|
||
|
||
$tr = static fn(string $key, array $vars = []): string => $ctx->t($key, $vars);
|
||
$et = static fn(string $key, array $vars = []): string => AppContext::e($ctx->t($key, $vars));
|
||
|
||
$daUser = $ctx->daUser->username();
|
||
$prefix = $ctx->daUser->prefix();
|
||
|
||
$queue = new BackupQueueService($ctx->settings, $ctx->daUser);
|
||
try {
|
||
$queue->ensureInfrastructure();
|
||
} catch (Throwable $e) {
|
||
$errors[] = $ctx->formatException($e);
|
||
}
|
||
|
||
if (!$ctx->settings->enableUploadBackup()) {
|
||
$ctx->renderPage(
|
||
'Przywróć Backup',
|
||
'<section class="panel"><p class="muted">' . $et('Obsługa przywracania backupów jest wyłączona (`ENABLE_UPLOAD_BACKUP=false`).') . '</p></section>',
|
||
$ok,
|
||
$errors
|
||
);
|
||
return;
|
||
}
|
||
|
||
$jsonResponse = static function (array $payload, int $statusCode = 200): void {
|
||
http_response_code($statusCode);
|
||
header('Content-Type: text/plain; charset=UTF-8');
|
||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
if ($json === false) {
|
||
$json = '{"ok":false,"error":"JSON encode failed"}';
|
||
}
|
||
$marker = '__DA_MYSQL_UPLOAD_JSON__';
|
||
echo $marker . "\n" . $json . "\n" . $marker;
|
||
};
|
||
|
||
$requestAction = strtolower($ctx->queryString('action'));
|
||
if ($ctx->isPost() && $requestAction === 'upload_blob') {
|
||
$jsonResponse([
|
||
'ok' => false,
|
||
'error' => $ctx->t('Tryb przesyłania został zaktualizowany. Odśwież stronę i spróbuj ponownie.'),
|
||
], 409);
|
||
return;
|
||
}
|
||
|
||
$activeTab = strtolower($ctx->postString('active_tab', $ctx->queryString('tab', 'local')));
|
||
if (!in_array($activeTab, ['local', 'user', 'file'], true)) {
|
||
$activeTab = 'local';
|
||
}
|
||
|
||
$sourceFilePathValue = $ctx->postString('source_file_path');
|
||
if ($sourceFilePathValue === '') {
|
||
$rawPath = $_POST['source_file_path'] ?? null;
|
||
if (is_array($rawPath) && isset($rawPath[0])) {
|
||
$sourceFilePathValue = trim((string)$rawPath[0]);
|
||
}
|
||
}
|
||
if ($sourceFilePathValue === '') {
|
||
$sourceFilePathValue = $ctx->queryString('source_file_path');
|
||
}
|
||
$uploadedTmpPathValue = $ctx->postString('uploaded_tmp_path', $ctx->queryString('uploaded_tmp_path'));
|
||
$uploadedOriginalNameValue = $ctx->postString('uploaded_original_name', $ctx->queryString('uploaded_original_name'));
|
||
$sourceLocationMode = strtolower($ctx->postString('source_mode', $ctx->queryString('source_mode', 'local')));
|
||
if (!in_array($sourceLocationMode, ['local', 'server'], true)) {
|
||
$sourceLocationMode = 'local';
|
||
}
|
||
|
||
$allowRestoreOther = $ctx->settings->allowRestoreToOtherDatabase();
|
||
$restoreMode = strtolower($ctx->postString('restore_mode', $ctx->queryString('restore_mode', 'overwrite')));
|
||
if (!in_array($restoreMode, ['overwrite', 'new_db'], true)) {
|
||
$restoreMode = 'overwrite';
|
||
}
|
||
if (!$allowRestoreOther) {
|
||
$restoreMode = 'overwrite';
|
||
}
|
||
$restoreTargetDb = trim($ctx->postString('target_db', $ctx->queryString('target_db', '')));
|
||
if ($restoreTargetDb !== '' && strpos($restoreTargetDb, $prefix) !== 0) {
|
||
$restoreTargetDb = $prefix . $restoreTargetDb;
|
||
}
|
||
|
||
$availableDbs = [];
|
||
try {
|
||
$availableDbs = array_map(static fn(array $db): string => (string)$db['name'], $ctx->mysql->listDatabasesForUser($daUser, false));
|
||
} catch (Throwable $e) {
|
||
$errors[] = $ctx->formatException($e);
|
||
}
|
||
if ($restoreTargetDb !== '' && !in_array($restoreTargetDb, $availableDbs, true)) {
|
||
$restoreTargetDb = '';
|
||
}
|
||
if ($restoreTargetDb === '' && !empty($availableDbs)) {
|
||
$restoreTargetDb = $availableDbs[0];
|
||
}
|
||
|
||
$renderTargetRadios = function (array $dbs, string $selected, string $name, array $exclude = [], bool $required = false, bool $disabled = false, string $idPrefix = '', string $formId = '') use ($et): string {
|
||
$exclude = array_fill_keys($exclude, true);
|
||
$filtered = [];
|
||
foreach ($dbs as $dbName) {
|
||
if (isset($exclude[$dbName])) {
|
||
continue;
|
||
}
|
||
$filtered[] = $dbName;
|
||
}
|
||
|
||
if (empty($filtered)) {
|
||
return '<p class="muted">' . $et('Brak baz danych do wyboru.') . '</p>';
|
||
}
|
||
|
||
if ($selected === '' || !in_array($selected, $filtered, true)) {
|
||
$selected = $filtered[0] ?? '';
|
||
}
|
||
|
||
$safePrefix = preg_replace('/[^A-Za-z0-9_-]+/', '_', $idPrefix) ?? '';
|
||
$items = '<div class="db-choice-grid">';
|
||
foreach ($filtered as $idx => $dbName) {
|
||
$id = 'restore-target-' . $safePrefix . '-' . $idx;
|
||
$checked = ($dbName === $selected) ? ' checked' : '';
|
||
$req = ($required && $idx === 0) ? ' required' : '';
|
||
$dis = $disabled ? ' disabled' : '';
|
||
$formAttr = $formId !== '' ? ' form="' . AppContext::e($formId) . '"' : '';
|
||
$items .= '<label class="db-choice-card" for="' . AppContext::e($id) . '">';
|
||
$items .= '<input type="radio" name="' . AppContext::e($name) . '" id="' . AppContext::e($id) . '" value="' . AppContext::e($dbName) . '"' . $checked . $req . $dis . $formAttr . '>';
|
||
$items .= '<span>' . AppContext::e($dbName) . '</span></label>';
|
||
}
|
||
$items .= '</div>';
|
||
return $items;
|
||
};
|
||
|
||
$ensureDbExists = static function (AppContext $ctx, string $dbName): void {
|
||
try {
|
||
$ctx->mysql->getDatabaseInfo($dbName);
|
||
} catch (Throwable $e) {
|
||
throw new RuntimeException('Wskazana docelowa baza danych nie istnieje. Utwórz ją ręcznie w DirectAdmin.');
|
||
}
|
||
};
|
||
|
||
$formatBackupLabel = static function (array $entry): string {
|
||
$date = (string)($entry['date'] ?? '');
|
||
$time = (string)($entry['time'] ?? '');
|
||
$raw = trim($date . ' ' . $time);
|
||
if ($raw === '') {
|
||
return '-';
|
||
}
|
||
$ts = strtotime($raw);
|
||
if ($ts === false) {
|
||
return $raw;
|
||
}
|
||
return date('d-m-Y H:i:s', $ts);
|
||
};
|
||
|
||
$displayFileName = static function (string $value): string {
|
||
$name = basename(trim($value));
|
||
if ($name === '') {
|
||
return '';
|
||
}
|
||
if (preg_match('/^stream-[^_]+_(.+)$/', $name, $m) === 1) {
|
||
$name = $m[1];
|
||
}
|
||
if (preg_match('/^[A-Za-z]+-[0-9]{8,}-[0-9a-f]{6,}_(.+)$/', $name, $m) === 1) {
|
||
$name = $m[1];
|
||
}
|
||
return $name;
|
||
};
|
||
|
||
$renderHiddenInputs = static function (array $fields): string {
|
||
$out = '';
|
||
foreach ($fields as $name => $value) {
|
||
if (!is_string($name)) {
|
||
continue;
|
||
}
|
||
if (is_array($value)) {
|
||
foreach ($value as $item) {
|
||
if (is_array($item) || is_object($item)) {
|
||
continue;
|
||
}
|
||
$out .= '<input type="hidden" name="' . AppContext::e($name) . '[]" value="' . AppContext::e((string)$item) . '">';
|
||
}
|
||
continue;
|
||
}
|
||
if (is_object($value)) {
|
||
continue;
|
||
}
|
||
$out .= '<input type="hidden" name="' . AppContext::e($name) . '" value="' . AppContext::e((string)$value) . '">';
|
||
}
|
||
return $out;
|
||
};
|
||
|
||
$renderConfirmOverlay = static function (string $bodyHtml, array $fields) use ($ctx, $et, $renderHiddenInputs): string {
|
||
$hidden = $renderHiddenInputs($fields);
|
||
$html = '';
|
||
$html .= '<div class="br-overlay">';
|
||
$html .= '<div class="br-modal br-modal-danger">';
|
||
$html .= '<h3>' . $et('Potwierdzenie operacji') . '</h3>';
|
||
$html .= $bodyHtml;
|
||
$html .= '<form method="post">';
|
||
$html .= $ctx->csrfField('upload_backup_submit');
|
||
$html .= $hidden;
|
||
$html .= '<input type="hidden" name="confirm_action" value="1">';
|
||
$html .= '<div class="br-modal-actions">';
|
||
$html .= '<a class="br-btn br-btn-ghost" href="' . AppContext::e($ctx->url('upload_backup.html', ['tab' => $fields['active_tab'] ?? 'local'])) . '">' . $et('Nie') . '</a>';
|
||
$html .= '<button class="br-btn br-btn-danger" type="submit">' . $et('Tak, kontynuuj') . '</button>';
|
||
$html .= '</div>';
|
||
$html .= '</form>';
|
||
$html .= '</div></div>';
|
||
return $html;
|
||
};
|
||
|
||
$confirmOverlay = '';
|
||
|
||
$findUserBackupEntry = static function (BackupQueueService $queue, string $relPath): ?array {
|
||
$relPath = ltrim(trim($relPath), '/');
|
||
if ($relPath === '') {
|
||
return null;
|
||
}
|
||
$all = $queue->listUserBackupsByDate(800);
|
||
foreach ($all as $entries) {
|
||
foreach ($entries as $entry) {
|
||
if (!is_array($entry)) {
|
||
continue;
|
||
}
|
||
if (($entry['rel_path'] ?? '') === $relPath) {
|
||
return $entry;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
$streamUserBackup = static function (string $path): void {
|
||
$downloadName = basename($path);
|
||
$downloadName = preg_replace('/[^A-Za-z0-9._-]+/', '_', $downloadName) ?? 'backup.sql';
|
||
$contentType = 'application/octet-stream';
|
||
if (preg_match('/\.sql$/i', $downloadName)) {
|
||
$contentType = 'application/sql';
|
||
} elseif (preg_match('/\.gz$/i', $downloadName)) {
|
||
$contentType = 'application/gzip';
|
||
}
|
||
|
||
clearstatcache(true, $path);
|
||
$contentLength = @filesize($path);
|
||
|
||
while (ob_get_level() > 0) {
|
||
@ob_end_clean();
|
||
}
|
||
if (function_exists('apache_setenv')) {
|
||
@apache_setenv('no-gzip', '1');
|
||
}
|
||
@ini_set('zlib.output_compression', '0');
|
||
@ini_set('output_buffering', '0');
|
||
|
||
header_remove('Content-Encoding');
|
||
header('Content-Type: ' . $contentType);
|
||
header('Content-Disposition: attachment; filename="' . str_replace('"', '', $downloadName) . '"');
|
||
header('X-Content-Type-Options: nosniff');
|
||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||
header('Pragma: no-cache');
|
||
if (is_int($contentLength) && $contentLength > 0) {
|
||
header('Content-Length: ' . (string)$contentLength);
|
||
}
|
||
|
||
@readfile($path);
|
||
exit;
|
||
};
|
||
|
||
if ($ctx->isPost()) {
|
||
try {
|
||
$ctx->requireCsrf('upload_backup_submit');
|
||
$action = $ctx->postString('form_action');
|
||
$restoreMode = strtolower($ctx->postString('restore_mode', $restoreMode));
|
||
if (!in_array($restoreMode, ['overwrite', 'new_db'], true)) {
|
||
$restoreMode = 'overwrite';
|
||
}
|
||
if (!$allowRestoreOther) {
|
||
$restoreMode = 'overwrite';
|
||
}
|
||
|
||
if ($action === 'restore_local') {
|
||
$backupId = $ctx->postString('local_backup');
|
||
$entry = $queue->getLocalBackupEntry($backupId);
|
||
$sourceDb = (string)($entry['db_name'] ?? '');
|
||
$backupLabel = $formatBackupLabel($entry);
|
||
$confirmAction = $ctx->postString('confirm_action') === '1';
|
||
|
||
$targetDb = $sourceDb;
|
||
if ($restoreMode === 'new_db') {
|
||
$targetDb = NamePolicy::normalizeDatabaseName($ctx->postString('target_db'), $prefix);
|
||
if ($sourceDb !== '' && $targetDb === $sourceDb) {
|
||
throw new RuntimeException('Docelowa baza danych musi być inna niż źródłowa baza z backupu.');
|
||
}
|
||
$ensureDbExists($ctx, $targetDb);
|
||
}
|
||
|
||
if (!$confirmAction) {
|
||
$sourceHtml = '<strong>' . AppContext::e($sourceDb !== '' ? $sourceDb : $targetDb) . '</strong>';
|
||
$targetHtml = '<strong>' . AppContext::e($targetDb !== '' ? $targetDb : $sourceDb) . '</strong>';
|
||
$targetHtmlDanger = '<strong class="br-text-danger">' . AppContext::e($targetDb !== '' ? $targetDb : $sourceDb) . '</strong>';
|
||
$dateHtml = '<strong>' . AppContext::e($backupLabel) . '</strong>';
|
||
$overwriteHtml = '<strong class="br-text-danger">' . $et('nadpisując obecne dane') . '</strong>';
|
||
|
||
if ($restoreMode === 'new_db') {
|
||
$line1 = $ctx->t(
|
||
'Czy na pewno chcesz przywrócić dane z kopii zapasowej bazy danych {source} z dn. {date} do innej bazy danych o nazwie {target}?',
|
||
['source' => $sourceHtml, 'date' => $dateHtml, 'target' => $targetHtmlDanger]
|
||
);
|
||
$line2 = $ctx->t(
|
||
'Uwaga: dane znajdujące się obecnie w bazie danych {target} zostaną usunięte i nadpisane danymi z kopii zapasowej dla bazy {source}.',
|
||
['target' => $targetHtmlDanger, 'source' => $sourceHtml]
|
||
);
|
||
$confirmBody = '<p>' . $line1 . '</p><p>' . $line2 . '</p>';
|
||
} else {
|
||
$confirmBody = '<p>' . $ctx->t(
|
||
'Czy na pewno chcesz przywrócić kopię zapasową bazy danych {source} z dn. {date} {overwrite} znajdujące się w bazie danych {target}?',
|
||
['source' => $sourceHtml, 'date' => $dateHtml, 'overwrite' => $overwriteHtml, 'target' => $targetHtml]
|
||
) . '</p>';
|
||
}
|
||
|
||
$confirmOverlay = $renderConfirmOverlay($confirmBody, [
|
||
'active_tab' => 'local',
|
||
'form_action' => 'restore_local',
|
||
'local_backup' => $backupId,
|
||
'restore_mode' => $restoreMode,
|
||
'target_db' => $targetDb,
|
||
]);
|
||
$activeTab = 'local';
|
||
} else {
|
||
if ($restoreMode === 'new_db') {
|
||
$jobId = $queue->queueRestoreFromLocal($backupId, $restoreMode, $targetDb);
|
||
} else {
|
||
$jobId = $queue->queueRestoreFromLocal($backupId, $restoreMode, '');
|
||
}
|
||
$ok[] = $tr('Dodano zadanie przywracania backupu lokalnego (ID: {id}).', ['id' => $jobId]);
|
||
$activeTab = 'local';
|
||
}
|
||
} elseif ($action === 'restore_file') {
|
||
$targetDb = NamePolicy::normalizeDatabaseName($ctx->postString('target_db'), $prefix);
|
||
$ensureDbExists($ctx, $targetDb);
|
||
$confirmAction = $ctx->postString('confirm_action') === '1';
|
||
$sourceMode = strtolower($ctx->postString('source_mode', 'local'));
|
||
if (!in_array($sourceMode, ['local', 'server'], true)) {
|
||
$sourceMode = 'local';
|
||
}
|
||
$stagedPath = trim($ctx->postString('uploaded_tmp_path'));
|
||
$uploadedOriginalName = trim($ctx->postString('uploaded_original_name'));
|
||
$localUploadField = trim($ctx->postString('source_file_upload'));
|
||
if ($localUploadField === '') {
|
||
$rawUploadField = $_POST['source_file_upload'] ?? null;
|
||
if (is_array($rawUploadField) && isset($rawUploadField[0])) {
|
||
$localUploadField = trim((string)$rawUploadField[0]);
|
||
} elseif (!is_array($rawUploadField) && $rawUploadField !== null) {
|
||
$localUploadField = trim((string)$rawUploadField);
|
||
}
|
||
}
|
||
$serverPath = trim($sourceFilePathValue);
|
||
if ($sourceMode === 'server' && $serverPath === '') {
|
||
throw new RuntimeException('Wskaż ścieżkę do pliku SQL lub SQL.GZ.');
|
||
}
|
||
if ($sourceMode === 'local' && $stagedPath === '') {
|
||
$uploadFile = $_FILES['source_file_upload'] ?? null;
|
||
if (is_array($uploadFile) && !empty($uploadFile) && (int)($uploadFile['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
|
||
$stagedPath = $queue->stageRestoreUploadFromPhpUpload($uploadFile, $uploadedOriginalName);
|
||
if ($uploadedOriginalName === '') {
|
||
$uploadedOriginalName = basename((string)($uploadFile['name'] ?? ''));
|
||
}
|
||
} elseif ($localUploadField !== '') {
|
||
$stagedPath = $queue->stageRestoreUploadFromDirectAdminTemp($localUploadField, $uploadedOriginalName);
|
||
} else {
|
||
throw new RuntimeException('Nie udało się przygotować pliku do przywrócenia. Wybierz plik ponownie.');
|
||
}
|
||
}
|
||
if ($sourceMode === 'local' && $stagedPath === '') {
|
||
throw new RuntimeException('Nie udało się przygotować pliku do przywrócenia. Wybierz plik ponownie.');
|
||
}
|
||
|
||
$fileLabel = '';
|
||
if ($sourceMode === 'local') {
|
||
if ($uploadedOriginalName !== '') {
|
||
$fileLabel = $displayFileName($uploadedOriginalName);
|
||
}
|
||
if ($fileLabel === '' && $stagedPath !== '') {
|
||
$fileLabel = $displayFileName($stagedPath);
|
||
}
|
||
} else {
|
||
$fileLabel = $displayFileName($serverPath);
|
||
}
|
||
if ($fileLabel === '') {
|
||
$fileLabel = 'backup.sql.gz';
|
||
}
|
||
// Bezpieczeństwo: dla restore z pliku nie wykrywamy i nie ufamy nazwie bazy z nazwy pliku.
|
||
// Import zawsze ma trafić do bazy docelowej wskazanej przez użytkownika.
|
||
$sourceDbName = '';
|
||
$postedRestoreMode = strtolower(trim($ctx->postString('restore_mode')));
|
||
$restoreModeFile = in_array($postedRestoreMode, ['overwrite', 'new_db'], true) ? $postedRestoreMode : 'overwrite';
|
||
if (!$allowRestoreOther) {
|
||
$restoreModeFile = 'overwrite';
|
||
}
|
||
|
||
@error_log(sprintf(
|
||
"[%s] RESTORE_FILE action=%s user=%s source_mode=%s has_files=%s local_field=%s source_path=%s uploaded_tmp=%s uploaded_name=%s source_db=%s target_db=%s restore_mode=%s confirm=%s\n",
|
||
date('c'),
|
||
$ctx->action(),
|
||
$daUser,
|
||
$sourceMode,
|
||
empty($_FILES) ? '0' : '1',
|
||
$localUploadField,
|
||
$serverPath,
|
||
$stagedPath,
|
||
$uploadedOriginalName,
|
||
$sourceDbName,
|
||
$targetDb,
|
||
$restoreModeFile,
|
||
$confirmAction ? '1' : '0'
|
||
), 3, PLUGIN_ROOT . '/error.log');
|
||
|
||
if (!$confirmAction) {
|
||
$fileHtml = '<strong>' . AppContext::e($fileLabel) . '</strong>';
|
||
$targetHtmlDanger = '<strong class="br-text-danger">' . AppContext::e($targetDb) . '</strong>';
|
||
if ($restoreModeFile === 'new_db' && $sourceDbName !== '') {
|
||
$sourceHtml = '<strong>' . AppContext::e($sourceDbName) . '</strong>';
|
||
$line1 = $ctx->t(
|
||
'Czy na pewno chcesz przywrócić dane z kopii zapasowej bazy danych {source} z pliku {file} do innej bazy danych o nazwie {target}?',
|
||
['source' => $sourceHtml, 'file' => $fileHtml, 'target' => $targetHtmlDanger]
|
||
);
|
||
$line2 = $ctx->t(
|
||
'Uwaga: dane znajdujące się obecnie w bazie danych {target} zostaną usunięte i nadpisane danymi z kopii zapasowej dla bazy {source}.',
|
||
['target' => $targetHtmlDanger, 'source' => $sourceHtml]
|
||
);
|
||
} else {
|
||
$line1 = $ctx->t(
|
||
'Czy na pewno chcesz przywrócić plik kopii zapasowej {file} do bazy danych {target}?',
|
||
['file' => $fileHtml, 'target' => $targetHtmlDanger]
|
||
);
|
||
$line2 = $ctx->t(
|
||
'Uwaga: jeśli baza danych {target} zawiera dane, zostaną one nadpisane.',
|
||
['target' => $targetHtmlDanger]
|
||
);
|
||
}
|
||
$confirmOverlay = $renderConfirmOverlay('<p>' . $line1 . '</p><p>' . $line2 . '</p>', [
|
||
'active_tab' => 'file',
|
||
'form_action' => 'restore_file',
|
||
'source_mode' => $sourceMode,
|
||
'source_file_path' => $serverPath,
|
||
'uploaded_tmp_path' => $stagedPath,
|
||
'uploaded_original_name' => ($uploadedOriginalName !== '' ? $uploadedOriginalName : $fileLabel),
|
||
'source_db_name' => $sourceDbName,
|
||
'restore_mode' => $restoreModeFile,
|
||
'target_db' => $targetDb,
|
||
]);
|
||
$activeTab = 'file';
|
||
} else {
|
||
if ($sourceMode === 'local') {
|
||
$jobId = $queue->queueRestoreFromFilePath($targetDb, $stagedPath, $restoreModeFile, $sourceDbName);
|
||
} else {
|
||
$filePath = $serverPath;
|
||
$jobId = $queue->queueRestoreFromFilePath($targetDb, $filePath, $restoreModeFile, $sourceDbName);
|
||
}
|
||
$ok[] = $tr('Zadanie przywrócenia kopii zapasowej {file} do bazy danych {db} zostało dodane do kolejki', [
|
||
'file' => $fileLabel,
|
||
'db' => $targetDb,
|
||
]);
|
||
$activeTab = 'file';
|
||
}
|
||
} elseif ($action === 'download_user_backup') {
|
||
$jobId = trim($ctx->postString('backup_job_id'));
|
||
$backupPath = trim($ctx->postString('backup_path'));
|
||
$path = '';
|
||
if ($jobId !== '') {
|
||
$path = $queue->resolveBackupTargetFileForCurrentUser($jobId);
|
||
}
|
||
if ($path === '' && $backupPath !== '') {
|
||
$path = $queue->resolveUserBackupFileForCurrentUser($backupPath);
|
||
}
|
||
if ($path === '') {
|
||
throw new RuntimeException('Nie można odnaleźć pliku backupu.');
|
||
}
|
||
$streamUserBackup($path);
|
||
} elseif ($action === 'delete_user_backup') {
|
||
$jobId = trim($ctx->postString('backup_job_id'));
|
||
$backupPath = trim($ctx->postString('backup_path'));
|
||
if ($jobId === '' && $backupPath === '') {
|
||
throw new RuntimeException('Brak identyfikatora zadania backupu.');
|
||
}
|
||
$confirmAction = $ctx->postString('confirm_action') === '1';
|
||
|
||
if (!$confirmAction) {
|
||
$dbLabel = 'nieznana baza';
|
||
$dateLabel = '-';
|
||
if ($jobId !== '') {
|
||
$job = $queue->getJobForCurrentUser($jobId);
|
||
if (($job['type'] ?? '') !== 'backup') {
|
||
throw new RuntimeException('Wskazane zadanie nie jest zadaniem backupu.');
|
||
}
|
||
$dbLabel = (string)($job['db_name'] ?? '') !== '' ? (string)$job['db_name'] : $dbLabel;
|
||
$startTs = (int)($job['start_ts'] ?? 0);
|
||
$dateLabel = $startTs > 0 ? date('Y-m-d H:i:s', $startTs) : '-';
|
||
} elseif ($backupPath !== '') {
|
||
$entry = $findUserBackupEntry($queue, $backupPath);
|
||
if ($entry !== null) {
|
||
$dbLabel = (string)($entry['db_name'] ?? '') !== '' ? (string)$entry['db_name'] : $dbLabel;
|
||
$ts = (int)($entry['mtime'] ?? 0);
|
||
$dateLabel = $ts > 0 ? date('Y-m-d H:i:s', $ts) : $dateLabel;
|
||
}
|
||
}
|
||
$confirmBody = '<p>' . $ctx->t(
|
||
'Czy na pewno chcesz usunąć backup bazy danych {db} z dn. {date}?',
|
||
['db' => '<strong>' . AppContext::e($dbLabel) . '</strong>', 'date' => '<strong>' . AppContext::e($dateLabel) . '</strong>']
|
||
) . '</p>';
|
||
$confirmOverlay = $renderConfirmOverlay($confirmBody, [
|
||
'active_tab' => 'user',
|
||
'form_action' => 'delete_user_backup',
|
||
'backup_job_id' => $jobId,
|
||
'backup_path' => $backupPath,
|
||
'user_month' => $ctx->postString('user_month', $ctx->queryString('user_month')),
|
||
'user_date' => $ctx->postString('user_date', $ctx->queryString('user_date')),
|
||
'user_slot' => $ctx->postString('user_slot', $ctx->queryString('user_slot')),
|
||
]);
|
||
$activeTab = 'user';
|
||
} else {
|
||
if ($jobId !== '') {
|
||
$deletedPath = $queue->deleteBackupTargetFileForCurrentUser($jobId);
|
||
try {
|
||
$queue->deleteBackupLogForCurrentUser($jobId);
|
||
} catch (Throwable $logErr) {
|
||
// ignore log cleanup failure
|
||
}
|
||
} else {
|
||
$deletedPath = $queue->deleteUserBackupFileForCurrentUser($backupPath);
|
||
}
|
||
$ok[] = $tr('Usunięto plik backupu: {path}', ['path' => $deletedPath]);
|
||
$activeTab = 'user';
|
||
}
|
||
}
|
||
} catch (Throwable $e) {
|
||
$errors[] = $ctx->formatException($e);
|
||
}
|
||
}
|
||
|
||
$backupsByDate = $queue->listLocalBackupsByDate();
|
||
$availableDates = array_keys($backupsByDate);
|
||
$availableMonths = [];
|
||
foreach ($availableDates as $dateKey) {
|
||
if (strlen($dateKey) >= 7) {
|
||
$availableMonths[substr($dateKey, 0, 7)] = true;
|
||
}
|
||
}
|
||
$availableMonths = array_keys($availableMonths);
|
||
rsort($availableMonths, SORT_STRING);
|
||
|
||
$selectedMonth = trim($ctx->postString('backup_month', $ctx->queryString('month', '')));
|
||
if ($selectedMonth === '' || (!empty($availableMonths) && !in_array($selectedMonth, $availableMonths, true))) {
|
||
$selectedMonth = $availableMonths[0] ?? date('Y-m');
|
||
}
|
||
|
||
$selectedDate = trim($ctx->postString('backup_date', $ctx->queryString('backup_date')));
|
||
$today = date('Y-m-d');
|
||
$monthRequested = ($ctx->postString('backup_month', '') !== '' || $ctx->queryString('month', '') !== '');
|
||
$dateRequested = ($ctx->postString('backup_date', '') !== '' || $ctx->queryString('backup_date', '') !== '');
|
||
if (!$monthRequested && !$dateRequested && isset($backupsByDate[$today])) {
|
||
$selectedMonth = substr($today, 0, 7);
|
||
$selectedDate = $today;
|
||
}
|
||
|
||
if ($selectedDate === '' || !isset($backupsByDate[$selectedDate]) || substr($selectedDate, 0, 7) !== $selectedMonth) {
|
||
$selectedDate = '';
|
||
foreach ($availableDates as $dayKey) {
|
||
if (substr($dayKey, 0, 7) === $selectedMonth) {
|
||
$selectedDate = $dayKey;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
$selectedEntries = ($selectedDate !== '' && isset($backupsByDate[$selectedDate])) ? $backupsByDate[$selectedDate] : [];
|
||
$selectedSlot = trim($ctx->postString('backup_slot', $ctx->queryString('backup_slot', '')));
|
||
$timeSlots = [];
|
||
foreach ($selectedEntries as $entry) {
|
||
$timeLabel = trim((string)($entry['time'] ?? ''));
|
||
$mtime = (int)($entry['mtime'] ?? 0);
|
||
if ($timeLabel === '' && $mtime > 0) {
|
||
$timeLabel = date('H:i:s', $mtime);
|
||
}
|
||
if ($timeLabel === '') {
|
||
$timeLabel = '00:00:00';
|
||
}
|
||
if (!isset($timeSlots[$timeLabel])) {
|
||
$timeSlots[$timeLabel] = [];
|
||
}
|
||
$timeSlots[$timeLabel][] = $entry;
|
||
}
|
||
$hasMultipleSlots = count($timeSlots) > 1;
|
||
if ($hasMultipleSlots) {
|
||
if ($selectedSlot === '' || !isset($timeSlots[$selectedSlot])) {
|
||
$slotKeys = array_keys($timeSlots);
|
||
$selectedSlot = $slotKeys[0] ?? '';
|
||
}
|
||
} else {
|
||
$selectedSlot = '';
|
||
}
|
||
$entriesToShow = $hasMultipleSlots && $selectedSlot !== '' ? ($timeSlots[$selectedSlot] ?? []) : $selectedEntries;
|
||
|
||
$formatMonthLabel = static function (string $monthValue, string $langCode): string {
|
||
if (!preg_match('/^(\\d{4})-(\\d{2})$/', $monthValue, $m)) {
|
||
return $monthValue;
|
||
}
|
||
$year = $m[1];
|
||
$month = (int)$m[2];
|
||
$monthsPl = [
|
||
1 => 'styczeń', 2 => 'luty', 3 => 'marzec', 4 => 'kwiecień',
|
||
5 => 'maj', 6 => 'czerwiec', 7 => 'lipiec', 8 => 'sierpień',
|
||
9 => 'wrzesień', 10 => 'październik', 11 => 'listopad', 12 => 'grudzień',
|
||
];
|
||
$monthsEn = [
|
||
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April',
|
||
5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August',
|
||
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December',
|
||
];
|
||
$list = ($langCode === 'pl') ? $monthsPl : $monthsEn;
|
||
$name = $list[$month] ?? $monthValue;
|
||
return $name . ' ' . $year;
|
||
};
|
||
|
||
$formatFullDate = static function (string $dateValue, string $langCode): string {
|
||
if (!preg_match('/^(\\d{4})-(\\d{2})-(\\d{2})$/', $dateValue, $m)) {
|
||
return $dateValue;
|
||
}
|
||
$year = (int)$m[1];
|
||
$month = (int)$m[2];
|
||
$day = (int)$m[3];
|
||
$monthsPl = [
|
||
1 => 'stycznia', 2 => 'lutego', 3 => 'marca', 4 => 'kwietnia',
|
||
5 => 'maja', 6 => 'czerwca', 7 => 'lipca', 8 => 'sierpnia',
|
||
9 => 'września', 10 => 'października', 11 => 'listopada', 12 => 'grudnia',
|
||
];
|
||
if ($langCode === 'pl') {
|
||
$monthName = $monthsPl[$month] ?? $m[2];
|
||
return $day . ' ' . $monthName . ' ' . $year;
|
||
}
|
||
$dt = DateTime::createFromFormat('Y-m-d', $dateValue);
|
||
return $dt instanceof DateTime ? $dt->format('F j, Y') : $dateValue;
|
||
};
|
||
|
||
$formatTimeLabel = static function (string $timeValue): string {
|
||
if (preg_match('/^(\\d{2}:\\d{2})/', $timeValue, $m)) {
|
||
return $m[1];
|
||
}
|
||
return $timeValue;
|
||
};
|
||
|
||
$monthNavTargets = static function (array $availableMonths, string $selectedMonth): array {
|
||
$prev = '';
|
||
$next = '';
|
||
$idx = array_search($selectedMonth, $availableMonths, true);
|
||
if ($idx !== false) {
|
||
if ($idx < count($availableMonths) - 1) {
|
||
$prev = $availableMonths[$idx + 1];
|
||
}
|
||
if ($idx > 0) {
|
||
$next = $availableMonths[$idx - 1];
|
||
}
|
||
}
|
||
return ['prev' => $prev, 'next' => $next];
|
||
};
|
||
|
||
$calendarWeekdays = static function (string $langCode): array {
|
||
if ($langCode === 'pl') {
|
||
return ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd'];
|
||
}
|
||
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||
};
|
||
|
||
$langCode = $ctx->daUser->language();
|
||
$selectedDateLabel = $selectedDate !== '' ? $formatFullDate($selectedDate, $langCode) : '';
|
||
|
||
$userBackupsByDate = $queue->listUserBackupsByDate(500);
|
||
$userAvailableDates = array_keys($userBackupsByDate);
|
||
$userAvailableMonths = [];
|
||
foreach ($userAvailableDates as $dateKey) {
|
||
if (strlen($dateKey) >= 7) {
|
||
$userAvailableMonths[substr($dateKey, 0, 7)] = true;
|
||
}
|
||
}
|
||
$userAvailableMonths = array_keys($userAvailableMonths);
|
||
rsort($userAvailableMonths, SORT_STRING);
|
||
|
||
$userSelectedMonth = trim($ctx->postString('user_month', $ctx->queryString('user_month', '')));
|
||
if ($userSelectedMonth === '' || (!empty($userAvailableMonths) && !in_array($userSelectedMonth, $userAvailableMonths, true))) {
|
||
$userSelectedMonth = $userAvailableMonths[0] ?? date('Y-m');
|
||
}
|
||
|
||
$userSelectedDate = trim($ctx->postString('user_date', $ctx->queryString('user_date')));
|
||
$userMonthRequested = ($ctx->postString('user_month', '') !== '' || $ctx->queryString('user_month', '') !== '');
|
||
$userDateRequested = ($ctx->postString('user_date', '') !== '' || $ctx->queryString('user_date', '') !== '');
|
||
if (!$userMonthRequested && !$userDateRequested && isset($userBackupsByDate[$today])) {
|
||
$userSelectedMonth = substr($today, 0, 7);
|
||
$userSelectedDate = $today;
|
||
}
|
||
|
||
if ($userSelectedDate === '' || !isset($userBackupsByDate[$userSelectedDate]) || substr($userSelectedDate, 0, 7) !== $userSelectedMonth) {
|
||
$userSelectedDate = '';
|
||
foreach ($userAvailableDates as $dayKey) {
|
||
if (substr($dayKey, 0, 7) === $userSelectedMonth) {
|
||
$userSelectedDate = $dayKey;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
$userSelectedEntries = ($userSelectedDate !== '' && isset($userBackupsByDate[$userSelectedDate])) ? $userBackupsByDate[$userSelectedDate] : [];
|
||
$userSelectedSlot = trim($ctx->postString('user_slot', $ctx->queryString('user_slot', '')));
|
||
$userTimeSlots = [];
|
||
foreach ($userSelectedEntries as $entry) {
|
||
$timeLabel = trim((string)($entry['time'] ?? ''));
|
||
$mtime = (int)($entry['mtime'] ?? 0);
|
||
if ($timeLabel === '' && $mtime > 0) {
|
||
$timeLabel = date('H:i:s', $mtime);
|
||
}
|
||
if ($timeLabel === '') {
|
||
$timeLabel = '00:00:00';
|
||
}
|
||
if (!isset($userTimeSlots[$timeLabel])) {
|
||
$userTimeSlots[$timeLabel] = [];
|
||
}
|
||
$userTimeSlots[$timeLabel][] = $entry;
|
||
}
|
||
$userHasMultipleSlots = count($userTimeSlots) > 1;
|
||
if ($userHasMultipleSlots) {
|
||
if ($userSelectedSlot === '' || !isset($userTimeSlots[$userSelectedSlot])) {
|
||
$slotKeys = array_keys($userTimeSlots);
|
||
$userSelectedSlot = $slotKeys[0] ?? '';
|
||
}
|
||
} else {
|
||
$userSelectedSlot = '';
|
||
}
|
||
$userEntriesToShow = $userHasMultipleSlots && $userSelectedSlot !== '' ? ($userTimeSlots[$userSelectedSlot] ?? []) : $userSelectedEntries;
|
||
$userSelectedDateLabel = $userSelectedDate !== '' ? $formatFullDate($userSelectedDate, $langCode) : '';
|
||
|
||
$jobs = $queue->listJobsForCurrentUser(500);
|
||
|
||
$tabs = '';
|
||
$tabs .= '<div class="actions" style="margin-bottom:10px">';
|
||
$tabs .= '<a class="btn' . ($activeTab === 'local' ? ' amber' : '') . '" href="' . AppContext::e($ctx->url('upload_backup.html', ['tab' => 'local', 'restore_mode' => $restoreMode, 'target_db' => $restoreTargetDb])) . '">' . $et('Backupy HITME.PL') . '</a>';
|
||
$tabs .= '<a class="btn' . ($activeTab === 'user' ? ' amber' : '') . '" href="' . AppContext::e($ctx->url('upload_backup.html', ['tab' => 'user'])) . '">' . $et('Backupy użytkownika') . '</a>';
|
||
$tabs .= '<a class="btn' . ($activeTab === 'file' ? ' amber' : '') . '" href="' . AppContext::e($ctx->url('upload_backup.html', ['tab' => 'file', 'restore_mode' => $restoreMode, 'target_db' => $restoreTargetDb])) . '">' . $et('Backupy z pliku') . '</a>';
|
||
$tabs .= '</div>';
|
||
|
||
$restoreModeSection = '';
|
||
if ($activeTab === 'local' && $allowRestoreOther) {
|
||
$restoreModeSection .= '<section class="panel" style="margin-bottom:14px">';
|
||
$restoreModeSection .= '<h3>' . $et('Tryb przywracania') . '</h3>';
|
||
$restoreModeSection .= '<div class="actions" id="restore-mode-group">';
|
||
$restoreModeSection .= '<label><input type="radio" name="restore_mode_global" value="overwrite"' . ($restoreMode === 'overwrite' ? ' checked' : '') . '> ' . $et('Nadpisanie istniejącej bazy danych (obecne działanie)') . '</label>';
|
||
$restoreModeSection .= '<label><input type="radio" name="restore_mode_global" value="new_db"' . ($restoreMode === 'new_db' ? ' checked' : '') . '> ' . $et('Przywrócenie wskazanej bazy do nowej bazy danych') . '</label>';
|
||
$restoreModeSection .= '</div>';
|
||
$restoreModeSection .= '</section>';
|
||
}
|
||
|
||
$renderRestorePicker = function () use (
|
||
$ctx,
|
||
$selectedEntries,
|
||
$entriesToShow,
|
||
$hasMultipleSlots,
|
||
$timeSlots,
|
||
$selectedSlot,
|
||
$selectedDateLabel,
|
||
$selectedDate,
|
||
$selectedMonth,
|
||
$restoreMode,
|
||
$allowRestoreOther,
|
||
$availableDbs,
|
||
$restoreTargetDb,
|
||
$renderTargetRadios,
|
||
$formatTimeLabel,
|
||
$et
|
||
): string {
|
||
$html = '';
|
||
if (empty($selectedEntries)) {
|
||
$html .= '<p class="muted">' . $et('Brak backupów HITME.PL dla wybranej daty.') . '</p>';
|
||
return $html;
|
||
}
|
||
|
||
if ($selectedDateLabel !== '') {
|
||
$html .= '<div class="br-date-label">' . $et('Data:') . ' ' . AppContext::e($selectedDateLabel) . '</div>';
|
||
}
|
||
|
||
if ($hasMultipleSlots) {
|
||
$html .= '<form method="get" class="br-time-picker">';
|
||
$html .= '<input type="hidden" name="tab" value="local">';
|
||
$html .= '<input type="hidden" name="month" value="' . AppContext::e($selectedMonth) . '">';
|
||
$html .= '<input type="hidden" name="backup_date" value="' . AppContext::e($selectedDate) . '">';
|
||
$html .= '<input type="hidden" name="restore_mode" value="' . AppContext::e($restoreMode) . '">';
|
||
$html .= '<div class="br-time-title">' . $et('Wybierz godzinę backupu') . '</div>';
|
||
$html .= '<div class="br-time-list">';
|
||
foreach ($timeSlots as $slotKey => $_entries) {
|
||
$timeLabel = $formatTimeLabel($slotKey);
|
||
$checked = ($slotKey === $selectedSlot) ? ' checked' : '';
|
||
$html .= '<label class="br-time-item">';
|
||
$html .= '<input type="radio" name="backup_slot" value="' . AppContext::e($slotKey) . '"' . $checked . '>';
|
||
$html .= '<span>' . AppContext::e($timeLabel) . '</span>';
|
||
$html .= '</label>';
|
||
}
|
||
$html .= '</div></form>';
|
||
}
|
||
|
||
$showTargetColumn = $allowRestoreOther;
|
||
$rows = '';
|
||
foreach ($entriesToShow as $entry) {
|
||
$sourceDb = (string)($entry['source_db'] ?? '');
|
||
$formId = 'restore-local-' . preg_replace('/[^A-Za-z0-9_-]+/', '_', (string)$entry['id']);
|
||
$timeLabel = $formatTimeLabel(trim((string)($entry['time'] ?? '')));
|
||
$dateLabel = $selectedDateLabel !== '' ? $selectedDateLabel : (string)$entry['date'];
|
||
if ($hasMultipleSlots && $timeLabel !== '') {
|
||
$dateLabel .= ' ' . $timeLabel;
|
||
}
|
||
|
||
$rows .= '<tr>';
|
||
$rows .= '<td>' . AppContext::e(trim($dateLabel)) . '</td>';
|
||
$rows .= '<td>' . AppContext::e($sourceDb !== '' ? $sourceDb : '-') . '</td>';
|
||
if ($showTargetColumn) {
|
||
$rows .= '<td class="restore-target-col" data-restore-target-col style="' . ($restoreMode === 'new_db' ? '' : 'display:none;') . '">';
|
||
if ($sourceDb !== '') {
|
||
$rows .= '<div class="restore-target-per-row" data-restore-targets>';
|
||
$rows .= $renderTargetRadios($availableDbs, $restoreTargetDb, 'target_db', [$sourceDb], $restoreMode === 'new_db', $restoreMode !== 'new_db', $entry['id'], $formId);
|
||
$rows .= '</div>';
|
||
} else {
|
||
$rows .= '<span class="muted">' . $et('Nie można ustalić bazy z nazwy pliku.') . '</span>';
|
||
}
|
||
$rows .= '</td>';
|
||
}
|
||
$rows .= '<td>' . AppContext::e($entry['file']) . '</td>';
|
||
$rows .= '<td>' . AppContext::e($entry['size']) . '</td>';
|
||
$rows .= '<td class="actions">';
|
||
$rows .= '<form method="post" id="' . AppContext::e($formId) . '">';
|
||
$rows .= $ctx->csrfField('upload_backup_submit');
|
||
$rows .= '<input type="hidden" name="active_tab" value="local">';
|
||
$rows .= '<input type="hidden" name="form_action" value="restore_local">';
|
||
$rows .= '<input type="hidden" name="local_backup" value="' . AppContext::e($entry['id']) . '">';
|
||
$rows .= '<input type="hidden" name="restore_mode" value="' . AppContext::e($restoreMode) . '" class="js-restore-mode-field">';
|
||
if ($sourceDb !== '') {
|
||
$rows .= '<button class="primary" type="submit">' . $et('Przywróć') . '</button>';
|
||
} else {
|
||
$rows .= '<button class="primary" type="submit" disabled>' . $et('Przywróć') . '</button>';
|
||
}
|
||
$rows .= '</form>';
|
||
$rows .= '</td>';
|
||
$rows .= '</tr>';
|
||
}
|
||
|
||
$html .= '<table>';
|
||
$header = '<th>' . $et('Data i godzina') . '</th><th>' . $et('Baza źródłowa') . '</th>';
|
||
if ($showTargetColumn) {
|
||
$header .= '<th data-restore-target-col style="' . ($restoreMode === 'new_db' ? '' : 'display:none;') . '">' . $et('Baza docelowa') . '</th>';
|
||
}
|
||
$header .= '<th>' . $et('Plik') . '</th><th>' . $et('Rozmiar') . '</th><th>' . $et('Akcja') . '</th>';
|
||
$html .= '<thead><tr>' . $header . '</tr></thead>';
|
||
$html .= '<tbody>' . $rows . '</tbody></table>';
|
||
return $html;
|
||
};
|
||
|
||
$renderUserBackupPicker = function () use (
|
||
$ctx,
|
||
$userSelectedEntries,
|
||
$userEntriesToShow,
|
||
$userHasMultipleSlots,
|
||
$userTimeSlots,
|
||
$userSelectedSlot,
|
||
$userSelectedDateLabel,
|
||
$userSelectedDate,
|
||
$userSelectedMonth,
|
||
$formatTimeLabel,
|
||
$et
|
||
): string {
|
||
$html = '';
|
||
if (empty($userSelectedEntries)) {
|
||
$html .= '<p class="muted">' . $et('Brak backupów użytkownika dla wybranej daty.') . '</p>';
|
||
return $html;
|
||
}
|
||
|
||
if ($userSelectedDateLabel !== '') {
|
||
$html .= '<div class="br-date-label">' . $et('Data:') . ' ' . AppContext::e($userSelectedDateLabel) . '</div>';
|
||
}
|
||
|
||
if ($userHasMultipleSlots) {
|
||
$html .= '<form method="get" class="br-time-picker br-user-time-picker">';
|
||
$html .= '<input type="hidden" name="tab" value="user">';
|
||
$html .= '<input type="hidden" name="user_month" value="' . AppContext::e($userSelectedMonth) . '">';
|
||
$html .= '<input type="hidden" name="user_date" value="' . AppContext::e($userSelectedDate) . '">';
|
||
$html .= '<div class="br-time-title">' . $et('Wybierz godzinę backupu') . '</div>';
|
||
$html .= '<div class="br-time-list">';
|
||
foreach ($userTimeSlots as $slotKey => $_entries) {
|
||
$timeLabel = $formatTimeLabel($slotKey);
|
||
$checked = ($slotKey === $userSelectedSlot) ? ' checked' : '';
|
||
$html .= '<label class="br-time-item">';
|
||
$html .= '<input type="radio" name="user_slot" value="' . AppContext::e($slotKey) . '"' . $checked . '>';
|
||
$html .= '<span>' . AppContext::e($timeLabel) . '</span>';
|
||
$html .= '</label>';
|
||
}
|
||
$html .= '</div></form>';
|
||
}
|
||
|
||
$rows = '';
|
||
foreach ($userEntriesToShow as $entry) {
|
||
$jobId = (string)($entry['job_id'] ?? '');
|
||
$dbName = (string)($entry['db_name'] ?? '');
|
||
$hasFile = (bool)($entry['has_file'] ?? false);
|
||
$relPath = (string)($entry['rel_path'] ?? '');
|
||
$overlayId = 'log-overlay-' . preg_replace('/[^A-Za-z0-9_-]+/', '-', $jobId);
|
||
|
||
$rows .= '<tr>';
|
||
$rows .= '<td>' . AppContext::e($dbName !== '' ? $dbName : '-') . '</td>';
|
||
$rows .= '<td class="actions">';
|
||
|
||
if ($hasFile) {
|
||
$rows .= '<form method="get" action="' . AppContext::e($ctx->url('download.raw')) . '" style="display:inline-flex;gap:6px;margin:0">';
|
||
if ($jobId !== '') {
|
||
$rows .= '<input type="hidden" name="job" value="' . AppContext::e($jobId) . '">';
|
||
} elseif ($relPath !== '') {
|
||
$rows .= '<input type="hidden" name="path" value="' . AppContext::e($relPath) . '">';
|
||
}
|
||
$rows .= '<button class="btn success" type="submit">' . $et('Pobierz Backup') . '</button>';
|
||
$rows .= '</form>';
|
||
}
|
||
|
||
if ($jobId !== '') {
|
||
$rows .= '<button class="btn" type="button" data-overlay-open="' . AppContext::e($overlayId) . '" data-overlay-job="' . AppContext::e($jobId) . '">' . $et('Pokaż logi') . '</button>';
|
||
}
|
||
|
||
if ($hasFile) {
|
||
$rows .= '<form method="post" style="display:inline-flex;gap:6px;margin:0">';
|
||
$rows .= $ctx->csrfField('upload_backup_submit');
|
||
$rows .= '<input type="hidden" name="active_tab" value="user">';
|
||
$rows .= '<input type="hidden" name="form_action" value="delete_user_backup">';
|
||
if ($jobId !== '') {
|
||
$rows .= '<input type="hidden" name="backup_job_id" value="' . AppContext::e($jobId) . '">';
|
||
}
|
||
if ($relPath !== '') {
|
||
$rows .= '<input type="hidden" name="backup_path" value="' . AppContext::e($relPath) . '">';
|
||
}
|
||
$rows .= '<input type="hidden" name="user_month" value="' . AppContext::e($userSelectedMonth) . '">';
|
||
$rows .= '<input type="hidden" name="user_date" value="' . AppContext::e($userSelectedDate) . '">';
|
||
$rows .= '<input type="hidden" name="user_slot" value="' . AppContext::e($userSelectedSlot) . '">';
|
||
$rows .= '<button class="btn warn" type="submit">' . $et('Usuń Backup') . '</button>';
|
||
$rows .= '</form>';
|
||
}
|
||
|
||
$rows .= '</td>';
|
||
$rows .= '</tr>';
|
||
}
|
||
|
||
$html .= '<table>';
|
||
$html .= '<thead><tr><th>' . $et('Nazwa bazy danych') . '</th><th>' . $et('Akcje') . '</th></tr></thead>';
|
||
$html .= '<tbody>' . $rows . '</tbody></table>';
|
||
return $html;
|
||
};
|
||
|
||
if ($ctx->queryString('action') === 'local_picker') {
|
||
header('Content-Type: text/html; charset=UTF-8');
|
||
echo $renderRestorePicker();
|
||
return;
|
||
}
|
||
|
||
if ($ctx->queryString('action') === 'user_picker') {
|
||
header('Content-Type: text/html; charset=UTF-8');
|
||
echo $renderUserBackupPicker();
|
||
return;
|
||
}
|
||
|
||
$localSection = '';
|
||
if ($activeTab === 'local') {
|
||
$localSection .= '<section class="panel" style="margin-bottom:14px">';
|
||
$localSection .= '<h3>' . $et('Backupy HITME.PL') . '</h3>';
|
||
$localSection .= '<div class="br-restore-layout">';
|
||
$localSection .= '<div class="br-restore-calendar">';
|
||
$localSection .= '<h4>' . $et('Kalendarz backupów') . '</h4>';
|
||
|
||
$nav = $monthNavTargets($availableMonths, $selectedMonth);
|
||
$langCode = $ctx->daUser->language();
|
||
$localSection .= '<div class="br-month-nav">';
|
||
$localSection .= '<form method="get">';
|
||
$localSection .= '<input type="hidden" name="tab" value="local">';
|
||
$localSection .= '<input type="hidden" name="month" value="' . AppContext::e($nav['prev']) . '">';
|
||
$localSection .= '<input type="hidden" name="backup_slot" value="">';
|
||
$localSection .= '<input type="hidden" name="restore_mode" value="' . AppContext::e($restoreMode) . '">';
|
||
$localSection .= '<button class="br-month-btn" type="submit"' . ($nav['prev'] === '' ? ' disabled' : '') . ' title="' . $et('Poprzedni miesiąc') . '" aria-label="' . $et('Poprzedni miesiąc') . '">◀</button>';
|
||
$localSection .= '</form>';
|
||
$localSection .= '<div class="br-month-label">' . AppContext::e($formatMonthLabel($selectedMonth, $langCode)) . '</div>';
|
||
$localSection .= '<form method="get">';
|
||
$localSection .= '<input type="hidden" name="tab" value="local">';
|
||
$localSection .= '<input type="hidden" name="month" value="' . AppContext::e($nav['next']) . '">';
|
||
$localSection .= '<input type="hidden" name="backup_slot" value="">';
|
||
$localSection .= '<input type="hidden" name="restore_mode" value="' . AppContext::e($restoreMode) . '">';
|
||
$localSection .= '<button class="br-month-btn" type="submit"' . ($nav['next'] === '' ? ' disabled' : '') . ' title="' . $et('Następny miesiąc') . '" aria-label="' . $et('Następny miesiąc') . '">▶</button>';
|
||
$localSection .= '</form>';
|
||
$localSection .= '</div>';
|
||
|
||
$monthTs = strtotime($selectedMonth . '-01');
|
||
if ($monthTs === false) {
|
||
$monthTs = strtotime(date('Y-m-01'));
|
||
}
|
||
$calendarDays = (int)date('t', $monthTs);
|
||
$calendarOffset = (int)date('N', $monthTs);
|
||
$weekdays = $calendarWeekdays($langCode);
|
||
|
||
$localSection .= '<form method="get" class="br-calendar-grid-wrap">';
|
||
$localSection .= '<input type="hidden" name="tab" value="local">';
|
||
$localSection .= '<input type="hidden" name="month" value="' . AppContext::e($selectedMonth) . '">';
|
||
$localSection .= '<input type="hidden" name="backup_slot" value="">';
|
||
$localSection .= '<input type="hidden" name="restore_mode" value="' . AppContext::e($restoreMode) . '">';
|
||
$localSection .= '<table class="br-calendar-grid"><thead><tr>';
|
||
foreach ($weekdays as $dayLabel) {
|
||
$localSection .= '<th>' . AppContext::e($dayLabel) . '</th>';
|
||
}
|
||
$localSection .= '</tr></thead><tbody>';
|
||
$dayNum = 1;
|
||
for ($row = 0; $row < 6; $row++) {
|
||
$localSection .= '<tr>';
|
||
for ($col = 1; $col <= 7; $col++) {
|
||
if (($row === 0 && $col < $calendarOffset) || $dayNum > $calendarDays) {
|
||
$localSection .= '<td class="br-cal-empty"></td>';
|
||
continue;
|
||
}
|
||
$dayKey = date('Y-m-', $monthTs) . str_pad((string)$dayNum, 2, '0', STR_PAD_LEFT);
|
||
$hasBackup = isset($backupsByDate[$dayKey]);
|
||
$isSelected = ($dayKey === $selectedDate);
|
||
$localSection .= '<td>';
|
||
if ($hasBackup) {
|
||
$localSection .= '<button class="br-cal-day' . ($isSelected ? ' is-selected' : '') . '" type="submit" name="backup_date" value="' . AppContext::e($dayKey) . '">' . AppContext::e((string)$dayNum) . '</button>';
|
||
} else {
|
||
$localSection .= '<span class="br-cal-day-disabled">' . AppContext::e((string)$dayNum) . '</span>';
|
||
}
|
||
$localSection .= '</td>';
|
||
$dayNum++;
|
||
}
|
||
$localSection .= '</tr>';
|
||
if ($dayNum > $calendarDays) {
|
||
break;
|
||
}
|
||
}
|
||
$localSection .= '</tbody></table>';
|
||
$localSection .= '</form>';
|
||
$localSection .= '</div>';
|
||
|
||
$localSection .= '<div class="br-restore-picker">';
|
||
$localSection .= $renderRestorePicker();
|
||
$localSection .= '</div>';
|
||
$localSection .= '</div>';
|
||
$localSection .= '</section>';
|
||
}
|
||
|
||
$userSection = '';
|
||
if ($activeTab === 'user') {
|
||
$userSection .= '<section class="panel" style="margin-bottom:14px">';
|
||
$userSection .= '<h3>' . $et('Backupy użytkownika') . '</h3>';
|
||
$userSection .= '<div class="br-restore-layout">';
|
||
$userSection .= '<div class="br-restore-calendar">';
|
||
$userSection .= '<h4>' . $et('Kalendarz backupów') . '</h4>';
|
||
|
||
$userNav = $monthNavTargets($userAvailableMonths, $userSelectedMonth);
|
||
$userSection .= '<div class="br-month-nav">';
|
||
$userSection .= '<form method="get">';
|
||
$userSection .= '<input type="hidden" name="tab" value="user">';
|
||
$userSection .= '<input type="hidden" name="user_month" value="' . AppContext::e($userNav['prev']) . '">';
|
||
$userSection .= '<input type="hidden" name="user_slot" value="">';
|
||
$userSection .= '<button class="br-month-btn" type="submit"' . ($userNav['prev'] === '' ? ' disabled' : '') . ' title="' . $et('Poprzedni miesiąc') . '" aria-label="' . $et('Poprzedni miesiąc') . '">◀</button>';
|
||
$userSection .= '</form>';
|
||
$userSection .= '<div class="br-month-label">' . AppContext::e($formatMonthLabel($userSelectedMonth, $langCode)) . '</div>';
|
||
$userSection .= '<form method="get">';
|
||
$userSection .= '<input type="hidden" name="tab" value="user">';
|
||
$userSection .= '<input type="hidden" name="user_month" value="' . AppContext::e($userNav['next']) . '">';
|
||
$userSection .= '<input type="hidden" name="user_slot" value="">';
|
||
$userSection .= '<button class="br-month-btn" type="submit"' . ($userNav['next'] === '' ? ' disabled' : '') . ' title="' . $et('Następny miesiąc') . '" aria-label="' . $et('Następny miesiąc') . '">▶</button>';
|
||
$userSection .= '</form>';
|
||
$userSection .= '</div>';
|
||
|
||
$userMonthTs = strtotime($userSelectedMonth . '-01');
|
||
if ($userMonthTs === false) {
|
||
$userMonthTs = strtotime(date('Y-m-01'));
|
||
}
|
||
$userCalendarDays = (int)date('t', $userMonthTs);
|
||
$userCalendarOffset = (int)date('N', $userMonthTs);
|
||
$weekdays = $calendarWeekdays($langCode);
|
||
|
||
$userSection .= '<form method="get" class="br-calendar-grid-wrap">';
|
||
$userSection .= '<input type="hidden" name="tab" value="user">';
|
||
$userSection .= '<input type="hidden" name="user_month" value="' . AppContext::e($userSelectedMonth) . '">';
|
||
$userSection .= '<input type="hidden" name="user_slot" value="">';
|
||
$userSection .= '<table class="br-calendar-grid"><thead><tr>';
|
||
foreach ($weekdays as $dayLabel) {
|
||
$userSection .= '<th>' . AppContext::e($dayLabel) . '</th>';
|
||
}
|
||
$userSection .= '</tr></thead><tbody>';
|
||
$dayNum = 1;
|
||
for ($row = 0; $row < 6; $row++) {
|
||
$userSection .= '<tr>';
|
||
for ($col = 1; $col <= 7; $col++) {
|
||
if (($row === 0 && $col < $userCalendarOffset) || $dayNum > $userCalendarDays) {
|
||
$userSection .= '<td class="br-cal-empty"></td>';
|
||
continue;
|
||
}
|
||
$dayKey = date('Y-m-', $userMonthTs) . str_pad((string)$dayNum, 2, '0', STR_PAD_LEFT);
|
||
$hasBackup = isset($userBackupsByDate[$dayKey]);
|
||
$isSelected = ($dayKey === $userSelectedDate);
|
||
$userSection .= '<td>';
|
||
if ($hasBackup) {
|
||
$userSection .= '<button class="br-cal-day' . ($isSelected ? ' is-selected' : '') . '" type="submit" name="user_date" value="' . AppContext::e($dayKey) . '">' . AppContext::e((string)$dayNum) . '</button>';
|
||
} else {
|
||
$userSection .= '<span class="br-cal-day-disabled">' . AppContext::e((string)$dayNum) . '</span>';
|
||
}
|
||
$userSection .= '</td>';
|
||
$dayNum++;
|
||
}
|
||
$userSection .= '</tr>';
|
||
if ($dayNum > $userCalendarDays) {
|
||
break;
|
||
}
|
||
}
|
||
$userSection .= '</tbody></table>';
|
||
$userSection .= '</form>';
|
||
$userSection .= '</div>';
|
||
|
||
$userSection .= '<div class="br-user-backup-picker">';
|
||
$userSection .= $renderUserBackupPicker();
|
||
$userSection .= '</div>';
|
||
|
||
$userSection .= '</div>';
|
||
$userSection .= '</section>';
|
||
}
|
||
|
||
$fileSection = '';
|
||
if ($activeTab === 'file') {
|
||
$fileSection .= '<section class="panel" style="margin-bottom:14px">';
|
||
$fileSection .= '<h3 class="section-title-center">' . $et('Backupy z pliku') . '</h3>';
|
||
$fileSection .= '<p class="section-text">' . AppContext::e($tr('Wskaż plik `.sql`, `.gz` lub `.sql.gz` z komputera albo podaj ścieżkę pliku na serwerze (w katalogu `/home/{user}`).', ['user' => $daUser])) . '</p>';
|
||
$fileSection .= '<form method="post" enctype="multipart/form-data" data-restore-file-form>';
|
||
$fileSection .= $ctx->csrfField('upload_backup_submit');
|
||
$fileSection .= '<input type="hidden" name="active_tab" value="file">';
|
||
$fileSection .= '<input type="hidden" name="form_action" value="restore_file">';
|
||
$fileSection .= '<div class="file-source-mode" data-source-mode-group>';
|
||
$fileSection .= '<label>' . $et('Lokalizacja pliku') . '</label>';
|
||
$fileSection .= '<div class="file-source-options">';
|
||
$fileSection .= '<label class="file-source-option"><input type="radio" name="source_mode" value="local"' . ($sourceLocationMode === 'local' ? ' checked' : '') . '> <span>' . $et('Przesyłanie pliku z komputera') . '</span></label>';
|
||
$fileSection .= '<label class="file-source-option"><input type="radio" name="source_mode" value="server"' . ($sourceLocationMode === 'server' ? ' checked' : '') . '> <span>' . $et('Wskaż plik na koncie użytkownika') . '</span></label>';
|
||
$fileSection .= '</div></div>';
|
||
$fileSection .= '<input type="hidden" name="uploaded_tmp_path" id="uploaded-tmp-path" value="' . AppContext::e($uploadedTmpPathValue) . '">';
|
||
$fileSection .= '<input type="hidden" name="uploaded_original_name" id="uploaded-original-name" value="' . AppContext::e($uploadedOriginalNameValue) . '">';
|
||
|
||
$fileSection .= '<div class="row file-source-panel" data-source-panel="local"' . ($sourceLocationMode === 'local' ? '' : ' style="display:none"') . '>';
|
||
$fileSection .= '<div><label>' . $et('Plik backupu (.sql, .gz lub .sql.gz)') . '</label>';
|
||
$fileSection .= '<div class="dropzone" data-dropzone data-input="source-file-upload" data-path-input="source-file-path" data-staged-input="uploaded-tmp-path" data-staged-name-input="uploaded-original-name" role="button" tabindex="0">';
|
||
$fileSection .= '<input type="file" id="source-file-upload" name="source_file_upload" accept=".sql,.gz,.sql.gz" class="dropzone-input">';
|
||
$fileSection .= '<div class="dropzone-placeholder">';
|
||
$fileSection .= '<div class="dropzone-title">' . $et('Przeciągnij plik backupu tutaj') . '</div>';
|
||
$fileSection .= '<div class="dropzone-subtitle">' . $et('lub kliknij, aby wybrać plik') . '</div>';
|
||
$fileSection .= '</div>';
|
||
$fileSection .= '<div class="dropzone-info">';
|
||
$fileSection .= '<div class="dropzone-meta">';
|
||
$fileSection .= '<div><strong>' . $et('Plik') . ':</strong> <span class="dropzone-name">-</span></div>';
|
||
$fileSection .= '<div><strong>' . $et('Rozmiar') . ':</strong> <span class="dropzone-size">-</span></div>';
|
||
$fileSection .= '</div>';
|
||
$fileSection .= '<button type="button" class="btn warn dropzone-clear">' . $et('Usuń plik') . '</button>';
|
||
$fileSection .= '</div>';
|
||
$fileSection .= '</div></div></div>';
|
||
$fileSection .= '<div class="muted" style="margin-top:6px">' . $et('Po dodaniu pliku zostanie on przesłany na serwer i dodany do kolejki przywracania.') . '</div>';
|
||
|
||
$fileSection .= '<div class="row file-source-panel" data-source-panel="server"' . ($sourceLocationMode === 'server' ? '' : ' style="display:none"') . '>';
|
||
$fileSection .= '<div><label>' . $et('Ścieżka pliku SQL / SQL.GZ') . '</label>';
|
||
$fileSection .= '<div class="input-with-tools"><input type="text" id="source-file-path" name="source_file_path" value="' . AppContext::e($sourceFilePathValue) . '" placeholder="/home/' . AppContext::e($daUser) . '/dump.sql.gz">';
|
||
$fileSection .= '<span class="field-tools"><button type="button" class="br-icon-btn" onclick="openPicker(\'source-file-path\', \'file\');" aria-label="' . $et('Wybierz plik') . '" title="' . $et('Wybierz plik') . '">📁</button></span></div>';
|
||
$fileSection .= '<p class="muted" style="margin:6px 0 0">' . $et('Dozwolone są tylko ścieżki z katalogu /home/{user}.', ['user' => $daUser]) . '</p></div>';
|
||
$fileSection .= '</div>';
|
||
$fileSection .= '<div class="row">';
|
||
$fileSection .= '<div><label>' . $et('Baza docelowa') . '</label>' . $renderTargetRadios($availableDbs, $restoreTargetDb, 'target_db', [], true, false, 'file') . '</div>';
|
||
$fileSection .= '</div>';
|
||
$fileSection .= '<div class="actions"><button class="primary" type="submit">' . $et('Przywróć Backup') . '</button></div>';
|
||
$fileSection .= '</form>';
|
||
$fileSection .= '</section>';
|
||
|
||
$fileSection .= FilePicker::renderOverlay(
|
||
$ctx,
|
||
'file-picker-overlay',
|
||
'/home/' . $daUser,
|
||
$ctx->url('file_picker.html'),
|
||
'source-file-path'
|
||
);
|
||
}
|
||
|
||
$statusRows = '';
|
||
$logOverlays = [];
|
||
foreach ($jobs as $job) {
|
||
$statusLabel = match ($job['status']) {
|
||
'pending' => $tr('Oczekuje'),
|
||
'processing' => $tr('W toku'),
|
||
'done_ok' => $tr('Zakończone OK'),
|
||
'done_fail' => $tr('Błąd'),
|
||
'done_cancel' => $tr('Anulowane'),
|
||
default => $job['status'],
|
||
};
|
||
$jobType = (string)($job['type'] ?? '');
|
||
$operationLabel = ($jobType === 'backup') ? $tr('Tworzenie kopii zapasowej') : $tr('Przywracanie kopii zapasowej');
|
||
$restoreModeRaw = strtolower(trim((string)($job['restore_mode'] ?? '')));
|
||
$restoreModeLabel = $jobType === 'restore'
|
||
? ($restoreModeRaw === 'new_db' ? $tr('Przywracanie do nowej bazy') : $tr('Przywracanie z nadpisaniem'))
|
||
: $tr('Nie dotyczy');
|
||
|
||
$jobId = (string)($job['job_id'] ?? '');
|
||
$overlayId = 'log-overlay-' . preg_replace('/[^A-Za-z0-9_-]+/', '-', $jobId);
|
||
$contentId = $overlayId . '-content';
|
||
$sourceDbName = trim((string)($job['source_db_name'] ?? ''));
|
||
$targetDbName = (string)($job['db_name'] ?? '-');
|
||
$hasDistinctSourceTarget = ($jobType === 'restore' && $sourceDbName !== '' && $sourceDbName !== $targetDbName);
|
||
$dbDisplay = $hasDistinctSourceTarget
|
||
? ($sourceDbName . ' → ' . $targetDbName)
|
||
: $targetDbName;
|
||
$logText = '';
|
||
try {
|
||
$logText = $queue->readLogForCurrentUser($jobId);
|
||
} catch (Throwable $e) {
|
||
$logText = $ctx->formatException($e);
|
||
}
|
||
|
||
$statusRows .= '<tr>';
|
||
$statusRows .= '<td>' . AppContext::e($job['start_ts'] > 0 ? date('Y-m-d H:i:s', $job['start_ts']) : '-') . '</td>';
|
||
$statusRows .= '<td>' . AppContext::e($dbDisplay) . '</td>';
|
||
$statusRows .= '<td>' . AppContext::e($operationLabel) . '</td>';
|
||
$statusRows .= '<td>' . AppContext::e($restoreModeLabel) . '</td>';
|
||
$statusRows .= '<td>' . AppContext::e($statusLabel) . '</td>';
|
||
$statusRows .= '<td class="actions">';
|
||
$statusRows .= '<button type="button" class="btn" data-overlay-open="' . AppContext::e($overlayId) . '" data-overlay-job="' . AppContext::e($jobId) . '">' . $et('Podgląd logu') . '</button>';
|
||
$statusRows .= '</td>';
|
||
$statusRows .= '</tr>';
|
||
|
||
$logMeta = '<div class="log-meta">';
|
||
$logMeta .= '<div><strong>' . $et('Data rozpoczęcia') . ':</strong> ' . AppContext::e($job['start_ts'] > 0 ? date('Y-m-d H:i:s', $job['start_ts']) : '-') . '</div>';
|
||
$logMeta .= '<div><strong>' . $et('Rodzaj operacji') . ':</strong> ' . AppContext::e($operationLabel) . '</div>';
|
||
$logMeta .= '<div><strong>' . $et('Typ przywracania') . ':</strong> ' . AppContext::e($restoreModeLabel) . '</div>';
|
||
if ($hasDistinctSourceTarget) {
|
||
$logMeta .= '<div><strong>' . $et('Baza źródłowa') . ':</strong> ' . AppContext::e($sourceDbName !== '' ? $sourceDbName : '-') . '</div>';
|
||
$logMeta .= '<div><strong>' . $et('Baza docelowa') . ':</strong> ' . AppContext::e($targetDbName) . '</div>';
|
||
} else {
|
||
$logMeta .= '<div><strong>' . $et('Nazwa bazy') . ':</strong> ' . AppContext::e($targetDbName) . '</div>';
|
||
}
|
||
$logMeta .= '<div><strong>' . $et('Status') . ':</strong> ' . AppContext::e($statusLabel) . '</div>';
|
||
$logMeta .= '</div>';
|
||
|
||
$logOverlays[] = '<div class="job-overlay log-overlay" id="' . AppContext::e($overlayId) . '" data-overlay-job-id="' . AppContext::e($jobId) . '" aria-hidden="true">'
|
||
. '<div class="job-overlay-backdrop" data-overlay-close="' . AppContext::e($overlayId) . '"></div>'
|
||
. '<div class="job-overlay-card">'
|
||
. '<div class="job-overlay-head"><h3>' . $et('Log zadania') . '</h3>'
|
||
. '<div class="job-overlay-head-actions">'
|
||
. '<button type="button" class="br-icon-btn" data-log-copy="' . AppContext::e($contentId) . '">' . $et('Kopiuj') . '</button>'
|
||
. '<button type="button" class="tool-btn" data-overlay-close="' . AppContext::e($overlayId) . '" aria-label="' . $et('Zamknij') . '">×</button>'
|
||
. '</div></div>'
|
||
. $logMeta
|
||
. '<pre id="' . AppContext::e($contentId) . '" class="log-content">' . AppContext::e($logText !== '' ? $logText : $et('Log jest niedostępny dla tego zadania.')) . '</pre>'
|
||
. '</div></div>';
|
||
}
|
||
if ($statusRows === '') {
|
||
$statusRows = '<tr><td colspan="6" class="muted" style="text-align:center;">' . $et('Brak zadań backup/restore do wyświetlenia.') . '</td></tr>';
|
||
}
|
||
|
||
$statusTable = '<table><thead><tr><th>' . $et('Data rozpoczęcia') . '</th><th>' . $et('Nazwa bazy') . '</th><th>' . $et('Rodzaj operacji') . '</th><th>' . $et('Typ przywracania') . '</th><th>' . $et('Status') . '</th><th>' . $et('Akcja') . '</th></tr></thead><tbody>' . $statusRows . '</tbody></table>';
|
||
$statusInner = $statusTable . (!empty($logOverlays) ? implode('', $logOverlays) : '');
|
||
|
||
if ($ctx->queryString('action') === 'status') {
|
||
header('Content-Type: text/html; charset=UTF-8');
|
||
echo $statusInner;
|
||
return;
|
||
}
|
||
|
||
$logsSection = '';
|
||
$logsSection .= '<section class="panel">';
|
||
$logsSection .= '<h3 class="section-title-center">' . $et('Kolejka zadań i logi') . '</h3>';
|
||
$logsSection .= '<div id="job-status">' . $statusInner . '</div>';
|
||
$logsSection .= '</section>';
|
||
|
||
$body = $tabs . $restoreModeSection . $localSection . $userSection . $fileSection . $confirmOverlay;
|
||
$shouldRefresh = false;
|
||
foreach ($jobs as $job) {
|
||
if (in_array($job['status'], ['pending', 'processing'], true)) {
|
||
$shouldRefresh = true;
|
||
break;
|
||
}
|
||
}
|
||
if ($shouldRefresh) {
|
||
$body .= '<div id="backup-auto-refresh" data-enabled="1" data-seconds="2" data-status-url="' . AppContext::e($ctx->url('upload_backup.html', ['action' => 'status'])) . '"></div>';
|
||
}
|
||
$ctx->renderPage('Przywróć Backup', $body, $ok, $errors, $logsSection);
|
||
};
|