3240 lines
124 KiB
PHP
Executable File
3240 lines
124 KiB
PHP
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',
|
|
'WEB_RESTORE_LOCATION' => '/home/DA_USER/domains',
|
|
'MAIL_RESTORE_LOCATION' => '/home/DA_USER/imap',
|
|
'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',
|
|
];
|
|
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|WEB_RESTORE_LOCATION|MAIL_RESTORE_LOCATION|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)=(.*)$/', $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 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-muted"><?php echo h(t('Brak zadań do wyświetlenia.')); ?></p>
|
|
<?php
|
|
}
|
|
return 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';
|
|
$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);
|
|
|
|
$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 = [];
|
|
$warnings = [];
|
|
$message_status = '';
|
|
$selected_archive = $params['archive'] ?? '';
|
|
$restore_mode = $params['restore_mode'] ?? 'location';
|
|
$restore_type = $params['restore_type'] ?? 'web';
|
|
if ($restore_type !== 'web' && $restore_type !== 'mail') {
|
|
$restore_type = 'web';
|
|
}
|
|
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) {
|
|
$errors = array_merge($errors, $security_errors);
|
|
}
|
|
|
|
foreach ($settings_errors as $err) {
|
|
$errors[] = $err;
|
|
}
|
|
if ($vars_path === '' || !is_readable($vars_path)) {
|
|
$errors[] = 'Brak pliku bbvars.sh: ' . ($vars_path !== '' ? $vars_path : 'nie ustawiono BB_PATH');
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if ($action === 'status') {
|
|
$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);
|
|
}
|
|
echo render_jobs_section($jobs, $csrf_token, $enable_logs_overlay);
|
|
exit;
|
|
}
|
|
|
|
|
|
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 (!$security_ok) {
|
|
$errors[] = 'Nieprawidłowe uprawnienia plików konfiguracyjnych lub zadań. Przywracanie zablokowane.';
|
|
}
|
|
if (!$csrf_valid) {
|
|
$errors[] = 'Nieprawidłowy token CSRF.';
|
|
}
|
|
$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 (empty($errors) && $security_ok) {
|
|
if (empty($repo)) {
|
|
$errors[] = 'Brak zmiennej REPO w ' . $vars_path;
|
|
} elseif ($borg_bin === '') {
|
|
$errors[] = 'BORG_PATH jest puste w plugin-settings.conf.';
|
|
} elseif (!is_executable($borg_bin)) {
|
|
$errors[] = 'BORG_PATH nie jest plikiem wykonywalnym: ' . $borg_bin;
|
|
} else {
|
|
$env = $_ENV;
|
|
if (!empty($passphrase)) {
|
|
$env['BORG_PASSPHRASE'] = $passphrase;
|
|
}
|
|
if ($borg_ssh_key !== '') {
|
|
if (!is_readable($borg_ssh_key)) {
|
|
$errors[] = 'BORG_SSH_KEY nie jest plikiem: ' . $borg_ssh_key;
|
|
} else {
|
|
$env['BORG_RSH'] = 'ssh -i ' . escapeshellarg($borg_ssh_key);
|
|
}
|
|
}
|
|
|
|
if (empty($errors)) {
|
|
$command = $borg_bin . ' list ' . escapeshellarg($repo);
|
|
$descriptors = [
|
|
1 => ['pipe', 'w'],
|
|
2 => ['pipe', 'w'],
|
|
];
|
|
$process = @proc_open($command, $descriptors, $pipes, null, $env);
|
|
if (!is_resource($process)) {
|
|
$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 ($exit_code !== 0) {
|
|
$msg = trim($stderr);
|
|
if ($msg === '') {
|
|
$msg = 'Kod bledu: ' . $exit_code;
|
|
}
|
|
$errors[] = 'Błąd borg list: ' . $msg;
|
|
} else {
|
|
$lines = preg_split('/\r?\n/', trim($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 ($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(); ?>
|
|
<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 (!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>Lista backupow (borg list)</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>
|
|
<p>
|
|
Ścieżka do przywrócenia:
|
|
<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>
|
|
</p>
|
|
<p>
|
|
Rodzaj przywracanych danych:
|
|
<label>
|
|
<input
|
|
type="radio"
|
|
name="restore_type"
|
|
value="web"
|
|
<?php echo $restore_type === 'web' ? 'checked' : ''; ?>
|
|
onclick="updateRestoreType();"
|
|
/>
|
|
Odzyskiwanie plików stron www
|
|
</label>
|
|
<label style="margin-left:12px;">
|
|
<input
|
|
type="radio"
|
|
name="restore_type"
|
|
value="mail"
|
|
<?php echo $restore_type === 'mail' ? 'checked' : ''; ?>
|
|
onclick="updateRestoreType();"
|
|
/>
|
|
Odzyskiwanie zawartości skrzynki pocztowej
|
|
</label>
|
|
</p>
|
|
<p>
|
|
Tryb przywracania danych:
|
|
<label>
|
|
<input
|
|
type="radio"
|
|
name="restore_mode"
|
|
value="location"
|
|
<?php echo $restore_mode === 'location' ? 'checked' : ''; ?>
|
|
onclick="updateRestoreMode();"
|
|
/>
|
|
Przywracanie do lokalizacji
|
|
</label>
|
|
<label id="restore_mode_update_row" style="margin-left:12px;<?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" style="margin-left:12px;">
|
|
<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>
|
|
</p>
|
|
<p>Przykłady użycia:</p>
|
|
<p class="br-muted">/home/uzytkownik/domains/uzytkownik.pl/public_html/</p>
|
|
<p class="br-muted">home/uzytkownik/domains/uzytkownik.pl/public_html</p>
|
|
<p class="br-muted">home/uzytkownik/domains/uzytkownik.pl/public_html/</p>
|
|
<p><strong>Uwaga: znaki / z początku i końca linii zostaną usunięte.</strong></p>
|
|
<p id="target_path_row">
|
|
Lokalizacja docelowa:
|
|
<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>
|
|
<span class="br-muted">Domyślna lokalizacja docelowa: <span id="default-target-location"><?php echo h($default_restore_destination); ?></span></span>
|
|
</p>
|
|
<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 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() {
|
|
var inputs = document.getElementsByName('restore_type');
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
if (inputs[i].checked) {
|
|
return inputs[i].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 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 (html) {
|
|
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>
|
|
|
|
<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>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
$page = ob_get_clean();
|
|
echo translate_html_safe($page, $lang_map ?? []);
|
|
?>
|