Files
borg-restore/user/index.html
T
2026-07-04 11:20:44 +02:00

3386 lines
131 KiB
HTML
Executable File

#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/borg-restore/php.ini
<?php
function parse_sh_value(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
if ($value[0] === "'" && substr($value, -1) === "'") {
return substr($value, 1, -1);
}
if ($value[0] === '"' && substr($value, -1) === '"') {
return stripcslashes(substr($value, 1, -1));
}
$value = preg_replace('/\s+#.*$/', '', $value);
return trim($value);
}
function parse_bool(string $value, bool $default = false): bool
{
$normalized = strtolower(trim($value));
if ($normalized === '') {
return $default;
}
if (in_array($normalized, ['1', 'true', 'yes', 'y', 'on'], true)) {
return true;
}
if (in_array($normalized, ['0', 'false', 'no', 'n', 'off'], true)) {
return false;
}
return $default;
}
function normalize_location(string $path, string $fallback): string
{
$path = trim($path);
if ($path === '') {
$path = $fallback;
}
if ($path !== '' && $path[0] !== '/') {
$path = '/' . $path;
}
return rtrim($path, '/');
}
function load_borg_vars(string $path): array
{
$vars = [
'BORG_PASSPHRASE' => null,
'REPO' => null,
'BORG_SSH_KEY' => null,
];
if (!is_readable($path)) {
return $vars;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $vars;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(?:export\s+)?(BORG_PASSPHRASE|REPO|BORG_SSH_KEY)=(.*)$/', $line, $m)) {
$key = $m[1];
$vars[$key] = parse_sh_value($m[2]);
}
}
return $vars;
}
function load_settings(string $path): array
{
$settings = [
'BB_PATH' => '/opt/bb',
'BORG_PATH' => '/usr/bin/borg',
'BORG_VARIABLES_FILE' => '/opt/bb/bbvars.sh',
'ADMIN_MODE_ENABLE' => 'TRUE',
'RESULTS_PER_PAGE' => '10',
'WEB_RESTORE_LOCATION' => '/home/DA_USER/domains',
'MAIL_RESTORE_LOCATION' => '/home/DA_USER/imap',
'ENABLE_WEB_RESTORE' => 'TRUE',
'ENABLE_MAILBOX_RESTORE' => 'TRUE',
'DEFAULT_RESTORE_DESTINATION' => '/home/DA_USER/bb',
'BLOCK_RESTORE_OUTSIDE_LOCATION' => 'TRUE',
'PREVENT_RESTORE_MAIN_DOMAINS_DIR' => 'TRUE',
'PREVENT_RESTORE_MAIN_IMAP_DIR' => 'TRUE',
'PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN' => 'TRUE',
'PREVENT_CREATE_DIRS_IN_MAIN_IMAP' => 'TRUE',
'HASH_VISIBILITY' => 'FALSE',
'ENABLE_LOGS_OVERLAY' => 'TRUE',
'SELECTABLE_DOMAIN_RESTORE_DESTINATION' => 'TRUE',
'SELECTABLE_MAIL_RESTORE_DESTINATION' => 'TRUE',
'CLEAR_DOVECOT_INDEXES_AFTER_MERGE' => 'TRUE',
'RESTORE_PATH_MODE' => 'dir',
'ADD_DATE_TO_RESTORED_DIR' => 'FALSE',
'DEBUG_MODE' => 'FALSE',
];
if (!is_readable($path)) {
return $settings;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $settings;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(BB_PATH|BORG_PATH|BORG_VARIABLES_FILE|ADMIN_MODE_ENABLE|RESULTS_PER_PAGE|WEB_RESTORE_LOCATION|MAIL_RESTORE_LOCATION|ENABLE_WEB_RESTORE|ENABLE_MAILBOX_RESTORE|DEFAULT_RESTORE_DESTINATION|BLOCK_RESTORE_OUTSIDE_LOCATION|PREVENT_RESTORE_MAIN_DOMAINS_DIR|PREVENT_RESTORE_MAIN_IMAP_DIR|PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN|PREVENT_CREATE_DIRS_IN_MAIN_IMAP|HASH_VISIBILITY|ENABLE_LOGS_OVERLAY|SELECTABLE_DOMAIN_RESTORE_DESTINATION|SELECTABLE_MAIL_RESTORE_DESTINATION|CLEAR_DOVECOT_INDEXES_AFTER_MERGE|RESTORE_PATH_MODE|ADD_DATE_TO_RESTORED_DIR|DEBUG_MODE)=(.*)$/', $line, $m)) {
$settings[$m[1]] = parse_sh_value($m[2]);
}
}
return $settings;
}
function file_owner_name(int $uid): string
{
if (function_exists('posix_getpwuid')) {
$info = posix_getpwuid($uid);
if (is_array($info) && !empty($info['name'])) {
return (string)$info['name'];
}
}
return (string)$uid;
}
function validate_secure_file(string $path, string $label, bool $check_writable = true): string
{
if ($path === '') {
return $label . ' ma pustą ścieżkę.';
}
if (!file_exists($path)) {
return $label . ' nie istnieje: ' . $path;
}
if (is_link($path) || !is_file($path)) {
return $label . ' nie jest zwykłym plikiem: ' . $path;
}
$perms = fileperms($path);
if ($perms === false) {
return $label . ' ma nieprawidłowe uprawnienia: ' . $path;
}
if ($check_writable && (($perms & 0x0010) !== 0 || ($perms & 0x0002) !== 0)) {
return $label . ' nie może być zapisywalny dla grupy/innych: ' . $path;
}
$uid = fileowner($path);
if ($uid === false) {
return $label . ' ma nieprawidłowego właściciela: ' . $path;
}
$owner = file_owner_name((int)$uid);
if ($uid !== 0 && $uid !== 500 && $owner !== 'root' && $owner !== 'diradmin') {
return $label . ' ma nieprawidłowego właściciela (' . $owner . '): ' . $path;
}
return '';
}
function validate_job_files(string $job_base): array
{
$errors = [];
foreach (['pending' => '*.env', 'processing' => '*.env', 'done' => '*.{ok,fail,cancel,env.ok,env.fail,env.cancel}'] as $dir => $pattern) {
foreach (glob($job_base . '/' . $dir . '/' . $pattern, GLOB_BRACE) ?: [] as $path) {
$err = validate_secure_file($path, 'Plik zadania', false);
if ($err !== '') {
$errors[] = $err;
}
}
}
return $errors;
}
function safe_user_key(string $value): string
{
$key = preg_replace('/[^A-Za-z0-9_.-]/', '_', $value);
return $key === '' ? 'unknown' : $key;
}
function get_csrf_token(string $data_dir, string $da_user): string
{
$dir = rtrim($data_dir, '/') . '/csrf';
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
$path = $dir . '/' . safe_user_key($da_user) . '.token';
if (is_readable($path)) {
$token = trim((string)file_get_contents($path));
if ($token !== '') {
return $token;
}
}
$token = bin2hex(random_bytes(32));
@file_put_contents($path, $token, LOCK_EX);
@chmod($path, 0600);
return $token;
}
function csrf_is_valid(array $params, string $token): bool
{
$sent = $params['csrf_token'] ?? '';
if (!is_string($sent) || $sent === '' || $token === '') {
return false;
}
return hash_equals($token, $sent);
}
function read_params(): array
{
$params = [];
if (!empty($_GET)) {
$params = array_merge($params, $_GET);
}
if (!empty($_POST)) {
$params = array_merge($params, $_POST);
}
$get_raw = getenv('GET') ?: getenv('get') ?: '';
if ($get_raw !== '') {
parse_str($get_raw, $parsed);
$params = array_merge($parsed, $params);
}
$qs = $_SERVER['QUERY_STRING'] ?? '';
if ($qs !== '') {
parse_str($qs, $parsed);
$params = array_merge($parsed, $params);
}
$post_raw = getenv('POST') ?: getenv('post') ?: '';
if ($post_raw !== '') {
parse_str($post_raw, $parsed);
$params = array_merge($parsed, $params);
}
return $params;
}
function load_job_vars(string $path): array
{
$vars = [
'DA_USER' => '',
'REPONAME' => '',
'USERRESTOREPATH' => '',
'TARGETPATH' => '',
'RESTOREMODE' => '',
'RESTORETYPE' => '',
'START_TS' => '',
];
if (!is_readable($path)) {
return $vars;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $vars;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(DA_USER|REPONAME|USERRESTOREPATH|TARGETPATH|RESTOREMODE|RESTORETYPE|START_TS|USERLANG)=(.*)$/', $line, $m)) {
$vars[$m[1]] = parse_sh_value($m[2]);
}
}
return $vars;
}
function job_id_from_file(string $file): string
{
$base = basename($file);
if (substr($base, -3) === '.ok') {
$base = substr($base, 0, -3);
} elseif (substr($base, -5) === '.fail') {
$base = substr($base, 0, -5);
}
if (substr($base, -4) === '.env') {
$base = substr($base, 0, -4);
}
return $base;
}
function find_job_file(string $job_base, string $file, string $job_id): string
{
$candidates = [
$job_base . '/processing/' . $file,
$job_base . '/pending/' . $file,
$job_base . '/done/' . $file,
];
if ($job_id !== '') {
$candidates[] = $job_base . '/processing/' . $job_id . '.env';
$candidates[] = $job_base . '/pending/' . $job_id . '.env';
$candidates[] = $job_base . '/done/' . $job_id . '.ok';
$candidates[] = $job_base . '/done/' . $job_id . '.fail';
$candidates[] = $job_base . '/done/' . $job_id . '.cancel';
}
foreach ($candidates as $path) {
if (is_file($path)) {
return $path;
}
}
return '';
}
function job_belongs_to_user(string $job_file_path, string $da_user): bool
{
if ($job_file_path === '' || !is_readable($job_file_path)) {
return false;
}
$vars = load_job_vars($job_file_path);
$owner = (string)($vars['DA_USER'] ?? '');
return $owner !== '' && $owner === $da_user;
}
function find_job_file_for_user(string $job_base, string $file, string $job_id, string $da_user): string
{
$path = find_job_file($job_base, $file, $job_id);
if ($path === '') {
return '';
}
return job_belongs_to_user($path, $da_user) ? $path : '';
}
function format_log_body_for_download(string $text): string
{
$text = normalize_log($text);
$text = preg_replace('/[ \t]+(?=\d{1,3}(?:\.\d+)?%\s+(?:Extracting|Deleting|Pruning|Compacting|Saving|Uploading|Downloading|Processing|Restoring|Creating)\b)/i', "\n", $text);
$text = preg_replace('/\n{2,}(?=\d{1,3}(?:\.\d+)?%)/', "\n", $text);
$text = preg_replace('/\n{3,}/', "\n\n", $text);
return rtrim($text) . "\n";
}
function log_missing_include_explanation(string $log_text, string $restore_path_label = ''): string
{
if (stripos($log_text, 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki') !== false) {
return '';
}
if (stripos($log_text, 'The specified restore path was not found in the backup') !== false) {
return '';
}
if (preg_match("/Include pattern '.*' never matched\\./i", $log_text)) {
$suffix = '';
if ($restore_path_label !== '' && $restore_path_label !== '-') {
$suffix = ' (' . $restore_path_label . ')';
}
return 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki' . $suffix . '. Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować.';
}
return '';
}
function format_datetime(string $value, int $fallback = 0): string
{
$ts = is_numeric($value) ? (int)$value : 0;
if ($ts <= 0) {
$ts = $fallback > 0 ? $fallback : time();
}
return date('d-m-Y-H-i-s', $ts);
}
function build_log_download_content(string $job_id, string $job_file_path, string $log_path, string $job_base): string
{
$vars = load_job_vars($job_file_path);
$log_meta_start = format_datetime($vars['START_TS'] ?? '', time());
$log_meta_path = display_restore_path($vars['USERRESTOREPATH'] ?? '');
$log_meta_mode = restore_mode_label($vars['RESTOREMODE'] ?? '');
$log_meta_type = restore_type_label($vars['RESTORETYPE'] ?? '');
$log_meta_target = display_restore_path($vars['TARGETPATH'] ?? '');
if ($log_meta_path === '') {
$log_meta_path = '-';
}
if ($log_meta_mode === '') {
$log_meta_mode = '-';
}
if ($log_meta_type === '') {
$log_meta_type = '-';
}
if ($log_meta_target === '') {
$log_meta_target = '-';
}
$log_text = is_file($log_path) && is_readable($log_path) ? (string)file_get_contents($log_path) : '';
$log_text = normalize_log($log_text);
$last_line = last_non_empty_line($log_text);
$progress = extract_progress($last_line);
$base_status = get_job_status($job_id, $job_base);
$display = display_status($base_status, $last_line);
$status_label = status_label($display);
if (in_array($display, ['done', 'failed', 'canceled'], true)) {
$progress_text = $display === 'done' ? '100%' : '0%';
} else {
$progress_text = $progress !== null ? ($progress . '%') : ($last_line !== '' ? $last_line : '-');
}
$header = "Data rozpoczęcia: {$log_meta_start}\n";
$header .= "Rodzaj przywracanych danych: {$log_meta_type}\n";
$header .= "Ścieżka do przywrócenia: {$log_meta_path}\n";
$header .= "Tryb przywracania: {$log_meta_mode}\n";
$header .= "Lokalizacja docelowa: {$log_meta_target}\n";
$header .= "Status: {$status_label}\n";
$header .= "Postęp: {$progress_text}\n";
$header .= "----- logi Borg -----\n";
$body = $log_text !== '' ? format_log_body_for_download($log_text) : "Log nie istnieje.\n";
$explanation = log_missing_include_explanation($log_text, $log_meta_path);
if ($explanation !== '') {
$body = rtrim($body) . "\n\n" . $explanation . "\n";
}
return $header . $body;
}
function ajax_output(array $payload): void
{
global $lang_map;
header('Content-Type: text/plain; charset=utf-8');
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
echo '__BORG_AJAX_JSON__';
if ($json === false) {
$json = '{}';
}
echo translate_text($json, $lang_map ?? []);
echo '__BORG_AJAX_JSON__';
exit;
}
function log_text_output(string $text): void
{
header('Content-Type: text/plain; charset=utf-8');
echo '__BORG_LOG_TEXT__';
echo $text;
echo '__BORG_LOG_TEXT__';
exit;
}
function status_html_output(string $html): void
{
header('Content-Type: text/plain; charset=utf-8');
echo '__BORG_STATUS_HTML__';
echo $html;
echo '__BORG_STATUS_HTML__';
exit;
}
function delete_job_assets(string $file, string $job_base, string $log_dir, string $pid_dir, string $cancel_dir): bool
{
$file = basename($file);
if ($file === '' || !preg_match('/^[-A-Za-z0-9_.]+$/', $file)) {
return false;
}
$job_id = job_id_from_file($file);
if ($job_id === '') {
return false;
}
$deleted = false;
foreach (['pending', 'processing', 'done'] as $dir) {
$dir_path = $job_base . '/' . $dir;
foreach (glob($dir_path . '/' . $job_id . '*') ?: [] as $path) {
if (is_file($path)) {
@unlink($path);
$deleted = true;
}
}
$exact = $dir_path . '/' . $file;
if (is_file($exact)) {
@unlink($exact);
$deleted = true;
}
}
$log_path = $log_dir . '/' . $job_id . '.log';
if (is_file($log_path)) {
@unlink($log_path);
}
$pid_path = $pid_dir . '/' . $job_id . '.pid';
if (is_file($pid_path)) {
@unlink($pid_path);
}
$cancel_path = $cancel_dir . '/' . $job_id . '.flag';
if (is_file($cancel_path)) {
@unlink($cancel_path);
}
return $deleted;
}
function list_jobs(string $dir, string $status, string $pattern, string $log_dir, string $pid_dir, string $cancel_dir, string $da_user): array
{
$items = [];
foreach (glob($dir . '/' . $pattern) ?: [] as $path) {
$vars = load_job_vars($path);
$owner = (string)($vars['DA_USER'] ?? '');
if ($owner === '' || $owner !== $da_user) {
continue;
}
$job_id = job_id_from_file($path);
$log_path = $log_dir . '/' . $job_id . '.log';
$cancel_requested = is_file($cancel_dir . '/' . $job_id . '.flag');
$pid = '';
if ($status === 'processing') {
$pid_path = $pid_dir . '/' . $job_id . '.pid';
if (is_readable($pid_path)) {
$pid = trim((string)file_get_contents($pid_path));
} elseif (!empty($vars['PID'])) {
$pid = (string)$vars['PID'];
}
if ($pid !== '' && !preg_match('/^[0-9]+$/', $pid)) {
$pid = '';
}
if ($pid !== '' && function_exists('posix_kill')) {
if (!@posix_kill((int)$pid, 0)) {
$pid = '';
}
}
}
$progress_text = '-';
$display_status = $status;
if ($status === 'processing' && is_readable($log_path)) {
$info = get_progress_info($log_path);
$display_status = display_status($status, $info['last']);
if ($cancel_requested) {
$display_status = 'canceled';
$progress_text = 'Anulowane';
} elseif ($display_status === 'calculating') {
$progress_text = 'Przygotowanie';
} elseif ($info['progress'] !== null) {
$progress_text = $info['progress'] . '%';
} elseif ($info['last'] !== '') {
$progress_text = $info['last'];
}
} elseif ($status === 'done') {
$progress_text = '100%';
} elseif ($status === 'failed') {
$progress_text = 'Błąd';
} elseif ($status === 'canceled') {
$progress_text = 'Anulowane';
}
$items[] = [
'status' => $status,
'display_status' => $display_status,
'file' => basename($path),
'time' => @filemtime($path) ?: 0,
'user' => $vars['DA_USER'] ?? '',
'repo' => $vars['REPONAME'] ?? '',
'path' => $vars['USERRESTOREPATH'] ?? '',
'target' => $vars['TARGETPATH'] ?? '',
'restore_mode' => $vars['RESTOREMODE'] ?? '',
'restore_mode_label' => restore_mode_label($vars['RESTOREMODE'] ?? ''),
'restore_type' => $vars['RESTORETYPE'] ?? '',
'restore_type_label' => restore_type_label($vars['RESTORETYPE'] ?? ''),
'job_id' => $job_id,
'log_exists' => is_readable($log_path),
'progress_text' => $progress_text,
'pid' => $pid,
'cancel_requested' => $cancel_requested,
];
}
return $items;
}
function h(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
function status_label(string $status): string
{
$map = [
'pending' => 'Oczekuje',
'processing' => 'W trakcie',
'calculating' => 'Przygotowanie',
'done' => 'Zakończone',
'failed' => 'Błąd',
'canceled' => 'Anulowane',
];
$label = $map[$status] ?? 'Nieznany';
return t($label);
}
function read_log_tail(string $path, int $max_bytes = 200000): string
{
if (!is_readable($path)) {
return '';
}
$size = filesize($path);
if ($size === false || $size <= $max_bytes) {
return (string)file_get_contents($path);
}
$fp = fopen($path, 'rb');
if ($fp === false) {
return '';
}
fseek($fp, -$max_bytes, SEEK_END);
$data = fread($fp, $max_bytes);
fclose($fp);
return (string)$data;
}
function normalize_log(string $text): string
{
$text = str_replace("\r", "\n", $text);
$lines = preg_split('/\n/', $text);
if (!is_array($lines)) {
return $text;
}
$filtered = [];
foreach ($lines as $line) {
$trimmed = trim($line);
if ($trimmed !== '' && stripos($trimmed, 'Permanently added') !== false && stripos($trimmed, 'known host') !== false) {
continue;
}
$filtered[] = $line;
}
return implode("\n", $filtered);
}
function last_non_empty_line(string $text): string
{
$lines = preg_split('/\r\n|\n|\r/', $text);
if (!is_array($lines)) {
return '';
}
for ($i = count($lines) - 1; $i >= 0; $i--) {
$line = trim((string)$lines[$i]);
if ($line !== '') {
return $line;
}
}
return '';
}
function is_calculating_line(string $line): bool
{
return (bool)preg_match('/calculat/i', $line);
}
function extract_progress(string $line): ?int
{
if (preg_match('/\b(\d{1,3})%\b/', $line, $m)) {
$p = (int)$m[1];
if ($p < 0) {
$p = 0;
} elseif ($p > 100) {
$p = 100;
}
return $p;
}
return null;
}
function get_job_status(string $job_id, string $job_base): string
{
if ($job_id === '') {
return 'unknown';
}
if (is_file($job_base . '/processing/' . $job_id . '.env')) {
return 'processing';
}
if (is_file($job_base . '/pending/' . $job_id . '.env')) {
return 'pending';
}
if (is_file($job_base . '/done/' . $job_id . '.ok') || is_file($job_base . '/done/' . $job_id . '.env.ok')) {
return 'done';
}
if (is_file($job_base . '/done/' . $job_id . '.fail') || is_file($job_base . '/done/' . $job_id . '.env.fail')) {
return 'failed';
}
if (is_file($job_base . '/done/' . $job_id . '.cancel') || is_file($job_base . '/done/' . $job_id . '.env.cancel')) {
return 'canceled';
}
return 'unknown';
}
function get_progress_info(string $log_path): array
{
$text = normalize_log(read_log_tail($log_path, 20000));
$last = last_non_empty_line($text);
$progress = extract_progress($last);
$calculating = $last !== '' && is_calculating_line($last);
return [
'last' => $last,
'progress' => $progress,
'calculating' => $calculating,
];
}
function display_status(string $base_status, string $last_line): string
{
if ($base_status === 'processing' && $last_line !== '' && is_calculating_line($last_line)) {
return 'calculating';
}
return $base_status;
}
function apply_da_user(string $value, string $da_user): string
{
return str_replace('DA_USER', $da_user, $value);
}
function restore_mode_label(string $mode): string
{
if ($mode === 'overwrite') {
return t('Nadpisz aktualne dane');
}
if ($mode === 'update') {
return t('Scalenie');
}
if ($mode === 'location') {
return t('Przywracanie do lokalizacji');
}
return t('-');
}
function restore_mode_label_form(string $mode): string
{
if ($mode === 'overwrite') {
return t('Nadpisz aktualne dane');
}
if ($mode === 'update') {
return t('Połączenie danych kopii zapasowej z obecnymi danymi');
}
if ($mode === 'location') {
return t('Przywracanie do lokalizacji');
}
return t('-');
}
function restore_type_label(string $type): string
{
if ($type === 'mail') {
return t('Zawartość skrzynki pocztowej');
}
if ($type === 'web') {
return t('Pliki www');
}
return t('-');
}
function format_start_time($value, int $fallback): string
{
$ts = is_numeric($value) ? (int)$value : 0;
if ($ts <= 0) {
$ts = $fallback > 0 ? $fallback : time();
}
return date('d-m-Y-H-i-s', $ts);
}
function parse_bool_setting(?string $value, bool $default): bool
{
if ($value === null) {
return $default;
}
$value = strtolower(trim($value));
if ($value === '') {
return $default;
}
if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
return true;
}
if (in_array($value, ['0', 'false', 'no', 'off'], true)) {
return false;
}
return $default;
}
function display_restore_path(string $value): string
{
$trimmed = trim($value);
if ($trimmed === '') {
return '';
}
if ($trimmed[0] !== '/') {
$trimmed = '/' . $trimmed;
}
return $trimmed;
}
function append_date_to_last_dir(string $path, string $date): string
{
$path = rtrim($path, '/');
if ($path === '' || $path === '/') {
return $path;
}
$parent = dirname($path);
$base = basename($path);
if ($base === '' || $base === '.' || $base === '..') {
return $path;
}
if ($parent === '/' || $parent === '.') {
return '/' . $base . '_' . $date;
}
return $parent . '/' . $base . '_' . $date;
}
function describe_overwrite_backup(string $restore_path_raw): string
{
$restore_path = trim($restore_path_raw, "/");
if ($restore_path === '') {
return 'Backup zostanie utworzony jako *_przed_przywroceniem_' . date('d-m-Y_His') . ' w katalogu nadrzędnym ścieżki do przywrócenia.';
}
$full = '/' . $restore_path;
$base = basename($full);
$parent = dirname($full);
if ($base === '' || $parent === '' || $parent === '.') {
return 'Backup zostanie utworzony jako *_przed_przywroceniem_' . date('d-m-Y_His') . ' w katalogu nadrzędnym ścieżki do przywrócenia.';
}
$backup = $parent . '/' . $base . '_przed_przywroceniem_' . date('d-m-Y_His');
return 'Backup zostanie utworzony jako: ' . $backup . '.';
}
function render_confirm_overwrite_html(string $restore_path_raw, string $selected_archive, string $target_path_raw, string $restore_mode, string $restore_type, bool $ajax, string $csrf_token): string
{
$mode_label = restore_mode_label_form($restore_mode);
$title = $restore_mode === 'update' ? 'Potwierdź scalenie danych' : 'Potwierdź nadpisanie danych';
$warning = $restore_mode === 'update'
? 'Wybrano tryb scalenia danych. Istniejący katalog zostanie połączony z danymi z backupu.'
: 'Wybrano tryb nadpisywania danych. Istniejący katalog zostanie zastąpiony.';
$loss_warning = '';
if ($restore_mode === 'overwrite' && $restore_type === 'mail') {
$path_display = $restore_path_raw !== '' ? display_restore_path($restore_path_raw) : '-';
$loss_warning = 'DANE W KATALOGU ' . $path_display . ' ZOSTANĄ UTRACONE.';
}
ob_start();
?>
<div class="br-overlay">
<div class="br-modal br-modal-danger br-modal-warning">
<h3><?php echo h($title); ?></h3>
<p><strong>Uwaga:</strong> <?php echo h($warning); ?></p>
<?php if ($loss_warning !== '') { ?>
<p style="color:#b00020;font-weight:bold;"><?php echo h($loss_warning); ?></p>
<?php } ?>
<p><strong>Tryb przywracania:</strong> <?php echo h($mode_label); ?></p>
<p><strong>Przywracana ścieżka:</strong> <?php echo h(($restore_path_raw !== '' ? display_restore_path($restore_path_raw) : '-')); ?></p>
<p><?php echo h(describe_overwrite_backup($restore_path_raw)); ?></p>
<form method="post" class="br-confirm-form" data-ajax="<?php echo $ajax ? '1' : '0'; ?>">
<input type="hidden" name="action" value="restore" />
<input type="hidden" name="confirm_overwrite" value="1" />
<input type="hidden" name="csrf_token" value="<?php echo h($csrf_token); ?>" />
<?php if ($ajax) { ?>
<input type="hidden" name="ajax" value="1" />
<?php } ?>
<input type="hidden" name="archive" value="<?php echo h($selected_archive); ?>" />
<input type="hidden" name="restore_mode" value="<?php echo h($restore_mode); ?>" />
<input type="hidden" name="restore_type" value="<?php echo h($restore_type); ?>" />
<input type="hidden" name="restore_path" value="<?php echo h($restore_path_raw); ?>" />
<input type="hidden" name="target_path" value="<?php echo h($target_path_raw); ?>" />
<div class="br-modal-actions">
<a class="br-btn br-btn-ghost" href="/CMD_PLUGINS/borg-restore" data-close-overlay="1">Nie</a>
<button class="br-btn br-btn-danger" type="submit">Tak, kontynuuj</button>
</div>
</form>
</div>
</div>
<?php
return trim((string)ob_get_clean());
}
function render_log_view_html(string $view_log_job, string $log_meta_start, string $log_meta_type, string $log_meta_path, string $log_meta_mode, string $log_meta_target, string $log_status_value, ?int $log_progress, string $log_last_line, string $log_message, string $status, bool $overlay): string
{
ob_start();
if ($overlay) {
?>
<div class="br-overlay">
<div class="br-modal br-modal-log">
<?php
}
?>
<div class="br-log-header">
<h3>Log zadania</h3>
<div class="br-log-header-actions">
<button class="br-icon-btn" type="button" onclick="copyLogToClipboard();" title="Kopiuj do schowka" aria-label="Kopiuj do schowka">📋</button>
<?php if ($overlay) { ?>
<button class="br-icon-btn" type="button" data-close-overlay="1" title="Zamknij" aria-label="Zamknij"></button>
<?php } else { ?>
<a class="br-icon-btn" href="/CMD_PLUGINS/borg-restore" title="Zamknij" aria-label="Zamknij"></a>
<?php } ?>
</div>
</div>
<div class="br-modal-scroll">
<p class="br-muted">Podgląd na żywo (odświeżanie co 3s)</p>
<div class="br-log-meta">
<div><strong>Data rozpoczęcia:</strong> <?php echo h($log_meta_start); ?></div>
<div><strong>Rodzaj przywracanych danych:</strong> <?php echo h($log_meta_type); ?></div>
<div><strong>Ścieżka do przywrócenia:</strong> <?php echo h($log_meta_path); ?></div>
<div><strong>Tryb przywracania:</strong> <?php echo h($log_meta_mode); ?></div>
<div><strong>Lokalizacja docelowa:</strong> <?php echo h($log_meta_target); ?></div>
</div>
<div class="br-status-line">Status: <strong id="log-status"><?php echo h($log_status_value); ?></strong></div>
<div class="br-status-line">Postęp: <span id="log-progress-text"><?php echo h($log_progress !== null ? ($log_progress . '%') : $log_last_line); ?></span></div>
<div class="br-progress">
<div id="log-progress-bar" class="br-progress-bar" style="width: <?php echo h($log_progress !== null ? (string)$log_progress : '0'); ?>%;"></div>
</div>
<div class="br-log-actions">
<?php if ($view_log_job !== '' && in_array($status, ['done', 'failed', 'canceled'], true)) { ?>
<button class="br-btn br-btn-ghost" type="button" onclick="downloadLog('<?php echo h($view_log_job); ?>');">Zapisz log</button>
<?php } ?>
</div>
<pre id="log-content"><?php echo h($log_message); ?></pre>
</div>
<?php
if ($overlay) {
?>
</div>
</div>
<?php
}
return trim((string)ob_get_clean());
}
function normalize_picker_base(string $base): string
{
$base = trim($base);
if ($base === '') {
return '/';
}
if ($base[0] !== '/') {
$base = '/' . $base;
}
if ($base !== '/' && substr($base, -1) === '/') {
$base = rtrim($base, '/');
}
return $base === '' ? '/' : $base;
}
function resolve_picker_path(string $requested, string $base): array
{
$base = normalize_picker_base($base);
$candidate = trim($requested);
if ($candidate === '') {
$candidate = $base;
}
if ($candidate[0] !== '/') {
$candidate = '/' . $candidate;
}
$resolved = @realpath($candidate);
if ($resolved === false || !is_dir($resolved)) {
$resolved = @realpath($base);
}
if ($resolved === false || !is_dir($resolved)) {
return [$base, $base, false];
}
if ($base !== '/' && strpos($resolved, $base . '/') !== 0 && $resolved !== $base) {
$resolved = @realpath($base);
}
if ($resolved === false || !is_dir($resolved)) {
return [$base, $base, false];
}
return [$resolved, $base, true];
}
function picker_output(array $payload): void
{
$json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if ($json === false) {
$json = json_encode(['ok' => false, 'error' => 'json_encode_failed'], JSON_UNESCAPED_UNICODE);
}
$marker = '__BORG_PICKER_JSON__';
header('Content-Type: text/plain; charset=utf-8');
echo $marker . "\n" . $json . "\n" . $marker;
}
function picker_base_for_scope(array $params, string $web_location, string $mail_location, bool $block_restore_outside_location, string $da_user): string
{
$restore_type = strtolower((string)($params['restore_type'] ?? 'web'));
if ($restore_type !== 'mail') {
$restore_type = 'web';
}
$scope = strtolower((string)($params['scope'] ?? 'restore'));
$base = $restore_type === 'mail' ? $mail_location : $web_location;
if ($scope === 'target') {
$home_base = '/home';
if ($da_user !== '') {
$home_base .= '/' . $da_user;
}
return $home_base;
}
if ($scope !== 'restore' || !$block_restore_outside_location) {
return '';
}
return $base;
}
function file_picker_handle(array $params, string $path_prefix): void
{
$base = $path_prefix !== '' ? $path_prefix : '/';
$mode = strtolower((string)($params['mode'] ?? 'dir'));
$mode = ($mode === 'file') ? 'file' : 'dir';
$requested = trim((string)($params['path'] ?? ''));
if ($requested !== '' && $requested[0] !== '/') {
$requested = '/' . $requested;
}
$selected = '';
if ($mode === 'file' && $requested !== '') {
$real_req = @realpath($requested);
if ($real_req !== false && is_file($real_req)) {
$selected = $real_req;
$requested = dirname($real_req);
}
}
[$resolved, $base, $ok] = resolve_picker_path($requested, $base);
if ($base !== '/' && $selected !== '') {
if (strpos($selected, $base . '/') !== 0 && $selected !== $base) {
$selected = '';
}
}
$dir_items = [];
$file_items = [];
if ($ok && is_dir($resolved)) {
$entries = @scandir($resolved);
if (is_array($entries)) {
foreach ($entries as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$full = $resolved . '/' . $entry;
if (@is_dir($full)) {
$dir_items[] = $entry;
} elseif ($mode === 'file' && @is_file($full)) {
$file_items[] = $entry;
}
}
}
}
natcasesort($dir_items);
natcasesort($file_items);
$items = [];
foreach ($dir_items as $name) {
$items[] = ['name' => $name, 'type' => 'dir'];
}
foreach ($file_items as $name) {
$items[] = ['name' => $name, 'type' => 'file'];
}
picker_output([
'ok' => $ok,
'path' => $resolved,
'base' => $base,
'items' => $items,
'selected' => $selected,
]);
}
function file_picker_create(array $params, string $path_prefix, string $da_user, bool $prevent_main_domains, bool $prevent_main_imap): void
{
$base = $path_prefix !== '' ? $path_prefix : '/';
$parent_req = trim((string)($params['path'] ?? ''));
if ($parent_req !== '' && $parent_req[0] !== '/') {
$parent_req = '/' . $parent_req;
}
[$parent, $base, $ok] = resolve_picker_path($parent_req, $base);
$name = trim((string)($params['name'] ?? ''));
if ($name === '' || $name === '.' || $name === '..' || strpos($name, '/') !== false || strpos($name, '\\') !== false) {
picker_output(['ok' => false, 'error' => 'invalid_name']);
return;
}
if (!$ok || !is_dir($parent)) {
picker_output(['ok' => false, 'error' => 'invalid_parent']);
return;
}
if ($da_user !== '') {
$parent_real = @realpath($parent);
if ($parent_real !== false) {
$domains_root = '/home/' . $da_user . '/domains';
$imap_root = '/home/' . $da_user . '/imap';
if ($prevent_main_domains && $parent_real === $domains_root) {
picker_output(['ok' => false, 'error' => 'main_domain_blocked']);
return;
}
if ($prevent_main_imap && $parent_real === $imap_root) {
picker_output(['ok' => false, 'error' => 'main_imap_blocked']);
return;
}
}
}
$new_path = rtrim($parent, '/') . '/' . $name;
if ($base !== '/' && strpos($new_path, $base . '/') !== 0 && $new_path !== $base) {
picker_output(['ok' => false, 'error' => 'outside_base']);
return;
}
if (file_exists($new_path)) {
picker_output(['ok' => false, 'error' => 'exists']);
return;
}
if (!@mkdir($new_path, 0700, true)) {
picker_output(['ok' => false, 'error' => 'mkdir_failed']);
return;
}
@chown($new_path, $da_user);
@chgrp($new_path, $da_user);
picker_output(['ok' => true, 'path' => $parent, 'base' => $base, 'created' => $new_path]);
}
function render_jobs_section(array $jobs, string $csrf_token, bool $enable_logs_overlay): string
{
ob_start();
if (!empty($jobs)) {
?>
<form method="post" onsubmit="return confirm('<?php echo h(t('Czy na pewno usunąć zaznaczone zdarzenia?')); ?>');">
<input type="hidden" name="action" value="delete_selected" />
<input type="hidden" name="csrf_token" value="<?php echo h($csrf_token); ?>" />
<table class="br-table" border="1" cellpadding="6" cellspacing="0">
<thead>
<tr>
<th class="br-col-select">
<input type="checkbox" id="jobs_select_all" onclick="toggleSelectAll(this);" title="<?php echo h(t('Zaznacz wszystkie')); ?>" />
<?php echo h(t('PID')); ?>
</th>
<th class="br-col-status"><?php echo h(t('Status')); ?></th>
<th class="br-col-progress"><?php echo h(t('Postęp')); ?></th>
<th class="br-col-type"><?php echo h(t('Rodzaj przywracanych danych')); ?></th>
<th class="br-col-mode"><?php echo h(t('Tryb przywracania')); ?></th>
<th class="br-col-user"><?php echo h(t('Użytkownik')); ?></th>
<th class="br-col-backup"><?php echo h(t('Backup')); ?></th>
<th class="br-col-path"><?php echo h(t('Ścieżka')); ?></th>
<th class="br-col-target"><?php echo h(t('Cel')); ?></th>
<th class="br-col-time"><?php echo h(t('Czas')); ?></th>
<th class="br-col-actions"><?php echo h(t('Akcje')); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($jobs as $job) { ?>
<tr data-job-id="<?php echo h($job['job_id']); ?>" data-status="<?php echo h($job['display_status'] ?? $job['status']); ?>">
<td class="br-col-select">
<input type="checkbox" name="jobs[]" value="<?php echo h($job['file']); ?>" />
<div class="br-muted" style="font-size:12px;"><?php echo h($job['pid'] !== '' ? $job['pid'] : '-'); ?></div>
</td>
<td><?php echo h(status_label($job['display_status'] ?? $job['status'])); ?></td>
<td><?php echo h($job['progress_text'] ?? '-'); ?></td>
<td><?php echo h($job['restore_type_label'] ?? '-'); ?></td>
<td><?php echo h($job['restore_mode_label'] ?? '-'); ?></td>
<td><?php echo h($job['user']); ?></td>
<td class="br-col-backup"><?php echo h($job['repo']); ?></td>
<td class="br-col-path"><?php echo h($job['path']); ?></td>
<td class="br-col-target"><?php echo h($job['target']); ?></td>
<td><?php echo h($job['time'] ? date('Y-m-d H:i:s', $job['time']) : ''); ?></td>
<td>
<?php
$actions = [];
if ($job['log_exists'] || $job['status'] === 'processing') {
if ($enable_logs_overlay) {
$safe_job = str_replace("'", "\\'", $job['file']);
$actions[] = '<a href="/CMD_PLUGINS/borg-restore?action=view_log&job=' . h($job['file']) . '" onclick="if (typeof openLogOverlay === \'function\') { openLogOverlay(\'' . h($safe_job) . '\'); return false; }">' . h(t('Logi')) . '</a>';
} else {
$actions[] = '<a href="/CMD_PLUGINS/borg-restore?action=view_log&job=' . h($job['file']) . '">' . h(t('Logi')) . '</a>';
}
}
if (in_array($job['status'], ['done', 'failed', 'canceled'], true) && $job['log_exists']) {
$actions[] = '<a href="#" onclick="downloadLog(\'' . h($job['file']) . '\'); return false;">' . h(t('Zapisz log')) . '</a>';
}
if (($job['status'] === 'processing' && empty($job['cancel_requested'])) || $job['status'] === 'pending' || $job['status'] === 'calculating') {
$actions[] = '<form method="post" class="br-inline-form" onsubmit="return confirm(\'' . h(t('Anulować zadanie?')) . '\');">'
. '<input type="hidden" name="action" value="cancel_job" />'
. '<input type="hidden" name="job" value="' . h($job['file']) . '" />'
. '<input type="hidden" name="csrf_token" value="' . h($csrf_token) . '" />'
. '<button type="submit" class="br-link-button">' . h(t('Anuluj')) . '</button>'
. '</form>';
}
if ($job['log_exists'] || $job['status'] === 'processing') {
$actions[] = '<form method="post" class="br-inline-form" onsubmit="return confirm(\'' . h(t('Usunąć zadanie?')) . '\');">'
. '<input type="hidden" name="action" value="delete_job" />'
. '<input type="hidden" name="job" value="' . h($job['file']) . '" />'
. '<input type="hidden" name="csrf_token" value="' . h($csrf_token) . '" />'
. '<button type="submit" class="br-link-button br-link-danger">' . h(t('Usuń')) . '</button>'
. '</form>';
}
echo !empty($actions) ? implode(' | ', $actions) : '-';
?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<div class="br-actions-row">
<button class="br-btn br-btn-danger" type="submit"><?php echo h(t('Usuń zaznaczone zdarzenia')); ?></button>
</div>
</form>
<?php
} else {
?>
<p class="br-jobs-empty"><?php echo h(t('Brak zadań do wyświetlenia.')); ?></p>
<?php
}
return ob_get_clean();
}
function render_system_error_section(string $message, array $details, bool $debug_mode): string
{
ob_start();
?>
<div class="br-alert br-alert-error">
<p><?php echo h($message); ?></p>
<?php if ($debug_mode && !empty($details)) { ?>
<pre class="br-error-debug"><?php echo h(implode("\n", $details)); ?></pre>
<?php } ?>
</div>
<?php
return (string)ob_get_clean();
}
function load_user_skin(string $user): string
{
$path = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
if (!is_readable($path)) {
return 'enhanced';
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return 'enhanced';
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^skin=(.+)$/', $line, $m)) {
$skin = strtolower(trim($m[1]));
if ($skin === 'evolution') {
return 'evolution';
}
if ($skin === 'enhanced') {
return 'enhanced';
}
return $skin;
}
}
return 'enhanced';
}
function normalize_lang_code(string $value): string
{
$value = strtolower(trim($value));
if ($value === '') {
return 'en';
}
if (strpos($value, 'pl') === 0 || strpos($value, 'polish') !== false) {
return 'pl';
}
if (strpos($value, 'en') === 0 || strpos($value, 'english') !== false) {
return 'en';
}
return 'en';
}
function load_user_language(string $user): string
{
$path = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
if (!is_readable($path)) {
return 'en';
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return 'en';
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(?:lang|language|locale)=(.+)$/i', $line, $m)) {
return normalize_lang_code($m[1]);
}
}
return 'en';
}
function load_lang_map(string $lang, string $plugin_dir): array
{
$lang = normalize_lang_code($lang);
$lang_dir = $plugin_dir . '/lang';
$path = $lang_dir . '/' . $lang . '.php';
$fallback = $lang_dir . '/en.php';
$map = [];
if (is_readable($path)) {
$map = include $path;
} elseif (is_readable($fallback)) {
$map = include $fallback;
}
return is_array($map) ? $map : [];
}
function translate_text(string $text, array $map): string
{
if (empty($map)) {
return $text;
}
uksort($map, function ($a, $b) {
return strlen($b) <=> strlen($a);
});
return str_replace(array_keys($map), array_values($map), $text);
}
function translate_html_safe(string $html, array $map): string
{
if (empty($map)) {
return $html;
}
$parts = preg_split('~(<script\\b[^>]*>.*?</script>)~is', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
if ($parts === false) {
return translate_text($html, $map);
}
$out = '';
foreach ($parts as $part) {
if (preg_match('~^<script\\b~i', $part)) {
$out .= $part;
} else {
$out .= translate_text($part, $map);
}
}
return $out;
}
function t(string $text): string
{
global $lang_map;
if (empty($lang_map)) {
return $text;
}
return $lang_map[$text] ?? $text;
}
$params = read_params();
$action = $params['action'] ?? '';
$da_user = getenv('USERNAME');
if ($da_user === false || $da_user === '') {
$da_user = getenv('LOGNAME') ?: getenv('USER') ?: 'unknown';
}
$lang_code = load_user_language($da_user);
$plugin_dir = realpath(__DIR__ . '/..') ?: '/usr/local/directadmin/plugins/borg-restore';
$plugin_data_dir = $plugin_dir . '/data';
if (!is_dir($plugin_data_dir)) {
@mkdir($plugin_data_dir, 0700, true);
}
$plugin_log = $plugin_data_dir . '/plugin.log';
$lang_map = load_lang_map($lang_code, $plugin_dir);
ini_set('log_errors', '1');
ini_set('error_log', $plugin_log);
$request_line = date('Y-m-d H:i:s') . ' ' . ($_SERVER['REQUEST_METHOD'] ?? '') . ' ' . ($_SERVER['REQUEST_URI'] ?? '');
$request_line .= ' action=' . ($action !== '' ? $action : '-');
$request_line .= ' qs=' . ($_SERVER['QUERY_STRING'] ?? '');
@file_put_contents($plugin_log, $request_line . "\n", FILE_APPEND);
$method = $_SERVER['REQUEST_METHOD'] ?? '';
$csrf_token = get_csrf_token($plugin_data_dir, $da_user);
$csrf_actions = ['restore', 'delete_selected', 'delete_job', 'cancel_job', 'download_log', 'dir_create', 'clear_jobs'];
$csrf_required = in_array($action, $csrf_actions, true);
if ($csrf_required && $method !== 'POST') {
header('HTTP/1.1 405 Method Not Allowed');
echo t('Method Not Allowed.');
exit;
}
$csrf_valid = !$csrf_required || csrf_is_valid($params, $csrf_token);
if ($csrf_required && !$csrf_valid && $action !== 'restore') {
header('HTTP/1.1 403 Forbidden');
echo t('Nieprawidłowy token CSRF.');
exit;
}
$skin = load_user_skin($da_user);
$is_evolution = ($skin === 'evolution');
$css_file = $skin === 'evolution' ? 'evolution.css' : 'enhanced.css';
$settings_path = $plugin_dir . '/plugin-settings.conf';
$settings = load_settings($settings_path);
if ($action === 'css') {
$skin_req = strtolower((string)($params['skin'] ?? ''));
if ($skin_req !== 'evolution' && $skin_req !== 'enhanced') {
$skin_req = 'enhanced';
}
$css_path = $plugin_dir . '/user/' . $skin_req . '.css';
if (!is_readable($css_path)) {
header('HTTP/1.1 404 Not Found');
exit;
}
header('Content-Type: text/css; charset=utf-8');
readfile($css_path);
exit;
}
$inline_css = '';
$css_path = $plugin_dir . '/user/' . $css_file;
if (is_readable($css_path)) {
$inline_css = (string)file_get_contents($css_path);
} else {
@file_put_contents($plugin_log, date('Y-m-d H:i:s') . " css_missing=" . $css_path . "\n", FILE_APPEND);
}
$settings_errors = [];
if (!is_readable($settings_path)) {
$settings_errors[] = 'Brak pliku plugin-settings.conf.';
}
$bb_path = $settings['BB_PATH'] ?? '';
$borg_bin = $settings['BORG_PATH'] ?? '';
$borg_vars_file = $settings['BORG_VARIABLES_FILE'] ?? '';
$web_restore_location = $settings['WEB_RESTORE_LOCATION'] ?? '/home/DA_USER/domains';
$mail_restore_location = $settings['MAIL_RESTORE_LOCATION'] ?? '/home/DA_USER/imap';
$enable_web_restore = parse_bool((string)($settings['ENABLE_WEB_RESTORE'] ?? 'TRUE'), true);
$enable_mailbox_restore = parse_bool((string)($settings['ENABLE_MAILBOX_RESTORE'] ?? 'TRUE'), true);
$default_restore_destination = $settings['DEFAULT_RESTORE_DESTINATION'] ?? '/home/DA_USER/bb';
$block_restore_outside_location = parse_bool((string)($settings['BLOCK_RESTORE_OUTSIDE_LOCATION'] ?? 'TRUE'), true);
$prevent_restore_main_domains_dir = parse_bool((string)($settings['PREVENT_RESTORE_MAIN_DOMAINS_DIR'] ?? 'TRUE'), true);
$prevent_restore_main_imap_dir = parse_bool((string)($settings['PREVENT_RESTORE_MAIN_IMAP_DIR'] ?? 'TRUE'), true);
$prevent_create_dirs_in_main_domain = parse_bool((string)($settings['PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN'] ?? 'TRUE'), true);
$prevent_create_dirs_in_main_imap = parse_bool((string)($settings['PREVENT_CREATE_DIRS_IN_MAIN_IMAP'] ?? 'TRUE'), true);
$hash_visibility = parse_bool((string)($settings['HASH_VISIBILITY'] ?? 'FALSE'), false);
$enable_logs_overlay = parse_bool((string)($settings['ENABLE_LOGS_OVERLAY'] ?? 'TRUE'), true);
$selectable_domain_restore_destination = parse_bool((string)($settings['SELECTABLE_DOMAIN_RESTORE_DESTINATION'] ?? 'TRUE'), true);
$selectable_mail_restore_destination = parse_bool((string)($settings['SELECTABLE_MAIL_RESTORE_DESTINATION'] ?? 'TRUE'), true);
$restore_path_mode = strtolower(trim((string)($settings['RESTORE_PATH_MODE'] ?? 'dir')));
if ($restore_path_mode !== 'full' && $restore_path_mode !== 'dir') {
$restore_path_mode = 'full';
}
$add_date_to_restored_dir = parse_bool((string)($settings['ADD_DATE_TO_RESTORED_DIR'] ?? 'FALSE'), false);
$debug_mode = parse_bool((string)($settings['DEBUG_MODE'] ?? 'FALSE'), false);
$web_restore_location = normalize_location(apply_da_user($web_restore_location, $da_user), '/home/' . $da_user . '/domains');
$mail_restore_location = normalize_location(apply_da_user($mail_restore_location, $da_user), '/home/' . $da_user . '/imap');
$default_restore_destination = normalize_location(apply_da_user($default_restore_destination, $da_user), '/home/' . $da_user . '/bb');
$borg_vars_file = trim((string)apply_da_user($borg_vars_file, $da_user));
if ($bb_path === '') {
$settings_errors[] = 'BB_PATH jest puste w plugin-settings.conf.';
}
if ($borg_bin === '') {
$settings_errors[] = 'BORG_PATH jest puste w plugin-settings.conf.';
}
if ($borg_vars_file === '') {
$settings_errors[] = 'BORG_VARIABLES_FILE jest puste w plugin-settings.conf.';
}
if ($web_restore_location === '') {
$settings_errors[] = 'WEB_RESTORE_LOCATION jest puste w plugin-settings.conf.';
}
if ($mail_restore_location === '') {
$settings_errors[] = 'MAIL_RESTORE_LOCATION jest puste w plugin-settings.conf.';
}
if ($default_restore_destination === '') {
$settings_errors[] = 'DEFAULT_RESTORE_DESTINATION jest puste w plugin-settings.conf.';
}
$vars_path = $borg_vars_file !== '' ? $borg_vars_file : $bb_path;
if ($vars_path !== '' && substr($vars_path, -3) !== '.sh') {
$vars_path = rtrim($vars_path, '/') . '/bbvars.sh';
}
$vars = load_borg_vars($vars_path);
$passphrase = $vars['BORG_PASSPHRASE'] ?? null;
$repo = $vars['REPO'] ?? null;
$borg_ssh_key = trim((string)($vars['BORG_SSH_KEY'] ?? ''));
$entries = [];
$errors = [];
$system_errors = [];
$system_debug_lines = [];
$warnings = [];
$message_status = '';
$selected_archive = $params['archive'] ?? '';
$restore_mode = $params['restore_mode'] ?? 'location';
$enabled_restore_types = [];
if ($enable_web_restore) {
$enabled_restore_types[] = 'web';
}
if ($enable_mailbox_restore) {
$enabled_restore_types[] = 'mail';
}
$show_restore_type_box = count($enabled_restore_types) > 1;
$default_restore_type = $enabled_restore_types[0] ?? 'web';
$restore_type = $params['restore_type'] ?? $default_restore_type;
if ($restore_type !== 'web' && $restore_type !== 'mail') {
$restore_type = $default_restore_type;
}
if (!in_array($restore_type, $enabled_restore_types, true)) {
$restore_type = $default_restore_type;
}
$params['restore_type'] = $restore_type;
if (!in_array($restore_mode, ['location', 'overwrite', 'update'], true)) {
$restore_mode = 'location';
}
if ($restore_type === 'mail' && !in_array($restore_mode, ['location', 'update', 'overwrite'], true)) {
$restore_mode = 'update';
}
if ($restore_type === 'web' && !in_array($restore_mode, ['location', 'overwrite'], true)) {
$restore_mode = 'location';
}
$selectable_target = $restore_type === 'mail' ? $selectable_mail_restore_destination : $selectable_domain_restore_destination;
$confirm_overwrite = (($params['confirm_overwrite'] ?? '') === '1');
$ajax_requested = (($params['ajax'] ?? '') === '1');
$confirm_overwrite_view = false;
$toast_job = '';
$restore_path_raw = $params['restore_path'] ?? '';
$target_path_raw = $params['target_path'] ?? '';
$jobs = [];
$log_message = '';
$redirect_url = '';
$queued_job = '';
$queued_message = 'Zadanie przywracania danych dodane do kolejki, zostaniesz powiadomiony przez system wiadomości po zakończeniu tego procesu.';
$confirm_view = false;
$floating_errors = [];
$view_log_job = '';
$log_status_value = '';
$log_last_line = '';
$log_progress = null;
$log_meta_start = '';
$log_meta_path = '';
$log_meta_mode = '';
$log_meta_type = '';
$log_meta_target = '';
$confirm_replace_url = '';
$toast_message = '';
$active_restore_location = $restore_type === 'mail' ? $mail_restore_location : $web_restore_location;
$job_base = '/usr/local/directadmin/plugins/borg-restore/data/jobs';
$log_dir = '/usr/local/directadmin/plugins/borg-restore/data/logs';
$pid_dir = '/usr/local/directadmin/plugins/borg-restore/data/pids';
$cancel_dir = '/usr/local/directadmin/plugins/borg-restore/data/cancel';
$security_errors = [];
$security_errors[] = validate_secure_file($settings_path, 'plugin-settings.conf');
$security_errors[] = validate_secure_file($vars_path, 'bbvars.sh', false);
$security_errors = array_filter($security_errors, static fn($value) => $value !== '');
$security_errors = array_merge($security_errors, validate_job_files($job_base));
$security_ok = empty($security_errors);
if (!$security_ok) {
$system_errors = array_merge($system_errors, $security_errors);
}
foreach ($settings_errors as $err) {
$system_errors[] = $err;
}
if ($vars_path === '' || !is_readable($vars_path)) {
$system_errors[] = 'Brak pliku bbvars.sh: ' . ($vars_path !== '' ? $vars_path : 'nie ustawiono BB_PATH');
}
if (empty($enabled_restore_types)) {
$system_errors[] = 'ENABLE_WEB_RESTORE i ENABLE_MAILBOX_RESTORE są ustawione na FALSE.';
}
if (empty($repo)) {
$system_errors[] = 'Brak zmiennej REPO w ' . $vars_path;
}
if ($borg_bin === '') {
$system_errors[] = 'BORG_PATH jest puste w plugin-settings.conf.';
} elseif (!is_executable($borg_bin)) {
$system_errors[] = 'BORG_PATH nie jest plikiem wykonywalnym: ' . $borg_bin;
}
if ($borg_ssh_key !== '' && !is_readable($borg_ssh_key)) {
$system_errors[] = 'BORG_SSH_KEY nie jest plikiem: ' . $borg_ssh_key;
}
if (!$block_restore_outside_location) {
$warnings[] = 'BLOCK_RESTORE_OUTSIDE_LOCATION jest wyłączone - ścieżka do przywrócenia nie jest ograniczona.';
}
$input_prefix = $active_restore_location !== '' ? $active_restore_location : '/home/' . $da_user;
if ($restore_path_raw === '') {
$restore_path_raw = $input_prefix . '/';
}
if ($restore_mode === 'location' && (!$selectable_target || $target_path_raw === '')) {
$target_path_raw = $default_restore_destination;
}
$ui_config_disabled_message = 'Błąd konfiguracji - skontaktuj się z supportem w celu zgłoszenia problemu';
$ui_system_error_message = 'Wystąpił problem z konfiguracją środowiska backupowego - skontaktuj się z administratorem w celu uzyskania pomocy';
$ui_locked_by_feature_flags = empty($enabled_restore_types);
$ui_locked_message = $ui_locked_by_feature_flags ? $ui_config_disabled_message : $ui_system_error_message;
$ui_locked = $ui_locked_by_feature_flags || !empty($system_errors);
$system_error_details_for_ui = [];
if ($debug_mode) {
$system_error_details_for_ui = $system_errors;
}
if ($ui_locked && in_array($action, ['dir_list', 'dir_create', 'log_stream'], true)) {
header('HTTP/1.1 403 Forbidden');
exit;
}
if ($action === 'status') {
if ($ui_locked) {
status_html_output(render_system_error_section($ui_locked_message, $system_error_details_for_ui, $debug_mode));
}
$jobs = array_merge(
list_jobs($job_base . '/pending', 'pending', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/processing', 'processing', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'done', '*.ok', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'failed', '*.fail', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'canceled', '*.cancel', $log_dir, $pid_dir, $cancel_dir, $da_user)
);
usort($jobs, function ($a, $b) {
return $b['time'] <=> $a['time'];
});
if (count($jobs) > 50) {
$jobs = array_slice($jobs, 0, 50);
}
status_html_output(render_jobs_section($jobs, $csrf_token, $enable_logs_overlay));
}
if ($action === 'log_stream') {
$file = basename((string)($params['job'] ?? ''));
if (!preg_match('/^[-A-Za-z0-9_.]+$/', $file)) {
header('HTTP/1.1 400 Bad Request');
exit;
}
$job_id = job_id_from_file($file);
$job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user);
if ($job_file_path === '') {
header('HTTP/1.1 403 Forbidden');
exit;
}
$log_path = $log_dir . '/' . $job_id . '.log';
$log_text = normalize_log(read_log_tail($log_path));
$explanation = log_missing_include_explanation($log_text);
if ($explanation !== '') {
$log_text = rtrim($log_text) . "\n\n" . $explanation;
}
$last_line = last_non_empty_line($log_text);
$progress = extract_progress($last_line);
$status = get_job_status($job_id, $job_base);
$display = display_status($status, $last_line);
if (in_array($display, ['done', 'failed', 'canceled'], true)) {
$progress = ($display === 'done') ? 100 : 0;
$progress_text = status_label($display);
} else {
$progress_text = $progress !== null ? ($progress . '%') : ($display === 'calculating' ? 'Przygotowanie' : $last_line);
}
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => $status,
'display_status' => $display,
'status_label' => status_label($status),
'display_label' => status_label($display),
'log' => $log_text,
'last_line' => $last_line,
'progress' => $progress,
'progress_text' => $progress_text,
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'dir_list') {
if (!$security_ok) {
header('HTTP/1.1 403 Forbidden');
exit;
}
$picker_base = picker_base_for_scope($params, $web_restore_location, $mail_restore_location, $block_restore_outside_location, $da_user);
file_picker_handle($params, $picker_base);
exit;
}
if ($action === 'dir_create') {
if (!$security_ok) {
header('HTTP/1.1 403 Forbidden');
exit;
}
$picker_base = picker_base_for_scope($params, $web_restore_location, $mail_restore_location, $block_restore_outside_location, $da_user);
file_picker_create($params, $picker_base, $da_user, $prevent_create_dirs_in_main_domain, $prevent_create_dirs_in_main_imap);
exit;
}
if ($action === 'restore') {
if ($ui_locked) {
$errors[] = $ui_locked_message;
}
if (!$ui_locked && !$security_ok) {
$errors[] = 'Nieprawidłowe uprawnienia plików konfiguracyjnych lub zadań. Przywracanie zablokowane.';
}
if (!$ui_locked && !$csrf_valid) {
$errors[] = 'Nieprawidłowy token CSRF.';
}
if (!$ui_locked) {
$selected_archive = trim($selected_archive);
if ($restore_mode === 'location' && !$selectable_target) {
$target_path_raw = $default_restore_destination;
}
$restore_path_raw = trim($restore_path_raw);
$target_path_raw = trim($target_path_raw);
$restore_path = trim($restore_path_raw, "/");
$target_path = trim($target_path_raw);
$restore_full_path = $restore_path !== '' ? '/' . $restore_path : '';
$target_full_path = $target_path !== '' && $target_path[0] !== '/' ? '/' . $target_path : $target_path;
if ($restore_mode === 'overwrite' || $restore_mode === 'update') {
$target_full_path = $restore_full_path;
}
if ($restore_mode === 'location' && $add_date_to_restored_dir && $target_full_path !== '') {
$date_suffix = date('d-m-Y-His');
$target_full_path = append_date_to_last_dir($target_full_path, $date_suffix);
$target_path_raw = $target_full_path;
$target_path = $target_full_path;
}
if ($selected_archive === '') {
$errors[] = 'Nie wybrano backupu.';
}
if ($restore_path === '') {
$errors[] = 'Nie podano ścieżki do przywrócenia.';
}
if ($restore_mode === 'location') {
if ($target_path === '') {
$errors[] = 'Nie podano lokalizacji docelowej.';
}
if ($target_path !== '' && $target_path[0] !== '/') {
$errors[] = 'Lokalizacja docelowa musi zaczynać się od /.';
}
}
if ($restore_path !== '' && strpos($restore_path, '..') !== false) {
$errors[] = 'Ścieżka nie może zawierać "..".';
}
if ($restore_mode === 'location') {
if ($target_path !== '' && strpos($target_path, '..') !== false) {
$errors[] = 'Lokalizacja docelowa nie może zawierać "..".';
}
}
if ($restore_mode === 'location' && $target_full_path !== '') {
$home_base = '/home/' . $da_user;
if (strpos($target_full_path, $home_base . '/') !== 0 && $target_full_path !== $home_base) {
$errors[] = 'Lokalizacja docelowa musi być w katalogu: ' . $home_base;
}
}
if ($restore_mode === 'location' && $target_full_path !== '') {
$domains_root = '/home/' . $da_user . '/domains';
$imap_root = '/home/' . $da_user . '/imap';
$parent_dir = rtrim(dirname($target_full_path), '/');
if ($prevent_create_dirs_in_main_domain && $parent_dir === $domains_root && !file_exists($target_full_path)) {
$errors[] = 'Nie można utworzyć katalogu bezpośrednio w ' . $domains_root . '.';
}
if ($prevent_create_dirs_in_main_imap && $parent_dir === $imap_root && !file_exists($target_full_path)) {
$errors[] = 'Nie można utworzyć katalogu bezpośrednio w ' . $imap_root . '.';
}
}
if ($block_restore_outside_location && $active_restore_location !== '' && $restore_full_path !== '') {
$base = rtrim($active_restore_location, '/');
if (strpos($restore_full_path, $base . '/') !== 0 && $restore_full_path !== $base) {
$errors[] = 'Ścieżka musi być w katalogu: ' . $base;
}
}
if ($restore_type === 'web' && $prevent_restore_main_domains_dir && $restore_mode === 'overwrite' && $active_restore_location !== '') {
$base = rtrim($active_restore_location, '/');
if ($restore_full_path === $base) {
$errors[] = 'Nie można przywrócić całego ' . $base . ' w trybie nadpisywania danych. Podaj podkatalog w ramach ' . $base . ' lub wybierz tryb przywracania do lokalizacji.';
}
}
if ($restore_type === 'mail' && $prevent_restore_main_imap_dir && ($restore_mode === 'overwrite' || $restore_mode === 'update') && $active_restore_location !== '') {
$base = rtrim($active_restore_location, '/');
if ($restore_full_path === $base) {
$errors[] = 'Nie można przywrócić całego ' . $base . ' w trybie nadpisywania lub scalania danych. Podaj podkatalog w ramach ' . $base . ' lub wybierz tryb przywracania do lokalizacji.';
}
}
if ($restore_mode === 'location' && $target_full_path !== '') {
if (@file_exists($target_full_path) && !@is_dir($target_full_path)) {
$errors[] = 'Lokalizacja docelowa istnieje, ale nie jest katalogiem.';
} elseif (@is_dir($target_full_path)) {
$entries = @scandir($target_full_path);
if ($entries === false) {
$errors[] = 'Nie można odczytać katalogu docelowego.';
} elseif (count($entries) > 2) {
$errors[] = 'Lokalizacja docelowa nie jest pusta.';
}
}
}
if (empty($errors)) {
if (($restore_mode === 'overwrite' || $restore_mode === 'update') && !$confirm_overwrite) {
$confirm_overwrite_view = true;
} else {
$job_dir = '/usr/local/directadmin/plugins/borg-restore/data/jobs/pending';
if (!is_dir($job_dir) && !@mkdir($job_dir, 0700, true) && !is_dir($job_dir)) {
$errors[] = 'Nie udało się utworzyć katalogu zadań.';
} else {
$job_id = date('YmdHis') . '-' . bin2hex(random_bytes(4));
$job_file = $job_dir . '/restore-' . $job_id . '.env';
$job_content = "DA_USER=" . escapeshellarg($da_user) . "\n";
$job_content .= "START_TS=" . escapeshellarg((string)time()) . "\n";
$job_content .= "REPONAME=" . escapeshellarg($selected_archive) . "\n";
$job_content .= "USERRESTOREPATH=" . escapeshellarg($restore_path) . "\n";
$job_content .= "TARGETPATH=" . escapeshellarg($target_full_path) . "\n";
$job_content .= "RESTOREMODE=" . escapeshellarg($restore_mode) . "\n";
$job_content .= "RESTORETYPE=" . escapeshellarg($restore_type) . "\n";
$job_content .= "RESTOREPATHMODE=" . escapeshellarg($restore_path_mode) . "\n";
$job_content .= "USERLANG=" . escapeshellarg($lang_code) . "\n";
$old_umask = umask(0077);
$write_ok = @file_put_contents($job_file, $job_content, LOCK_EX);
if ($old_umask !== false) {
umask($old_umask);
}
if ($write_ok === false) {
$errors[] = 'Nie udało się zapisać pliku zadania.';
} else {
@chmod($job_file, 0600);
$queued_job = 'restore-' . $job_id;
$confirm_replace_url = '/CMD_PLUGINS/borg-restore?queued=1&view=queued&job=' . rawurlencode($queued_job);
if ($ajax_requested) {
$toast_message = $queued_message;
$toast_job = $queued_job;
} elseif ($skin === 'evolution') {
$toast_message = $queued_message;
$toast_job = $queued_job;
} else {
$confirm_view = true;
}
}
}
}
}
}
}
if (!empty($errors)) {
$remaining = [];
foreach ($errors as $err) {
if ($err === 'Nie wybrano backupu.' || strpos($err, 'Nie można przywrócić całego') === 0) {
$floating_errors[] = $err;
} else {
$remaining[] = $err;
}
}
$errors = $remaining;
}
if ($action === 'restore' && $ajax_requested) {
$payload = [
'ok' => empty($errors) && !$confirm_overwrite_view,
'errors' => $errors,
'floating_errors' => $floating_errors,
];
if ($confirm_overwrite_view) {
$payload['confirm_html'] = render_confirm_overwrite_html($restore_path_raw, $selected_archive, $target_path_raw, $restore_mode, $restore_type, true, $csrf_token);
} elseif (empty($errors)) {
$payload['toast_message'] = $toast_message;
$payload['toast_job'] = $toast_job;
}
ajax_output($payload);
}
if (!$ui_locked && empty($errors) && $security_ok) {
$env = $_ENV;
if (!empty($passphrase)) {
$env['BORG_PASSPHRASE'] = $passphrase;
}
if ($borg_ssh_key !== '' && is_readable($borg_ssh_key)) {
$env['BORG_RSH'] = 'ssh -i ' . escapeshellarg($borg_ssh_key);
}
$command = $borg_bin . ' list ' . escapeshellarg((string)$repo);
$descriptors = [
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$process = @proc_open($command, $descriptors, $pipes, null, $env);
if (!is_resource($process)) {
$system_errors[] = 'Nie udało się uruchomić borg.';
} else {
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exit_code = proc_close($process);
if ($debug_mode) {
$system_debug_lines[] = '[borg list command] ' . $command;
$system_debug_lines[] = '[exit_code] ' . $exit_code;
$system_debug_lines[] = '[stdout]';
$system_debug_lines[] = trim((string)$stdout) !== '' ? trim((string)$stdout) : '(pusty)';
$system_debug_lines[] = '[stderr]';
$system_debug_lines[] = trim((string)$stderr) !== '' ? trim((string)$stderr) : '(pusty)';
}
if ($exit_code !== 0) {
$msg = trim((string)$stderr);
if ($msg === '') {
$msg = 'Kod bledu: ' . $exit_code;
}
$system_errors[] = 'Błąd borg list: ' . $msg;
} else {
$lines = preg_split('/\r?\n/', trim((string)$stdout));
foreach ($lines as $line) {
$line = trim($line);
if ($line === '') {
continue;
}
if (preg_match('/^(\\S+)\\s+(.+?)\\s+\\[([0-9a-fA-F]+)\\]\\s*$/', $line, $m)) {
$entries[] = [
'name' => $m[1],
'date' => $m[2],
'hash' => $m[3],
];
} else {
$entries[] = [
'name' => $line,
'date' => '',
'hash' => '',
];
}
}
}
}
}
if ($debug_mode) {
$system_error_details_for_ui = array_values(array_unique(array_merge($system_errors, $system_debug_lines)));
}
if (!empty($system_errors)) {
$ui_locked = true;
if (!$ui_locked_by_feature_flags) {
$ui_locked_message = $ui_system_error_message;
}
}
if ($action === 'clear_jobs' && $method === 'POST') {
$deleted = 0;
$owned_jobs = array_merge(
list_jobs($job_base . '/pending', 'pending', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/processing', 'processing', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'done', '*.ok', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'failed', '*.fail', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'canceled', '*.cancel', $log_dir, $pid_dir, $cancel_dir, $da_user)
);
foreach ($owned_jobs as $job) {
if (delete_job_assets($job['file'], $job_base, $log_dir, $pid_dir, $cancel_dir)) {
$deleted++;
}
}
$message_status = $deleted > 0 ? 'Zadania zostały wyczyszczone.' : 'Brak zadań do wyczyszczenia.';
}
if ($action === 'delete_selected' && $method === 'POST') {
$to_delete = $params['jobs'] ?? [];
if (!is_array($to_delete)) {
$to_delete = [$to_delete];
}
$deleted = 0;
foreach ($to_delete as $file) {
$file = basename((string)$file);
$job_id = job_id_from_file($file);
$job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user);
if ($job_file_path === '') {
continue;
}
if (delete_job_assets($file, $job_base, $log_dir, $pid_dir, $cancel_dir)) {
$deleted++;
}
}
$log_message = $deleted > 0 ? 'Wybrane zadania zostały usunięte.' : 'Nie wybrano żadnych zadań.';
}
if ($action === 'delete_job') {
$file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($file);
$job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user);
if ($job_file_path === '') {
$log_message = 'Brak dostępu do zadania.';
} elseif (delete_job_assets($file, $job_base, $log_dir, $pid_dir, $cancel_dir)) {
$log_message = 'Zadanie zostało usunięte.';
} else {
$log_message = 'Nie znaleziono zadania.';
}
}
if ($action === 'cancel_job') {
$file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($file);
$job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user);
$pending_file = $job_base . '/pending/' . $file;
$processing_file = $job_base . '/processing/' . $file;
$cancel_file = $cancel_dir . '/' . $job_id . '.flag';
if ($job_id === '' || !preg_match('/^[-A-Za-z0-9_.]+$/', $file)) {
$log_message = 'Nieprawidłowe zadanie.';
} elseif ($job_file_path === '') {
$log_message = 'Brak dostępu do zadania.';
} elseif (is_file($pending_file)) {
$target = $job_base . '/done/' . $job_id . '.cancel';
if (@rename($pending_file, $target)) {
$log_message = 'Zadanie zostało anulowane.';
} else {
if (delete_job_assets($file, $job_base, $log_dir, $pid_dir, $cancel_dir)) {
$log_message = 'Zadanie zostało anulowane.';
} else {
$log_message = 'Nie udało się anulować zadania.';
}
}
} elseif (!is_file($processing_file)) {
$log_message = 'Zadanie nie jest w trakcie.';
} else {
$pid = '';
$pid_path = $pid_dir . '/' . $job_id . '.pid';
if (is_readable($pid_path)) {
$pid = trim((string)file_get_contents($pid_path));
} else {
$vars = load_job_vars($processing_file);
if (!empty($vars['PID'])) {
$pid = (string)$vars['PID'];
}
}
if ($pid === '' || !preg_match('/^[0-9]+$/', $pid)) {
$log_message = 'Nie znaleziono procesu do anulowania.';
} elseif (!function_exists('posix_kill')) {
$log_message = 'Brak obsługi posix_kill na serwerze.';
} else {
$ok = @posix_kill((int)$pid, SIGTERM);
usleep(300000);
if (@posix_kill((int)$pid, 0)) {
@posix_kill((int)$pid, SIGKILL);
usleep(300000);
}
if ($ok && !@posix_kill((int)$pid, 0)) {
@file_put_contents($cancel_file, "1");
$log_message = 'Zadanie zostało anulowane.';
} else {
$log_message = 'Nie udało się anulować zadania.';
}
}
}
}
if ($action === 'download_log') {
$file = basename((string)($params['job'] ?? ''));
$raw_download = (($params['raw'] ?? '') === '1');
if (preg_match('/^[-A-Za-z0-9_.]+$/', $file)) {
$job_id = job_id_from_file($file);
$log_path = $log_dir . '/' . $job_id . '.log';
$job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user);
$filename = $job_id . '.log';
if ($job_file_path === '') {
header('HTTP/1.1 403 Forbidden');
echo t("Brak dostępu do logu.\n");
exit;
}
$content = build_log_download_content($job_id, $job_file_path, $log_path, $job_base);
if (!empty($lang_map)) {
$content = translate_text($content, $lang_map);
}
if ($raw_download) {
log_text_output($content);
}
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
echo $content;
} else {
header('Content-Type: text/plain; charset=utf-8');
echo t("Nieprawidłowy identyfikator logu.\n");
}
exit;
}
if ($action === 'view_log') {
$file = basename((string)($params['job'] ?? ''));
if (preg_match('/^[-A-Za-z0-9_.]+$/', $file)) {
$job_id = job_id_from_file($file);
$job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user);
$log_path = $log_dir . '/' . $job_id . '.log';
if ($job_file_path === '') {
$log_message = 'Brak dostępu do zadania.';
$log_status_value = 'Brak dostępu';
$log_meta_start = '-';
$log_meta_path = '-';
$log_meta_mode = '-';
$log_meta_type = '-';
$log_meta_target = '-';
$view_log_job = '';
} else {
$view_log_job = $file;
$status = get_job_status($job_id, $job_base);
$log_status_value = status_label($status);
$vars = load_job_vars($job_file_path);
$start_ts = $vars['START_TS'] ?? '';
$log_meta_start = format_start_time($start_ts, @filemtime($job_file_path) ?: 0);
$log_meta_path = display_restore_path($vars['USERRESTOREPATH'] ?? '');
$log_meta_mode = restore_mode_label($vars['RESTOREMODE'] ?? '');
$log_meta_type = restore_type_label($vars['RESTORETYPE'] ?? '');
$log_meta_target = display_restore_path($vars['TARGETPATH'] ?? '');
if ($log_meta_path === '') {
$log_meta_path = '-';
}
if ($log_meta_type === '') {
$log_meta_type = '-';
}
if ($log_meta_target === '') {
$log_meta_target = '-';
}
if (is_file($log_path) && is_readable($log_path)) {
$log_message = normalize_log((string)file_get_contents($log_path));
} else {
$log_message = 'Log nie istnieje.';
}
$explanation = log_missing_include_explanation($log_message, $log_meta_path);
if ($explanation !== '') {
$log_message = rtrim($log_message) . "\n\n" . $explanation;
}
$log_last_line = last_non_empty_line($log_message);
$log_progress = extract_progress($log_last_line);
$display = display_status($status, $log_last_line);
$log_status_value = status_label($display);
if (in_array($display, ['done', 'failed', 'canceled'], true)) {
$log_progress = $display === 'done' ? 100 : null;
$log_last_line = status_label($display);
}
}
} else {
$log_message = 'Nieprawidłowy identyfikator logu.';
}
$view_log_overlay = $enable_logs_overlay && (($params['overlay'] ?? '') === '1');
if ($view_log_overlay) {
$overlay_html = render_log_view_html($view_log_job, $log_meta_start ?? '-', $log_meta_type ?? '-', $log_meta_path ?? '-', $log_meta_mode ?? '-', $log_meta_target ?? '-', $log_status_value ?? '-', $log_progress ?? null, $log_last_line ?? '', $log_message ?? '', $status ?? '', true);
echo translate_html_safe($overlay_html, $lang_map ?? []);
exit;
}
}
if (($params['queued'] ?? '') === '1') {
$queued_job = isset($params['job']) ? trim((string)$params['job']) : '';
$queued_label = $queued_message;
if ($queued_job !== '' && preg_match('/^restore-[A-Za-z0-9-]+$/', $queued_job)) {
$queued_label = $queued_message . ' ID: ' . $queued_job . '.';
}
if ($skin === 'evolution') {
$toast_message = $queued_label;
$toast_job = $queued_job;
$confirm_view = false;
} elseif (($params['view'] ?? '') === 'queued') {
$confirm_view = true;
} else {
$message_status = $queued_label;
}
}
$confirm_view = false;
$jobs = array_merge(
list_jobs($job_base . '/pending', 'pending', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/processing', 'processing', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'done', '*.ok', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'failed', '*.fail', $log_dir, $pid_dir, $cancel_dir, $da_user),
list_jobs($job_base . '/done', 'canceled', '*.cancel', $log_dir, $pid_dir, $cancel_dir, $da_user)
);
usort($jobs, function ($a, $b) {
return $b['time'] <=> $a['time'];
});
if (count($jobs) > 50) {
$jobs = array_slice($jobs, 0, 50);
}
?>
<?php ob_start(); ?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>borg-restore</title>
<?php if ($inline_css !== '') { ?>
<style><?php echo $inline_css; ?></style>
<?php } ?>
</head>
<body>
<div class="br-container">
<div id="br-overlay-container">
<?php if ($confirm_overwrite_view) { ?>
<?php echo render_confirm_overwrite_html($restore_path_raw, $selected_archive, $target_path_raw, $restore_mode, $restore_type, false, $csrf_token); ?>
<?php } ?>
</div>
<div class="br-header">
<div class="br-title">BorgBackup - Przywracanie Plików</div>
</div>
<?php if ($ui_locked) { ?>
<div class="br-card">
<?php echo render_system_error_section($ui_locked_message, $system_error_details_for_ui, $debug_mode); ?>
</div>
<?php } else { ?>
<?php if (!empty($floating_errors) && !$confirm_view) { ?>
<div class="br-toast-stack">
<?php foreach ($floating_errors as $err) { ?>
<div class="br-toast br-toast-error" data-autohide="5000"><?php echo h($err); ?></div>
<?php } ?>
</div>
<?php } ?>
<?php if ($toast_message !== '') { ?>
<div id="job-toast" class="br-toast br-toast-success" data-job="<?php echo h($toast_job); ?>"><?php echo h($toast_message); ?></div>
<?php } ?>
<div class="br-card">
<h3 style="text-align:center;">Lista backupow</h3>
<?php if (!empty($errors)) { ?>
<div class="br-alert br-alert-error">
<?php if (!$security_ok) { ?>
<p>Problem z konfiguracją środowiska backupowego - skontaktuj się z administratorem w celu uzyskania pomocy</p>
<?php } ?>
<?php foreach ($errors as $err) { ?>
<p><?php echo h($err); ?></p>
<?php } ?>
</div>
<?php } ?>
<?php if (!empty($warnings)) { ?>
<div class="br-alert br-alert-warn">
<?php foreach ($warnings as $warn) { ?>
<p><?php echo h($warn); ?></p>
<?php } ?>
</div>
<?php } ?>
<?php if ($message_status !== '') { ?>
<div class="br-alert br-alert-success"><?php echo h($message_status); ?></div>
<?php } ?>
<?php if ($action === 'view_log') { ?>
<?php echo render_log_view_html($view_log_job, $log_meta_start ?? '-', $log_meta_type ?? '-', $log_meta_path ?? '-', $log_meta_mode ?? '-', $log_meta_target ?? '-', $log_status_value ?? '-', $log_progress ?? null, $log_last_line ?? '', $log_message ?? '', $status ?? '', false); ?>
<?php } ?>
<?php if (!empty($entries)) { ?>
<form id="restore-form" method="post">
<input type="hidden" name="action" value="restore" />
<input type="hidden" name="csrf_token" value="<?php echo h($csrf_token); ?>" />
<table class="br-table" border="1" cellpadding="6" cellspacing="0">
<thead>
<tr>
<th>Nazwa</th>
<th>Data</th>
<?php if ($hash_visibility) { ?>
<th>Hash</th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php foreach ($entries as $entry) { ?>
<tr>
<td>
<label>
<input
type="radio"
name="archive"
value="<?php echo h($entry['name']); ?>"
<?php echo ($entry['name'] === $selected_archive) ? 'checked' : ''; ?>
/>
<?php echo h($entry['name']); ?>
</label>
</td>
<td><?php echo h($entry['date']); ?></td>
<?php if ($hash_visibility) { ?>
<td><?php echo h($entry['hash']); ?></td>
<?php } ?>
</tr>
<?php } ?>
</tbody>
</table>
<div class="br-restore-grid">
<?php if ($show_restore_type_box) { ?>
<section class="br-restore-box">
<h4 class="br-restore-box-title">Rodzaj przywracanych danych</h4>
<label class="br-radio-row">
<input
type="radio"
name="restore_type"
value="web"
<?php echo $restore_type === 'web' ? 'checked' : ''; ?>
onclick="updateRestoreType();"
/>
<span>Odzyskiwanie plików stron www</span>
</label>
<label class="br-radio-row">
<input
type="radio"
name="restore_type"
value="mail"
<?php echo $restore_type === 'mail' ? 'checked' : ''; ?>
onclick="updateRestoreType();"
/>
<span>Odzyskiwanie zawartości skrzynki pocztowej</span>
</label>
<div class="br-target-panel br-restore-path-panel">
<label for="restore_path" class="br-target-label">Ścieżka do przywrócenia:</label>
<div class="br-target-input-wrap">
<input
type="text"
id="restore_path"
name="restore_path"
size="60"
value="<?php echo h($restore_path_raw); ?>"
data-base-web="<?php echo h($web_restore_location); ?>"
data-base-mail="<?php echo h($mail_restore_location); ?>"
/>
<button class="br-icon-btn" type="button" onclick="openPicker('restore_path', 'file', 'restore');" aria-label="Wybierz lokalizację" title="Wybierz lokalizację">📁</button>
</div>
</div>
</section>
<?php } else { ?>
<input type="hidden" name="restore_type" value="<?php echo h($restore_type); ?>" />
<?php } ?>
<section class="br-restore-box<?php echo !$show_restore_type_box ? ' br-restore-box-full' : ''; ?>">
<h4 class="br-restore-box-title">Tryb przywracanych danych</h4>
<?php if (!$show_restore_type_box) { ?>
<p class="br-muted small m-0">Rodzaj przywracanych danych: <?php echo h(restore_type_label($restore_type)); ?></p>
<div class="br-target-panel br-restore-path-panel">
<label for="restore_path" class="br-target-label">Ścieżka do przywrócenia:</label>
<div class="br-target-input-wrap">
<input
type="text"
id="restore_path"
name="restore_path"
size="60"
value="<?php echo h($restore_path_raw); ?>"
data-base-web="<?php echo h($web_restore_location); ?>"
data-base-mail="<?php echo h($mail_restore_location); ?>"
/>
<button class="br-icon-btn" type="button" onclick="openPicker('restore_path', 'file', 'restore');" aria-label="Wybierz lokalizację" title="Wybierz lokalizację">📁</button>
</div>
</div>
<?php } ?>
<label class="br-radio-row">
<input
type="radio"
name="restore_mode"
value="location"
<?php echo $restore_mode === 'location' ? 'checked' : ''; ?>
onclick="updateRestoreMode();"
/>
<span>Przywracanie do lokalizacji</span>
</label>
<label id="restore_mode_update_row" class="br-radio-row" style="<?php echo $restore_type === 'web' ? 'display:none;' : ''; ?>">
<input
id="restore_mode_update"
type="radio"
name="restore_mode"
value="update"
<?php echo $restore_mode === 'update' ? 'checked' : ''; ?>
onclick="updateRestoreMode();"
/>
<span id="restore_mode_update_label">Połączenie danych kopii zapasowej z obecnymi danymi</span>
</label>
<label id="restore_mode_overwrite_row" class="br-radio-row">
<input
id="restore_mode_overwrite"
type="radio"
name="restore_mode"
value="overwrite"
<?php echo $restore_mode === 'overwrite' ? 'checked' : ''; ?>
onclick="updateRestoreMode();"
/>
<span id="restore_mode_overwrite_label">Nadpisz aktualne dane</span>
</label>
<div id="target_path_row" class="br-target-panel" style="<?php echo $restore_mode === 'location' ? '' : 'display:none;'; ?>">
<label for="target_path" class="br-target-label">Lokalizacja docelowa:</label>
<div class="br-target-input-wrap">
<input
type="text"
id="target_path"
name="target_path"
size="60"
value="<?php echo h($target_path_raw); ?>"
data-base-web="<?php echo h($default_restore_destination); ?>"
data-base-mail="<?php echo h($default_restore_destination); ?>"
data-default-target="<?php echo h($default_restore_destination); ?>"
data-selectable-web="<?php echo $selectable_domain_restore_destination ? '1' : '0'; ?>"
data-selectable-mail="<?php echo $selectable_mail_restore_destination ? '1' : '0'; ?>"
/>
<button id="target_path_picker" class="br-icon-btn" type="button" onclick="openPicker('target_path', 'dir', 'target');" aria-label="Wybierz lokalizację" title="Wybierz lokalizację">📁</button>
</div>
<span class="br-d-block br-muted m-0 small">Domyślna lokalizacja docelowa: <span id="default-target-location" class="br-italic"><?php echo h($default_restore_destination); ?></span></span>
</div>
</section>
</div>
<section class="br-usage-box">
<h4 class="br-usage-box-title">Przykłady użycia</h4>
<p class="br-muted br-italic small m-0">/home/uzytkownik/domains/uzytkownik.pl/public_html/</p>
<p class="br-muted br-italic small m-0">home/uzytkownik/domains/uzytkownik.pl/public_html</p>
<p class="br-muted br-italic small m-0">home/uzytkownik/domains/uzytkownik.pl/public_html/</p>
<p class="small m-0"><strong>Uwaga!</strong> Znaki / z początku i końca linii zostaną usunięte.</p>
</section>
<p>
<button class="br-btn" type="submit">Przywróć</button>
</p>
</form>
<?php } else { ?>
<?php if (empty($errors)) { ?>
<p class="br-muted">Brak wpisów do wyświetlenia.</p>
<?php } ?>
<?php } ?>
</div>
<div class="br-card">
<h3>Status zadań</h3>
<?php if ($action !== 'view_log') { ?>
<div class="br-actions">
<button class="br-btn br-btn-ghost" type="button" onclick="refreshStatus();">Odśwież</button>
</div>
<?php } ?>
<?php if (!empty($log_message) && $action !== 'view_log') { ?>
<p><?php echo h($log_message); ?></p>
<?php } ?>
<div id="job-status">
<?php echo render_jobs_section($jobs, $csrf_token, $enable_logs_overlay); ?>
</div>
</div>
<?php if ($action !== 'view_log') { ?>
<script>
<?php if ($confirm_view && !$is_evolution && $confirm_replace_url !== '') { ?>
try {
history.replaceState({}, '', '<?php echo h($confirm_replace_url); ?>');
} catch (e) {}
<?php } ?>
var blockRestoreOutside = <?php echo $block_restore_outside_location ? 'true' : 'false'; ?>;
var targetHomeBase = <?php echo json_encode('/home/' . $da_user); ?>;
var csrfToken = <?php echo json_encode($csrf_token); ?>;
var logsOverlayEnabled = <?php echo $enable_logs_overlay ? 'true' : 'false'; ?>;
var forcedRestoreType = <?php echo json_encode($show_restore_type_box ? '' : $restore_type); ?>;
var logOverlayTimer = null;
var logOverlayJob = '';
var I18N = <?php echo json_encode([
'restore_path_base' => t('Ścieżka musi być w katalogu: '),
'target_path_home' => t('Lokalizacja docelowa musi być w katalogu: '),
'response_error' => t('Nie udało się przetworzyć odpowiedzi.'),
'request_error' => t('Nie udało się wysłać żądania.'),
'download_error' => t('Nie udało się pobrać logu.'),
'picker_no_dirs' => t('Brak katalogów'),
'picker_load_error' => t('Nie udało się wczytać katalogów.'),
'picker_name_required' => t('Podaj nazwę katalogu.'),
'picker_main_domains' => t('Nie można utworzyć katalogu bezpośrednio w '),
'picker_main_imap' => t('Nie można utworzyć katalogu bezpośrednio w '),
'picker_exists' => t('Katalog już istnieje.'),
'picker_invalid_name' => t('Nieprawidłowa nazwa katalogu.'),
'picker_outside_base' => t('Nie można utworzyć katalogu poza dozwoloną ścieżką.'),
'picker_create_failed' => t('Nie udało się utworzyć katalogu.'),
], JSON_UNESCAPED_UNICODE); ?>;
function getRestoreMode() {
var inputs = document.getElementsByName('restore_mode');
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].checked) {
return inputs[i].value;
}
}
return 'location';
}
function getRestoreType() {
if (forcedRestoreType === 'web' || forcedRestoreType === 'mail') {
return forcedRestoreType;
}
var inputs = document.getElementsByName('restore_type');
for (var i = 0; i < inputs.length; i++) {
if (inputs[i].checked) {
return inputs[i].value;
}
}
for (var j = 0; j < inputs.length; j++) {
if ((inputs[j].type || '').toLowerCase() === 'hidden' && (inputs[j].value === 'web' || inputs[j].value === 'mail')) {
return inputs[j].value;
}
}
return 'web';
}
function getBaseForType(type, inputId) {
var input = document.getElementById(inputId);
if (!input) { return ''; }
if (type === 'mail') {
return (input.getAttribute('data-base-mail') || '').trim();
}
return (input.getAttribute('data-base-web') || '').trim();
}
function getDefaultTarget() {
var input = document.getElementById('target_path');
if (!input) { return ''; }
return (input.getAttribute('data-default-target') || '').trim();
}
function isTargetSelectable(type) {
var input = document.getElementById('target_path');
if (!input) { return true; }
var attr = type === 'mail' ? input.getAttribute('data-selectable-mail') : input.getAttribute('data-selectable-web');
return attr !== '0';
}
function normalizePath(value) {
var trimmed = (value || '').trim();
if (trimmed === '') { return ''; }
if (trimmed.charAt(0) !== '/') {
trimmed = '/' + trimmed;
}
return trimmed;
}
function matchesPrefix(path, prefix) {
if (prefix === '') { return true; }
if (path === prefix) { return true; }
return path.indexOf(prefix + '/') === 0;
}
function showFormError(message) {
if (typeof pushToast === 'function') {
pushToast(message, 'error');
return;
}
alert(message);
}
function validateRestorePathBase() {
if (!blockRestoreOutside) { return true; }
var restoreInput = document.getElementById('restore_path');
if (!restoreInput) { return true; }
var restorePath = normalizePath(restoreInput.value || '');
if (restorePath === '') { return true; }
var base = normalizePath(getBaseForType(getRestoreType(), 'restore_path'));
if (base === '') { return true; }
if (!matchesPrefix(restorePath, base)) {
showFormError((I18N.restore_path_base || 'Ścieżka musi być w katalogu: ') + base);
return false;
}
return true;
}
function validateTargetPathHome() {
if (getRestoreMode() !== 'location') { return true; }
if (!isTargetSelectable(getRestoreType())) { return true; }
var targetInput = document.getElementById('target_path');
if (!targetInput) { return true; }
var targetPath = normalizePath(targetInput.value || '');
if (targetPath === '') { return true; }
var homeBase = normalizePath(targetHomeBase || '');
if (homeBase === '') { return true; }
if (!matchesPrefix(targetPath, homeBase)) {
showFormError((I18N.target_path_home || 'Lokalizacja docelowa musi być w katalogu: ') + homeBase);
return false;
}
return true;
}
var lastRestoreType = '';
var lastRestoreMode = '';
function updateRestoreMode() {
var mode = getRestoreMode();
var row = document.getElementById('target_path_row');
var target = document.getElementById('target_path');
var pickerBtn = document.getElementById('target_path_picker');
var selectable = isTargetSelectable(getRestoreType());
if (mode !== lastRestoreMode) {
var restoreInput = document.getElementById('restore_path');
if (restoreInput) {
var baseRestore = getBaseForType(getRestoreType(), 'restore_path');
restoreInput.value = baseRestore ? (baseRestore + '/') : '';
restoreInput.disabled = false;
restoreInput.readOnly = false;
}
if (target) {
var baseTarget = getDefaultTarget();
target.value = baseTarget || '';
}
}
if (mode === 'overwrite' || mode === 'update') {
if (row) { row.style.display = 'none'; }
if (target) { target.disabled = true; }
if (pickerBtn) { pickerBtn.style.display = 'none'; }
} else {
if (row) { row.style.display = ''; }
if (target) {
target.disabled = !selectable;
target.readOnly = !selectable;
if (!selectable) {
var baseTarget = getDefaultTarget();
if (baseTarget !== '') { target.value = baseTarget; }
}
}
if (pickerBtn) { pickerBtn.style.display = selectable ? '' : 'none'; }
}
lastRestoreMode = mode;
}
function updateRestoreType() {
var type = getRestoreType();
var typeChanged = type !== lastRestoreType;
var updateRow = document.getElementById('restore_mode_update_row');
var updateInput = document.getElementById('restore_mode_update');
var overwriteInput = document.getElementById('restore_mode_overwrite');
var currentMode = getRestoreMode();
if (type === 'web') {
if (updateRow) { updateRow.style.display = 'none'; }
if (updateInput) { updateInput.disabled = true; }
if (currentMode === 'update' && overwriteInput) {
overwriteInput.checked = true;
}
} else {
if (updateRow) { updateRow.style.display = ''; }
if (updateInput) { updateInput.disabled = false; }
}
if (typeChanged) {
var restoreInput = document.getElementById('restore_path');
if (restoreInput) {
var baseRestore = getBaseForType(type, 'restore_path');
restoreInput.value = baseRestore ? (baseRestore + '/') : '';
restoreInput.disabled = false;
restoreInput.readOnly = false;
}
var targetInputReset = document.getElementById('target_path');
if (targetInputReset) {
var baseTarget = getDefaultTarget();
targetInputReset.value = baseTarget || '';
}
}
updateRestoreMode();
var defaultTarget = document.getElementById('default-target-location');
var baseTarget = getDefaultTarget();
if (defaultTarget) { defaultTarget.textContent = baseTarget || ''; }
var restoreInput = document.getElementById('restore_path');
if (restoreInput) {
var current = (restoreInput.value || '').trim();
var baseRestore = getBaseForType(type, 'restore_path');
var baseWeb = getBaseForType('web', 'restore_path');
var baseMail = getBaseForType('mail', 'restore_path');
if (current === '' || current === baseWeb || current === baseWeb + '/' || current === baseMail || current === baseMail + '/') {
restoreInput.value = baseRestore ? (baseRestore + '/') : '';
}
}
var targetInput = document.getElementById('target_path');
if (targetInput) {
var targetCurrent = (targetInput.value || '').trim();
if (targetCurrent === '' || !isTargetSelectable(type)) {
targetInput.value = baseTarget;
}
}
lastRestoreType = type;
}
function ensureToastStack() {
var stack = document.querySelector('.br-toast-stack');
if (!stack) {
stack = document.createElement('div');
stack.className = 'br-toast-stack';
var container = document.querySelector('.br-container');
if (container) {
container.insertBefore(stack, container.firstChild);
} else {
document.body.appendChild(stack);
}
}
return stack;
}
function pushToast(message, type, jobId) {
if (!message) { return; }
var stack = ensureToastStack();
var toast = document.createElement('div');
toast.className = 'br-toast ' + (type === 'error' ? 'br-toast-error' : 'br-toast-success');
toast.textContent = message;
toast.setAttribute('data-autohide', '5000');
if (jobId) {
toast.id = 'job-toast';
toast.setAttribute('data-job', jobId);
}
stack.appendChild(toast);
setupToastAutohide();
updateJobToast();
}
function closeOverlay() {
var container = document.getElementById('br-overlay-container');
if (container) {
container.innerHTML = '';
}
if (logOverlayTimer) {
clearInterval(logOverlayTimer);
logOverlayTimer = null;
}
logOverlayJob = '';
}
function bindOverlay(container) {
if (!container) { return; }
var overlays = container.querySelectorAll('.br-overlay');
for (var o = 0; o < overlays.length; o++) {
overlays[o].addEventListener('click', function (e) {
if (e.target === e.currentTarget) {
e.preventDefault();
closeOverlay();
}
});
}
var closeButtons = container.querySelectorAll('[data-close-overlay]');
for (var i = 0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener('click', function (e) {
e.preventDefault();
closeOverlay();
});
}
var confirmForm = container.querySelector('form.br-confirm-form');
if (confirmForm && confirmForm.getAttribute('data-ajax') === '1') {
confirmForm.addEventListener('submit', submitRestoreAjax);
}
}
function showOverlay(html) {
var container = document.getElementById('br-overlay-container');
if (!container) { return; }
container.innerHTML = html || '';
bindOverlay(container);
}
function applyDefaultFieldValues() {
var type = getRestoreType();
var restoreInput = document.getElementById('restore_path');
if (restoreInput) {
var baseRestore = getBaseForType(type, 'restore_path');
restoreInput.value = baseRestore ? (baseRestore + '/') : '';
}
var targetInput = document.getElementById('target_path');
if (targetInput) {
var baseTarget = getDefaultTarget();
targetInput.value = baseTarget || '';
}
}
function copyLogToClipboard() {
var job = logOverlayJob || '';
var el = document.getElementById('log-content');
function doCopy(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(function () {});
return;
}
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.top = '-1000px';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
}
if (!job) {
if (!el) { return; }
doCopy(el.textContent || '');
return;
}
var formData = new FormData();
formData.append('action', 'download_log');
formData.append('raw', '1');
formData.append('job', job);
formData.append('csrf_token', csrfToken || '');
fetch('/CMD_PLUGINS/borg-restore', { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var content = extractLogText(text || '');
if (content === null) {
content = text || '';
}
doCopy(content);
})
.catch(function () {
if (el) { doCopy(el.textContent || ''); }
});
}
function refreshLogOverlay() {
if (!logOverlayJob) { return; }
fetch('/CMD_PLUGINS/borg-restore?action=log_stream&job=' + encodeURIComponent(logOverlayJob), { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) {
var el = document.getElementById('log-content');
if (el && typeof data.log === 'string') {
el.textContent = data.log;
el.scrollTop = el.scrollHeight;
}
var statusEl = document.getElementById('log-status');
if (statusEl && data.display_label) {
statusEl.textContent = data.display_label;
}
var lastLineEl = document.getElementById('log-progress-text');
if (lastLineEl) {
if (typeof data.progress_text === 'string') {
lastLineEl.textContent = data.progress_text;
} else if (typeof data.last_line === 'string') {
lastLineEl.textContent = data.last_line;
}
}
var bar = document.getElementById('log-progress-bar');
if (bar) {
var p = (typeof data.progress === 'number') ? data.progress : 0;
bar.style.width = p + '%';
}
if (data.status && data.status !== 'processing' && logOverlayTimer) {
clearInterval(logOverlayTimer);
logOverlayTimer = null;
}
})
.catch(function () {});
}
function openLogOverlay(job) {
if (!job) { return; }
if (!logsOverlayEnabled) {
window.location.href = '/CMD_PLUGINS/borg-restore?action=view_log&job=' + encodeURIComponent(job);
return;
}
logOverlayJob = job;
fetch('/CMD_PLUGINS/borg-restore?action=view_log&overlay=1&job=' + encodeURIComponent(job), { credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (html) {
showOverlay(html);
refreshLogOverlay();
if (logOverlayTimer) {
clearInterval(logOverlayTimer);
}
logOverlayTimer = setInterval(refreshLogOverlay, 3000);
})
.catch(function () {});
}
function handleRestoreResponse(data) {
if (!data) {
pushToast(I18N.response_error || 'Nie udało się przetworzyć odpowiedzi.', 'error');
return;
}
if (data.confirm_html) {
showOverlay(data.confirm_html);
return;
}
closeOverlay();
updateRestoreType();
var restoreInput = document.getElementById('restore_path');
if (restoreInput) {
restoreInput.disabled = false;
restoreInput.readOnly = false;
var currentRestore = (restoreInput.value || '').trim();
if (currentRestore === '') {
var baseRestore = getBaseForType(getRestoreType(), 'restore_path');
if (baseRestore) {
restoreInput.value = baseRestore + '/';
}
}
}
if (data.floating_errors && data.floating_errors.length) {
for (var i = 0; i < data.floating_errors.length; i++) {
pushToast(data.floating_errors[i], 'error');
}
}
if (data.errors && data.errors.length) {
for (var j = 0; j < data.errors.length; j++) {
pushToast(data.errors[j], 'error');
}
}
if (data.toast_message) {
pushToast(data.toast_message, 'success', data.toast_job || '');
refreshStatus();
applyDefaultFieldValues();
}
}
function submitRestoreAjax(e) {
if (e && e.preventDefault) { e.preventDefault(); }
var form = e && e.target ? e.target : document.getElementById('restore-form');
if (!form) { return false; }
var formData = new FormData(form);
if (!formData.has('ajax')) {
formData.append('ajax', '1');
}
fetch('/CMD_PLUGINS/borg-restore', {
method: 'POST',
body: formData,
credentials: 'same-origin'
})
.then(function (r) { return r.text(); })
.then(function (text) {
var data = extractAjaxJson(text || '');
if (!data) {
pushToast(I18N.response_error || 'Nie udało się przetworzyć odpowiedzi.', 'error');
return;
}
handleRestoreResponse(data);
})
.catch(function () {
pushToast(I18N.request_error || 'Nie udało się wysłać żądania.', 'error');
});
return false;
}
function normalizeJobId(job) {
if (!job) { return ''; }
var id = job.replace(/\\.(ok|fail|cancel)$/i, '');
id = id.replace(/\\.env$/i, '');
id = id.replace(/\\.env\\.(ok|fail|cancel)$/i, '');
return id;
}
function downloadLog(job) {
if (!job) { return; }
var id = normalizeJobId(job);
var formData = new FormData();
formData.append('action', 'download_log');
formData.append('raw', '1');
formData.append('job', job);
formData.append('csrf_token', csrfToken || '');
fetch('/CMD_PLUGINS/borg-restore', { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var content = extractLogText(text || '');
if (content === null) {
content = text || '';
}
var blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (id || 'log') + '.log';
document.body.appendChild(a);
a.click();
setTimeout(function () {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
}, 0);
})
.catch(function () {
pushToast(I18N.download_error || 'Nie udało się pobrać logu.', 'error');
});
}
function toggleSelectAll(el) {
var boxes = document.querySelectorAll('#job-status input[type=\"checkbox\"][name=\"jobs[]\"]');
for (var i = 0; i < boxes.length; i++) {
boxes[i].checked = !!el.checked;
}
}
var pickerTarget = null;
var pickerPath = '';
var pickerBase = '';
var pickerMode = 'dir';
var pickerSelected = '';
var pickerScope = 'restore';
function openPicker(inputId, mode, scope) {
var target = document.getElementById(inputId);
if (!target) { return; }
pickerTarget = target;
pickerMode = mode === 'file' ? 'file' : 'dir';
pickerScope = scope || 'restore';
pickerSelected = '';
var initial = (target.value || '').trim();
if (initial === '') {
if (pickerScope === 'restore') {
initial = getBaseForType(getRestoreType(), inputId);
} else {
initial = '';
}
}
var createRow = document.getElementById('br-picker-create');
if (createRow) {
createRow.style.display = (pickerMode === 'dir') ? 'flex' : 'none';
}
loadPicker(initial);
var modal = document.getElementById('br-picker');
if (modal) { modal.style.display = 'flex'; }
}
function closePicker() {
var modal = document.getElementById('br-picker');
if (modal) { modal.style.display = 'none'; }
}
function pickerSetError(msg) {
var el = document.getElementById('br-picker-error');
if (!el) { return; }
if (msg) {
el.textContent = msg;
el.style.display = 'block';
} else {
el.textContent = '';
el.style.display = 'none';
}
}
function renderPickerList(items) {
var list = document.getElementById('br-picker-list');
if (!list) { return; }
list.innerHTML = '';
if (!items || !items.length) {
var empty = document.createElement('li');
empty.textContent = I18N.picker_no_dirs || 'Brak katalogów';
empty.className = 'br-muted';
list.appendChild(empty);
return;
}
items.forEach(function (entry) {
var li = document.createElement('li');
if (entry.type === 'dir') {
li.textContent = '📁 ' + entry.name + '/';
li.addEventListener('click', function () {
pickerSelected = '';
loadPicker(pickerPath.replace(/\/+$/, '') + '/' + entry.name);
});
} else {
var full = pickerPath.replace(/\/+$/, '') + '/' + entry.name;
li.textContent = '📄 ' + entry.name;
li.addEventListener('click', function () {
pickerSelected = full;
var active = list.querySelectorAll('li.active');
for (var i = 0; i < active.length; i++) {
active[i].classList.remove('active');
}
li.classList.add('active');
});
if (pickerSelected === full) {
li.classList.add('active');
}
}
list.appendChild(li);
});
}
function extractPickerJson(text) {
var marker = '__BORG_PICKER_JSON__';
var start = text.indexOf(marker);
if (start === -1) { return null; }
start += marker.length;
var end = text.indexOf(marker, start);
if (end === -1) { return null; }
var jsonText = text.substring(start, end).trim();
try { return JSON.parse(jsonText); } catch (e) { return null; }
}
function extractAjaxJson(text) {
var marker = '__BORG_AJAX_JSON__';
var start = text.indexOf(marker);
if (start === -1) { return null; }
start += marker.length;
var end = text.indexOf(marker, start);
if (end === -1) { return null; }
var jsonText = text.substring(start, end).trim();
try { return JSON.parse(jsonText); } catch (e) { return null; }
}
function extractLogText(text) {
var marker = '__BORG_LOG_TEXT__';
var start = text.indexOf(marker);
if (start === -1) { return null; }
start += marker.length;
var end = text.indexOf(marker, start);
if (end === -1) { return null; }
return text.substring(start, end);
}
function extractStatusHtml(text) {
var marker = '__BORG_STATUS_HTML__';
var start = text.indexOf(marker);
if (start === -1) { return null; }
start += marker.length;
var end = text.indexOf(marker, start);
if (end === -1) { return null; }
return text.substring(start, end);
}
function loadPicker(path) {
pickerSetError('');
fetch('/CMD_PLUGINS/borg-restore?action=dir_list&mode=' + encodeURIComponent(pickerMode) + '&path=' + encodeURIComponent(path || '') + '&restore_type=' + encodeURIComponent(getRestoreType()) + '&scope=' + encodeURIComponent(pickerScope), { credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var data = extractPickerJson(text || '');
if (!data || !data.ok) {
pickerSetError(I18N.picker_load_error || 'Nie udało się wczytać katalogów.');
return;
}
pickerPath = data.path || '';
pickerBase = data.base || '';
if (data.selected) {
pickerSelected = data.selected;
}
var current = document.getElementById('br-picker-current');
if (current) { current.textContent = pickerPath; }
renderPickerList(data.items || []);
})
.catch(function () {
pickerSetError(I18N.picker_load_error || 'Nie udało się wczytać katalogów.');
});
}
function pickerGoUp() {
if (!pickerPath) { return; }
if (pickerBase && pickerPath === pickerBase) { return; }
var up = pickerPath.replace(/\/+$/, '');
up = up.substring(0, up.lastIndexOf('/')) || '/';
loadPicker(up);
}
function createPickerDir() {
var input = document.getElementById('br-picker-new');
if (!input) { return; }
var name = (input.value || '').trim();
if (name === '') {
pickerSetError(I18N.picker_name_required || 'Podaj nazwę katalogu.');
return;
}
pickerSetError('');
function pickerErrorMessage(code) {
var baseDomains = (targetHomeBase || '/home') + '/domains';
var baseImap = (targetHomeBase || '/home') + '/imap';
if (code === 'main_domain_blocked') {
return (I18N.picker_main_domains || 'Nie można utworzyć katalogu bezpośrednio w ') + baseDomains + '.';
}
if (code === 'main_imap_blocked') {
return (I18N.picker_main_imap || 'Nie można utworzyć katalogu bezpośrednio w ') + baseImap + '.';
}
if (code === 'exists') {
return I18N.picker_exists || 'Katalog już istnieje.';
}
if (code === 'invalid_name') {
return I18N.picker_invalid_name || 'Nieprawidłowa nazwa katalogu.';
}
if (code === 'outside_base') {
return I18N.picker_outside_base || 'Nie można utworzyć katalogu poza dozwoloną ścieżką.';
}
return I18N.picker_create_failed || 'Nie udało się utworzyć katalogu.';
}
var formData = new FormData();
formData.append('action', 'dir_create');
formData.append('path', pickerPath || '');
formData.append('name', name);
formData.append('restore_type', getRestoreType());
formData.append('scope', pickerScope);
formData.append('csrf_token', csrfToken || '');
fetch('/CMD_PLUGINS/borg-restore', { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var data = extractPickerJson(text || '');
if (!data || !data.ok) {
var code = data && data.error ? data.error : '';
pickerSetError(pickerErrorMessage(code));
return;
}
input.value = '';
loadPicker(data.created || pickerPath);
})
.catch(function () {
pickerSetError(I18N.picker_create_failed || 'Nie udało się utworzyć katalogu.');
});
}
function selectPickerPath() {
if (!pickerTarget) { closePicker(); return; }
if (pickerMode === 'file' && pickerSelected) {
pickerTarget.value = pickerSelected || '';
} else {
pickerTarget.value = pickerPath || '';
}
closePicker();
}
function refreshStatus() {
fetch('/CMD_PLUGINS/borg-restore?action=status', { credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var html = extractStatusHtml(text || '');
if (html === null) {
html = text || '';
}
var el = document.getElementById('job-status');
if (el) { el.innerHTML = html; }
updateJobToast();
})
.catch(function () {});
}
function updateJobToast() {
var toast = document.getElementById('job-toast');
if (!toast) { return; }
var jobId = toast.getAttribute('data-job') || '';
if (jobId === '') { return; }
var rows = document.querySelectorAll('#job-status tr[data-job-id]');
var status = '';
for (var i = 0; i < rows.length; i++) {
if (rows[i].getAttribute('data-job-id') === jobId) {
status = rows[i].getAttribute('data-status') || '';
break;
}
}
if (status === '') {
return;
}
if (status !== 'pending' && status !== 'processing' && status !== 'calculating') {
toast.remove();
}
}
function setupToastAutohide() {
var toasts = document.querySelectorAll('.br-toast[data-autohide]');
for (var i = 0; i < toasts.length; i++) {
(function (toast) {
var ms = parseInt(toast.getAttribute('data-autohide') || '5000', 10);
if (!isNaN(ms) && ms > 0) {
setTimeout(function () {
if (toast && toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, ms);
}
})(toasts[i]);
}
}
lastRestoreType = getRestoreType();
lastRestoreMode = getRestoreMode();
updateRestoreType();
setupToastAutohide();
updateJobToast();
bindOverlay(document.getElementById('br-overlay-container'));
var restoreForm = document.getElementById('restore-form');
var isEvolution = <?php echo $skin === 'evolution' ? 'true' : 'false'; ?>;
if (restoreForm) {
restoreForm.addEventListener('submit', function (e) {
if (!validateRestorePathBase() || !validateTargetPathHome()) {
e.preventDefault();
return false;
}
if (isEvolution) {
e.preventDefault();
submitRestoreAjax(e);
return false;
}
return true;
});
}
setInterval(refreshStatus, 5000);
</script>
<?php } ?>
<?php if ($action === 'view_log' && $view_log_job !== '') { ?>
<script>
var csrfToken = <?php echo json_encode($csrf_token); ?>;
function normalizeJobId(job) {
if (!job) { return ''; }
var id = job.replace(/\.(ok|fail|cancel)$/i, '');
id = id.replace(/\.env$/i, '');
id = id.replace(/\.env\.(ok|fail|cancel)$/i, '');
return id;
}
function extractLogText(text) {
var marker = '__BORG_LOG_TEXT__';
var start = text.indexOf(marker);
if (start === -1) { return null; }
start += marker.length;
var end = text.indexOf(marker, start);
if (end === -1) { return null; }
return text.substring(start, end);
}
function downloadLog(job) {
if (!job) { return; }
var id = normalizeJobId(job);
var formData = new FormData();
formData.append('action', 'download_log');
formData.append('raw', '1');
formData.append('job', job);
formData.append('csrf_token', csrfToken || '');
fetch('/CMD_PLUGINS/borg-restore', { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var content = extractLogText(text || '');
if (content === null) {
content = text || '';
}
var blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (id || 'log') + '.log';
document.body.appendChild(a);
a.click();
setTimeout(function () {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
}, 0);
})
.catch(function () {});
}
var logTimer = null;
function copyLogToClipboard() {
var job = <?php echo json_encode($view_log_job); ?>;
var el = document.getElementById('log-content');
function doCopy(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(function () {});
return;
}
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.top = '-1000px';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
}
if (!job) {
if (!el) { return; }
doCopy(el.textContent || '');
return;
}
var formData = new FormData();
formData.append('action', 'download_log');
formData.append('raw', '1');
formData.append('job', job);
formData.append('csrf_token', csrfToken || '');
fetch('/CMD_PLUGINS/borg-restore', { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var content = extractLogText(text || '');
if (content === null) {
content = text || '';
}
doCopy(content);
})
.catch(function () {
if (el) { doCopy(el.textContent || ''); }
});
}
function refreshLog() {
fetch('/CMD_PLUGINS/borg-restore?action=log_stream&job=<?php echo h($view_log_job); ?>', { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (data) {
var el = document.getElementById('log-content');
if (el && typeof data.log === 'string') {
el.textContent = data.log;
el.scrollTop = el.scrollHeight;
}
var statusEl = document.getElementById('log-status');
if (statusEl && data.display_label) {
statusEl.textContent = data.display_label;
}
var lastLineEl = document.getElementById('log-progress-text');
if (lastLineEl) {
if (typeof data.progress_text === 'string') {
lastLineEl.textContent = data.progress_text;
} else if (typeof data.last_line === 'string') {
lastLineEl.textContent = data.last_line;
}
}
var bar = document.getElementById('log-progress-bar');
if (bar) {
var p = (typeof data.progress === 'number') ? data.progress : 0;
bar.style.width = p + '%';
}
if (data.status && data.status !== 'processing' && logTimer) {
clearInterval(logTimer);
logTimer = null;
}
})
.catch(function () {});
}
logTimer = setInterval(refreshLog, 3000);
</script>
<?php } ?>
<div id="br-picker" class="br-overlay" style="display:none;">
<div class="br-modal">
<div class="br-modal-header">
<div class="br-modal-title">Wybierz katalog</div>
<button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="closePicker();">Zamknij</button>
</div>
<div class="br-modal-body">
<div class="br-picker-path">
<button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="pickerGoUp();">⟵ W górę</button>
<span id="br-picker-current" class="br-muted"></span>
</div>
<div id="br-picker-error" class="br-alert br-alert-error" style="display:none;"></div>
<ul id="br-picker-list" class="br-picker-list"></ul>
<div id="br-picker-create" class="br-picker-create" style="display:none;">
<input id="br-picker-new" type="text" placeholder="Nowy katalog" />
<button class="br-btn br-btn-ghost br-btn-small" type="button" onclick="createPickerDir();">Utwórz</button>
</div>
</div>
<div class="br-modal-actions">
<button class="br-btn br-btn-ghost" type="button" onclick="closePicker();">Anuluj</button>
<button class="br-btn" type="button" onclick="selectPickerPath();">Wybierz</button>
</div>
</div>
</div>
<?php } ?>
</div>
</body>
</html>
<?php
$page = ob_get_clean();
echo translate_html_safe($page, $lang_map ?? []);
?>