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

1291 lines
66 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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_POSTGRESQL_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->pg->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->pg->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);
};