From bd66a92f30f7ec2b41f16e9baa93ebbbc4f8e908 Mon Sep 17 00:00:00 2001 From: Marek Miklewicz Date: Sat, 4 Jul 2026 11:20:44 +0200 Subject: [PATCH] Initial import of borg-restore --- WORKLOG.md | 64 + admin/index.html | 787 +++++++++ hooks/admin_img.html | 1 + hooks/admin_txt.html | 1 + hooks/menu_user.sh | 27 + hooks/user_txt.html | 1 + images/admin_icon.svg | 1 + images/user_icon.svg | 1 + install.sh | 11 + lang/en.php | 164 ++ lang/pl.php | 7 + php.ini | 1961 +++++++++++++++++++++ plugin-settings.conf | 47 + plugin.conf | 19 + scripts/fix_permissions.sh | 129 ++ scripts/install.sh | 32 + scripts/restore.sh | 495 ++++++ scripts/uninstall.sh | 21 + scripts/worker.sh | 60 + uninstall.sh | 11 + user/_index.html.bak | 3239 ++++++++++++++++++++++++++++++++++ user/enhanced.css | 450 +++++ user/evolution.css | 451 +++++ user/index.html | 3385 ++++++++++++++++++++++++++++++++++++ 24 files changed, 11365 insertions(+) create mode 100644 WORKLOG.md create mode 100755 admin/index.html create mode 100644 hooks/admin_img.html create mode 100644 hooks/admin_txt.html create mode 100755 hooks/menu_user.sh create mode 100644 hooks/user_txt.html create mode 100644 images/admin_icon.svg create mode 100644 images/user_icon.svg create mode 100755 install.sh create mode 100644 lang/en.php create mode 100644 lang/pl.php create mode 100644 php.ini create mode 100644 plugin-settings.conf create mode 100644 plugin.conf create mode 100755 scripts/fix_permissions.sh create mode 100755 scripts/install.sh create mode 100755 scripts/restore.sh create mode 100755 scripts/uninstall.sh create mode 100755 scripts/worker.sh create mode 100755 uninstall.sh create mode 100755 user/_index.html.bak create mode 100644 user/enhanced.css create mode 100644 user/evolution.css create mode 100755 user/index.html diff --git a/WORKLOG.md b/WORKLOG.md new file mode 100644 index 0000000..dba810c --- /dev/null +++ b/WORKLOG.md @@ -0,0 +1,64 @@ +# borg-restore 2.x - state snapshot + +## Current state +- Working dir: `borg-restore_2x/borg-restore` +- Latest version in `plugin.conf`: `2.0.7` +- Latest package: `borg-restore_2x/2.0.7/borg-restore.tar.gz` + +## Packaging layout (required) +- Source (editable): `borg-restore_2x/borg-restore/` +- Packages: `borg-restore_2x//borg-restore.tar.gz` + +## Key behavior (current) +- Restore types: + - `web` (Pliki www) + - `mail` (Zawartosc skrzynki pocztowej) +- Restore modes: + - `location` (przywracanie do lokalizacji) + - `overwrite` (nadpisywanie) + - `update` (scalenie; UI label: "Polaczenie danych kopii zapasowej z obecnymi danymi") +- For mail merge: + - rsync excludes Dovecot indexes and can optionally delete them after merge. +- Target path picker (location mode) is limited to `/home/DA_USER`. +- Restore path restriction (if enabled) is applied only to restore path and overwrite/update. +- Warning modal uses pink background; overlay uses transparent dark scrim. + +## Config (plugin-settings.conf) +- `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` +- `CLEAR_DOVECOT_INDEXES_AFTER_MERGE=TRUE` +- `RESTORE_PATH_MODE=full` + +## Notable logic changes +- Mail merge uses rsync with excludes for: + - `dovecot.index*`, `dovecot-uidlist`, `dovecot-uidvalidity`, + `dovecot.list.index*`, `dovecot.list.log` + - Optional cleanup of these indexes after merge if config flag is TRUE. +- Logs: lines containing "Permanently added" and "known host" are filtered out of user-facing logs. +- New config flags to allow/deny restoring whole `/home/DA_USER/domains` and `/home/DA_USER/imap`. +- Default target path for location mode uses `DEFAULT_RESTORE_DESTINATION`. +- Picker: creating a new dir jumps into it. + +## CSS/UI +- Overlay: `rgba(15, 23, 42, 0.35)` +- Warning modal: class `br-modal-warning` with background `#f3c8c8` +- Picker modal: white background (default `.br-modal`) + +## Recent files touched +- `borg-restore_2x/borg-restore/user/index.html` +- `borg-restore_2x/borg-restore/scripts/restore.sh` +- `borg-restore_2x/borg-restore/plugin-settings.conf` +- `borg-restore_2x/borg-restore/user/enhanced.css` +- `borg-restore_2x/borg-restore/user/evolution.css` + +## Next step template (for new chat) +- "Pracujemy w `borg-restore_2x/borg-restore/`. Aktualna wersja w plugin.conf: 2.0.7." +- "Ostatnia paczka: `borg-restore_2x/2.0.7/borg-restore.tar.gz`." +- "Cel: ." diff --git a/admin/index.html b/admin/index.html new file mode 100755 index 0000000..8ef6010 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,787 @@ +#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/borg-restore/php.ini + '/opt/bb', + 'BORG_VARIABLES_FILE' => '/opt/bb/bbvars.sh', + 'ADMIN_MODE_ENABLE' => 'TRUE', + 'RESULTS_PER_PAGE' => '10', + 'DEBUG_MODE' => 'FALSE', + ]; + if (!is_readable($path)) { + return $settings; + } + $lines = file($path, FILE_IGNORE_NEW_LINES); + if ($lines === false) { + return $settings; + } + foreach ($lines as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') { + continue; + } + if (preg_match('/^(BB_PATH|BORG_VARIABLES_FILE|ADMIN_MODE_ENABLE|RESULTS_PER_PAGE|DEBUG_MODE)=(.*)$/', $line, $m)) { + $settings[$m[1]] = parse_sh_value($m[2]); + } + } + return $settings; +} + +function load_borg_vars(string $path): array +{ + $vars = [ + 'BORG_PASSPHRASE' => null, + 'REPO' => 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)=(.*)$/', $line, $m)) { + $vars[$m[1]] = parse_sh_value($m[2]); + } + } + return $vars; +} + +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])); + return $skin !== '' ? $skin : 'enhanced'; + } + } + return 'enhanced'; +} + +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); + } elseif (substr($base, -7) === '.cancel') { + $base = substr($base, 0, -7); + } + 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'; + $candidates[] = $job_base . '/done/' . $job_id . '.env.ok'; + $candidates[] = $job_base . '/done/' . $job_id . '.env.fail'; + $candidates[] = $job_base . '/done/' . $job_id . '.env.cancel'; + } + foreach ($candidates as $path) { + if (is_file($path)) { + return $path; + } + } + return ''; +} + +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 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 normalize_admin_status(string $status): string +{ + if (in_array($status, ['pending', 'processing', 'calculating'], true)) { + return 'processing'; + } + if ($status === 'done') { + return 'done'; + } + if (in_array($status, ['failed', 'canceled'], true)) { + return 'failed'; + } + return 'failed'; +} + +function admin_status_label(string $status): string +{ + if ($status === 'done') { + return 'Zakończone'; + } + if ($status === 'processing') { + return 'W trakcie'; + } + return 'Błąd'; +} + +function admin_status_sort(string $status): int +{ + if ($status === 'processing') { + return 1; + } + if ($status === 'done') { + return 2; + } + return 3; +} + +function restore_mode_label(string $mode): string +{ + if ($mode === 'overwrite') { + return 'Nadpisz aktualne dane'; + } + if ($mode === 'update') { + return 'Scalenie'; + } + if ($mode === 'location') { + return 'Przywracanie do lokalizacji'; + } + return '-'; +} + +function restore_type_label(string $type): string +{ + if ($type === 'mail') { + return 'Zawartość skrzynki pocztowej'; + } + if ($type === 'web') { + return 'Pliki www'; + } + return '-'; +} + +function display_restore_path(string $value): string +{ + $trimmed = trim($value); + if ($trimmed === '') { + return '-'; + } + if ($trimmed[0] !== '/') { + $trimmed = '/' . $trimmed; + } + return $trimmed; +} + +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 collect_jobs(string $job_base, string $log_dir): array +{ + $rows = []; + $sources = [ + ['dir' => $job_base . '/pending', 'pattern' => '*.env', 'status' => 'pending'], + ['dir' => $job_base . '/processing', 'pattern' => '*.env', 'status' => 'processing'], + ['dir' => $job_base . '/done', 'pattern' => '*.ok', 'status' => 'done'], + ['dir' => $job_base . '/done', 'pattern' => '*.env.ok', 'status' => 'done'], + ['dir' => $job_base . '/done', 'pattern' => '*.fail', 'status' => 'failed'], + ['dir' => $job_base . '/done', 'pattern' => '*.env.fail', 'status' => 'failed'], + ['dir' => $job_base . '/done', 'pattern' => '*.cancel', 'status' => 'canceled'], + ['dir' => $job_base . '/done', 'pattern' => '*.env.cancel', 'status' => 'canceled'], + ]; + + foreach ($sources as $source) { + foreach (glob($source['dir'] . '/' . $source['pattern']) ?: [] as $path) { + $file = basename($path); + if (isset($rows[$file])) { + continue; + } + $vars = load_job_vars($path); + $job_id = job_id_from_file($file); + if ($job_id === '') { + continue; + } + $log_path = $log_dir . '/' . $job_id . '.log'; + $base_status = $source['status']; + if ($base_status === 'processing' && is_readable($log_path)) { + $tail = normalize_log(read_log_tail($log_path, 20000)); + $last = last_non_empty_line($tail); + $base_status = display_status($base_status, $last); + } + $status = normalize_admin_status($base_status); + $start_ts = is_numeric($vars['START_TS'] ?? '') ? (int)$vars['START_TS'] : (int)(@filemtime($path) ?: 0); + if ($start_ts <= 0) { + $start_ts = time(); + } + + $rows[$file] = [ + 'file' => $file, + 'job_id' => $job_id, + 'user' => (string)($vars['DA_USER'] ?? ''), + 'start_ts' => $start_ts, + 'start_text' => date('Y-m-d H:i:s', $start_ts), + 'type' => (string)($vars['RESTORETYPE'] ?? ''), + 'type_label' => restore_type_label((string)($vars['RESTORETYPE'] ?? '')), + 'mode' => (string)($vars['RESTOREMODE'] ?? ''), + 'mode_label' => restore_mode_label((string)($vars['RESTOREMODE'] ?? '')), + 'status' => $status, + 'status_label' => admin_status_label($status), + 'status_sort' => admin_status_sort($status), + ]; + } + } + + return array_values($rows); +} + +function compare_rows(array $a, array $b, string $sort, string $dir): int +{ + $cmp = 0; + switch ($sort) { + case 'user': + $cmp = strcasecmp((string)$a['user'], (string)$b['user']); + break; + case 'type': + $cmp = strcasecmp((string)$a['type_label'], (string)$b['type_label']); + break; + case 'mode': + $cmp = strcasecmp((string)$a['mode_label'], (string)$b['mode_label']); + break; + case 'status': + $cmp = ((int)$a['status_sort']) <=> ((int)$b['status_sort']); + break; + case 'start': + default: + $cmp = ((int)$a['start_ts']) <=> ((int)$b['start_ts']); + break; + } + + if ($cmp === 0) { + $cmp = ((int)$a['start_ts']) <=> ((int)$b['start_ts']); + } + if ($cmp === 0) { + $cmp = strcmp((string)$a['job_id'], (string)$b['job_id']); + } + + return $dir === 'asc' ? $cmp : -$cmp; +} + +function build_admin_url(array $base, array $override = []): string +{ + $query = array_merge($base, $override); + foreach ($query as $key => $value) { + if ($value === null || $value === '') { + unset($query[$key]); + } + } + $qs = http_build_query($query); + return '/CMD_PLUGINS_ADMIN/borg-restore/index.html' . ($qs !== '' ? ('?' . $qs) : ''); +} + +$params = read_params(); +$action = (string)($params['action'] ?? ''); + +$username = getenv('USERNAME'); +if ($username === false || $username === '') { + $username = getenv('LOGNAME') ?: getenv('USER') ?: 'admin'; +} + +$plugin_dir = realpath(__DIR__ . '/..') ?: '/usr/local/directadmin/plugins/borg-restore'; +$settings_path = $plugin_dir . '/plugin-settings.conf'; +$settings = load_settings($settings_path); + +$admin_mode_enabled = parse_bool((string)($settings['ADMIN_MODE_ENABLE'] ?? 'TRUE'), true); +$debug_mode = parse_bool((string)($settings['DEBUG_MODE'] ?? 'FALSE'), false); +$bb_path = trim((string)($settings['BB_PATH'] ?? '/opt/bb')); +$borg_vars_file = trim((string)($settings['BORG_VARIABLES_FILE'] ?? '/opt/bb/bbvars.sh')); +$bbvars_path = $borg_vars_file !== '' ? $borg_vars_file : $bb_path; +if ($bbvars_path !== '' && substr($bbvars_path, -3) !== '.sh') { + $bbvars_path = rtrim($bbvars_path, '/') . '/bbvars.sh'; +} +$bbvars = load_borg_vars($bbvars_path); +$admin_missing_passphrase = $debug_mode && is_readable($bbvars_path) && trim((string)($bbvars['BORG_PASSPHRASE'] ?? '')) === ''; +$results_per_page = (int)trim((string)($settings['RESULTS_PER_PAGE'] ?? '10')); +if ($results_per_page <= 0) { + $results_per_page = 10; +} +if ($results_per_page > 200) { + $results_per_page = 200; +} + +$skin = load_user_skin($username); +$css_file = $skin === 'evolution' ? 'evolution.css' : 'enhanced.css'; +$inline_css = ''; +$css_path = $plugin_dir . '/user/' . $css_file; +if (is_readable($css_path)) { + $inline_css = (string)file_get_contents($css_path); +} + +$job_base = '/usr/local/directadmin/plugins/borg-restore/data/jobs'; +$log_dir = '/usr/local/directadmin/plugins/borg-restore/data/logs'; + +$allowed_sorts = ['user', 'start', 'type', 'mode', 'status']; +$sort = strtolower((string)($params['sort'] ?? 'start')); +if (!in_array($sort, $allowed_sorts, true)) { + $sort = 'start'; +} +$dir = strtolower((string)($params['dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; +$page = (int)($params['page'] ?? 1); +if ($page < 1) { + $page = 1; +} + +$base_link_params = ['sort' => $sort, 'dir' => $dir, 'page' => $page]; +$jobs = []; +$total = 0; +$total_pages = 1; +$page_start = 0; +$page_end = 0; +$page_jobs = []; + +if ($admin_mode_enabled) { + $jobs = collect_jobs($job_base, $log_dir); + usort($jobs, static function (array $a, array $b) use ($sort, $dir): int { + return compare_rows($a, $b, $sort, $dir); + }); + + $total = count($jobs); + $total_pages = max(1, (int)ceil($total / $results_per_page)); + if ($page > $total_pages) { + $page = $total_pages; + $base_link_params['page'] = $page; + } + $offset = ($page - 1) * $results_per_page; + $page_jobs = array_slice($jobs, $offset, $results_per_page); + if ($total > 0) { + $page_start = $offset + 1; + $page_end = min($offset + count($page_jobs), $total); + } +} + +$view_log = null; +if ($admin_mode_enabled && $action === 'view_log') { + $file = basename((string)($params['job'] ?? '')); + if ($file !== '' && preg_match('/^[-A-Za-z0-9_.]+$/', $file)) { + $job_id = job_id_from_file($file); + $job_file_path = find_job_file($job_base, $file, $job_id); + if ($job_file_path !== '') { + $vars = load_job_vars($job_file_path); + $log_path = $log_dir . '/' . $job_id . '.log'; + $log_message = is_readable($log_path) ? normalize_log((string)file_get_contents($log_path)) : 'Log nie istnieje.'; + $restore_path_label = display_restore_path((string)($vars['USERRESTOREPATH'] ?? '')); + $explanation = log_missing_include_explanation($log_message, $restore_path_label); + if ($explanation !== '') { + $log_message = rtrim($log_message) . "\n\n" . $explanation; + } + + $base_status = get_job_status($job_id, $job_base); + $last_line = last_non_empty_line($log_message); + $progress = extract_progress($last_line); + $display = display_status($base_status, $last_line); + $status = normalize_admin_status($display); + if ($status === 'done') { + $progress_text = '100%'; + } elseif ($status === 'failed') { + $progress_text = '0%'; + } else { + $progress_text = $progress !== null ? ($progress . '%') : ($last_line !== '' ? $last_line : '-'); + } + + $start_ts = is_numeric($vars['START_TS'] ?? '') ? (int)$vars['START_TS'] : (int)(@filemtime($job_file_path) ?: 0); + if ($start_ts <= 0) { + $start_ts = time(); + } + + $view_log = [ + 'job' => $file, + 'user' => (string)($vars['DA_USER'] ?? '-'), + 'start' => date('Y-m-d H:i:s', $start_ts), + 'type' => restore_type_label((string)($vars['RESTORETYPE'] ?? '')), + 'mode' => restore_mode_label((string)($vars['RESTOREMODE'] ?? '')), + 'path' => $restore_path_label, + 'target' => display_restore_path((string)($vars['TARGETPATH'] ?? '')), + 'status' => admin_status_label($status), + 'progress' => $progress_text, + 'content' => $log_message, + ]; + } else { + $view_log = [ + 'error' => 'Nie znaleziono wskazanego zadania.', + ]; + } + } else { + $view_log = [ + 'error' => 'Nieprawidłowy identyfikator zadania.', + ]; + } +} + +$sort_labels = [ + 'user' => 'Nazwa użytkownika', + 'start' => 'Data rozpoczęcia', + 'type' => 'Rodzaj przywracanych danych', + 'mode' => 'Tryb przywracania', + 'status' => 'Status', +]; + +$sort_arrow = static function (string $field) use ($sort, $dir): string { + if ($field !== $sort) { + return ''; + } + return $dir === 'asc' ? ' ▲' : ' ▼'; +}; + +$sort_link = static function (string $field) use ($sort, $dir, $base_link_params): string { + $next_dir = ($sort === $field && $dir === 'asc') ? 'desc' : 'asc'; + return build_admin_url($base_link_params, ['sort' => $field, 'dir' => $next_dir, 'page' => 1]); +}; + +$view_back_url = build_admin_url($base_link_params, ['action' => null, 'job' => null]); +?> + + + + + BorgBackup - Przywracanie plików (Admin) + + + + + + +
+
+
BorgBackup - Przywracanie plików (Admin)
+
+ + +
+
Poziom administratora został wyłączony
+
+ + + +
+ + +
+ +

Log zadania

+
+
Nazwa użytkownika:
+
Data rozpoczęcia:
+
Rodzaj przywracanych danych:
+
Tryb przywracania:
+
Ścieżka do przywrócenia:
+
Lokalizacja docelowa:
+
+
Status:
+
Postęp:
+
+ +
+ + +
+

Zestawienie operacji przywracania

+

Wyniki na stronę:

+ +
+ Brak zmiennej BORG_PASSPHRASE w pliku . +
+ + + +

Brak operacji do wyświetlenia.

+ + + + + $label) { ?> + + + + + + + + + + + + + + + + + +
+ + + + Logi
+ Pokaż logi +
+ +
+
+ +
+
+ 1) { ?> + Poprzednia + + + Następna + +
+
+ +
+ +
+ + diff --git a/hooks/admin_img.html b/hooks/admin_img.html new file mode 100644 index 0000000..24053fd --- /dev/null +++ b/hooks/admin_img.html @@ -0,0 +1 @@ +BorgBackup - Przywracanie plików (Admin)
diff --git a/hooks/admin_txt.html b/hooks/admin_txt.html new file mode 100644 index 0000000..b62996e --- /dev/null +++ b/hooks/admin_txt.html @@ -0,0 +1 @@ +BorgBackup - Przywracanie plików (Admin) diff --git a/hooks/menu_user.sh b/hooks/menu_user.sh new file mode 100755 index 0000000..f05675b --- /dev/null +++ b/hooks/menu_user.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +user="${USERNAME:-${LOGNAME:-${USER:-}}}" +lang="${LANGUAGE:-}" + +normalize_lang() { + val="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + case "$val" in + pl*|polish*) echo "pl" ;; + en*|english*) echo "en" ;; + *) echo "" ;; + esac +} + +lang="$(normalize_lang "$lang")" +if [ -z "$lang" ] && [ -n "$user" ] && [ -r "/usr/local/directadmin/data/users/$user/user.conf" ]; then + cfg_lang="$(grep -i -m1 -E '^(lang|language|locale)=' "/usr/local/directadmin/data/users/$user/user.conf" | cut -d= -f2-)" + lang="$(normalize_lang "$cfg_lang")" +fi + +if [ "$lang" = "pl" ]; then + label="BorgBackup - Przywracanie plików" +else + label="BorgBackup - File Restore" +fi + +printf '[{"name":"%s","icon":"","entries":[{"name":"%s","icon":"","href":"/CMD_PLUGINS/borg-restore","newTab":false,"updates":0}]}]\n' "$label" "$label" diff --git a/hooks/user_txt.html b/hooks/user_txt.html new file mode 100644 index 0000000..012a52f --- /dev/null +++ b/hooks/user_txt.html @@ -0,0 +1 @@ +BorgBackup - Przywracanie plików diff --git a/images/admin_icon.svg b/images/admin_icon.svg new file mode 100644 index 0000000..a2bceb0 --- /dev/null +++ b/images/admin_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/user_icon.svg b/images/user_icon.svg new file mode 100644 index 0000000..a2bceb0 --- /dev/null +++ b/images/user_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..5a6fbdb --- /dev/null +++ b/install.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/install.sh" + +if [ ! -x "$SCRIPT" ]; then + chmod 755 "$SCRIPT" 2>/dev/null || true +fi + +exec "$SCRIPT" diff --git a/lang/en.php b/lang/en.php new file mode 100644 index 0000000..1869f54 --- /dev/null +++ b/lang/en.php @@ -0,0 +1,164 @@ + 'BorgBackup - File Restore', + 'Lista backupow (borg list)' => 'Backup list (borg list)', + 'Status zadań' => 'Job status', + 'Nazwa' => 'Name', + 'Data' => 'Date', + 'Hash' => 'Hash', + 'PID' => 'PID', + 'Status' => 'Status', + 'Backup' => 'Backup', + 'Ścieżka do przywrócenia:' => 'Restore path:', + 'Rodzaj przywracanych danych' => 'Type of data to restore', + 'Rodzaj przywracanych danych:' => 'Type of data to restore:', + 'Odzyskiwanie plików stron www' => 'Restore website files', + 'Odzyskiwanie zawartości skrzynki pocztowej' => 'Restore mailbox contents', + 'Pliki www' => 'Website files', + 'Zawartość skrzynki pocztowej' => 'Mailbox contents', + 'Tryb przywracania' => 'Restore mode', + 'Tryb przywracania:' => 'Restore mode:', + 'Tryb przywracania danych:' => 'Restore data mode:', + 'Przywracanie do lokalizacji' => 'Restore to location', + 'Połączenie danych kopii zapasowej z obecnymi danymi' => 'Merge backup data with existing data', + 'Scalenie' => 'Merge', + 'Nadpisz aktualne dane' => 'Overwrite existing data', + 'Przykłady użycia:' => 'Usage examples:', + 'Uwaga: znaki / z początku i końca linii zostaną usunięte.' => 'Note: leading and trailing / characters will be removed.', + 'Lokalizacja docelowa:' => 'Target location:', + 'Domyślna lokalizacja docelowa:' => 'Default target location:', + 'Przywróć' => 'Restore', + 'Brak wpisów do wyświetlenia.' => 'No entries to display.', + 'Odśwież' => 'Refresh', + 'Log zadania' => 'Job log', + 'Kopiuj do schowka' => 'Copy to clipboard', + 'Zamknij' => 'Close', + 'Podgląd na żywo (odświeżanie co 3s)' => 'Live view (refresh every 3s)', + 'Data rozpoczęcia:' => 'Start time:', + 'Status:' => 'Status:', + 'Postęp:' => 'Progress:', + 'Zapisz log' => 'Download log', + 'Wybierz lokalizację' => 'Select location', + 'Wybierz katalog' => 'Select directory', + 'Nowy katalog' => 'New directory', + 'Utwórz' => 'Create', + 'Anuluj' => 'Cancel', + 'Wybierz' => 'Select', + 'Zaznacz wszystkie' => 'Select all', + 'Usuń zaznaczone zdarzenia' => 'Delete selected entries', + 'Czy na pewno usunąć zaznaczone zdarzenia?' => 'Are you sure you want to delete the selected entries?', + 'Anulować zadanie?' => 'Cancel the job?', + 'Usunąć zadanie?' => 'Delete the job?', + 'Użytkownik' => 'User', + 'Ścieżka' => 'Path', + 'Cel' => 'Target', + 'Czas' => 'Time', + 'Akcje' => 'Actions', + 'Postęp' => 'Progress', + 'Logi' => 'Logs', + 'Usuń' => 'Delete', + 'Potwierdź nadpisanie danych' => 'Confirm data overwrite', + 'Potwierdź scalenie danych' => 'Confirm data merge', + 'Wybrano tryb nadpisywania danych. Istniejący katalog zostanie zastąpiony.' => 'Overwrite mode selected. The existing directory will be replaced.', + 'Wybrano tryb scalenia danych. Istniejący katalog zostanie połączony z danymi z backupu.' => 'Merge mode selected. The existing directory will be merged with backup data.', + 'DANE W KATALOGU ' => 'DATA IN DIRECTORY ', + 'ZOSTANĄ UTRACONE.' => 'WILL BE LOST.', + 'Tak, kontynuuj' => 'Yes, continue', + 'Uwaga:' => 'Warning:', + 'Brak zadań do wyświetlenia.' => 'No jobs to display.', + 'Brak zadań do wyczyszczenia.' => 'No jobs to clear.', + 'Zadania zostały wyczyszczone.' => 'Jobs have been cleared.', + 'Zadanie zostało anulowane.' => 'Job has been canceled.', + 'Zadanie zostało usunięte.' => 'Job has been deleted.', + 'Zadanie nie jest w trakcie.' => 'Job is not in progress.', + 'Wybrane zadania zostały usunięte.' => 'Selected jobs have been deleted.', + 'Oczekuje' => 'Pending', + 'W trakcie' => 'In progress', + 'Przygotowanie' => 'Preparing', + 'Zakończone' => 'Completed', + 'Nie udało się anulować zadania.' => 'Failed to cancel job.', + 'Nie znaleziono procesu do anulowania.' => 'No process found to cancel.', + 'Brak obsługi posix_kill na serwerze.' => 'posix_kill is not available on the server.', + 'Nie znaleziono zadania.' => 'Job not found.', + 'Nieprawidłowe zadanie.' => 'Invalid job.', + 'Nieprawidłowy identyfikator logu.' => 'Invalid log identifier.', + 'Nieprawidłowy identyfikator logu.\n' => "Invalid log identifier.\n", + 'Nieprawidłowy token CSRF.' => 'Invalid CSRF token.', + 'Nieprawidłowa nazwa katalogu.' => 'Invalid directory name.', + 'Katalog już istnieje.' => 'Directory already exists.', + 'Podaj nazwę katalogu.' => 'Provide a directory name.', + 'Nie udało się utworzyć katalogu.' => 'Failed to create directory.', + 'Nie udało się utworzyć katalogu zadań.' => 'Failed to create jobs directory.', + 'Nie udało się zapisać pliku zadania.' => 'Failed to write job file.', + 'Nie udało się wczytać katalogów.' => 'Failed to load directories.', + 'Nie udało się wysłać żądania.' => 'Failed to send request.', + 'Nie udało się przetworzyć odpowiedzi.' => 'Failed to process response.', + 'Nie udało się pobrać logu.' => 'Failed to download log.', + 'Nie udało się uruchomić borg.' => 'Failed to run borg.', + 'Nie podano ścieżki do przywrócenia.' => 'Restore path not provided.', + 'Nie podano lokalizacji docelowej.' => 'Target location not provided.', + 'Nie wybrano backupu.' => 'No backup selected.', + 'Nie wybrano żadnych zadań.' => 'No jobs selected.', + 'Brak katalogów' => 'No directories', + 'Brak wpisów do wyświetlenia.' => 'No entries to display.', + 'Brak dostępu' => 'Access denied', + 'Brak dostępu do zadania.' => 'No access to the job.', + "Brak dostępu do logu.\n" => "No access to the log.\n", + 'Log nie istnieje.' => 'Log does not exist.', + "Log nie istnieje.\n" => "Log does not exist.\n", + 'Kod bledu: ' => 'Error code: ', + 'Błąd borg list: ' => 'borg list error: ', + 'Błąd' => 'Error', + 'Anulowane' => 'Canceled', + 'Nieznany' => 'Unknown', + 'BB_PATH jest puste w plugin-settings.conf.' => 'BB_PATH is empty in plugin-settings.conf.', + 'BORG_PATH jest puste w plugin-settings.conf.' => 'BORG_PATH is empty in plugin-settings.conf.', + 'BORG_PATH nie jest plikiem wykonywalnym: ' => 'BORG_PATH is not executable: ', + 'BORG_SSH_KEY nie jest plikiem: ' => 'BORG_SSH_KEY is not a file: ', + 'BORG_VARIABLES_FILE jest puste w plugin-settings.conf.' => 'BORG_VARIABLES_FILE is empty in plugin-settings.conf.', + 'WEB_RESTORE_LOCATION jest puste w plugin-settings.conf.' => 'WEB_RESTORE_LOCATION is empty in plugin-settings.conf.', + 'MAIL_RESTORE_LOCATION jest puste w plugin-settings.conf.' => 'MAIL_RESTORE_LOCATION is empty in plugin-settings.conf.', + 'DEFAULT_RESTORE_DESTINATION jest puste w plugin-settings.conf.' => 'DEFAULT_RESTORE_DESTINATION is empty in plugin-settings.conf.', + 'Brak pliku plugin-settings.conf.' => 'Missing plugin-settings.conf file.', + 'Brak pliku bbvars.sh: ' => 'Missing bbvars.sh file: ', + 'Brak zmiennej REPO w ' => 'Missing REPO variable in ', + 'nie ustawiono BB_PATH' => 'BB_PATH not set', + 'BLOCK_RESTORE_OUTSIDE_LOCATION jest wyłączone - ścieżka do przywrócenia nie jest ograniczona.' => 'BLOCK_RESTORE_OUTSIDE_LOCATION is disabled - restore path is not restricted.', + 'Ścieżka nie może zawierać "..".' => 'Path cannot contain "..".', + 'Lokalizacja docelowa nie może zawierać "..".' => 'Target location cannot contain "..".', + 'Lokalizacja docelowa musi zaczynać się od /.' => 'Target location must start with /.', + 'Lokalizacja docelowa musi być w katalogu: ' => 'Target location must be inside: ', + 'Ścieżka musi być w katalogu: ' => 'Path must be inside: ', + 'Lokalizacja docelowa istnieje, ale nie jest katalogiem.' => 'Target location exists but is not a directory.', + 'Lokalizacja docelowa nie jest pusta.' => 'Target location is not empty.', + 'Nie można odczytać katalogu docelowego.' => 'Cannot read target directory.', + 'Nie można utworzyć katalogu poza dozwoloną ścieżką.' => 'Cannot create directory outside the allowed path.', + 'Nie można utworzyć katalogu bezpośrednio w ' => 'Cannot create directory directly in ', + ' w katalogu nadrzędnym ścieżki do przywrócenia.' => ' in the parent directory of the restore path.', + 'Nie można przywrócić całego ' => 'Cannot restore the entire ', + ' w trybie nadpisywania danych. Podaj podkatalog w ramach ' => ' in overwrite mode. Provide a subdirectory under ', + ' lub wybierz tryb przywracania do lokalizacji.' => ' or choose restore-to-location mode.', + ' w trybie nadpisywania lub scalania danych. Podaj podkatalog w ramach ' => ' in overwrite or merge mode. Provide a subdirectory under ', + 'Backup zostanie utworzony jako *_przed_przywroceniem_' => 'Backup will be created as *_before_restore_', + 'Backup zostanie utworzony jako: ' => 'Backup will be created as: ', + 'Nieprawidłowe uprawnienia plików konfiguracyjnych lub zadań. Przywracanie zablokowane.' => 'Invalid permissions for configuration or job files. Restore blocked.', + 'Problem z konfiguracją środowiska backupowego - skontaktuj się z administratorem w celu uzyskania pomocy' => 'Backup environment configuration problem — contact the administrator for help', + 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki' => 'The specified restore path was not found in the backup', + 'Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować.' => 'Please try restoring from another archive that may contain those files.', + 'Wyjaśnienie: wskazana ścieżka do przywrócenia' => 'Explanation: the specified restore path', + 'nie została odnaleziona w kopii zapasowej.' => 'was not found in the backup.', + '----- logi Borg -----' => '----- Borg logs -----', + 'Przywracana ścieżka:' => 'Restored path:', + 'Zadanie przywracania danych dodane do kolejki, zostaniesz powiadomiony przez system wiadomości po zakończeniu tego procesu.' => 'Restore job queued; you will be notified by the message system when it completes.', + 'Plik zadania' => 'Job file', + '⟵ W górę' => '⟵ Up', + 'Metoda niedozwolona.' => 'Method Not Allowed.', + 'Method Not Allowed.' => 'Method Not Allowed.', + 'ma nieprawidłowe uprawnienia: ' => 'has invalid permissions: ', + 'ma nieprawidłowego właściciela (' => 'has invalid owner (', + 'ma nieprawidłowego właściciela: ' => 'has invalid owner: ', + 'ma pustą ścieżkę.' => 'has an empty path.', + 'nie istnieje: ' => 'does not exist: ', + 'nie jest zwykłym plikiem: ' => 'is not a regular file: ', + 'nie może być zapisywalny dla grupy/innych: ' => 'cannot be group/other writable: ', +]; diff --git a/lang/pl.php b/lang/pl.php new file mode 100644 index 0000000..478b5ba --- /dev/null +++ b/lang/pl.php @@ -0,0 +1,7 @@ + $_value) { + $pl[$key] = $key; +} +return $pl; diff --git a/php.ini b/php.ini new file mode 100644 index 0000000..468b8fc --- /dev/null +++ b/php.ini @@ -0,0 +1,1961 @@ +[PHP] + +;;;;;;;;;;;;;;;;;;; +; About php.ini ; +;;;;;;;;;;;;;;;;;;; +; PHP's initialization file, generally called php.ini, is responsible for +; configuring many of the aspects of PHP's behavior. + +; PHP attempts to find and load this configuration from a number of locations. +; The following is a summary of its search order: +; 1. SAPI module specific location. +; 2. The PHPRC environment variable. +; 3. A number of predefined registry keys on Windows +; 4. Current working directory (except CLI) +; 5. The web server's directory (for SAPI modules), or directory of PHP +; (otherwise in Windows) +; 6. The directory from the --with-config-file-path compile time option, or the +; Windows directory (usually C:\windows) +; See the PHP docs for more specific information. +; https://php.net/configuration.file + +; The syntax of the file is extremely simple. Whitespace and lines +; beginning with a semicolon are silently ignored (as you probably guessed). +; Section headers (e.g. [Foo]) are also silently ignored, even though +; they might mean something in the future. + +; Directives following the section heading [PATH=/www/mysite] only +; apply to PHP files in the /www/mysite directory. Directives +; following the section heading [HOST=www.example.com] only apply to +; PHP files served from www.example.com. Directives set in these +; special sections cannot be overridden by user-defined INI files or +; at runtime. Currently, [PATH=] and [HOST=] sections only work under +; CGI/FastCGI. +; https://php.net/ini.sections + +; Directives are specified using the following syntax: +; directive = value +; Directive names are *case sensitive* - foo=bar is different from FOO=bar. +; Directives are variables used to configure PHP or PHP extensions. +; There is no name validation. If PHP can't find an expected +; directive because it is not set or is mistyped, a default value will be used. + +; The value can be a string, a number, a PHP constant (e.g. E_ALL or M_PI), one +; of the INI constants (On, Off, True, False, Yes, No and None) or an expression +; (e.g. E_ALL & ~E_NOTICE), a quoted string ("bar"), or a reference to a +; previously set variable or directive (e.g. ${foo}) + +; Expressions in the INI file are limited to bitwise operators and parentheses: +; | bitwise OR +; ^ bitwise XOR +; & bitwise AND +; ~ bitwise NOT +; ! boolean NOT + +; Boolean flags can be turned on using the values 1, On, True or Yes. +; They can be turned off using the values 0, Off, False or No. + +; An empty string can be denoted by simply not writing anything after the equal +; sign, or by using the None keyword: + +; foo = ; sets foo to an empty string +; foo = None ; sets foo to an empty string +; foo = "None" ; sets foo to the string 'None' + +; If you use constants in your value, and these constants belong to a +; dynamically loaded extension (either a PHP extension or a Zend extension), +; you may only use these constants *after* the line that loads the extension. + +;;;;;;;;;;;;;;;;;;; +; About this file ; +;;;;;;;;;;;;;;;;;;; +; PHP comes packaged with two INI files. One that is recommended to be used +; in production environments and one that is recommended to be used in +; development environments. + +; php.ini-production contains settings which hold security, performance and +; best practices at its core. But please be aware, these settings may break +; compatibility with older or less security-conscious applications. We +; recommending using the production ini in production and testing environments. + +; php.ini-development is very similar to its production variant, except it is +; much more verbose when it comes to errors. We recommend using the +; development version only in development environments, as errors shown to +; application users can inadvertently leak otherwise secure information. + +; This is the php.ini-production INI file. + +;;;;;;;;;;;;;;;;;;; +; Quick Reference ; +;;;;;;;;;;;;;;;;;;; + +; The following are all the settings which are different in either the production +; or development versions of the INIs with respect to PHP's default behavior. +; Please see the actual settings later in the document for more details as to why +; we recommend these changes in PHP's behavior. + +; display_errors +; Default Value: On +; Development Value: On +; Production Value: Off + +; display_startup_errors +; Default Value: On +; Development Value: On +; Production Value: Off + +; error_reporting +; Default Value: E_ALL +; Development Value: E_ALL +; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT + +; log_errors +; Default Value: Off +; Development Value: On +; Production Value: On + +; max_input_time +; Default Value: -1 (Unlimited) +; Development Value: 60 (60 seconds) +; Production Value: 60 (60 seconds) + +; output_buffering +; Default Value: Off +; Development Value: 4096 +; Production Value: 4096 + +; register_argc_argv +; Default Value: On +; Development Value: Off +; Production Value: Off + +; request_order +; Default Value: None +; Development Value: "GP" +; Production Value: "GP" + +; session.gc_divisor +; Default Value: 100 +; Development Value: 1000 +; Production Value: 1000 + +; session.sid_bits_per_character +; Default Value: 4 +; Development Value: 5 +; Production Value: 5 + +; session.sid_length +; Default Value: 32 +; Development Value: 26 +; Production Value: 26 + +; short_open_tag +; Default Value: On +; Development Value: Off +; Production Value: Off + +; variables_order +; Default Value: "EGPCS" +; Development Value: "GPCS" +; Production Value: "GPCS" + +; zend.assertions +; Default Value: 1 +; Development Value: 1 +; Production Value: -1 + +; zend.exception_ignore_args +; Default Value: Off +; Development Value: Off +; Production Value: On + +; zend.exception_string_param_max_len +; Default Value: 15 +; Development Value: 15 +; Production Value: 0 + +;;;;;;;;;;;;;;;;;;;; +; php.ini Options ; +;;;;;;;;;;;;;;;;;;;; +; Name for user-defined php.ini (.htaccess) files. Default is ".user.ini" +;user_ini.filename = ".user.ini" + +; To disable this feature set this option to an empty value +;user_ini.filename = + +; TTL for user-defined php.ini files (time-to-live) in seconds. Default is 300 seconds (5 minutes) +;user_ini.cache_ttl = 300 + +;;;;;;;;;;;;;;;;;;;; +; Language Options ; +;;;;;;;;;;;;;;;;;;;; + +; Enable the PHP scripting language engine under Apache. +; https://php.net/engine +engine = On + +; This directive determines whether or not PHP will recognize code between +; tags as PHP source which should be processed as such. It is +; generally recommended that should be used and that this feature +; should be disabled, as enabling it may result in issues when generating XML +; documents, however this remains supported for backward compatibility reasons. +; Note that this directive does not control the would work. +; https://php.net/syntax-highlighting +;highlight.string = #DD0000 +;highlight.comment = #FF9900 +;highlight.keyword = #007700 +;highlight.default = #0000BB +;highlight.html = #000000 + +; If enabled, the request will be allowed to complete even if the user aborts +; the request. Consider enabling it if executing long requests, which may end up +; being interrupted by the user or a browser timing out. PHP's default behavior +; is to disable this feature. +; https://php.net/ignore-user-abort +;ignore_user_abort = On + +; Determines the size of the realpath cache to be used by PHP. This value should +; be increased on systems where PHP opens many files to reflect the quantity of +; the file operations performed. +; Note: if open_basedir is set, the cache is disabled +; https://php.net/realpath-cache-size +;realpath_cache_size = 4096k + +; Duration of time, in seconds for which to cache realpath information for a given +; file or directory. For systems with rarely changing files, consider increasing this +; value. +; https://php.net/realpath-cache-ttl +;realpath_cache_ttl = 120 + +; Enables or disables the circular reference collector. +; https://php.net/zend.enable-gc +zend.enable_gc = On + +; If enabled, scripts may be written in encodings that are incompatible with +; the scanner. CP936, Big5, CP949 and Shift_JIS are the examples of such +; encodings. To use this feature, mbstring extension must be enabled. +;zend.multibyte = Off + +; Allows to set the default encoding for the scripts. This value will be used +; unless "declare(encoding=...)" directive appears at the top of the script. +; Only affects if zend.multibyte is set. +;zend.script_encoding = + +; Allows to include or exclude arguments from stack traces generated for exceptions. +; In production, it is recommended to turn this setting on to prohibit the output +; of sensitive information in stack traces +; Default Value: Off +; Development Value: Off +; Production Value: On +zend.exception_ignore_args = On + +; Allows setting the maximum string length in an argument of a stringified stack trace +; to a value between 0 and 1000000. +; This has no effect when zend.exception_ignore_args is enabled. +; Default Value: 15 +; Development Value: 15 +; Production Value: 0 +; In production, it is recommended to set this to 0 to reduce the output +; of sensitive information in stack traces. +zend.exception_string_param_max_len = 0 + +;;;;;;;;;;;;;;;;; +; Miscellaneous ; +;;;;;;;;;;;;;;;;; + +; Decides whether PHP may expose the fact that it is installed on the server +; (e.g. by adding its signature to the Web server header). It is no security +; threat in any way, but it makes it possible to determine whether you use PHP +; on your server or not. +; https://php.net/expose-php +expose_php = Off + +;;;;;;;;;;;;;;;;;;; +; Resource Limits ; +;;;;;;;;;;;;;;;;;;; + +; Maximum execution time of each script, in seconds +; https://php.net/max-execution-time +; Note: This directive is hardcoded to 0 for the CLI SAPI +max_execution_time = 30 + +; Maximum amount of time each script may spend parsing request data. It's a good +; idea to limit this time on productions servers in order to eliminate unexpectedly +; long running scripts. +; Note: This directive is hardcoded to -1 for the CLI SAPI +; Default Value: -1 (Unlimited) +; Development Value: 60 (60 seconds) +; Production Value: 60 (60 seconds) +; https://php.net/max-input-time +max_input_time = 60 + +; Maximum input variable nesting level +; https://php.net/max-input-nesting-level +;max_input_nesting_level = 64 + +; How many GET/POST/COOKIE input variables may be accepted +;max_input_vars = 1000 + +; How many multipart body parts (combined input variable and file uploads) may +; be accepted. +; Default Value: -1 (Sum of max_input_vars and max_file_uploads) +;max_multipart_body_parts = 1500 + +; Maximum amount of memory a script may consume +; https://php.net/memory-limit +memory_limit = 128M + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +; Error handling and logging ; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +; This directive informs PHP of which errors, warnings and notices you would like +; it to take action for. The recommended way of setting values for this +; directive is through the use of the error level constants and bitwise +; operators. The error level constants are below here for convenience as well as +; some common settings and their meanings. +; By default, PHP is set to take action on all errors, notices and warnings EXCEPT +; those related to E_NOTICE and E_STRICT, which together cover best practices and +; recommended coding standards in PHP. For performance reasons, this is the +; recommend error reporting setting. Your production server shouldn't be wasting +; resources complaining about best practices and coding standards. That's what +; development servers and development settings are for. +; Note: The php.ini-development file has this setting as E_ALL. This +; means it pretty much reports everything which is exactly what you want during +; development and early testing. +; +; Error Level Constants: +; E_ALL - All errors and warnings +; E_ERROR - fatal run-time errors +; E_RECOVERABLE_ERROR - almost fatal run-time errors +; E_WARNING - run-time warnings (non-fatal errors) +; E_PARSE - compile-time parse errors +; E_NOTICE - run-time notices (these are warnings which often result +; from a bug in your code, but it's possible that it was +; intentional (e.g., using an uninitialized variable and +; relying on the fact it is automatically initialized to an +; empty string) +; E_STRICT - run-time notices, enable to have PHP suggest changes +; to your code which will ensure the best interoperability +; and forward compatibility of your code +; E_CORE_ERROR - fatal errors that occur during PHP's initial startup +; E_CORE_WARNING - warnings (non-fatal errors) that occur during PHP's +; initial startup +; E_COMPILE_ERROR - fatal compile-time errors +; E_COMPILE_WARNING - compile-time warnings (non-fatal errors) +; E_USER_ERROR - user-generated error message +; E_USER_WARNING - user-generated warning message +; E_USER_NOTICE - user-generated notice message +; E_DEPRECATED - warn about code that will not work in future versions +; of PHP +; E_USER_DEPRECATED - user-generated deprecation warnings +; +; Common Values: +; E_ALL (Show all errors, warnings and notices including coding standards.) +; E_ALL & ~E_NOTICE (Show all errors, except for notices) +; E_ALL & ~E_NOTICE & ~E_STRICT (Show all errors, except for notices and coding standards warnings.) +; E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR (Show only errors) +; Default Value: E_ALL +; Development Value: E_ALL +; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT +; https://php.net/error-reporting +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT + +; This directive controls whether or not and where PHP will output errors, +; notices and warnings too. Error output is very useful during development, but +; it could be very dangerous in production environments. Depending on the code +; which is triggering the error, sensitive information could potentially leak +; out of your application such as database usernames and passwords or worse. +; For production environments, we recommend logging errors rather than +; sending them to STDOUT. +; Possible Values: +; Off = Do not display any errors +; stderr = Display errors to STDERR (affects only CGI/CLI binaries!) +; On or stdout = Display errors to STDOUT +; Default Value: On +; Development Value: On +; Production Value: Off +; https://php.net/display-errors +display_errors = Off + +; The display of errors which occur during PHP's startup sequence are handled +; separately from display_errors. We strongly recommend you set this to 'off' +; for production servers to avoid leaking configuration details. +; Default Value: On +; Development Value: On +; Production Value: Off +; https://php.net/display-startup-errors +display_startup_errors = Off + +; Besides displaying errors, PHP can also log errors to locations such as a +; server-specific log, STDERR, or a location specified by the error_log +; directive found below. While errors should not be displayed on productions +; servers they should still be monitored and logging is a great way to do that. +; Default Value: Off +; Development Value: On +; Production Value: On +; https://php.net/log-errors +log_errors = On + +; Do not log repeated messages. Repeated errors must occur in same file on same +; line unless ignore_repeated_source is set true. +; https://php.net/ignore-repeated-errors +ignore_repeated_errors = Off + +; Ignore source of message when ignoring repeated messages. When this setting +; is On you will not log errors with repeated messages from different files or +; source lines. +; https://php.net/ignore-repeated-source +ignore_repeated_source = Off + +; If this parameter is set to Off, then memory leaks will not be shown (on +; stdout or in the log). This is only effective in a debug compile, and if +; error reporting includes E_WARNING in the allowed list +; https://php.net/report-memleaks +report_memleaks = On + +; This setting is off by default. +;report_zend_debug = 0 + +; Turn off normal error reporting and emit XML-RPC error XML +; https://php.net/xmlrpc-errors +;xmlrpc_errors = 0 + +; An XML-RPC faultCode +;xmlrpc_error_number = 0 + +; When PHP displays or logs an error, it has the capability of formatting the +; error message as HTML for easier reading. This directive controls whether +; the error message is formatted as HTML or not. +; Note: This directive is hardcoded to Off for the CLI SAPI +; https://php.net/html-errors +;html_errors = On + +; If html_errors is set to On *and* docref_root is not empty, then PHP +; produces clickable error messages that direct to a page describing the error +; or function causing the error in detail. +; You can download a copy of the PHP manual from https://php.net/docs +; and change docref_root to the base URL of your local copy including the +; leading '/'. You must also specify the file extension being used including +; the dot. PHP's default behavior is to leave these settings empty, in which +; case no links to documentation are generated. +; Note: Never use this feature for production boxes. +; https://php.net/docref-root +; Examples +;docref_root = "/phpmanual/" + +; https://php.net/docref-ext +;docref_ext = .html + +; String to output before an error message. PHP's default behavior is to leave +; this setting blank. +; https://php.net/error-prepend-string +; Example: +;error_prepend_string = "" + +; String to output after an error message. PHP's default behavior is to leave +; this setting blank. +; https://php.net/error-append-string +; Example: +;error_append_string = "" + +; Log errors to specified file. PHP's default behavior is to leave this value +; empty. +; https://php.net/error-log +; Example: +;error_log = php_errors.log +; Log errors to syslog (Event Log on Windows). +;error_log = syslog + +; The syslog ident is a string which is prepended to every message logged +; to syslog. Only used when error_log is set to syslog. +;syslog.ident = php + +; The syslog facility is used to specify what type of program is logging +; the message. Only used when error_log is set to syslog. +;syslog.facility = user + +; Set this to disable filtering control characters (the default). +; Some loggers only accept NVT-ASCII, others accept anything that's not +; control characters. If your logger accepts everything, then no filtering +; is needed at all. +; Allowed values are: +; ascii (all printable ASCII characters and NL) +; no-ctrl (all characters except control characters) +; all (all characters) +; raw (like "all", but messages are not split at newlines) +; https://php.net/syslog.filter +;syslog.filter = ascii + +;windows.show_crt_warning +; Default value: 0 +; Development value: 0 +; Production value: 0 + +;;;;;;;;;;;;;;;;; +; Data Handling ; +;;;;;;;;;;;;;;;;; + +; The separator used in PHP generated URLs to separate arguments. +; PHP's default setting is "&". +; https://php.net/arg-separator.output +; Example: +;arg_separator.output = "&" + +; List of separator(s) used by PHP to parse input URLs into variables. +; PHP's default setting is "&". +; NOTE: Every character in this directive is considered as separator! +; https://php.net/arg-separator.input +; Example: +;arg_separator.input = ";&" + +; This directive determines which super global arrays are registered when PHP +; starts up. G,P,C,E & S are abbreviations for the following respective super +; globals: GET, POST, COOKIE, ENV and SERVER. There is a performance penalty +; paid for the registration of these arrays and because ENV is not as commonly +; used as the others, ENV is not recommended on productions servers. You +; can still get access to the environment variables through getenv() should you +; need to. +; Default Value: "EGPCS" +; Development Value: "GPCS" +; Production Value: "GPCS"; +; https://php.net/variables-order +variables_order = "GPCS" + +; This directive determines which super global data (G,P & C) should be +; registered into the super global array REQUEST. If so, it also determines +; the order in which that data is registered. The values for this directive +; are specified in the same manner as the variables_order directive, +; EXCEPT one. Leaving this value empty will cause PHP to use the value set +; in the variables_order directive. It does not mean it will leave the super +; globals array REQUEST empty. +; Default Value: None +; Development Value: "GP" +; Production Value: "GP" +; https://php.net/request-order +request_order = "GP" + +; This directive determines whether PHP registers $argv & $argc each time it +; runs. $argv contains an array of all the arguments passed to PHP when a script +; is invoked. $argc contains an integer representing the number of arguments +; that were passed when the script was invoked. These arrays are extremely +; useful when running scripts from the command line. When this directive is +; enabled, registering these variables consumes CPU cycles and memory each time +; a script is executed. For performance reasons, this feature should be disabled +; on production servers. +; Note: This directive is hardcoded to On for the CLI SAPI +; Default Value: On +; Development Value: Off +; Production Value: Off +; https://php.net/register-argc-argv +register_argc_argv = Off + +; When enabled, the ENV, REQUEST and SERVER variables are created when they're +; first used (Just In Time) instead of when the script starts. If these +; variables are not used within a script, having this directive on will result +; in a performance gain. The PHP directive register_argc_argv must be disabled +; for this directive to have any effect. +; https://php.net/auto-globals-jit +auto_globals_jit = On + +; Whether PHP will read the POST data. +; This option is enabled by default. +; Most likely, you won't want to disable this option globally. It causes $_POST +; and $_FILES to always be empty; the only way you will be able to read the +; POST data will be through the php://input stream wrapper. This can be useful +; to proxy requests or to process the POST data in a memory efficient fashion. +; https://php.net/enable-post-data-reading +;enable_post_data_reading = Off + +; Maximum size of POST data that PHP will accept. +; Its value may be 0 to disable the limit. It is ignored if POST data reading +; is disabled through enable_post_data_reading. +; https://php.net/post-max-size +post_max_size = 64M + +; Automatically add files before PHP document. +; https://php.net/auto-prepend-file +auto_prepend_file = + +; Automatically add files after PHP document. +; https://php.net/auto-append-file +auto_append_file = + +; By default, PHP will output a media type using the Content-Type header. To +; disable this, simply set it to be empty. +; +; PHP's built-in default media type is set to text/html. +; https://php.net/default-mimetype +default_mimetype = "text/html" + +; PHP's default character set is set to UTF-8. +; https://php.net/default-charset +default_charset = "UTF-8" + +; PHP internal character encoding is set to empty. +; If empty, default_charset is used. +; https://php.net/internal-encoding +;internal_encoding = + +; PHP input character encoding is set to empty. +; If empty, default_charset is used. +; https://php.net/input-encoding +;input_encoding = + +; PHP output character encoding is set to empty. +; If empty, default_charset is used. +; See also output_buffer. +; https://php.net/output-encoding +;output_encoding = + +;;;;;;;;;;;;;;;;;;;;;;;;; +; Paths and Directories ; +;;;;;;;;;;;;;;;;;;;;;;;;; + +; UNIX: "/path1:/path2" +;include_path = ".:/php/includes" +; +; Windows: "\path1;\path2" +;include_path = ".;c:\php\includes" +; +; PHP's default setting for include_path is ".;/path/to/php/pear" +; https://php.net/include-path + +; The root of the PHP pages, used only if nonempty. +; if PHP was not compiled with FORCE_REDIRECT, you SHOULD set doc_root +; if you are running php as a CGI under any web server (other than IIS) +; see documentation for security issues. The alternate is to use the +; cgi.force_redirect configuration below +; https://php.net/doc-root +doc_root = + +; The directory under which PHP opens the script using /~username used only +; if nonempty. +; https://php.net/user-dir +user_dir = + +; Directory in which the loadable extensions (modules) reside. +; https://php.net/extension-dir +;extension_dir = "./" +; On windows: +;extension_dir = "ext" + +; Directory where the temporary files should be placed. +; Defaults to the system default (see sys_get_temp_dir) +;sys_temp_dir = "/tmp" + +; Whether or not to enable the dl() function. The dl() function does NOT work +; properly in multithreaded servers, such as IIS or Zeus, and is automatically +; disabled on them. +; https://php.net/enable-dl +enable_dl = Off + +; cgi.force_redirect is necessary to provide security running PHP as a CGI under +; most web servers. Left undefined, PHP turns this on by default. You can +; turn it off here AT YOUR OWN RISK +; **You CAN safely turn this off for IIS, in fact, you MUST.** +; https://php.net/cgi.force-redirect +;cgi.force_redirect = 1 + +; if cgi.nph is enabled it will force cgi to always sent Status: 200 with +; every request. PHP's default behavior is to disable this feature. +;cgi.nph = 1 + +; if cgi.force_redirect is turned on, and you are not running under Apache or Netscape +; (iPlanet) web servers, you MAY need to set an environment variable name that PHP +; will look for to know it is OK to continue execution. Setting this variable MAY +; cause security issues, KNOW WHAT YOU ARE DOING FIRST. +; https://php.net/cgi.redirect-status-env +;cgi.redirect_status_env = + +; cgi.fix_pathinfo provides *real* PATH_INFO/PATH_TRANSLATED support for CGI. PHP's +; previous behaviour was to set PATH_TRANSLATED to SCRIPT_FILENAME, and to not grok +; what PATH_INFO is. For more information on PATH_INFO, see the cgi specs. Setting +; this to 1 will cause PHP CGI to fix its paths to conform to the spec. A setting +; of zero causes PHP to behave as before. Default is 1. You should fix your scripts +; to use SCRIPT_FILENAME rather than PATH_TRANSLATED. +; https://php.net/cgi.fix-pathinfo +;cgi.fix_pathinfo=1 + +; if cgi.discard_path is enabled, the PHP CGI binary can safely be placed outside +; of the web tree and people will not be able to circumvent .htaccess security. +;cgi.discard_path=1 + +; FastCGI under IIS supports the ability to impersonate +; security tokens of the calling client. This allows IIS to define the +; security context that the request runs under. mod_fastcgi under Apache +; does not currently support this feature (03/17/2002) +; Set to 1 if running under IIS. Default is zero. +; https://php.net/fastcgi.impersonate +;fastcgi.impersonate = 1 + +; Disable logging through FastCGI connection. PHP's default behavior is to enable +; this feature. +;fastcgi.logging = 0 + +; cgi.rfc2616_headers configuration option tells PHP what type of headers to +; use when sending HTTP response code. If set to 0, PHP sends Status: header that +; is supported by Apache. When this option is set to 1, PHP will send +; RFC2616 compliant header. +; Default is zero. +; https://php.net/cgi.rfc2616-headers +;cgi.rfc2616_headers = 0 + +; cgi.check_shebang_line controls whether CGI PHP checks for line starting with #! +; (shebang) at the top of the running script. This line might be needed if the +; script support running both as stand-alone script and via PHP CGI<. PHP in CGI +; mode skips this line and ignores its content if this directive is turned on. +; https://php.net/cgi.check-shebang-line +;cgi.check_shebang_line=1 + +;;;;;;;;;;;;;;;; +; File Uploads ; +;;;;;;;;;;;;;;;; + +; Whether to allow HTTP file uploads. +; https://php.net/file-uploads +file_uploads = On + +; Temporary directory for HTTP uploaded files (will use system default if not +; specified). +; https://php.net/upload-tmp-dir +;upload_tmp_dir = + +; Maximum allowed size for uploaded files. +; https://php.net/upload-max-filesize +upload_max_filesize = 64M + +; Maximum number of files that can be uploaded via a single request +max_file_uploads = 20 + +;;;;;;;;;;;;;;;;;; +; Fopen wrappers ; +;;;;;;;;;;;;;;;;;; + +; Whether to allow the treatment of URLs (like http:// or ftp://) as files. +; https://php.net/allow-url-fopen +allow_url_fopen = On + +; Whether to allow include/require to open URLs (like https:// or ftp://) as files. +; https://php.net/allow-url-include +allow_url_include = Off + +; Define the anonymous ftp password (your email address). PHP's default setting +; for this is empty. +; https://php.net/from +;from="john@doe.com" + +; Define the User-Agent string. PHP's default setting for this is empty. +; https://php.net/user-agent +;user_agent="PHP" + +; Default timeout for socket based streams (seconds) +; https://php.net/default-socket-timeout +default_socket_timeout = 60 + +; If your scripts have to deal with files from Macintosh systems, +; or you are running on a Mac and need to deal with files from +; unix or win32 systems, setting this flag will cause PHP to +; automatically detect the EOL character in those files so that +; fgets() and file() will work regardless of the source of the file. +; https://php.net/auto-detect-line-endings +;auto_detect_line_endings = Off + +;;;;;;;;;;;;;;;;;;;;;; +; Dynamic Extensions ; +;;;;;;;;;;;;;;;;;;;;;; + +; If you wish to have an extension loaded automatically, use the following +; syntax: +; +; extension=modulename +; +; For example: +; +; extension=mysqli +; +; When the extension library to load is not located in the default extension +; directory, You may specify an absolute path to the library file: +; +; extension=/path/to/extension/mysqli.so +; +; Note : The syntax used in previous PHP versions ('extension=.so' and +; 'extension='php_.dll') is supported for legacy reasons and may be +; deprecated in a future PHP major version. So, when it is possible, please +; move to the new ('extension=) syntax. +; +; Notes for Windows environments : +; +; - Many DLL files are located in the ext/ +; extension folders as well as the separate PECL DLL download. +; Be sure to appropriately set the extension_dir directive. +; +;extension=bz2 + +; The ldap extension must be before curl if OpenSSL 1.0.2 and OpenLDAP is used +; otherwise it results in segfault when unloading after using SASL. +; See https://github.com/php/php-src/issues/8620 for more info. +;extension=ldap + +;extension=curl +;extension=ffi +;extension=ftp +;extension=fileinfo +;extension=gd +;extension=gettext +;extension=gmp +;extension=intl +;extension=imap +;extension=mbstring +;extension=exif ; Must be after mbstring as it depends on it +;extension=mysqli +;extension=oci8_12c ; Use with Oracle Database 12c Instant Client +;extension=oci8_19 ; Use with Oracle Database 19 Instant Client +;extension=odbc +;extension=openssl +;extension=pdo_firebird +;extension=pdo_mysql +;extension=pdo_oci +;extension=pdo_odbc +;extension=pdo_pgsql +;extension=pdo_sqlite +;extension=pgsql +;extension=shmop + +; The MIBS data available in the PHP distribution must be installed. +; See https://www.php.net/manual/en/snmp.installation.php +;extension=snmp + +;extension=soap +;extension=sockets +;extension=sodium +;extension=sqlite3 +;extension=tidy +;extension=xsl +;extension=zip + +;zend_extension=opcache + +;;;;;;;;;;;;;;;;;;; +; Module Settings ; +;;;;;;;;;;;;;;;;;;; + +[CLI Server] +; Whether the CLI web server uses ANSI color coding in its terminal output. +cli_server.color = On + +[Date] +; Defines the default timezone used by the date functions +; https://php.net/date.timezone +date.timezone = "Europe/Warsaw" + +; https://php.net/date.default-latitude +;date.default_latitude = 31.7667 + +; https://php.net/date.default-longitude +;date.default_longitude = 35.2333 + +; https://php.net/date.sunrise-zenith +;date.sunrise_zenith = 90.833333 + +; https://php.net/date.sunset-zenith +;date.sunset_zenith = 90.833333 + +[filter] +; https://php.net/filter.default +;filter.default = unsafe_raw + +; https://php.net/filter.default-flags +;filter.default_flags = + +[iconv] +; Use of this INI entry is deprecated, use global input_encoding instead. +; If empty, default_charset or input_encoding or iconv.input_encoding is used. +; The precedence is: default_charset < input_encoding < iconv.input_encoding +;iconv.input_encoding = + +; Use of this INI entry is deprecated, use global internal_encoding instead. +; If empty, default_charset or internal_encoding or iconv.internal_encoding is used. +; The precedence is: default_charset < internal_encoding < iconv.internal_encoding +;iconv.internal_encoding = + +; Use of this INI entry is deprecated, use global output_encoding instead. +; If empty, default_charset or output_encoding or iconv.output_encoding is used. +; The precedence is: default_charset < output_encoding < iconv.output_encoding +; To use an output encoding conversion, iconv's output handler must be set +; otherwise output encoding conversion cannot be performed. +;iconv.output_encoding = + +[imap] +; rsh/ssh logins are disabled by default. Use this INI entry if you want to +; enable them. Note that the IMAP library does not filter mailbox names before +; passing them to rsh/ssh command, thus passing untrusted data to this function +; with rsh/ssh enabled is insecure. +;imap.enable_insecure_rsh=0 + +[intl] +;intl.default_locale = +; This directive allows you to produce PHP errors when some error +; happens within intl functions. The value is the level of the error produced. +; Default is 0, which does not produce any errors. +;intl.error_level = E_WARNING +;intl.use_exceptions = 0 + +[sqlite3] +; Directory pointing to SQLite3 extensions +; https://php.net/sqlite3.extension-dir +;sqlite3.extension_dir = + +; SQLite defensive mode flag (only available from SQLite 3.26+) +; When the defensive flag is enabled, language features that allow ordinary +; SQL to deliberately corrupt the database file are disabled. This forbids +; writing directly to the schema, shadow tables (eg. FTS data tables), or +; the sqlite_dbpage virtual table. +; https://www.sqlite.org/c3ref/c_dbconfig_defensive.html +; (for older SQLite versions, this flag has no use) +;sqlite3.defensive = 1 + +[Pcre] +; PCRE library backtracking limit. +; https://php.net/pcre.backtrack-limit +;pcre.backtrack_limit=100000 + +; PCRE library recursion limit. +; Please note that if you set this value to a high number you may consume all +; the available process stack and eventually crash PHP (due to reaching the +; stack size limit imposed by the Operating System). +; https://php.net/pcre.recursion-limit +;pcre.recursion_limit=100000 + +; Enables or disables JIT compilation of patterns. This requires the PCRE +; library to be compiled with JIT support. +;pcre.jit=1 + +[Pdo] +; Whether to pool ODBC connections. Can be one of "strict", "relaxed" or "off" +; https://php.net/pdo-odbc.connection-pooling +;pdo_odbc.connection_pooling=strict + +[Pdo_mysql] +; Default socket name for local MySQL connects. If empty, uses the built-in +; MySQL defaults. +pdo_mysql.default_socket= + +[Phar] +; https://php.net/phar.readonly +;phar.readonly = On + +; https://php.net/phar.require-hash +;phar.require_hash = On + +;phar.cache_list = + +[mail function] +; For Win32 only. +; https://php.net/smtp +SMTP = localhost +; https://php.net/smtp-port +smtp_port = 25 + +; For Win32 only. +; https://php.net/sendmail-from +;sendmail_from = me@example.com + +; For Unix only. You may supply arguments as well (default: "sendmail -t -i"). +; https://php.net/sendmail-path +;sendmail_path = + +; Force the addition of the specified parameters to be passed as extra parameters +; to the sendmail binary. These parameters will always replace the value of +; the 5th parameter to mail(). +;mail.force_extra_parameters = + +; Add X-PHP-Originating-Script: that will include uid of the script followed by the filename +mail.add_x_header = On + +; Use mixed LF and CRLF line separators to keep compatibility with some +; RFC 2822 non conformant MTA. +mail.mixed_lf_and_crlf = Off + +; The path to a log file that will log all mail() calls. Log entries include +; the full path of the script, line number, To address and headers. +;mail.log = +; Log mail to syslog (Event Log on Windows). +;mail.log = syslog + +[ODBC] +; https://php.net/odbc.default-db +;odbc.default_db = Not yet implemented + +; https://php.net/odbc.default-user +;odbc.default_user = Not yet implemented + +; https://php.net/odbc.default-pw +;odbc.default_pw = Not yet implemented + +; Controls the ODBC cursor model. +; Default: SQL_CURSOR_STATIC (default). +;odbc.default_cursortype + +; Allow or prevent persistent links. +; https://php.net/odbc.allow-persistent +odbc.allow_persistent = On + +; Check that a connection is still valid before reuse. +; https://php.net/odbc.check-persistent +odbc.check_persistent = On + +; Maximum number of persistent links. -1 means no limit. +; https://php.net/odbc.max-persistent +odbc.max_persistent = -1 + +; Maximum number of links (persistent + non-persistent). -1 means no limit. +; https://php.net/odbc.max-links +odbc.max_links = -1 + +; Handling of LONG fields. Returns number of bytes to variables. 0 means +; passthru. +; https://php.net/odbc.defaultlrl +odbc.defaultlrl = 4096 + +; Handling of binary data. 0 means passthru, 1 return as is, 2 convert to char. +; See the documentation on odbc_binmode and odbc_longreadlen for an explanation +; of odbc.defaultlrl and odbc.defaultbinmode +; https://php.net/odbc.defaultbinmode +odbc.defaultbinmode = 1 + +[MySQLi] + +; Maximum number of persistent links. -1 means no limit. +; https://php.net/mysqli.max-persistent +mysqli.max_persistent = -1 + +; Allow accessing, from PHP's perspective, local files with LOAD DATA statements +; https://php.net/mysqli.allow_local_infile +mysqli.allow_local_infile = Off + +; It allows the user to specify a folder where files that can be sent via LOAD DATA +; LOCAL can exist. It is ignored if mysqli.allow_local_infile is enabled. +;mysqli.local_infile_directory = + +; Allow or prevent persistent links. +; https://php.net/mysqli.allow-persistent +mysqli.allow_persistent = On + +; Maximum number of links. -1 means no limit. +; https://php.net/mysqli.max-links +mysqli.max_links = -1 + +; Default port number for mysqli_connect(). If unset, mysqli_connect() will use +; the $MYSQL_TCP_PORT or the mysql-tcp entry in /etc/services or the +; compile-time value defined MYSQL_PORT (in that order). Win32 will only look +; at MYSQL_PORT. +; https://php.net/mysqli.default-port +mysqli.default_port = 3306 + +; Default socket name for local MySQL connects. If empty, uses the built-in +; MySQL defaults. +; https://php.net/mysqli.default-socket +mysqli.default_socket = + +; Default host for mysqli_connect() (doesn't apply in safe mode). +; https://php.net/mysqli.default-host +mysqli.default_host = + +; Default user for mysqli_connect() (doesn't apply in safe mode). +; https://php.net/mysqli.default-user +mysqli.default_user = + +; Default password for mysqli_connect() (doesn't apply in safe mode). +; Note that this is generally a *bad* idea to store passwords in this file. +; *Any* user with PHP access can run 'echo get_cfg_var("mysqli.default_pw") +; and reveal this password! And of course, any users with read access to this +; file will be able to reveal the password as well. +; https://php.net/mysqli.default-pw +mysqli.default_pw = + +; If this option is enabled, closing a persistent connection will rollback +; any pending transactions of this connection, before it is put back +; into the persistent connection pool. +;mysqli.rollback_on_cached_plink = Off + +[mysqlnd] +; Enable / Disable collection of general statistics by mysqlnd which can be +; used to tune and monitor MySQL operations. +mysqlnd.collect_statistics = On + +; Enable / Disable collection of memory usage statistics by mysqlnd which can be +; used to tune and monitor MySQL operations. +mysqlnd.collect_memory_statistics = Off + +; Records communication from all extensions using mysqlnd to the specified log +; file. +; https://php.net/mysqlnd.debug +;mysqlnd.debug = + +; Defines which queries will be logged. +;mysqlnd.log_mask = 0 + +; Default size of the mysqlnd memory pool, which is used by result sets. +;mysqlnd.mempool_default_size = 16000 + +; Size of a pre-allocated buffer used when sending commands to MySQL in bytes. +;mysqlnd.net_cmd_buffer_size = 2048 + +; Size of a pre-allocated buffer used for reading data sent by the server in +; bytes. +;mysqlnd.net_read_buffer_size = 32768 + +; Timeout for network requests in seconds. +;mysqlnd.net_read_timeout = 31536000 + +; SHA-256 Authentication Plugin related. File with the MySQL server public RSA +; key. +;mysqlnd.sha256_server_public_key = + +[OCI8] + +; Connection: Enables privileged connections using external +; credentials (OCI_SYSOPER, OCI_SYSDBA) +; https://php.net/oci8.privileged-connect +;oci8.privileged_connect = Off + +; Connection: The maximum number of persistent OCI8 connections per +; process. Using -1 means no limit. +; https://php.net/oci8.max-persistent +;oci8.max_persistent = -1 + +; Connection: The maximum number of seconds a process is allowed to +; maintain an idle persistent connection. Using -1 means idle +; persistent connections will be maintained forever. +; https://php.net/oci8.persistent-timeout +;oci8.persistent_timeout = -1 + +; Connection: The number of seconds that must pass before issuing a +; ping during oci_pconnect() to check the connection validity. When +; set to 0, each oci_pconnect() will cause a ping. Using -1 disables +; pings completely. +; https://php.net/oci8.ping-interval +;oci8.ping_interval = 60 + +; Connection: Set this to a user chosen connection class to be used +; for all pooled server requests with Oracle Database Resident +; Connection Pooling (DRCP). To use DRCP, this value should be set to +; the same string for all web servers running the same application, +; the database pool must be configured, and the connection string must +; specify to use a pooled server. +;oci8.connection_class = + +; High Availability: Using On lets PHP receive Fast Application +; Notification (FAN) events generated when a database node fails. The +; database must also be configured to post FAN events. +;oci8.events = Off + +; Tuning: This option enables statement caching, and specifies how +; many statements to cache. Using 0 disables statement caching. +; https://php.net/oci8.statement-cache-size +;oci8.statement_cache_size = 20 + +; Tuning: Enables row prefetching and sets the default number of +; rows that will be fetched automatically after statement execution. +; https://php.net/oci8.default-prefetch +;oci8.default_prefetch = 100 + +; Tuning: Sets the amount of LOB data that is internally returned from +; Oracle Database when an Oracle LOB locator is initially retrieved as +; part of a query. Setting this can improve performance by reducing +; round-trips. +; https://php.net/oci8.prefetch-lob-size +; oci8.prefetch_lob_size = 0 + +; Compatibility. Using On means oci_close() will not close +; oci_connect() and oci_new_connect() connections. +; https://php.net/oci8.old-oci-close-semantics +;oci8.old_oci_close_semantics = Off + +[PostgreSQL] +; Allow or prevent persistent links. +; https://php.net/pgsql.allow-persistent +pgsql.allow_persistent = On + +; Detect broken persistent links always with pg_pconnect(). +; Auto reset feature requires a little overheads. +; https://php.net/pgsql.auto-reset-persistent +pgsql.auto_reset_persistent = Off + +; Maximum number of persistent links. -1 means no limit. +; https://php.net/pgsql.max-persistent +pgsql.max_persistent = -1 + +; Maximum number of links (persistent+non persistent). -1 means no limit. +; https://php.net/pgsql.max-links +pgsql.max_links = -1 + +; Ignore PostgreSQL backends Notice message or not. +; Notice message logging require a little overheads. +; https://php.net/pgsql.ignore-notice +pgsql.ignore_notice = 0 + +; Log PostgreSQL backends Notice message or not. +; Unless pgsql.ignore_notice=0, module cannot log notice message. +; https://php.net/pgsql.log-notice +pgsql.log_notice = 0 + +[bcmath] +; Number of decimal digits for all bcmath functions. +; https://php.net/bcmath.scale +bcmath.scale = 0 + +[browscap] +; https://php.net/browscap +;browscap = extra/browscap.ini + +[Session] +; Handler used to store/retrieve data. +; https://php.net/session.save-handler +session.save_handler = files + +; Argument passed to save_handler. In the case of files, this is the path +; where data files are stored. Note: Windows users have to change this +; variable in order to use PHP's session functions. +; +; The path can be defined as: +; +; session.save_path = "N;/path" +; +; where N is an integer. Instead of storing all the session files in +; /path, what this will do is use subdirectories N-levels deep, and +; store the session data in those directories. This is useful if +; your OS has problems with many files in one directory, and is +; a more efficient layout for servers that handle many sessions. +; +; NOTE 1: PHP will not create this directory structure automatically. +; You can use the script in the ext/session dir for that purpose. +; NOTE 2: See the section on garbage collection below if you choose to +; use subdirectories for session storage +; +; The file storage module creates files using mode 600 by default. +; You can change that by using +; +; session.save_path = "N;MODE;/path" +; +; where MODE is the octal representation of the mode. Note that this +; does not overwrite the process's umask. +; https://php.net/session.save-path +;session.save_path = "/tmp" + +; Whether to use strict session mode. +; Strict session mode does not accept an uninitialized session ID, and +; regenerates the session ID if the browser sends an uninitialized session ID. +; Strict mode protects applications from session fixation via a session adoption +; vulnerability. It is disabled by default for maximum compatibility, but +; enabling it is encouraged. +; https://wiki.php.net/rfc/strict_sessions +session.use_strict_mode = 0 + +; Whether to use cookies. +; https://php.net/session.use-cookies +session.use_cookies = 1 + +; https://php.net/session.cookie-secure +;session.cookie_secure = + +; This option forces PHP to fetch and use a cookie for storing and maintaining +; the session id. We encourage this operation as it's very helpful in combating +; session hijacking when not specifying and managing your own session id. It is +; not the be-all and end-all of session hijacking defense, but it's a good start. +; https://php.net/session.use-only-cookies +session.use_only_cookies = 1 + +; Name of the session (used as cookie name). +; https://php.net/session.name +session.name = PHPSESSID + +; Initialize session on request startup. +; https://php.net/session.auto-start +session.auto_start = 0 + +; Lifetime in seconds of cookie or, if 0, until browser is restarted. +; https://php.net/session.cookie-lifetime +session.cookie_lifetime = 0 + +; The path for which the cookie is valid. +; https://php.net/session.cookie-path +session.cookie_path = / + +; The domain for which the cookie is valid. +; https://php.net/session.cookie-domain +session.cookie_domain = + +; Whether or not to add the httpOnly flag to the cookie, which makes it +; inaccessible to browser scripting languages such as JavaScript. +; https://php.net/session.cookie-httponly +session.cookie_httponly = + +; Add SameSite attribute to cookie to help mitigate Cross-Site Request Forgery (CSRF/XSRF) +; Current valid values are "Strict", "Lax" or "None". When using "None", +; make sure to include the quotes, as `none` is interpreted like `false` in ini files. +; https://tools.ietf.org/html/draft-west-first-party-cookies-07 +session.cookie_samesite = + +; Handler used to serialize data. php is the standard serializer of PHP. +; https://php.net/session.serialize-handler +session.serialize_handler = php + +; Defines the probability that the 'garbage collection' process is started on every +; session initialization. The probability is calculated by using gc_probability/gc_divisor, +; e.g. 1/100 means there is a 1% chance that the GC process starts on each request. +; Default Value: 1 +; Development Value: 1 +; Production Value: 1 +; https://php.net/session.gc-probability +session.gc_probability = 1 + +; Defines the probability that the 'garbage collection' process is started on every +; session initialization. The probability is calculated by using gc_probability/gc_divisor, +; e.g. 1/100 means there is a 1% chance that the GC process starts on each request. +; For high volume production servers, using a value of 1000 is a more efficient approach. +; Default Value: 100 +; Development Value: 1000 +; Production Value: 1000 +; https://php.net/session.gc-divisor +session.gc_divisor = 1000 + +; After this number of seconds, stored data will be seen as 'garbage' and +; cleaned up by the garbage collection process. +; https://php.net/session.gc-maxlifetime +session.gc_maxlifetime = 1440 + +; NOTE: If you are using the subdirectory option for storing session files +; (see session.save_path above), then garbage collection does *not* +; happen automatically. You will need to do your own garbage +; collection through a shell script, cron entry, or some other method. +; For example, the following script is the equivalent of setting +; session.gc_maxlifetime to 1440 (1440 seconds = 24 minutes): +; find /path/to/sessions -cmin +24 -type f | xargs rm + +; Check HTTP Referer to invalidate externally stored URLs containing ids. +; HTTP_REFERER has to contain this substring for the session to be +; considered as valid. +; https://php.net/session.referer-check +session.referer_check = + +; Set to {nocache,private,public,} to determine HTTP caching aspects +; or leave this empty to avoid sending anti-caching headers. +; https://php.net/session.cache-limiter +session.cache_limiter = nocache + +; Document expires after n minutes. +; https://php.net/session.cache-expire +session.cache_expire = 180 + +; trans sid support is disabled by default. +; Use of trans sid may risk your users' security. +; Use this option with caution. +; - User may send URL contains active session ID +; to other person via. email/irc/etc. +; - URL that contains active session ID may be stored +; in publicly accessible computer. +; - User may access your site with the same session ID +; always using URL stored in browser's history or bookmarks. +; https://php.net/session.use-trans-sid +session.use_trans_sid = 0 + +; Set session ID character length. This value could be between 22 to 256. +; Shorter length than default is supported only for compatibility reason. +; Users should use 32 or more chars. +; https://php.net/session.sid-length +; Default Value: 32 +; Development Value: 26 +; Production Value: 26 +session.sid_length = 26 + +; The URL rewriter will look for URLs in a defined set of HTML tags. +;
is special; if you include them here, the rewriter will +; add a hidden field with the info which is otherwise appended +; to URLs. tag's action attribute URL will not be modified +; unless it is specified. +; Note that all valid entries require a "=", even if no value follows. +; Default Value: "a=href,area=href,frame=src,form=" +; Development Value: "a=href,area=href,frame=src,form=" +; Production Value: "a=href,area=href,frame=src,form=" +; https://php.net/url-rewriter.tags +session.trans_sid_tags = "a=href,area=href,frame=src,form=" + +; URL rewriter does not rewrite absolute URLs by default. +; To enable rewrites for absolute paths, target hosts must be specified +; at RUNTIME. i.e. use ini_set() +; tags is special. PHP will check action attribute's URL regardless +; of session.trans_sid_tags setting. +; If no host is defined, HTTP_HOST will be used for allowed host. +; Example value: php.net,www.php.net,wiki.php.net +; Use "," for multiple hosts. No spaces are allowed. +; Default Value: "" +; Development Value: "" +; Production Value: "" +;session.trans_sid_hosts="" + +; Define how many bits are stored in each character when converting +; the binary hash data to something readable. +; Possible values: +; 4 (4 bits: 0-9, a-f) +; 5 (5 bits: 0-9, a-v) +; 6 (6 bits: 0-9, a-z, A-Z, "-", ",") +; Default Value: 4 +; Development Value: 5 +; Production Value: 5 +; https://php.net/session.hash-bits-per-character +session.sid_bits_per_character = 5 + +; Enable upload progress tracking in $_SESSION +; Default Value: On +; Development Value: On +; Production Value: On +; https://php.net/session.upload-progress.enabled +;session.upload_progress.enabled = On + +; Cleanup the progress information as soon as all POST data has been read +; (i.e. upload completed). +; Default Value: On +; Development Value: On +; Production Value: On +; https://php.net/session.upload-progress.cleanup +;session.upload_progress.cleanup = On + +; A prefix used for the upload progress key in $_SESSION +; Default Value: "upload_progress_" +; Development Value: "upload_progress_" +; Production Value: "upload_progress_" +; https://php.net/session.upload-progress.prefix +;session.upload_progress.prefix = "upload_progress_" + +; The index name (concatenated with the prefix) in $_SESSION +; containing the upload progress information +; Default Value: "PHP_SESSION_UPLOAD_PROGRESS" +; Development Value: "PHP_SESSION_UPLOAD_PROGRESS" +; Production Value: "PHP_SESSION_UPLOAD_PROGRESS" +; https://php.net/session.upload-progress.name +;session.upload_progress.name = "PHP_SESSION_UPLOAD_PROGRESS" + +; How frequently the upload progress should be updated. +; Given either in percentages (per-file), or in bytes +; Default Value: "1%" +; Development Value: "1%" +; Production Value: "1%" +; https://php.net/session.upload-progress.freq +;session.upload_progress.freq = "1%" + +; The minimum delay between updates, in seconds +; Default Value: 1 +; Development Value: 1 +; Production Value: 1 +; https://php.net/session.upload-progress.min-freq +;session.upload_progress.min_freq = "1" + +; Only write session data when session data is changed. Enabled by default. +; https://php.net/session.lazy-write +;session.lazy_write = On + +[Assertion] +; Switch whether to compile assertions at all (to have no overhead at run-time) +; -1: Do not compile at all +; 0: Jump over assertion at run-time +; 1: Execute assertions +; Changing from or to a negative value is only possible in php.ini! +; (For turning assertions on and off at run-time, toggle zend.assertions between the values 1 and 0) +; Default Value: 1 +; Development Value: 1 +; Production Value: -1 +; https://php.net/zend.assertions +zend.assertions = -1 + +[COM] +; path to a file containing GUIDs, IIDs or filenames of files with TypeLibs +; https://php.net/com.typelib-file +;com.typelib_file = + +; allow Distributed-COM calls +; https://php.net/com.allow-dcom +;com.allow_dcom = true + +; autoregister constants of a component's typelib on com_load() +; https://php.net/com.autoregister-typelib +;com.autoregister_typelib = true + +; register constants casesensitive +; https://php.net/com.autoregister-casesensitive +;com.autoregister_casesensitive = false + +; show warnings on duplicate constant registrations +; https://php.net/com.autoregister-verbose +;com.autoregister_verbose = true + +; The default character set code-page to use when passing strings to and from COM objects. +; Default: system ANSI code page +;com.code_page= + +; The version of the .NET framework to use. The value of the setting are the first three parts +; of the framework's version number, separated by dots, and prefixed with "v", e.g. "v4.0.30319". +;com.dotnet_version= + +[mbstring] +; language for internal character representation. +; This affects mb_send_mail() and mbstring.detect_order. +; https://php.net/mbstring.language +;mbstring.language = Japanese + +; Use of this INI entry is deprecated, use global internal_encoding instead. +; internal/script encoding. +; Some encoding cannot work as internal encoding. (e.g. SJIS, BIG5, ISO-2022-*) +; If empty, default_charset or internal_encoding or iconv.internal_encoding is used. +; The precedence is: default_charset < internal_encoding < iconv.internal_encoding +;mbstring.internal_encoding = + +; Use of this INI entry is deprecated, use global input_encoding instead. +; http input encoding. +; mbstring.encoding_translation = On is needed to use this setting. +; If empty, default_charset or input_encoding or mbstring.input is used. +; The precedence is: default_charset < input_encoding < mbstring.http_input +; https://php.net/mbstring.http-input +;mbstring.http_input = + +; Use of this INI entry is deprecated, use global output_encoding instead. +; http output encoding. +; mb_output_handler must be registered as output buffer to function. +; If empty, default_charset or output_encoding or mbstring.http_output is used. +; The precedence is: default_charset < output_encoding < mbstring.http_output +; To use an output encoding conversion, mbstring's output handler must be set +; otherwise output encoding conversion cannot be performed. +; https://php.net/mbstring.http-output +;mbstring.http_output = + +; enable automatic encoding translation according to +; mbstring.internal_encoding setting. Input chars are +; converted to internal encoding by setting this to On. +; Note: Do _not_ use automatic encoding translation for +; portable libs/applications. +; https://php.net/mbstring.encoding-translation +;mbstring.encoding_translation = Off + +; automatic encoding detection order. +; "auto" detect order is changed according to mbstring.language +; https://php.net/mbstring.detect-order +;mbstring.detect_order = auto + +; substitute_character used when character cannot be converted +; one from another +; https://php.net/mbstring.substitute-character +;mbstring.substitute_character = none + +; Enable strict encoding detection. +;mbstring.strict_detection = Off + +; This directive specifies the regex pattern of content types for which mb_output_handler() +; is activated. +; Default: mbstring.http_output_conv_mimetypes=^(text/|application/xhtml\+xml) +;mbstring.http_output_conv_mimetypes= + +; This directive specifies maximum stack depth for mbstring regular expressions. It is similar +; to the pcre.recursion_limit for PCRE. +;mbstring.regex_stack_limit=100000 + +; This directive specifies maximum retry count for mbstring regular expressions. It is similar +; to the pcre.backtrack_limit for PCRE. +;mbstring.regex_retry_limit=1000000 + +[gd] +; Tell the jpeg decode to ignore warnings and try to create +; a gd image. The warning will then be displayed as notices +; disabled by default +; https://php.net/gd.jpeg-ignore-warning +;gd.jpeg_ignore_warning = 1 + +[exif] +; Exif UNICODE user comments are handled as UCS-2BE/UCS-2LE and JIS as JIS. +; With mbstring support this will automatically be converted into the encoding +; given by corresponding encode setting. When empty mbstring.internal_encoding +; is used. For the decode settings you can distinguish between motorola and +; intel byte order. A decode setting cannot be empty. +; https://php.net/exif.encode-unicode +;exif.encode_unicode = ISO-8859-15 + +; https://php.net/exif.decode-unicode-motorola +;exif.decode_unicode_motorola = UCS-2BE + +; https://php.net/exif.decode-unicode-intel +;exif.decode_unicode_intel = UCS-2LE + +; https://php.net/exif.encode-jis +;exif.encode_jis = + +; https://php.net/exif.decode-jis-motorola +;exif.decode_jis_motorola = JIS + +; https://php.net/exif.decode-jis-intel +;exif.decode_jis_intel = JIS + +[Tidy] +; The path to a default tidy configuration file to use when using tidy +; https://php.net/tidy.default-config +;tidy.default_config = /usr/local/lib/php/default.tcfg + +; Should tidy clean and repair output automatically? +; WARNING: Do not use this option if you are generating non-html content +; such as dynamic images +; https://php.net/tidy.clean-output +tidy.clean_output = Off + +[soap] +; Enables or disables WSDL caching feature. +; https://php.net/soap.wsdl-cache-enabled +soap.wsdl_cache_enabled=1 + +; Sets the directory name where SOAP extension will put cache files. +; https://php.net/soap.wsdl-cache-dir +soap.wsdl_cache_dir="/tmp" + +; (time to live) Sets the number of second while cached file will be used +; instead of original one. +; https://php.net/soap.wsdl-cache-ttl +soap.wsdl_cache_ttl=86400 + +; Sets the size of the cache limit. (Max. number of WSDL files to cache) +soap.wsdl_cache_limit = 5 + +[sysvshm] +; A default size of the shared memory segment +;sysvshm.init_mem = 10000 + +[ldap] +; Sets the maximum number of open links or -1 for unlimited. +ldap.max_links = -1 + +[dba] +;dba.default_handler= + +[opcache] +; Determines if Zend OPCache is enabled +;opcache.enable=1 + +; Determines if Zend OPCache is enabled for the CLI version of PHP +;opcache.enable_cli=0 + +; The OPcache shared memory storage size. +;opcache.memory_consumption=128 + +; The amount of memory for interned strings in Mbytes. +;opcache.interned_strings_buffer=8 + +; The maximum number of keys (scripts) in the OPcache hash table. +; Only numbers between 200 and 1000000 are allowed. +;opcache.max_accelerated_files=10000 + +; The maximum percentage of "wasted" memory until a restart is scheduled. +;opcache.max_wasted_percentage=5 + +; When this directive is enabled, the OPcache appends the current working +; directory to the script key, thus eliminating possible collisions between +; files with the same name (basename). Disabling the directive improves +; performance, but may break existing applications. +;opcache.use_cwd=1 + +; When disabled, you must reset the OPcache manually or restart the +; webserver for changes to the filesystem to take effect. +;opcache.validate_timestamps=1 + +; How often (in seconds) to check file timestamps for changes to the shared +; memory storage allocation. ("1" means validate once per second, but only +; once per request. "0" means always validate) +;opcache.revalidate_freq=2 + +; Enables or disables file search in include_path optimization +;opcache.revalidate_path=0 + +; If disabled, all PHPDoc comments are dropped from the code to reduce the +; size of the optimized code. +;opcache.save_comments=1 + +; If enabled, compilation warnings (including notices and deprecations) will +; be recorded and replayed each time a file is included. Otherwise, compilation +; warnings will only be emitted when the file is first cached. +;opcache.record_warnings=0 + +; Allow file existence override (file_exists, etc.) performance feature. +;opcache.enable_file_override=0 + +; A bitmask, where each bit enables or disables the appropriate OPcache +; passes +;opcache.optimization_level=0x7FFFBFFF + +;opcache.dups_fix=0 + +; The location of the OPcache blacklist file (wildcards allowed). +; Each OPcache blacklist file is a text file that holds the names of files +; that should not be accelerated. The file format is to add each filename +; to a new line. The filename may be a full path or just a file prefix +; (i.e., /var/www/x blacklists all the files and directories in /var/www +; that start with 'x'). Line starting with a ; are ignored (comments). +;opcache.blacklist_filename= + +; Allows exclusion of large files from being cached. By default all files +; are cached. +;opcache.max_file_size=0 + +; How long to wait (in seconds) for a scheduled restart to begin if the cache +; is not being accessed. +;opcache.force_restart_timeout=180 + +; OPcache error_log file name. Empty string assumes "stderr". +;opcache.error_log= + +; All OPcache errors go to the Web server log. +; By default, only fatal errors (level 0) or errors (level 1) are logged. +; You can also enable warnings (level 2), info messages (level 3) or +; debug messages (level 4). +;opcache.log_verbosity_level=1 + +; Preferred Shared Memory back-end. Leave empty and let the system decide. +;opcache.preferred_memory_model= + +; Protect the shared memory from unexpected writing during script execution. +; Useful for internal debugging only. +;opcache.protect_memory=0 + +; Allows calling OPcache API functions only from PHP scripts which path is +; started from specified string. The default "" means no restriction +;opcache.restrict_api= + +; Mapping base of shared memory segments (for Windows only). All the PHP +; processes have to map shared memory into the same address space. This +; directive allows to manually fix the "Unable to reattach to base address" +; errors. +;opcache.mmap_base= + +; Facilitates multiple OPcache instances per user (for Windows only). All PHP +; processes with the same cache ID and user share an OPcache instance. +;opcache.cache_id= + +; Enables and sets the second level cache directory. +; It should improve performance when SHM memory is full, at server restart or +; SHM reset. The default "" disables file based caching. +;opcache.file_cache= + +; Enables or disables opcode caching in shared memory. +;opcache.file_cache_only=0 + +; Enables or disables checksum validation when script loaded from file cache. +;opcache.file_cache_consistency_checks=1 + +; Implies opcache.file_cache_only=1 for a certain process that failed to +; reattach to the shared memory (for Windows only). Explicitly enabled file +; cache is required. +;opcache.file_cache_fallback=1 + +; Enables or disables copying of PHP code (text segment) into HUGE PAGES. +; Under certain circumstances (if only a single global PHP process is +; started from which all others fork), this can increase performance +; by a tiny amount because TLB misses are reduced. On the other hand, this +; delays PHP startup, increases memory usage and degrades performance +; under memory pressure - use with care. +; Requires appropriate OS configuration. +;opcache.huge_code_pages=0 + +; Validate cached file permissions. +;opcache.validate_permission=0 + +; Prevent name collisions in chroot'ed environment. +;opcache.validate_root=0 + +; If specified, it produces opcode dumps for debugging different stages of +; optimizations. +;opcache.opt_debug_level=0 + +; Specifies a PHP script that is going to be compiled and executed at server +; start-up. +; https://php.net/opcache.preload +;opcache.preload= + +; Preloading code as root is not allowed for security reasons. This directive +; facilitates to let the preloading to be run as another user. +; https://php.net/opcache.preload_user +;opcache.preload_user= + +; Prevents caching files that are less than this number of seconds old. It +; protects from caching of incompletely updated files. In case all file updates +; on your site are atomic, you may increase performance by setting it to "0". +;opcache.file_update_protection=2 + +; Absolute path used to store shared lockfiles (for *nix only). +;opcache.lockfile_path=/tmp + +[curl] +; A default value for the CURLOPT_CAINFO option. This is required to be an +; absolute path. +;curl.cainfo = + +[openssl] +; The location of a Certificate Authority (CA) file on the local filesystem +; to use when verifying the identity of SSL/TLS peers. Most users should +; not specify a value for this directive as PHP will attempt to use the +; OS-managed cert stores in its absence. If specified, this value may still +; be overridden on a per-stream basis via the "cafile" SSL stream context +; option. +;openssl.cafile= + +; If openssl.cafile is not specified or if the CA file is not found, the +; directory pointed to by openssl.capath is searched for a suitable +; certificate. This value must be a correctly hashed certificate directory. +; Most users should not specify a value for this directive as PHP will +; attempt to use the OS-managed cert stores in its absence. If specified, +; this value may still be overridden on a per-stream basis via the "capath" +; SSL stream context option. +;openssl.capath= + +[ffi] +; FFI API restriction. Possible values: +; "preload" - enabled in CLI scripts and preloaded files (default) +; "false" - always disabled +; "true" - always enabled +;ffi.enable=preload + +; List of headers files to preload, wildcard patterns allowed. +;ffi.preload= diff --git a/plugin-settings.conf b/plugin-settings.conf new file mode 100644 index 0000000..723b256 --- /dev/null +++ b/plugin-settings.conf @@ -0,0 +1,47 @@ +# Ustawienia pluginu borg-restore. +# BB_PATH - katalog z plikiem bbvars.sh (zawiera BORG_PASSPHRASE, REPO i opcjonalnie BORG_SSH_KEY). +BB_PATH=/opt/bb +# BORG_PATH - pełna ścieżka do binarki borg. +BORG_PATH=/usr/bin/borg +# BORG_VARIABLES_FILE - ścieżka do pliku z REPO/BORG_PASSPHRASE/BORG_SSH_KEY. +BORG_VARIABLES_FILE=/opt/bb/bbvars.sh +# ADMIN_MODE_ENABLE - TRUE włącza panel administratora z raportem przywraceń. +ADMIN_MODE_ENABLE=TRUE +# RESULTS_PER_PAGE - liczba rekordów na stronę w tabeli administratora. +RESULTS_PER_PAGE=10 +# WEB_RESTORE_LOCATION - domyślna ścieżka bazowa dla przywracania stron www. +WEB_RESTORE_LOCATION=/home/DA_USER/domains +# MAIL_RESTORE_LOCATION - domyślna ścieżka bazowa dla przywracania poczty. +MAIL_RESTORE_LOCATION=/home/DA_USER/imap +# ENABLE_WEB_RESTORE - TRUE pozwala przywracać dane stron www. +ENABLE_WEB_RESTORE=TRUE +# ENABLE_MAILBOX_RESTORE - TRUE pozwala przywracać dane skrzynek pocztowych. +ENABLE_MAILBOX_RESTORE=TRUE +# DEFAULT_RESTORE_DESTINATION - domyślna lokalizacja docelowa dla trybu przywracania do lokalizacji. +DEFAULT_RESTORE_DESTINATION=/home/DA_USER/bb +# BLOCK_RESTORE_OUTSIDE_LOCATION - TRUE blokuje przywracanie poza WEB/MAIL_RESTORE_LOCATION. +BLOCK_RESTORE_OUTSIDE_LOCATION=TRUE +# PREVENT_RESTORE_MAIN_DOMAINS_DIR - TRUE blokuje przywrócenie całego katalogu WEB_RESTORE_LOCATION. +PREVENT_RESTORE_MAIN_DOMAINS_DIR=TRUE +# PREVENT_RESTORE_MAIN_IMAP_DIR - TRUE blokuje przywrócenie całego katalogu MAIL_RESTORE_LOCATION. +PREVENT_RESTORE_MAIN_IMAP_DIR=TRUE +# PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN - TRUE blokuje tworzenie katalogów bezpośrednio w /home/DA_USER/domains. +PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN=TRUE +# PREVENT_CREATE_DIRS_IN_MAIN_IMAP - TRUE blokuje tworzenie katalogów bezpośrednio w /home/DA_USER/imap. +PREVENT_CREATE_DIRS_IN_MAIN_IMAP=TRUE +# HASH_VISIBILITY - FALSE ukrywa kolumnę Hash w tabeli archiwów. +HASH_VISIBILITY=FALSE +# ENABLE_LOGS_OVERLAY - TRUE pokazuje logi jako overlay. +ENABLE_LOGS_OVERLAY=TRUE +# SELECTABLE_DOMAIN_RESTORE_DESTINATION - TRUE pozwala wybrać lokalizację docelową (www). +SELECTABLE_DOMAIN_RESTORE_DESTINATION=TRUE +# SELECTABLE_MAIL_RESTORE_DESTINATION - TRUE pozwala wybrać lokalizację docelową (poczta). +SELECTABLE_MAIL_RESTORE_DESTINATION=TRUE +# CLEAR_DOVECOT_INDEXES_AFTER_MERGE - TRUE usuwa indeksy Dovecota po scaleniu poczty. +CLEAR_DOVECOT_INDEXES_AFTER_MERGE=TRUE +# RESTORE_PATH_MODE - full (pełna ścieżka) lub dir (tylko ostatni katalog). +RESTORE_PATH_MODE=dir +# ADD_DATE_TO_RESTORED_DIR - TRUE dodaje datę do ostatniego katalogu w trybie przywrócenia do lokalizacji. +ADD_DATE_TO_RESTORED_DIR=FALSE +# DEBUG_MODE - TRUE pokazuje szczegółowe błędy i pełny output systemowy. +DEBUG_MODE=FALSE diff --git a/plugin.conf b/plugin.conf new file mode 100644 index 0000000..dad8dfa --- /dev/null +++ b/plugin.conf @@ -0,0 +1,19 @@ +active=yes +admin_run_as=root +admin_widgets= +author=HITME.PL +id=borg-restore +installed=yes +menu_admin= +menu_reseller= +menu_user= +name=Borg-restore +reseller_run_as= +reseller_widgets= +timeout= +type=user +update_url= +user_run_as=root +user_widgets= +version=2.3.3 +version_url= diff --git a/scripts/fix_permissions.sh b/scripts/fix_permissions.sh new file mode 100755 index 0000000..58ffa59 --- /dev/null +++ b/scripts/fix_permissions.sh @@ -0,0 +1,129 @@ +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf" +DATA_DIR="${PLUGIN_DIR}/data" +JOB_DIR="${DATA_DIR}/jobs" +LOG_DIR="${DATA_DIR}/logs" +PID_DIR="${DATA_DIR}/pids" +CANCEL_DIR="${DATA_DIR}/cancel" +TMP_DIR="${DATA_DIR}/tmp" +CSRF_DIR="${DATA_DIR}/csrf" + +warn() { + printf '[fix_permissions] %s\n' "$*" >&2 +} + +strip_quotes() { + local v="$1" + v="${v#"${v%%[![:space:]]*}"}" + v="${v%"${v##*[![:space:]]}"}" + if [[ "$v" == \"*\" && "$v" == *\" ]]; then + v="${v:1:${#v}-2}" + elif [[ "$v" == \'*\' && "$v" == *\' ]]; then + v="${v:1:${#v}-2}" + fi + printf '%s' "$v" +} + +get_cfg() { + local key="$1" + local value="" + if [ -r "$CONFIG_FILE" ]; then + while IFS= read -r line; do + line="${line%%#*}" + line="$(strip_quotes "$line")" + if [[ "$line" == "$key="* ]]; then + value="${line#*=}" + fi + done < "$CONFIG_FILE" + fi + printf '%s' "$value" +} + +owner_name="$(stat -c %U "$PLUGIN_DIR" 2>/dev/null || true)" +if [ "$owner_name" != "root" ] && [ "$owner_name" != "diradmin" ]; then + owner_name="root" +fi +owner_group="$owner_name" + +fix_dir() { + local path="$1" + if [ -z "$path" ]; then + return + fi + mkdir -p "$path" + chown "$owner_name":"$owner_group" "$path" || true + chmod 700 "$path" || true +} + +fix_file() { + local path="$1" + local label="$2" + if [ -z "$path" ]; then + return + fi + if [ ! -e "$path" ]; then + warn "${label} missing: ${path}" + return + fi + if [ -L "$path" ] || [ ! -f "$path" ]; then + warn "${label} not a regular file: ${path}" + return + fi + chown "$owner_name":"$owner_group" "$path" || true + chmod 600 "$path" || true +} + +BB_PATH="$(get_cfg "BB_PATH")" +BORG_VARIABLES_FILE="$(get_cfg "BORG_VARIABLES_FILE")" + +if [ -z "$BB_PATH" ]; then + BB_PATH="/opt/bb" +fi + +bbvars_path="" +if [ -n "$BORG_VARIABLES_FILE" ]; then + bbvars_path="${BORG_VARIABLES_FILE}" + if [[ "$bbvars_path" == *DA_USER* ]]; then + if [ -n "${DA_USER:-}" ]; then + bbvars_path="${bbvars_path//DA_USER/${DA_USER}}" + elif [ -f "/opt/bb/bbvars.sh" ]; then + bbvars_path="/opt/bb/bbvars.sh" + else + bbvars_path="" + fi + fi +else + if [[ "$BB_PATH" == *.sh ]]; then + bbvars_path="$BB_PATH" + else + bbvars_path="${BB_PATH%/}/bbvars.sh" + fi +fi + +fix_dir "$DATA_DIR" +fix_dir "$JOB_DIR/pending" +fix_dir "$JOB_DIR/processing" +fix_dir "$JOB_DIR/done" +fix_dir "$LOG_DIR" +fix_dir "$PID_DIR" +fix_dir "$CANCEL_DIR" +fix_dir "$TMP_DIR" +fix_dir "$CSRF_DIR" + +fix_file "$CONFIG_FILE" "plugin-settings.conf" +if [ -n "$bbvars_path" ]; then + fix_file "$bbvars_path" "bbvars.sh" +else + warn "bbvars.sh path not resolved; skipped" +fi + +for f in "$JOB_DIR"/pending/*.env "$JOB_DIR"/processing/*.env "$JOB_DIR"/done/*.{ok,fail,cancel,env.ok,env.fail,env.cancel}; do + if [ -f "$f" ]; then + fix_file "$f" "job file" + fi +done + +exit 0 diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..bcc53b0 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +# Ensure expected permissions and ownership so DirectAdmin can update plugin.conf. +if [ "$(id -u)" -eq 0 ]; then + chown -R diradmin:diradmin "$PLUGIN_DIR" || true +else + echo "borg-restore: warning: not running as root, cannot chown to diradmin" >&2 +fi + +chmod 755 "$PLUGIN_DIR" "$PLUGIN_DIR/hooks" "$PLUGIN_DIR/user" "$PLUGIN_DIR/scripts" "$PLUGIN_DIR/images" || true +chmod 755 "$PLUGIN_DIR/user/index.html" "$PLUGIN_DIR/scripts/install.sh" "$PLUGIN_DIR/scripts/uninstall.sh" "$PLUGIN_DIR/scripts/restore.sh" "$PLUGIN_DIR/scripts/worker.sh" || true +chmod 644 "$PLUGIN_DIR/plugin.conf" "$PLUGIN_DIR/hooks/user_txt.html" "$PLUGIN_DIR/plugin-settings.conf" "$PLUGIN_DIR/user/enhanced.css" "$PLUGIN_DIR/user/evolution.css" "$PLUGIN_DIR/images/user_icon.svg" || true + +DATA_DIR="$PLUGIN_DIR/data" +mkdir -p "$DATA_DIR/jobs/pending" "$DATA_DIR/jobs/processing" "$DATA_DIR/jobs/done" "$DATA_DIR/logs" "$DATA_DIR/pids" "$DATA_DIR/cancel" +chmod 700 "$DATA_DIR" "$DATA_DIR/jobs" "$DATA_DIR/jobs/pending" "$DATA_DIR/jobs/processing" "$DATA_DIR/jobs/done" "$DATA_DIR/logs" "$DATA_DIR/pids" "$DATA_DIR/cancel" || true +if [ "$(id -u)" -eq 0 ]; then + chown -R diradmin:diradmin "$DATA_DIR" || true +fi + +CRON_FILE="/etc/cron.d/borg-restore" +if [ "$(id -u)" -eq 0 ]; then + echo "* * * * * root /usr/local/directadmin/plugins/borg-restore/scripts/worker.sh >/dev/null 2>&1" > "$CRON_FILE" + chmod 644 "$CRON_FILE" || true +else + echo "borg-restore: warning: not running as root, cannot install cron job" >&2 +fi + +echo "borg-restore: install complete" diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100755 index 0000000..4770270 --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,495 @@ +#!/bin/bash +set -euo pipefail + +ERROR_MSG="" +SUCCESS_MSG="" +RESTORE_OK=0 +DA_USER="" + +JOB_FILE="${1:-}" +if [ -z "$JOB_FILE" ] || [ ! -f "$JOB_FILE" ]; then + echo "borg-restore: missing job file" >&2 + exit 1 +fi + +JOB_BASE="$(basename "$JOB_FILE")" +JOB_ID="${JOB_BASE%%.*}" + +PLUGIN_DIR="/usr/local/directadmin/plugins/borg-restore" +LOG_DIR="${PLUGIN_DIR}/data/logs" +LOG_FILE="${LOG_DIR}/${JOB_ID}.log" + +mkdir -p "$LOG_DIR" + +fail() { + local msg="$1" + ERROR_MSG="$msg" + echo "$msg" >> "$LOG_FILE" +} + +format_restore_path() { + local path="${1:-}" + path="$(echo "$path" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + if [ -z "$path" ]; then + echo "" + return + fi + if [[ "$path" != /* ]]; then + path="/$path" + fi + echo "$path" +} + +append_missing_include_explanation() { + if [ ! -f "$LOG_FILE" ]; then + return + fi + if ! grep -qi "Include pattern '.*' never matched" "$LOG_FILE"; then + return + fi + if grep -Fq "W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki" "$LOG_FILE" || grep -Fq "The specified restore path was not found in the backup" "$LOG_FILE"; then + return + fi + local label + label="$(format_restore_path "${USERRESTOREPATH:-}")" + local msg + if [ "${USERLANG:-}" = "en" ]; then + msg="The specified restore path" + else + msg="W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki" + fi + if [ -n "$label" ]; then + msg+=" (${label})" + fi + if [ "${USERLANG:-}" = "en" ]; then + msg+=" was not found in the backup. Please try restoring from another archive that may contain those files." + else + msg+=". Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować." + fi + printf "\n%s\n" "$msg" >> "$LOG_FILE" +} + +has_missing_include() { + if [ ! -f "$LOG_FILE" ]; then + return 1 + fi + grep -qi "Include pattern '.*' never matched" "$LOG_FILE" +} + +filter_log_content() { + awk '{ + line=tolower($0); + if (line ~ /permanently added/) next; + if (line ~ /known host/) next; + print; + }' +} + +secure_source_check() { + local path="$1" + local label="$2" + local check_writable="${3:-1}" + if [ -z "$path" ] || [ ! -e "$path" ]; then + fail "Missing ${label}: ${path}" + exit 1 + fi + if [ -L "$path" ] || [ ! -f "$path" ]; then + fail "Insecure ${label}: not a regular file (${path})" + exit 1 + fi + local owner_uid owner_name mode + owner_uid="$(stat -c %u "$path" 2>/dev/null || true)" + owner_name="$(stat -c %U "$path" 2>/dev/null || true)" + mode="$(stat -c %a "$path" 2>/dev/null || true)" + if [ -z "$owner_uid" ] || [ -z "$mode" ]; then + fail "Unable to stat ${label}: ${path}" + exit 1 + fi + if [ "$owner_uid" != "0" ] && [ "$owner_uid" != "500" ] && [ "$owner_name" != "root" ] && [ "$owner_name" != "diradmin" ]; then + fail "Insecure ${label} owner (${owner_name}:${owner_uid}): ${path}" + exit 1 + fi + mode=$((10#$mode)) + if [ "$check_writable" = "1" ] && (( (mode & 022) != 0 )); then + fail "Insecure ${label} permissions (group/other writable): ${path}" + exit 1 + fi +} + +urlencode() { + local input="$1" + local hex out i + hex="$(printf '%s' "$input" | od -An -tx1 | tr -d ' \n')" + out="" + for ((i=0; i<${#hex}; i+=2)); do + out+="%${hex:i:2}" + done + printf '%s' "$out" +} + +notify_user() { + local code="$1" + if [ -z "${DA_USER:-}" ]; then + return + fi + local subject message + if [ "$code" -eq 0 ] && [ "$RESTORE_OK" -eq 1 ]; then + if [ "${USERLANG:-}" = "en" ]; then + subject="BorgBackup restore completed successfully" + message="Restore of ${USERRESTOREPATH:-} from ${REPONAME:-} completed." + else + subject="Przywracanie plików BorgBackup zakończone pomyślnie" + message="${SUCCESS_MSG:-Przywracanie ${USERRESTOREPATH:-} z ${REPONAME:-} zakończone.}" + fi + else + if [ "${USERLANG:-}" = "en" ]; then + subject="BorgBackup restore error" + message="Restore failed for ${USERRESTOREPATH:-} from ${REPONAME:-}." + else + subject="Błąd przywracania plików BorgBackup" + if [ -n "$ERROR_MSG" ]; then + message="Błąd przywracania: ${ERROR_MSG}" + else + message="Błąd przywracania ${USERRESTOREPATH:-} z ${REPONAME:-}." + fi + fi + if [ -n "${LOG_FILE:-}" ] && [ -f "$LOG_FILE" ]; then + local log_content + log_content="$(cat "$LOG_FILE" | filter_log_content)" + message="${message}\n\nLog:\n${log_content}" + fi + fi + local task_queue="/usr/local/directadmin/data/task.queue" + local users="select1%3D$(urlencode "$DA_USER")" + local line="action=notify&value=users&subject=$(urlencode "$subject")&message=$(urlencode "$message")&users=${users}" + printf "%s\n" "$line" >> "$task_queue" +} + +on_exit() { + local code="$1" + notify_user "$code" +} +trap 'on_exit $?' EXIT + +secure_source_check "$JOB_FILE" "job file" 0 +# shellcheck disable=SC1090 +source "$JOB_FILE" + +DA_USER="${DA_USER:-}" +if [ -z "${DA_USER:-}" ]; then + echo "Missing DA_USER in job file" >&2 + exit 1 +fi + +CONFIG_FILE="${PLUGIN_DIR}/plugin-settings.conf" + +set_owner() { + local path="$1" + if [ -z "$path" ] || [ ! -e "$path" ]; then + return + fi + if [ "$RESTORETYPE" = "mail" ]; then + chown -R "${DA_USER}:mail" "$path" + else + chown -R "${DA_USER}:${DA_USER}" "$path" + fi +} + +is_true() { + local v="${1:-}" + v="$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')" + case "$v" in + 1|true|yes|on) return 0 ;; + *) return 1 ;; + esac +} + +if [ ! -f "$CONFIG_FILE" ]; then + fail "Missing ${CONFIG_FILE}" + exit 1 +fi + +secure_source_check "$CONFIG_FILE" "plugin-settings.conf" +# shellcheck disable=SC1090 +source "$CONFIG_FILE" + +BB_PATH="${BB_PATH:-/opt/bb}" +BORG_PATH="${BORG_PATH:-/usr/bin/borg}" +BORG_VARIABLES_FILE="${BORG_VARIABLES_FILE:-}" +WEB_RESTORE_LOCATION="${WEB_RESTORE_LOCATION:-/home/DA_USER/domains}" +MAIL_RESTORE_LOCATION="${MAIL_RESTORE_LOCATION:-/home/DA_USER/imap}" +BLOCK_RESTORE_OUTSIDE_LOCATION="${BLOCK_RESTORE_OUTSIDE_LOCATION:-TRUE}" +CLEAR_DOVECOT_INDEXES_AFTER_MERGE="${CLEAR_DOVECOT_INDEXES_AFTER_MERGE:-TRUE}" +RESTORE_PATH_MODE="${RESTORE_PATH_MODE:-dir}" + +if [ -z "$BB_PATH" ]; then + fail "BB_PATH is empty in plugin-settings.conf" + exit 1 +fi +if [ -z "$BORG_PATH" ]; then + fail "BORG_PATH is empty in plugin-settings.conf" + exit 1 +fi + +BBVARS_PATH="${BORG_VARIABLES_FILE:-}" +BBVARS_PATH="${BBVARS_PATH//DA_USER/${DA_USER}}" +if [ -z "$BBVARS_PATH" ]; then + BBVARS_PATH="$BB_PATH" + if [[ "$BBVARS_PATH" != *.sh ]]; then + BBVARS_PATH="${BBVARS_PATH%/}/bbvars.sh" + fi +fi + +if [ ! -f "$BBVARS_PATH" ]; then + fail "Missing ${BBVARS_PATH}" + exit 1 +fi + +secure_source_check "$BBVARS_PATH" "bbvars.sh" 0 +set -a +# shellcheck disable=SC1091 +source "$BBVARS_PATH" +set +a + +if [ -z "${REPO:-}" ]; then + fail "Missing REPO in ${BBVARS_PATH}" + exit 1 +fi +if [ -n "${BORG_SSH_KEY:-}" ]; then + if [ ! -f "${BORG_SSH_KEY}" ]; then + fail "BORG_SSH_KEY is not a file: ${BORG_SSH_KEY}" + exit 1 + fi + export BORG_RSH="ssh -i \"${BORG_SSH_KEY}\"" +fi + +if [ -z "${REPONAME:-}" ] || [ -z "${USERRESTOREPATH:-}" ]; then + fail "Missing REPONAME or USERRESTOREPATH in job file" + exit 1 +fi + +RESTOREMODE="${RESTOREMODE:-location}" +RESTORETYPE="${RESTORETYPE:-web}" +if [ "$RESTORETYPE" != "mail" ]; then + RESTORETYPE="web" +fi +if [ "$RESTORETYPE" = "mail" ]; then + if [ "$RESTOREMODE" != "location" ] && [ "$RESTOREMODE" != "update" ]; then + RESTOREMODE="update" + fi +else + if [ "$RESTOREMODE" != "location" ] && [ "$RESTOREMODE" != "overwrite" ]; then + RESTOREMODE="location" + fi +fi + +RESTOREPATHMODE="${RESTOREPATHMODE:-$RESTORE_PATH_MODE}" +RESTOREPATHMODE="$(echo "$RESTOREPATHMODE" | tr '[:upper:]' '[:lower:]')" +RESTOREPATHMODE_FLAG="full" +if [ "$RESTOREPATHMODE" = "dir" ]; then + RESTOREPATHMODE_FLAG="dir" +fi + +TARGETPATH="${TARGETPATH:-}" +RESTORE_FULL="/${USERRESTOREPATH#/}" +WEB_BASE="${WEB_RESTORE_LOCATION//DA_USER/$DA_USER}" +MAIL_BASE="${MAIL_RESTORE_LOCATION//DA_USER/$DA_USER}" +BASE_LOCATION="$WEB_BASE" +if [ "$RESTORETYPE" = "mail" ]; then + BASE_LOCATION="$MAIL_BASE" +fi +if [ -z "$BASE_LOCATION" ]; then + fail "Base restore location is empty" + exit 1 +fi +TARGET_DIR="$BASE_LOCATION" +if [ -z "$TARGETPATH" ]; then + TARGETPATH="$TARGET_DIR" +fi + +BLOCK_FLAG="$(echo "$BLOCK_RESTORE_OUTSIDE_LOCATION" | tr '[:upper:]' '[:lower:]')" +if [ "$BLOCK_FLAG" = "1" ] || [ "$BLOCK_FLAG" = "true" ] || [ "$BLOCK_FLAG" = "yes" ] || [ "$BLOCK_FLAG" = "on" ]; then + if [ -n "$BASE_LOCATION" ]; then + BASE_LOCATION="${BASE_LOCATION%/}" + if [[ "$RESTORE_FULL" != "$BASE_LOCATION" && "$RESTORE_FULL" != "$BASE_LOCATION/"* ]]; then + fail "Restore path outside base location: ${RESTORE_FULL}" + exit 1 + fi + fi +fi + +if [[ "$USERRESTOREPATH" == *".."* ]]; then + fail "Invalid restore path (contains ..)" + exit 1 +fi + +if [[ "$TARGETPATH" == *".."* ]]; then + fail "Invalid target path (contains ..)" + exit 1 +fi + +if [ "$RESTOREMODE" = "overwrite" ] || [ "$RESTOREMODE" = "update" ]; then + TMP_BASE="/home/${DA_USER}/bb.tmp" + RESTORE_BASE="${RESTORE_FULL##*/}" + RESTORE_PARENT="${RESTORE_FULL%/*}" + + if [ -z "$RESTORE_BASE" ] || [ -z "$RESTORE_PARENT" ]; then + fail "Nieprawidłowa ścieżka do przywrócenia." + exit 1 + fi + + mkdir -p "$TMP_BASE" + chown -R "${DA_USER}:${DA_USER}" "$TMP_BASE" + + rm -rf "${TMP_BASE}/${USERRESTOREPATH#/}" "${TMP_BASE}/${RESTORE_BASE}" 2>/dev/null || true + + cd "$TMP_BASE" + set +e + "$BORG_PATH" extract --progress "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1 + EXIT_CODE=$? + set -e + append_missing_include_explanation + + if [ "$EXIT_CODE" -ne 0 ]; then + if ! has_missing_include; then + fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu." + fi + else + EXTRACTED_PATH="${TMP_BASE}/${USERRESTOREPATH#/}" + TMP_ITEM="${TMP_BASE}/${RESTORE_BASE}" + if [ ! -e "$EXTRACTED_PATH" ]; then + fail "Nie znaleziono przywróconego katalogu: ${EXTRACTED_PATH}" + EXIT_CODE=1 + else + if [ -e "$TMP_ITEM" ]; then + rm -rf "$TMP_ITEM" 2>/dev/null || true + fi + mv "$EXTRACTED_PATH" "$TMP_ITEM" + + if [ "$RESTOREMODE" = "update" ]; then + if [ -e "$RESTORE_FULL" ]; then + PRE_SUFFIX="$(date +%d-%m-%Y_%H%M%S)" + PRE_NAME="${RESTORE_BASE}_przed_przywroceniem_${PRE_SUFFIX}" + PRE_PATH="${RESTORE_PARENT}/${PRE_NAME}" + mkdir -p "$PRE_PATH" + set +e + rsync -a "$RESTORE_FULL"/ "$PRE_PATH"/ >> "$LOG_FILE" 2>&1 + RSYNC_CODE=$? + set -e + if [ "$RSYNC_CODE" -ne 0 ]; then + fail "Backup katalogu przed aktualizacją zakończył się błędem." + EXIT_CODE=1 + fi + else + mkdir -p "$RESTORE_FULL" + fi + if [ "$EXIT_CODE" -eq 0 ]; then + RSYNC_EXCLUDES=() + if [ "$RESTORETYPE" = "mail" ]; then + RSYNC_EXCLUDES+=(--exclude 'dovecot.index*') + RSYNC_EXCLUDES+=(--exclude 'dovecot-uidlist') + RSYNC_EXCLUDES+=(--exclude 'dovecot-uidvalidity') + RSYNC_EXCLUDES+=(--exclude 'dovecot.list.index*') + RSYNC_EXCLUDES+=(--exclude 'dovecot.list.log') + fi + set +e + rsync -a "${RSYNC_EXCLUDES[@]}" "$TMP_ITEM"/ "$RESTORE_FULL"/ >> "$LOG_FILE" 2>&1 + RSYNC_CODE=$? + set -e + if [ "$RSYNC_CODE" -ne 0 ]; then + fail "Aktualizacja danych przez rsync zakończyła się błędem." + EXIT_CODE=1 + else + if [ "$RESTORETYPE" = "mail" ] && is_true "$CLEAR_DOVECOT_INDEXES_AFTER_MERGE"; then + find "$RESTORE_FULL" -type f \( -name 'dovecot.index*' -o -name 'dovecot-uidlist' -o -name 'dovecot-uidvalidity' -o -name 'dovecot.list.index*' -o -name 'dovecot.list.log' \) -delete >> "$LOG_FILE" 2>&1 || true + fi + set_owner "$RESTORE_FULL" + rm -rf "$TMP_BASE" + RESTORE_OK=1 + SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${RESTORE_FULL}." + echo "$SUCCESS_MSG" >> "$LOG_FILE" + fi + fi + else + if [ -e "$RESTORE_FULL" ]; then + PRE_SUFFIX="$(date +%d-%m-%Y_%H%M%S)" + PRE_NAME="${RESTORE_BASE}_przed_przywroceniem_${PRE_SUFFIX}" + PRE_PATH="${RESTORE_PARENT}/${PRE_NAME}" + mv "$RESTORE_FULL" "$PRE_PATH" + fi + + mkdir -p "$RESTORE_PARENT" + mv "$TMP_ITEM" "$RESTORE_FULL" + set_owner "$RESTORE_FULL" + rm -rf "$TMP_BASE" + RESTORE_OK=1 + SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${RESTORE_FULL}." + echo "$SUCCESS_MSG" >> "$LOG_FILE" + fi + fi + fi +else + if [ ! -d "$TARGETPATH" ]; then + mkdir -p "$TARGETPATH" + fi + if [ -d "$TARGETPATH" ] && [ "$(ls -A "$TARGETPATH" 2>/dev/null)" != "" ]; then + fail "Target directory is not empty: ${TARGETPATH}" + exit 1 + fi + set_owner "$TARGETPATH" + + TARGET_DIR="$TARGETPATH" + + if [ "$RESTOREPATHMODE_FLAG" = "dir" ]; then + IFS='/' read -r -a RESTORE_PARTS <<< "${USERRESTOREPATH#/}" + PARTS_COUNT="${#RESTORE_PARTS[@]}" + STRIP_COUNT=0 + if [ "$PARTS_COUNT" -gt 0 ]; then + STRIP_COUNT=$((PARTS_COUNT - 1)) + fi + + cd "$TARGET_DIR" + set +e + "$BORG_PATH" extract --progress --strip-components "$STRIP_COUNT" "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1 + EXIT_CODE=$? + set -e + append_missing_include_explanation + + if [ "$EXIT_CODE" -ne 0 ]; then + if ! has_missing_include; then + fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu." + fi + else + BASE_NAME="$(basename "${USERRESTOREPATH#/}")" + DEST_PATH="${TARGET_DIR%/}/${BASE_NAME}" + if [ -e "$DEST_PATH" ]; then + set_owner "$DEST_PATH" + fi + RESTORE_OK=1 + SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${DEST_PATH}." + echo "$SUCCESS_MSG" >> "$LOG_FILE" + fi + else + cd "$TARGET_DIR" + + set +e + "$BORG_PATH" extract --progress "${REPO}::${REPONAME}" -- "${USERRESTOREPATH}" >> "$LOG_FILE" 2>&1 + EXIT_CODE=$? + set -e + append_missing_include_explanation + + set_owner "$TARGET_DIR" + + if [ "$EXIT_CODE" -eq 0 ]; then + RESTORE_OK=1 + SUCCESS_MSG="Przywracanie ${USERRESTOREPATH} z ${REPONAME} zakończone. Pliki w ${TARGET_DIR}." + echo "$SUCCESS_MSG" >> "$LOG_FILE" + else + if ! has_missing_include; then + fail "Borg extract zakończył się błędem. Sprawdź logi w tabeli zdarzeń na stronie pluginu." + fi + fi + fi +fi + +exit "$EXIT_CODE" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 0000000..0daaab8 --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -euo pipefail + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" + +# Safety checks to avoid deleting the wrong path. +if [ "$(basename "$PLUGIN_DIR")" != "borg-restore" ]; then + echo "borg-restore: safety check failed (basename is not borg-restore): $PLUGIN_DIR" >&2 + exit 1 +fi +if [ ! -f "$PLUGIN_DIR/plugin.conf" ]; then + echo "borg-restore: safety check failed (plugin.conf missing): $PLUGIN_DIR" >&2 + exit 1 +fi + +CRON_FILE="/etc/cron.d/borg-restore" +if [ -f "$CRON_FILE" ]; then + rm -f "$CRON_FILE" || true +fi + +echo "borg-restore: uninstall complete" diff --git a/scripts/worker.sh b/scripts/worker.sh new file mode 100755 index 0000000..c273f30 --- /dev/null +++ b/scripts/worker.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -euo pipefail +umask 077 + +PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)" +DATA_DIR="$PLUGIN_DIR/data" +JOB_DIR="$DATA_DIR/jobs/pending" +PROCESSING_DIR="$DATA_DIR/jobs/processing" +DONE_DIR="$DATA_DIR/jobs/done" +LOCK_DIR="$DATA_DIR/lock" +PID_DIR="$DATA_DIR/pids" +CANCEL_DIR="$DATA_DIR/cancel" + +mkdir -p "$JOB_DIR" "$PROCESSING_DIR" "$DONE_DIR" "$PID_DIR" "$CANCEL_DIR" + +if ! mkdir "$LOCK_DIR" 2>/dev/null; then + exit 0 +fi + +cleanup() { + rmdir "$LOCK_DIR" 2>/dev/null || true +} +trap cleanup EXIT + +JOB_FILE=$(ls -1 "$JOB_DIR"/*.env 2>/dev/null | head -n 1) +if [ -z "${JOB_FILE:-}" ]; then + exit 0 +fi + +BASE_NAME="$(basename "$JOB_FILE")" +RUN_FILE="$PROCESSING_DIR/$BASE_NAME" +chmod 600 "$JOB_FILE" 2>/dev/null || true +mv "$JOB_FILE" "$RUN_FILE" +chmod 600 "$RUN_FILE" 2>/dev/null || true +JOB_ID="${BASE_NAME%.env}" +PID_FILE="$PID_DIR/${JOB_ID}.pid" + +set +e +"$PLUGIN_DIR/scripts/restore.sh" "$RUN_FILE" & +RESTORE_PID=$! +echo "PID=${RESTORE_PID}" >> "$RUN_FILE" +chmod 600 "$RUN_FILE" 2>/dev/null || true +echo "${RESTORE_PID}" > "$PID_FILE" +wait "$RESTORE_PID" +EXIT_CODE=$? +set -e + +rm -f "$PID_FILE" + +CANCEL_FLAG="${CANCEL_DIR}/${JOB_ID}.flag" +if [ -f "$CANCEL_FLAG" ]; then + rm -f "$CANCEL_FLAG" + mv "$RUN_FILE" "$DONE_DIR/${BASE_NAME}.cancel" +elif [ "$EXIT_CODE" -eq 0 ]; then + mv "$RUN_FILE" "$DONE_DIR/${BASE_NAME}.ok" +else + mv "$RUN_FILE" "$DONE_DIR/${BASE_NAME}.fail" +fi + +exit 0 diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..4cc4c95 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -eu + +PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)" +SCRIPT="$PLUGIN_DIR/scripts/uninstall.sh" + +if [ ! -x "$SCRIPT" ]; then + chmod 755 "$SCRIPT" 2>/dev/null || true +fi + +exec "$SCRIPT" diff --git a/user/_index.html.bak b/user/_index.html.bak new file mode 100755 index 0000000..1aa880f --- /dev/null +++ b/user/_index.html.bak @@ -0,0 +1,3239 @@ +#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/borg-restore/php.ini + 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(); + ?> +
+
+

+

Uwaga:

+ +

+ +

Tryb przywracania:

+

Przywracana ścieżka:

+

+ + + + + + + + + + + + +
+ Nie + +
+ +
+
+ +
+
+ +
+

Log zadania

+
+ + + + + + +
+
+
+

Podgląd na żywo (odświeżanie co 3s)

+
+
Data rozpoczęcia:
+
Rodzaj przywracanych danych:
+
Ścieżka do przywrócenia:
+
Tryb przywracania:
+
Lokalizacja docelowa:
+
+
Status:
+
Postęp:
+
+
+
+
+ + + +
+
+
+ +
+
+ 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)) { + ?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ ' . h(t('Logi')) . ''; + } else { + $actions[] = '' . h(t('Logi')) . ''; + } + } + if (in_array($job['status'], ['done', 'failed', 'canceled'], true) && $job['log_exists']) { + $actions[] = '' . h(t('Zapisz log')) . ''; + } + if (($job['status'] === 'processing' && empty($job['cancel_requested'])) || $job['status'] === 'pending' || $job['status'] === 'calculating') { + $actions[] = '' + . '' + . '' + . '' + . '' + . ''; + } + if ($job['log_exists'] || $job['status'] === 'processing') { + $actions[] = '
' + . '' + . '' + . '' + . '' + . '
'; + } + echo !empty($actions) ? implode(' | ', $actions) : '-'; + ?> +
+
+ +
+ + +

+ 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('~(]*>.*?)~is', $html, -1, PREG_SPLIT_DELIM_CAPTURE); + if ($parts === false) { + return translate_text($html, $map); + } + $out = ''; + foreach ($parts as $part) { + if (preg_match('~^ $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); +} +?> + + + + + borg-restore + + + + + +
+
+ + + +
+
+
BorgBackup - Przywracanie Plików
+
+ +
+ +
+ +
+ + +
+ +
+

Lista backupow (borg list)

+ + +
+ +

Problem z konfiguracją środowiska backupowego - skontaktuj się z administratorem w celu uzyskania pomocy

+ + +

+ +
+ + + +
+ +

+ +
+ + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
NazwaDataHash
+ +
+

+ Ścieżka do przywrócenia: + + +

+

+ Rodzaj przywracanych danych: + + +

+

+ Tryb przywracania danych: + + + +

+

Przykłady użycia:

+

/home/uzytkownik/domains/uzytkownik.pl/public_html/

+

home/uzytkownik/domains/uzytkownik.pl/public_html

+

home/uzytkownik/domains/uzytkownik.pl/public_html/

+

Uwaga: znaki / z początku i końca linii zostaną usunięte.

+

+ Lokalizacja docelowa: + + + Domyślna lokalizacja docelowa: +

+

+ +

+
+ + +

Brak wpisów do wyświetlenia.

+ + +
+ +
+

Status zadań

+ +
+ +
+ + +

+ +
+ +
+
+ + + + + + +
+ + + + + diff --git a/user/enhanced.css b/user/enhanced.css new file mode 100644 index 0000000..3511c5a --- /dev/null +++ b/user/enhanced.css @@ -0,0 +1,450 @@ +:root { + --br-bg: #f4f6f8; + --br-card: #ffffff; + --br-text: #1f2a37; + --br-muted: #6b7280; + --br-border: #d9dee5; + --br-accent: #0ea5a4; + --br-accent-dark: #0f766e; + --br-danger: #dc2626; + --br-warn: #f59e0b; + --br-shadow: 0 8px 24px rgba(31, 41, 55, 0.12); +} +* { box-sizing: border-box; } +body { + margin: 0; + background: var(--br-bg); + color: var(--br-text); + font-family: "Helvetica Neue", Arial, sans-serif; +} +.br-container { + max-width: 1100px; + margin: 24px auto; + padding: 16px; +} +.br-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.br-title { + font-size: 20px; + font-weight: 700; + letter-spacing: 0.2px; +} +.br-card { + background: var(--br-card); + border: 1px solid var(--br-border); + border-radius: 10px; + padding: 24px; + box-shadow: var(--br-shadow); + margin-bottom: 16px; +} +.br-muted { color: var(--br-muted); } +.br-alert { + border-radius: 8px; + padding: 10px 12px; + margin: 8px 0; + border: 1px solid transparent; +} +.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; } +.br-alert-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; } +.br-alert-success { background: #ecfdf3; border-color: #bbf7d0; color: #166534; } +.br-error-debug { margin: 8px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; } +.br-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.35); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} +.br-modal { + width: min(560px, 92vw); + background: #ffffff; + border-radius: 12px; + padding: 16px; + border: 2px solid var(--br-border); + box-shadow: var(--br-shadow); + display: flex; + flex-direction: column; + max-height: 90vh; +} +.br-modal-log { + width: 90vw; + background: #ffffff; +} +.br-modal-warning { + background: #f3c8c8; +} +.br-modal-warning .br-btn-ghost { + background: #ffffff; +} +.br-modal-warning .br-btn-ghost:hover { + background: #f8fafc; +} +.br-modal-danger { + border-color: var(--br-danger); +} +.br-modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} +.br-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; +} +.br-modal-title { + font-weight: 700; + font-size: 16px; +} +.br-modal-body { + max-height: 60vh; + overflow: auto; +} +.br-modal-scroll { + overflow: auto; + flex: 1; + min-height: 0; +} +.br-log-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.br-log-header-actions { + display: flex; + gap: 8px; +} +.br-log-meta { + display: grid; + gap: 6px; + margin: 10px 0 12px; + padding: 10px 12px; + border: 1px solid var(--br-border); + border-radius: 10px; + background: #f8fafc; + font-size: 13px; +} +.br-log-actions { + margin: 8px 0 10px; +} +.br-picker-path { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + flex-wrap: wrap; +} +.br-picker-list { + list-style: none; + padding: 0; + margin: 0; + border: 1px solid var(--br-border); + border-radius: 10px; + max-height: 45vh; + overflow: auto; +} +.br-picker-create { + margin-top: 10px; + display: flex; + gap: 8px; +} +.br-picker-create input { + flex: 1; + padding: 6px 8px; + border: 1px solid var(--br-border); + border-radius: 8px; +} +.br-picker-list li { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid var(--br-border); +} +.br-picker-list li:last-child { + border-bottom: none; +} +.br-picker-list li:hover { + background: #f8fafc; +} +.br-picker-list li.active { + background: #e0f2fe; +} +.br-btn-small { + padding: 6px 10px; + font-size: 12px; +} +.br-actions-row { + display: flex; + gap: 8px; + margin-top: 10px; + flex-wrap: wrap; +} +.br-actions-row form { + margin: 0; +} +.br-icon-btn { + border: 1px solid var(--br-border); + background: #fff; + border-radius: 8px; + padding: 4px 8px; + cursor: pointer; + font-size: 16px; + line-height: 1; +} +.br-icon-btn:hover { + background: #f8fafc; +} +.br-table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--br-border); + border-radius: 8px; + overflow: hidden; + background: #fff; + table-layout: fixed; +} +.br-table th { + text-align: left; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--br-muted); + background: #f3f4f6; + padding: 10px; +} +.br-table td { + padding: 10px; + border-top: 1px solid var(--br-border); + vertical-align: top; +} +.br-table tr:nth-child(even) td { background: #f9fafb; } +.br-col-select { width: 50px; text-align: center; } +.br-col-status { width: 120px; } +.br-col-progress { width: 90px; } +.br-col-mode { width: 180px; } +.br-col-user { width: 140px; } +.br-col-backup { width: 220px; overflow-wrap: anywhere; } +.br-col-path { width: 260px; overflow-wrap: anywhere; } +.br-col-target { width: 220px; overflow-wrap: anywhere; } +.br-col-time { width: 150px; } +.br-col-actions { width: 120px; } +.br-table input[type="checkbox"] { + accent-color: var(--br-accent); + width: 16px; + height: 16px; +} +.br-actions { display: flex; gap: 8px; align-items: center; margin: 10px 0; } +.br-btn { + background: var(--br-accent); + color: #fff; + border: none; + border-radius: 6px; + padding: 8px 14px; + cursor: pointer; + font-weight: 600; +} +.br-btn:hover { background: var(--br-accent-dark); } +.br-btn-ghost { + background: transparent; + color: var(--br-text); + border: 1px solid var(--br-border); +} +.br-btn-ghost:hover { background: #eef2f7; } +.br-btn-danger { + background: var(--br-danger); +} +.br-btn-danger:hover { background: #b91c1c; } +.br-link-danger { color: var(--br-danger); text-decoration: none; } +.br-link-danger:hover { text-decoration: underline; } +.br-inline-form { display: inline; } +.br-link-button { + background: none; + border: none; + padding: 0; + margin: 0; + color: inherit; + cursor: pointer; + font: inherit; +} +.br-link-button:hover { text-decoration: underline; } +.br-link-button.br-link-danger { color: var(--br-danger); } +input[type="text"] { + width: 100%; + max-width: 680px; + padding: 8px 10px; + border: 1px solid var(--br-border); + border-radius: 6px; + font-size: 14px; + background: #fff; +} +pre { + background: #0f172a; + color: #e2e8f0; + padding: 12px; + border-radius: 8px; + overflow: auto; +} +a { color: var(--br-accent-dark); text-decoration: none; } +a:hover { text-decoration: underline; } +.br-toast { + position: fixed; + bottom: 18px; + right: 18px; + background: #16a34a; + color: #fff; + padding: 12px 16px; + border-radius: 8px; + box-shadow: 0 12px 28px rgba(17, 24, 39, 0.25); + z-index: 9999; + font-weight: 600; +} +.br-toast-stack { + position: fixed; + top: 18px; + right: 18px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 9999; +} +.br-toast-stack .br-toast { + position: static; + top: auto; + bottom: auto; +} +.br-toast-error { + background: #dc2626; + top: 18px; + bottom: auto; +} +.br-toast-success { + background: #16a34a; +} +.br-status-line { + margin: 6px 0; + font-size: 14px; +} +.br-progress { + height: 8px; + background: #e5e7eb; + border-radius: 999px; + overflow: hidden; + margin: 6px 0 12px; +} +.br-progress-bar { + height: 100%; + background: var(--br-accent); + transition: width 0.3s ease; +} +.br-restore-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin: 12px 0 10px; +} +.br-restore-box { + border: 1px solid var(--br-border); + border-radius: 10px; + background: #f2f4f7; + padding: 12px 14px; +} +.br-restore-box-full { + grid-column: 1 / -1; +} +.br-restore-box-title { + margin: 0 0 10px; + font-size: 14px; + font-weight: 700; +} +.br-radio-row { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 0 0 10px; +} +.br-radio-row:last-child { + margin-bottom: 0; +} +.br-radio-row input[type="radio"] { + margin-top: 2px; +} +.br-target-panel { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #d1d5db; +} +.br-target-label { + display: block; + margin-bottom: 6px; + font-weight: 700; +} +.br-target-input-wrap { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} +.br-target-input-wrap input[type="text"] { + max-width: none; + flex: 1; +} +.br-usage-box { + border: 1px solid var(--br-border); + border-radius: 10px; + background: #f3f4f6; + padding: 12px 14px; + margin: 8px 0 0; +} +.br-usage-box-title { + margin: 0 0 8px; + font-size: 14px; + font-weight: 700; +} +@media (max-width: 980px) { + .br-restore-grid { + grid-template-columns: 1fr; + } +} +.br-italic { + font-style:italic; +} + +.small { font-size:85%; } +.m-0 {margin:0;} +.my-5 {margin: 12px 0px;} +.br-border { border: 1px solid #eaeaea;} +.br-bg-light { background-color: #f9f9f9;} +.br-pad-x { padding-left: 1.25rem; padding-right: 1.25rem; } +.br-strong { font-weight: bold;} +.br-d-inline-block { display: inline-block; } +.br-d-block { display: block; } +.br-d-flex { display: flex; } +.br-g-4 { gap: 4px; } +.br-flex-col { flex-direction: column;} +.br-btn { border-radius: 16px; text-transform: uppercase; font-weight: normal;} +.br-icon-btn { padding: 9px; } +.border-top { border-top: 1px solid #eaeaea; padding-top: 12px; } +.border-bottom { border-bottom: 1px solid #eaeaea; padding-bottom: 12px; } +.align-self-center { align-self: center; } +#job-status { + background: transparent !important; + background-image: none !important; +} +#job-status > img { + display: none !important; +} +.br-jobs-empty { + margin: 0; + color: var(--br-muted); + background: transparent !important; + background-image: none !important; +} diff --git a/user/evolution.css b/user/evolution.css new file mode 100644 index 0000000..6382efd --- /dev/null +++ b/user/evolution.css @@ -0,0 +1,451 @@ +:root { + --br-bg: #f5f8ff; + --br-card: #ffffff; + --br-text: #111827; + --br-muted: #64748b; + --br-border: #e2e8f0; + --br-accent: #2563eb; + --br-accent-dark: #1d4ed8; + --br-danger: #dc2626; + --br-warn: #f59e0b; + --br-shadow: 0 10px 28px rgba(15, 23, 42, 0.12); +} +* { box-sizing: border-box; } +body { + margin: 0; + background: var(--br-bg); + color: var(--br-text); + font-family: "IBM Plex Sans", "Segoe UI", Arial, sans-serif; +} +.br-container { + max-width: none; + width: 100%; + margin: 16px auto; + padding: 16px 24px; +} +.br-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} +.br-title { + font-size: 20px; + font-weight: 700; + letter-spacing: 0.2px; +} +.br-card { + background: var(--br-card); + border: 1px solid var(--br-border); + border-radius: 12px; + padding: 24px; + box-shadow: var(--br-shadow); + margin-bottom: 16px; +} +.br-muted { color: var(--br-muted); } +.br-alert { + border-radius: 10px; + padding: 10px 12px; + margin: 8px 0; + border: 1px solid transparent; +} +.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; } +.br-alert-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; } +.br-alert-success { background: #ecfdf3; border-color: #bbf7d0; color: #166534; } +.br-error-debug { margin: 8px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; } +.br-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.35); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} +.br-modal { + width: min(560px, 92vw); + background: #ffffff; + border-radius: 12px; + padding: 16px; + border: 2px solid var(--br-border); + box-shadow: var(--br-shadow); + display: flex; + flex-direction: column; + max-height: 90vh; +} +.br-modal-log { + width: 90vw; + background: #ffffff; +} +.br-modal-warning { + background: #f3c8c8; +} +.br-modal-warning .br-btn-ghost { + background: #ffffff; +} +.br-modal-warning .br-btn-ghost:hover { + background: #f8fafc; +} +.br-modal-danger { + border-color: var(--br-danger); +} +.br-modal-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} +.br-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; +} +.br-modal-title { + font-weight: 700; + font-size: 16px; +} +.br-modal-body { + max-height: 60vh; + overflow: auto; +} +.br-modal-scroll { + overflow: auto; + flex: 1; + min-height: 0; +} +.br-log-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.br-log-header-actions { + display: flex; + gap: 8px; +} +.br-log-meta { + display: grid; + gap: 6px; + margin: 10px 0 12px; + padding: 10px 12px; + border: 1px solid var(--br-border); + border-radius: 10px; + background: #f8fafc; + font-size: 13px; +} +.br-log-actions { + margin: 8px 0 10px; +} +.br-picker-path { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + flex-wrap: wrap; +} +.br-picker-list { + list-style: none; + padding: 0; + margin: 0; + border: 1px solid var(--br-border); + border-radius: 10px; + max-height: 45vh; + overflow: auto; +} +.br-picker-create { + margin-top: 10px; + display: flex; + gap: 8px; +} +.br-picker-create input { + flex: 1; + padding: 6px 8px; + border: 1px solid var(--br-border); + border-radius: 8px; +} +.br-picker-list li { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid var(--br-border); +} +.br-picker-list li:last-child { + border-bottom: none; +} +.br-picker-list li:hover { + background: #f8fafc; +} +.br-picker-list li.active { + background: #d1fae5; +} +.br-btn-small { + padding: 6px 10px; + font-size: 12px; +} +.br-actions-row { + display: flex; + gap: 8px; + margin-top: 10px; + flex-wrap: wrap; +} +.br-actions-row form { + margin: 0; +} +.br-icon-btn { + border: 1px solid var(--br-border); + background: #fff; + border-radius: 8px; + padding: 4px 8px; + cursor: pointer; + font-size: 16px; + line-height: 1; +} +.br-icon-btn:hover { + background: #f8fafc; +} +.br-table { + width: 100%; + border-collapse: collapse; + border: 1px solid var(--br-border); + border-radius: 10px; + overflow: hidden; + background: #fff; + table-layout: fixed; +} +.br-table th { + text-align: left; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--br-muted); + background: #f1f5f9; + padding: 10px; +} +.br-table td { + padding: 10px; + border-top: 1px solid var(--br-border); + vertical-align: top; +} +.br-table tr:nth-child(even) td { background: #f8fafc; } +.br-col-select { width: 50px; text-align: center; } +.br-col-status { width: 120px; } +.br-col-progress { width: 90px; } +.br-col-mode { width: 180px; } +.br-col-user { width: 140px; } +.br-col-backup { width: 220px; overflow-wrap: anywhere; } +.br-col-path { width: 260px; overflow-wrap: anywhere; } +.br-col-target { width: 220px; overflow-wrap: anywhere; } +.br-col-time { width: 150px; } +.br-col-actions { width: 120px; } +.br-table input[type="checkbox"] { + accent-color: var(--br-accent); + width: 16px; + height: 16px; +} +.br-actions { display: flex; gap: 8px; align-items: center; margin: 10px 0; } +.br-btn { + background: var(--br-accent); + color: #fff; + border: none; + border-radius: 8px; + padding: 8px 14px; + cursor: pointer; + font-weight: 600; +} +.br-btn:hover { background: var(--br-accent-dark); } +.br-btn-ghost { + background: transparent; + color: var(--br-text); + border: 1px solid var(--br-border); +} +.br-btn-ghost:hover { background: #e2e8f0; } +.br-btn-danger { + background: var(--br-danger); +} +.br-btn-danger:hover { background: #b91c1c; } +.br-link-danger { color: var(--br-danger); text-decoration: none; } +.br-link-danger:hover { text-decoration: underline; } +.br-inline-form { display: inline; } +.br-link-button { + background: none; + border: none; + padding: 0; + margin: 0; + color: inherit; + cursor: pointer; + font: inherit; +} +.br-link-button:hover { text-decoration: underline; } +.br-link-button.br-link-danger { color: var(--br-danger); } +input[type="text"] { + width: 100%; + max-width: 680px; + padding: 8px 10px; + border: 1px solid var(--br-border); + border-radius: 8px; + font-size: 14px; + background: #fff; +} +pre { + background: #0b1020; + color: #e2e8f0; + padding: 12px; + border-radius: 8px; + overflow: auto; +} +a { color: var(--br-accent-dark); text-decoration: none; } +a:hover { text-decoration: underline; } +.br-toast { + position: fixed; + bottom: 18px; + right: 18px; + background: #16a34a; + color: #fff; + padding: 12px 16px; + border-radius: 10px; + box-shadow: 0 12px 28px rgba(16, 24, 40, 0.25); + z-index: 9999; + font-weight: 600; +} +.br-toast-stack { + position: fixed; + top: 18px; + right: 18px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 9999; +} +.br-toast-stack .br-toast { + position: static; + top: auto; + bottom: auto; +} +.br-toast-error { + background: #dc2626; + top: 18px; + bottom: auto; +} +.br-toast-success { + background: #16a34a; +} +.br-status-line { + margin: 6px 0; + font-size: 14px; +} +.br-progress { + height: 8px; + background: #e5e7eb; + border-radius: 999px; + overflow: hidden; + margin: 6px 0 12px; +} +.br-progress-bar { + height: 100%; + background: var(--br-accent); + transition: width 0.3s ease; +} +.br-restore-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + margin: 12px 0 10px; +} +.br-restore-box { + border: 1px solid var(--br-border); + border-radius: 10px; + background: #f1f5f9; + padding: 12px 14px; +} +.br-restore-box-full { + grid-column: 1 / -1; +} +.br-restore-box-title { + margin: 0 0 10px; + font-size: 14px; + font-weight: 700; +} +.br-radio-row { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 0 0 10px; +} +.br-radio-row:last-child { + margin-bottom: 0; +} +.br-radio-row input[type="radio"] { + margin-top: 2px; +} +.br-target-panel { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #cbd5e1; +} +.br-target-label { + display: block; + margin-bottom: 6px; + font-weight: 700; +} +.br-target-input-wrap { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} +.br-target-input-wrap input[type="text"] { + max-width: none; + flex: 1; +} +.br-usage-box { + border: 1px solid var(--br-border); + border-radius: 10px; + background: #f1f5f9; + padding: 12px 14px; + margin: 8px 0 0; +} +.br-usage-box-title { + margin: 0 0 8px; + font-size: 14px; + font-weight: 700; +} +@media (max-width: 980px) { + .br-restore-grid { + grid-template-columns: 1fr; + } +} +.br-italic { + font-style:italic; +} + +.small { font-size:85%; } +.m-0 {margin:0;} +.my-5 {margin: 12px 0px;} +.br-border { border: 1px solid #eaeaea;} +.br-bg-light { background-color: #f9f9f9;} +.br-pad-x { padding-left: 1.25rem; padding-right: 1.25rem; } +.br-strong { font-weight: bold;} +.br-d-inline-block { display: inline-block; } +.br-d-block { display: block; } +.br-d-flex { display: flex; } +.br-g-4 { gap: 4px; } +.br-flex-col { flex-direction: column;} +.br-btn { border-radius: 16px; text-transform: uppercase; font-weight: normal;} +.br-icon-btn { padding: 8px; } +.border-top { border-top: 1px solid #eaeaea; padding-top: 12px; } +.border-bottom { border-bottom: 1px solid #eaeaea; padding-bottom: 12px; } +.align-self-center { align-self: center; } +#job-status { + background: transparent !important; + background-image: none !important; +} +#job-status > img { + display: none !important; +} +.br-jobs-empty { + margin: 0; + color: var(--br-muted); + background: transparent !important; + background-image: none !important; +} diff --git a/user/index.html b/user/index.html new file mode 100755 index 0000000..47e1401 --- /dev/null +++ b/user/index.html @@ -0,0 +1,3385 @@ +#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/borg-restore/php.ini + null, + 'REPO' => null, + 'BORG_SSH_KEY' => null, + ]; + if (!is_readable($path)) { + return $vars; + } + $lines = file($path, FILE_IGNORE_NEW_LINES); + if ($lines === false) { + return $vars; + } + foreach ($lines as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') { + continue; + } + if (preg_match('/^(?:export\s+)?(BORG_PASSPHRASE|REPO|BORG_SSH_KEY)=(.*)$/', $line, $m)) { + $key = $m[1]; + $vars[$key] = parse_sh_value($m[2]); + } + } + return $vars; +} + +function load_settings(string $path): array +{ + $settings = [ + 'BB_PATH' => '/opt/bb', + 'BORG_PATH' => '/usr/bin/borg', + 'BORG_VARIABLES_FILE' => '/opt/bb/bbvars.sh', + 'ADMIN_MODE_ENABLE' => 'TRUE', + 'RESULTS_PER_PAGE' => '10', + 'WEB_RESTORE_LOCATION' => '/home/DA_USER/domains', + 'MAIL_RESTORE_LOCATION' => '/home/DA_USER/imap', + 'ENABLE_WEB_RESTORE' => 'TRUE', + 'ENABLE_MAILBOX_RESTORE' => 'TRUE', + 'DEFAULT_RESTORE_DESTINATION' => '/home/DA_USER/bb', + 'BLOCK_RESTORE_OUTSIDE_LOCATION' => 'TRUE', + 'PREVENT_RESTORE_MAIN_DOMAINS_DIR' => 'TRUE', + 'PREVENT_RESTORE_MAIN_IMAP_DIR' => 'TRUE', + 'PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN' => 'TRUE', + 'PREVENT_CREATE_DIRS_IN_MAIN_IMAP' => 'TRUE', + 'HASH_VISIBILITY' => 'FALSE', + 'ENABLE_LOGS_OVERLAY' => 'TRUE', + 'SELECTABLE_DOMAIN_RESTORE_DESTINATION' => 'TRUE', + 'SELECTABLE_MAIL_RESTORE_DESTINATION' => 'TRUE', + 'CLEAR_DOVECOT_INDEXES_AFTER_MERGE' => 'TRUE', + 'RESTORE_PATH_MODE' => 'dir', + 'ADD_DATE_TO_RESTORED_DIR' => 'FALSE', + 'DEBUG_MODE' => 'FALSE', + ]; + if (!is_readable($path)) { + return $settings; + } + $lines = file($path, FILE_IGNORE_NEW_LINES); + if ($lines === false) { + return $settings; + } + foreach ($lines as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') { + continue; + } + if (preg_match('/^(BB_PATH|BORG_PATH|BORG_VARIABLES_FILE|ADMIN_MODE_ENABLE|RESULTS_PER_PAGE|WEB_RESTORE_LOCATION|MAIL_RESTORE_LOCATION|ENABLE_WEB_RESTORE|ENABLE_MAILBOX_RESTORE|DEFAULT_RESTORE_DESTINATION|BLOCK_RESTORE_OUTSIDE_LOCATION|PREVENT_RESTORE_MAIN_DOMAINS_DIR|PREVENT_RESTORE_MAIN_IMAP_DIR|PREVENT_CREATE_DIRS_IN_MAIN_DOMAIN|PREVENT_CREATE_DIRS_IN_MAIN_IMAP|HASH_VISIBILITY|ENABLE_LOGS_OVERLAY|SELECTABLE_DOMAIN_RESTORE_DESTINATION|SELECTABLE_MAIL_RESTORE_DESTINATION|CLEAR_DOVECOT_INDEXES_AFTER_MERGE|RESTORE_PATH_MODE|ADD_DATE_TO_RESTORED_DIR|DEBUG_MODE)=(.*)$/', $line, $m)) { + $settings[$m[1]] = parse_sh_value($m[2]); + } + } + return $settings; +} + +function file_owner_name(int $uid): string +{ + if (function_exists('posix_getpwuid')) { + $info = posix_getpwuid($uid); + if (is_array($info) && !empty($info['name'])) { + return (string)$info['name']; + } + } + return (string)$uid; +} + +function validate_secure_file(string $path, string $label, bool $check_writable = true): string +{ + if ($path === '') { + return $label . ' ma pustą ścieżkę.'; + } + if (!file_exists($path)) { + return $label . ' nie istnieje: ' . $path; + } + if (is_link($path) || !is_file($path)) { + return $label . ' nie jest zwykłym plikiem: ' . $path; + } + $perms = fileperms($path); + if ($perms === false) { + return $label . ' ma nieprawidłowe uprawnienia: ' . $path; + } + if ($check_writable && (($perms & 0x0010) !== 0 || ($perms & 0x0002) !== 0)) { + return $label . ' nie może być zapisywalny dla grupy/innych: ' . $path; + } + $uid = fileowner($path); + if ($uid === false) { + return $label . ' ma nieprawidłowego właściciela: ' . $path; + } + $owner = file_owner_name((int)$uid); + if ($uid !== 0 && $uid !== 500 && $owner !== 'root' && $owner !== 'diradmin') { + return $label . ' ma nieprawidłowego właściciela (' . $owner . '): ' . $path; + } + return ''; +} + +function validate_job_files(string $job_base): array +{ + $errors = []; + foreach (['pending' => '*.env', 'processing' => '*.env', 'done' => '*.{ok,fail,cancel,env.ok,env.fail,env.cancel}'] as $dir => $pattern) { + foreach (glob($job_base . '/' . $dir . '/' . $pattern, GLOB_BRACE) ?: [] as $path) { + $err = validate_secure_file($path, 'Plik zadania', false); + if ($err !== '') { + $errors[] = $err; + } + } + } + return $errors; +} + +function safe_user_key(string $value): string +{ + $key = preg_replace('/[^A-Za-z0-9_.-]/', '_', $value); + return $key === '' ? 'unknown' : $key; +} + +function get_csrf_token(string $data_dir, string $da_user): string +{ + $dir = rtrim($data_dir, '/') . '/csrf'; + if (!is_dir($dir)) { + @mkdir($dir, 0700, true); + } + $path = $dir . '/' . safe_user_key($da_user) . '.token'; + if (is_readable($path)) { + $token = trim((string)file_get_contents($path)); + if ($token !== '') { + return $token; + } + } + $token = bin2hex(random_bytes(32)); + @file_put_contents($path, $token, LOCK_EX); + @chmod($path, 0600); + return $token; +} + +function csrf_is_valid(array $params, string $token): bool +{ + $sent = $params['csrf_token'] ?? ''; + if (!is_string($sent) || $sent === '' || $token === '') { + return false; + } + return hash_equals($token, $sent); +} + +function read_params(): array +{ + $params = []; + if (!empty($_GET)) { + $params = array_merge($params, $_GET); + } + if (!empty($_POST)) { + $params = array_merge($params, $_POST); + } + + $get_raw = getenv('GET') ?: getenv('get') ?: ''; + if ($get_raw !== '') { + parse_str($get_raw, $parsed); + $params = array_merge($parsed, $params); + } + + $qs = $_SERVER['QUERY_STRING'] ?? ''; + if ($qs !== '') { + parse_str($qs, $parsed); + $params = array_merge($parsed, $params); + } + + $post_raw = getenv('POST') ?: getenv('post') ?: ''; + if ($post_raw !== '') { + parse_str($post_raw, $parsed); + $params = array_merge($parsed, $params); + } + + return $params; +} + +function load_job_vars(string $path): array +{ + $vars = [ + 'DA_USER' => '', + 'REPONAME' => '', + 'USERRESTOREPATH' => '', + 'TARGETPATH' => '', + 'RESTOREMODE' => '', + 'RESTORETYPE' => '', + 'START_TS' => '', + ]; + if (!is_readable($path)) { + return $vars; + } + $lines = file($path, FILE_IGNORE_NEW_LINES); + if ($lines === false) { + return $vars; + } + foreach ($lines as $line) { + $line = trim($line); + if ($line === '' || $line[0] === '#') { + continue; + } + if (preg_match('/^(DA_USER|REPONAME|USERRESTOREPATH|TARGETPATH|RESTOREMODE|RESTORETYPE|START_TS|USERLANG)=(.*)$/', $line, $m)) { + $vars[$m[1]] = parse_sh_value($m[2]); + } + } + return $vars; +} + +function job_id_from_file(string $file): string +{ + $base = basename($file); + if (substr($base, -3) === '.ok') { + $base = substr($base, 0, -3); + } elseif (substr($base, -5) === '.fail') { + $base = substr($base, 0, -5); + } + if (substr($base, -4) === '.env') { + $base = substr($base, 0, -4); + } + return $base; +} + +function find_job_file(string $job_base, string $file, string $job_id): string +{ + $candidates = [ + $job_base . '/processing/' . $file, + $job_base . '/pending/' . $file, + $job_base . '/done/' . $file, + ]; + if ($job_id !== '') { + $candidates[] = $job_base . '/processing/' . $job_id . '.env'; + $candidates[] = $job_base . '/pending/' . $job_id . '.env'; + $candidates[] = $job_base . '/done/' . $job_id . '.ok'; + $candidates[] = $job_base . '/done/' . $job_id . '.fail'; + $candidates[] = $job_base . '/done/' . $job_id . '.cancel'; + } + foreach ($candidates as $path) { + if (is_file($path)) { + return $path; + } + } + return ''; +} + +function job_belongs_to_user(string $job_file_path, string $da_user): bool +{ + if ($job_file_path === '' || !is_readable($job_file_path)) { + return false; + } + $vars = load_job_vars($job_file_path); + $owner = (string)($vars['DA_USER'] ?? ''); + return $owner !== '' && $owner === $da_user; +} + +function find_job_file_for_user(string $job_base, string $file, string $job_id, string $da_user): string +{ + $path = find_job_file($job_base, $file, $job_id); + if ($path === '') { + return ''; + } + return job_belongs_to_user($path, $da_user) ? $path : ''; +} + +function format_log_body_for_download(string $text): string +{ + $text = normalize_log($text); + $text = preg_replace('/[ \t]+(?=\d{1,3}(?:\.\d+)?%\s+(?:Extracting|Deleting|Pruning|Compacting|Saving|Uploading|Downloading|Processing|Restoring|Creating)\b)/i', "\n", $text); + $text = preg_replace('/\n{2,}(?=\d{1,3}(?:\.\d+)?%)/', "\n", $text); + $text = preg_replace('/\n{3,}/', "\n\n", $text); + return rtrim($text) . "\n"; +} + +function log_missing_include_explanation(string $log_text, string $restore_path_label = ''): string +{ + if (stripos($log_text, 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki') !== false) { + return ''; + } + if (stripos($log_text, 'The specified restore path was not found in the backup') !== false) { + return ''; + } + if (preg_match("/Include pattern '.*' never matched\\./i", $log_text)) { + $suffix = ''; + if ($restore_path_label !== '' && $restore_path_label !== '-') { + $suffix = ' (' . $restore_path_label . ')'; + } + return 'W przywracanej kopii zapasowej nie odnaleziono wskazanej ścieżki' . $suffix . '. Prosimy spróbować przywrócić dane z innego archiwum, w którym te pliki mogły się znajdować.'; + } + return ''; +} + +function format_datetime(string $value, int $fallback = 0): string +{ + $ts = is_numeric($value) ? (int)$value : 0; + if ($ts <= 0) { + $ts = $fallback > 0 ? $fallback : time(); + } + return date('d-m-Y-H-i-s', $ts); +} + +function build_log_download_content(string $job_id, string $job_file_path, string $log_path, string $job_base): string +{ + $vars = load_job_vars($job_file_path); + $log_meta_start = format_datetime($vars['START_TS'] ?? '', time()); + $log_meta_path = display_restore_path($vars['USERRESTOREPATH'] ?? ''); + $log_meta_mode = restore_mode_label($vars['RESTOREMODE'] ?? ''); + $log_meta_type = restore_type_label($vars['RESTORETYPE'] ?? ''); + $log_meta_target = display_restore_path($vars['TARGETPATH'] ?? ''); + if ($log_meta_path === '') { + $log_meta_path = '-'; + } + if ($log_meta_mode === '') { + $log_meta_mode = '-'; + } + if ($log_meta_type === '') { + $log_meta_type = '-'; + } + if ($log_meta_target === '') { + $log_meta_target = '-'; + } + + $log_text = is_file($log_path) && is_readable($log_path) ? (string)file_get_contents($log_path) : ''; + $log_text = normalize_log($log_text); + $last_line = last_non_empty_line($log_text); + $progress = extract_progress($last_line); + $base_status = get_job_status($job_id, $job_base); + $display = display_status($base_status, $last_line); + $status_label = status_label($display); + if (in_array($display, ['done', 'failed', 'canceled'], true)) { + $progress_text = $display === 'done' ? '100%' : '0%'; + } else { + $progress_text = $progress !== null ? ($progress . '%') : ($last_line !== '' ? $last_line : '-'); + } + + $header = "Data rozpoczęcia: {$log_meta_start}\n"; + $header .= "Rodzaj przywracanych danych: {$log_meta_type}\n"; + $header .= "Ścieżka do przywrócenia: {$log_meta_path}\n"; + $header .= "Tryb przywracania: {$log_meta_mode}\n"; + $header .= "Lokalizacja docelowa: {$log_meta_target}\n"; + $header .= "Status: {$status_label}\n"; + $header .= "Postęp: {$progress_text}\n"; + $header .= "----- logi Borg -----\n"; + + $body = $log_text !== '' ? format_log_body_for_download($log_text) : "Log nie istnieje.\n"; + $explanation = log_missing_include_explanation($log_text, $log_meta_path); + if ($explanation !== '') { + $body = rtrim($body) . "\n\n" . $explanation . "\n"; + } + return $header . $body; +} + +function ajax_output(array $payload): void +{ + global $lang_map; + header('Content-Type: text/plain; charset=utf-8'); + $json = json_encode($payload, JSON_UNESCAPED_UNICODE); + echo '__BORG_AJAX_JSON__'; + if ($json === false) { + $json = '{}'; + } + echo translate_text($json, $lang_map ?? []); + echo '__BORG_AJAX_JSON__'; + exit; +} + +function log_text_output(string $text): void +{ + header('Content-Type: text/plain; charset=utf-8'); + echo '__BORG_LOG_TEXT__'; + echo $text; + echo '__BORG_LOG_TEXT__'; + exit; +} + +function status_html_output(string $html): void +{ + header('Content-Type: text/plain; charset=utf-8'); + echo '__BORG_STATUS_HTML__'; + echo $html; + echo '__BORG_STATUS_HTML__'; + exit; +} + +function delete_job_assets(string $file, string $job_base, string $log_dir, string $pid_dir, string $cancel_dir): bool +{ + $file = basename($file); + if ($file === '' || !preg_match('/^[-A-Za-z0-9_.]+$/', $file)) { + return false; + } + $job_id = job_id_from_file($file); + if ($job_id === '') { + return false; + } + $deleted = false; + foreach (['pending', 'processing', 'done'] as $dir) { + $dir_path = $job_base . '/' . $dir; + foreach (glob($dir_path . '/' . $job_id . '*') ?: [] as $path) { + if (is_file($path)) { + @unlink($path); + $deleted = true; + } + } + $exact = $dir_path . '/' . $file; + if (is_file($exact)) { + @unlink($exact); + $deleted = true; + } + } + $log_path = $log_dir . '/' . $job_id . '.log'; + if (is_file($log_path)) { + @unlink($log_path); + } + $pid_path = $pid_dir . '/' . $job_id . '.pid'; + if (is_file($pid_path)) { + @unlink($pid_path); + } + $cancel_path = $cancel_dir . '/' . $job_id . '.flag'; + if (is_file($cancel_path)) { + @unlink($cancel_path); + } + return $deleted; +} + +function list_jobs(string $dir, string $status, string $pattern, string $log_dir, string $pid_dir, string $cancel_dir, string $da_user): array +{ + $items = []; + foreach (glob($dir . '/' . $pattern) ?: [] as $path) { + $vars = load_job_vars($path); + $owner = (string)($vars['DA_USER'] ?? ''); + if ($owner === '' || $owner !== $da_user) { + continue; + } + $job_id = job_id_from_file($path); + $log_path = $log_dir . '/' . $job_id . '.log'; + $cancel_requested = is_file($cancel_dir . '/' . $job_id . '.flag'); + $pid = ''; + if ($status === 'processing') { + $pid_path = $pid_dir . '/' . $job_id . '.pid'; + if (is_readable($pid_path)) { + $pid = trim((string)file_get_contents($pid_path)); + } elseif (!empty($vars['PID'])) { + $pid = (string)$vars['PID']; + } + if ($pid !== '' && !preg_match('/^[0-9]+$/', $pid)) { + $pid = ''; + } + if ($pid !== '' && function_exists('posix_kill')) { + if (!@posix_kill((int)$pid, 0)) { + $pid = ''; + } + } + } + $progress_text = '-'; + $display_status = $status; + if ($status === 'processing' && is_readable($log_path)) { + $info = get_progress_info($log_path); + $display_status = display_status($status, $info['last']); + if ($cancel_requested) { + $display_status = 'canceled'; + $progress_text = 'Anulowane'; + } elseif ($display_status === 'calculating') { + $progress_text = 'Przygotowanie'; + } elseif ($info['progress'] !== null) { + $progress_text = $info['progress'] . '%'; + } elseif ($info['last'] !== '') { + $progress_text = $info['last']; + } + } elseif ($status === 'done') { + $progress_text = '100%'; + } elseif ($status === 'failed') { + $progress_text = 'Błąd'; + } elseif ($status === 'canceled') { + $progress_text = 'Anulowane'; + } + $items[] = [ + 'status' => $status, + 'display_status' => $display_status, + 'file' => basename($path), + 'time' => @filemtime($path) ?: 0, + 'user' => $vars['DA_USER'] ?? '', + 'repo' => $vars['REPONAME'] ?? '', + 'path' => $vars['USERRESTOREPATH'] ?? '', + 'target' => $vars['TARGETPATH'] ?? '', + 'restore_mode' => $vars['RESTOREMODE'] ?? '', + 'restore_mode_label' => restore_mode_label($vars['RESTOREMODE'] ?? ''), + 'restore_type' => $vars['RESTORETYPE'] ?? '', + 'restore_type_label' => restore_type_label($vars['RESTORETYPE'] ?? ''), + 'job_id' => $job_id, + 'log_exists' => is_readable($log_path), + 'progress_text' => $progress_text, + 'pid' => $pid, + 'cancel_requested' => $cancel_requested, + ]; + } + return $items; +} + +function h(string $value): string +{ + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); +} + +function status_label(string $status): string +{ + $map = [ + 'pending' => 'Oczekuje', + 'processing' => 'W trakcie', + 'calculating' => 'Przygotowanie', + 'done' => 'Zakończone', + 'failed' => 'Błąd', + 'canceled' => 'Anulowane', + ]; + $label = $map[$status] ?? 'Nieznany'; + return t($label); +} + +function read_log_tail(string $path, int $max_bytes = 200000): string +{ + if (!is_readable($path)) { + return ''; + } + $size = filesize($path); + if ($size === false || $size <= $max_bytes) { + return (string)file_get_contents($path); + } + $fp = fopen($path, 'rb'); + if ($fp === false) { + return ''; + } + fseek($fp, -$max_bytes, SEEK_END); + $data = fread($fp, $max_bytes); + fclose($fp); + return (string)$data; +} + +function normalize_log(string $text): string +{ + $text = str_replace("\r", "\n", $text); + $lines = preg_split('/\n/', $text); + if (!is_array($lines)) { + return $text; + } + $filtered = []; + foreach ($lines as $line) { + $trimmed = trim($line); + if ($trimmed !== '' && stripos($trimmed, 'Permanently added') !== false && stripos($trimmed, 'known host') !== false) { + continue; + } + $filtered[] = $line; + } + return implode("\n", $filtered); +} + +function last_non_empty_line(string $text): string +{ + $lines = preg_split('/\r\n|\n|\r/', $text); + if (!is_array($lines)) { + return ''; + } + for ($i = count($lines) - 1; $i >= 0; $i--) { + $line = trim((string)$lines[$i]); + if ($line !== '') { + return $line; + } + } + return ''; +} + +function is_calculating_line(string $line): bool +{ + return (bool)preg_match('/calculat/i', $line); +} + +function extract_progress(string $line): ?int +{ + if (preg_match('/\b(\d{1,3})%\b/', $line, $m)) { + $p = (int)$m[1]; + if ($p < 0) { + $p = 0; + } elseif ($p > 100) { + $p = 100; + } + return $p; + } + return null; +} + +function get_job_status(string $job_id, string $job_base): string +{ + if ($job_id === '') { + return 'unknown'; + } + if (is_file($job_base . '/processing/' . $job_id . '.env')) { + return 'processing'; + } + if (is_file($job_base . '/pending/' . $job_id . '.env')) { + return 'pending'; + } + if (is_file($job_base . '/done/' . $job_id . '.ok') || is_file($job_base . '/done/' . $job_id . '.env.ok')) { + return 'done'; + } + if (is_file($job_base . '/done/' . $job_id . '.fail') || is_file($job_base . '/done/' . $job_id . '.env.fail')) { + return 'failed'; + } + if (is_file($job_base . '/done/' . $job_id . '.cancel') || is_file($job_base . '/done/' . $job_id . '.env.cancel')) { + return 'canceled'; + } + return 'unknown'; +} + +function get_progress_info(string $log_path): array +{ + $text = normalize_log(read_log_tail($log_path, 20000)); + $last = last_non_empty_line($text); + $progress = extract_progress($last); + $calculating = $last !== '' && is_calculating_line($last); + return [ + 'last' => $last, + 'progress' => $progress, + 'calculating' => $calculating, + ]; +} + +function display_status(string $base_status, string $last_line): string +{ + if ($base_status === 'processing' && $last_line !== '' && is_calculating_line($last_line)) { + return 'calculating'; + } + return $base_status; +} + +function apply_da_user(string $value, string $da_user): string +{ + return str_replace('DA_USER', $da_user, $value); +} + +function restore_mode_label(string $mode): string +{ + if ($mode === 'overwrite') { + return t('Nadpisz aktualne dane'); + } + if ($mode === 'update') { + return t('Scalenie'); + } + if ($mode === 'location') { + return t('Przywracanie do lokalizacji'); + } + return t('-'); +} + +function restore_mode_label_form(string $mode): string +{ + if ($mode === 'overwrite') { + return t('Nadpisz aktualne dane'); + } + if ($mode === 'update') { + return t('Połączenie danych kopii zapasowej z obecnymi danymi'); + } + if ($mode === 'location') { + return t('Przywracanie do lokalizacji'); + } + return t('-'); +} + +function restore_type_label(string $type): string +{ + if ($type === 'mail') { + return t('Zawartość skrzynki pocztowej'); + } + if ($type === 'web') { + return t('Pliki www'); + } + return t('-'); +} + +function format_start_time($value, int $fallback): string +{ + $ts = is_numeric($value) ? (int)$value : 0; + if ($ts <= 0) { + $ts = $fallback > 0 ? $fallback : time(); + } + return date('d-m-Y-H-i-s', $ts); +} + +function parse_bool_setting(?string $value, bool $default): bool +{ + if ($value === null) { + return $default; + } + $value = strtolower(trim($value)); + if ($value === '') { + return $default; + } + if (in_array($value, ['1', 'true', 'yes', 'on'], true)) { + return true; + } + if (in_array($value, ['0', 'false', 'no', 'off'], true)) { + return false; + } + return $default; +} + +function display_restore_path(string $value): string +{ + $trimmed = trim($value); + if ($trimmed === '') { + return ''; + } + if ($trimmed[0] !== '/') { + $trimmed = '/' . $trimmed; + } + return $trimmed; +} + +function append_date_to_last_dir(string $path, string $date): string +{ + $path = rtrim($path, '/'); + if ($path === '' || $path === '/') { + return $path; + } + $parent = dirname($path); + $base = basename($path); + if ($base === '' || $base === '.' || $base === '..') { + return $path; + } + if ($parent === '/' || $parent === '.') { + return '/' . $base . '_' . $date; + } + return $parent . '/' . $base . '_' . $date; +} + +function describe_overwrite_backup(string $restore_path_raw): string +{ + $restore_path = trim($restore_path_raw, "/"); + if ($restore_path === '') { + return 'Backup zostanie utworzony jako *_przed_przywroceniem_' . date('d-m-Y_His') . ' w katalogu nadrzędnym ścieżki do przywrócenia.'; + } + $full = '/' . $restore_path; + $base = basename($full); + $parent = dirname($full); + if ($base === '' || $parent === '' || $parent === '.') { + return 'Backup zostanie utworzony jako *_przed_przywroceniem_' . date('d-m-Y_His') . ' w katalogu nadrzędnym ścieżki do przywrócenia.'; + } + $backup = $parent . '/' . $base . '_przed_przywroceniem_' . date('d-m-Y_His'); + return 'Backup zostanie utworzony jako: ' . $backup . '.'; +} + +function render_confirm_overwrite_html(string $restore_path_raw, string $selected_archive, string $target_path_raw, string $restore_mode, string $restore_type, bool $ajax, string $csrf_token): string +{ + $mode_label = restore_mode_label_form($restore_mode); + $title = $restore_mode === 'update' ? 'Potwierdź scalenie danych' : 'Potwierdź nadpisanie danych'; + $warning = $restore_mode === 'update' + ? 'Wybrano tryb scalenia danych. Istniejący katalog zostanie połączony z danymi z backupu.' + : 'Wybrano tryb nadpisywania danych. Istniejący katalog zostanie zastąpiony.'; + $loss_warning = ''; + if ($restore_mode === 'overwrite' && $restore_type === 'mail') { + $path_display = $restore_path_raw !== '' ? display_restore_path($restore_path_raw) : '-'; + $loss_warning = 'DANE W KATALOGU ' . $path_display . ' ZOSTANĄ UTRACONE.'; + } + ob_start(); + ?> +
+
+

+

Uwaga:

+ +

+ +

Tryb przywracania:

+

Przywracana ścieżka:

+

+
+ + + + + + + + + + + +
+ Nie + +
+
+
+
+ +
+
+ +
+

Log zadania

+
+ + + + + + +
+
+
+

Podgląd na żywo (odświeżanie co 3s)

+
+
Data rozpoczęcia:
+
Rodzaj przywracanych danych:
+
Ścieżka do przywrócenia:
+
Tryb przywracania:
+
Lokalizacja docelowa:
+
+
Status:
+
Postęp:
+
+
+
+
+ + + +
+
+
+ +
+
+ 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)) { + ?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ ' . h(t('Logi')) . ''; + } else { + $actions[] = '' . h(t('Logi')) . ''; + } + } + if (in_array($job['status'], ['done', 'failed', 'canceled'], true) && $job['log_exists']) { + $actions[] = '' . h(t('Zapisz log')) . ''; + } + if (($job['status'] === 'processing' && empty($job['cancel_requested'])) || $job['status'] === 'pending' || $job['status'] === 'calculating') { + $actions[] = '' + . '' + . '' + . '' + . '' + . ''; + } + if ($job['log_exists'] || $job['status'] === 'processing') { + $actions[] = '
' + . '' + . '' + . '' + . '' + . '
'; + } + echo !empty($actions) ? implode(' | ', $actions) : '-'; + ?> +
+
+ +
+ + +

+ +
+

+ +
+ +
+ 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('~(]*>.*?)~is', $html, -1, PREG_SPLIT_DELIM_CAPTURE); + if ($parts === false) { + return translate_text($html, $map); + } + $out = ''; + foreach ($parts as $part) { + if (preg_match('~^ 1; +$default_restore_type = $enabled_restore_types[0] ?? 'web'; +$restore_type = $params['restore_type'] ?? $default_restore_type; +if ($restore_type !== 'web' && $restore_type !== 'mail') { + $restore_type = $default_restore_type; +} +if (!in_array($restore_type, $enabled_restore_types, true)) { + $restore_type = $default_restore_type; +} +$params['restore_type'] = $restore_type; +if (!in_array($restore_mode, ['location', 'overwrite', 'update'], true)) { + $restore_mode = 'location'; +} +if ($restore_type === 'mail' && !in_array($restore_mode, ['location', 'update', 'overwrite'], true)) { + $restore_mode = 'update'; +} +if ($restore_type === 'web' && !in_array($restore_mode, ['location', 'overwrite'], true)) { + $restore_mode = 'location'; +} +$selectable_target = $restore_type === 'mail' ? $selectable_mail_restore_destination : $selectable_domain_restore_destination; +$confirm_overwrite = (($params['confirm_overwrite'] ?? '') === '1'); +$ajax_requested = (($params['ajax'] ?? '') === '1'); +$confirm_overwrite_view = false; +$toast_job = ''; +$restore_path_raw = $params['restore_path'] ?? ''; +$target_path_raw = $params['target_path'] ?? ''; +$jobs = []; +$log_message = ''; +$redirect_url = ''; +$queued_job = ''; +$queued_message = 'Zadanie przywracania danych dodane do kolejki, zostaniesz powiadomiony przez system wiadomości po zakończeniu tego procesu.'; +$confirm_view = false; +$floating_errors = []; +$view_log_job = ''; +$log_status_value = ''; +$log_last_line = ''; +$log_progress = null; +$log_meta_start = ''; +$log_meta_path = ''; +$log_meta_mode = ''; +$log_meta_type = ''; +$log_meta_target = ''; +$confirm_replace_url = ''; +$toast_message = ''; + +$active_restore_location = $restore_type === 'mail' ? $mail_restore_location : $web_restore_location; + +$job_base = '/usr/local/directadmin/plugins/borg-restore/data/jobs'; +$log_dir = '/usr/local/directadmin/plugins/borg-restore/data/logs'; +$pid_dir = '/usr/local/directadmin/plugins/borg-restore/data/pids'; +$cancel_dir = '/usr/local/directadmin/plugins/borg-restore/data/cancel'; + +$security_errors = []; +$security_errors[] = validate_secure_file($settings_path, 'plugin-settings.conf'); +$security_errors[] = validate_secure_file($vars_path, 'bbvars.sh', false); +$security_errors = array_filter($security_errors, static fn($value) => $value !== ''); +$security_errors = array_merge($security_errors, validate_job_files($job_base)); +$security_ok = empty($security_errors); +if (!$security_ok) { + $system_errors = array_merge($system_errors, $security_errors); +} + +foreach ($settings_errors as $err) { + $system_errors[] = $err; +} +if ($vars_path === '' || !is_readable($vars_path)) { + $system_errors[] = 'Brak pliku bbvars.sh: ' . ($vars_path !== '' ? $vars_path : 'nie ustawiono BB_PATH'); +} +if (empty($enabled_restore_types)) { + $system_errors[] = 'ENABLE_WEB_RESTORE i ENABLE_MAILBOX_RESTORE są ustawione na FALSE.'; +} +if (empty($repo)) { + $system_errors[] = 'Brak zmiennej REPO w ' . $vars_path; +} +if ($borg_bin === '') { + $system_errors[] = 'BORG_PATH jest puste w plugin-settings.conf.'; +} elseif (!is_executable($borg_bin)) { + $system_errors[] = 'BORG_PATH nie jest plikiem wykonywalnym: ' . $borg_bin; +} +if ($borg_ssh_key !== '' && !is_readable($borg_ssh_key)) { + $system_errors[] = 'BORG_SSH_KEY nie jest plikiem: ' . $borg_ssh_key; +} + +if (!$block_restore_outside_location) { + $warnings[] = 'BLOCK_RESTORE_OUTSIDE_LOCATION jest wyłączone - ścieżka do przywrócenia nie jest ograniczona.'; +} + +$input_prefix = $active_restore_location !== '' ? $active_restore_location : '/home/' . $da_user; +if ($restore_path_raw === '') { + $restore_path_raw = $input_prefix . '/'; +} +if ($restore_mode === 'location' && (!$selectable_target || $target_path_raw === '')) { + $target_path_raw = $default_restore_destination; +} + +$ui_config_disabled_message = 'Błąd konfiguracji - skontaktuj się z supportem w celu zgłoszenia problemu'; +$ui_system_error_message = 'Wystąpił problem z konfiguracją środowiska backupowego - skontaktuj się z administratorem w celu uzyskania pomocy'; +$ui_locked_by_feature_flags = empty($enabled_restore_types); +$ui_locked_message = $ui_locked_by_feature_flags ? $ui_config_disabled_message : $ui_system_error_message; +$ui_locked = $ui_locked_by_feature_flags || !empty($system_errors); +$system_error_details_for_ui = []; +if ($debug_mode) { + $system_error_details_for_ui = $system_errors; +} + +if ($ui_locked && in_array($action, ['dir_list', 'dir_create', 'log_stream'], true)) { + header('HTTP/1.1 403 Forbidden'); + exit; +} + +if ($action === 'status') { + if ($ui_locked) { + status_html_output(render_system_error_section($ui_locked_message, $system_error_details_for_ui, $debug_mode)); + } + $jobs = array_merge( + list_jobs($job_base . '/pending', 'pending', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/processing', 'processing', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'done', '*.ok', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'failed', '*.fail', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'canceled', '*.cancel', $log_dir, $pid_dir, $cancel_dir, $da_user) + ); + usort($jobs, function ($a, $b) { + return $b['time'] <=> $a['time']; + }); + if (count($jobs) > 50) { + $jobs = array_slice($jobs, 0, 50); + } + status_html_output(render_jobs_section($jobs, $csrf_token, $enable_logs_overlay)); +} + + +if ($action === 'log_stream') { + $file = basename((string)($params['job'] ?? '')); + if (!preg_match('/^[-A-Za-z0-9_.]+$/', $file)) { + header('HTTP/1.1 400 Bad Request'); + exit; + } + $job_id = job_id_from_file($file); + $job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user); + if ($job_file_path === '') { + header('HTTP/1.1 403 Forbidden'); + exit; + } + $log_path = $log_dir . '/' . $job_id . '.log'; + $log_text = normalize_log(read_log_tail($log_path)); + $explanation = log_missing_include_explanation($log_text); + if ($explanation !== '') { + $log_text = rtrim($log_text) . "\n\n" . $explanation; + } + $last_line = last_non_empty_line($log_text); + $progress = extract_progress($last_line); + $status = get_job_status($job_id, $job_base); + $display = display_status($status, $last_line); + if (in_array($display, ['done', 'failed', 'canceled'], true)) { + $progress = ($display === 'done') ? 100 : 0; + $progress_text = status_label($display); + } else { + $progress_text = $progress !== null ? ($progress . '%') : ($display === 'calculating' ? 'Przygotowanie' : $last_line); + } + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'status' => $status, + 'display_status' => $display, + 'status_label' => status_label($status), + 'display_label' => status_label($display), + 'log' => $log_text, + 'last_line' => $last_line, + 'progress' => $progress, + 'progress_text' => $progress_text, + ], JSON_UNESCAPED_UNICODE); + exit; +} + +if ($action === 'dir_list') { + if (!$security_ok) { + header('HTTP/1.1 403 Forbidden'); + exit; + } + $picker_base = picker_base_for_scope($params, $web_restore_location, $mail_restore_location, $block_restore_outside_location, $da_user); + file_picker_handle($params, $picker_base); + exit; +} + +if ($action === 'dir_create') { + if (!$security_ok) { + header('HTTP/1.1 403 Forbidden'); + exit; + } + $picker_base = picker_base_for_scope($params, $web_restore_location, $mail_restore_location, $block_restore_outside_location, $da_user); + file_picker_create($params, $picker_base, $da_user, $prevent_create_dirs_in_main_domain, $prevent_create_dirs_in_main_imap); + exit; +} + +if ($action === 'restore') { + if ($ui_locked) { + $errors[] = $ui_locked_message; + } + if (!$ui_locked && !$security_ok) { + $errors[] = 'Nieprawidłowe uprawnienia plików konfiguracyjnych lub zadań. Przywracanie zablokowane.'; + } + if (!$ui_locked && !$csrf_valid) { + $errors[] = 'Nieprawidłowy token CSRF.'; + } + if (!$ui_locked) { + $selected_archive = trim($selected_archive); + if ($restore_mode === 'location' && !$selectable_target) { + $target_path_raw = $default_restore_destination; + } + $restore_path_raw = trim($restore_path_raw); + $target_path_raw = trim($target_path_raw); + $restore_path = trim($restore_path_raw, "/"); + $target_path = trim($target_path_raw); + $restore_full_path = $restore_path !== '' ? '/' . $restore_path : ''; + $target_full_path = $target_path !== '' && $target_path[0] !== '/' ? '/' . $target_path : $target_path; + if ($restore_mode === 'overwrite' || $restore_mode === 'update') { + $target_full_path = $restore_full_path; + } + if ($restore_mode === 'location' && $add_date_to_restored_dir && $target_full_path !== '') { + $date_suffix = date('d-m-Y-His'); + $target_full_path = append_date_to_last_dir($target_full_path, $date_suffix); + $target_path_raw = $target_full_path; + $target_path = $target_full_path; + } + + if ($selected_archive === '') { + $errors[] = 'Nie wybrano backupu.'; + } + if ($restore_path === '') { + $errors[] = 'Nie podano ścieżki do przywrócenia.'; + } + if ($restore_mode === 'location') { + if ($target_path === '') { + $errors[] = 'Nie podano lokalizacji docelowej.'; + } + if ($target_path !== '' && $target_path[0] !== '/') { + $errors[] = 'Lokalizacja docelowa musi zaczynać się od /.'; + } + } + if ($restore_path !== '' && strpos($restore_path, '..') !== false) { + $errors[] = 'Ścieżka nie może zawierać "..".'; + } + if ($restore_mode === 'location') { + if ($target_path !== '' && strpos($target_path, '..') !== false) { + $errors[] = 'Lokalizacja docelowa nie może zawierać "..".'; + } + } + if ($restore_mode === 'location' && $target_full_path !== '') { + $home_base = '/home/' . $da_user; + if (strpos($target_full_path, $home_base . '/') !== 0 && $target_full_path !== $home_base) { + $errors[] = 'Lokalizacja docelowa musi być w katalogu: ' . $home_base; + } + } + if ($restore_mode === 'location' && $target_full_path !== '') { + $domains_root = '/home/' . $da_user . '/domains'; + $imap_root = '/home/' . $da_user . '/imap'; + $parent_dir = rtrim(dirname($target_full_path), '/'); + if ($prevent_create_dirs_in_main_domain && $parent_dir === $domains_root && !file_exists($target_full_path)) { + $errors[] = 'Nie można utworzyć katalogu bezpośrednio w ' . $domains_root . '.'; + } + if ($prevent_create_dirs_in_main_imap && $parent_dir === $imap_root && !file_exists($target_full_path)) { + $errors[] = 'Nie można utworzyć katalogu bezpośrednio w ' . $imap_root . '.'; + } + } + if ($block_restore_outside_location && $active_restore_location !== '' && $restore_full_path !== '') { + $base = rtrim($active_restore_location, '/'); + if (strpos($restore_full_path, $base . '/') !== 0 && $restore_full_path !== $base) { + $errors[] = 'Ścieżka musi być w katalogu: ' . $base; + } + } + if ($restore_type === 'web' && $prevent_restore_main_domains_dir && $restore_mode === 'overwrite' && $active_restore_location !== '') { + $base = rtrim($active_restore_location, '/'); + if ($restore_full_path === $base) { + $errors[] = 'Nie można przywrócić całego ' . $base . ' w trybie nadpisywania danych. Podaj podkatalog w ramach ' . $base . ' lub wybierz tryb przywracania do lokalizacji.'; + } + } + if ($restore_type === 'mail' && $prevent_restore_main_imap_dir && ($restore_mode === 'overwrite' || $restore_mode === 'update') && $active_restore_location !== '') { + $base = rtrim($active_restore_location, '/'); + if ($restore_full_path === $base) { + $errors[] = 'Nie można przywrócić całego ' . $base . ' w trybie nadpisywania lub scalania danych. Podaj podkatalog w ramach ' . $base . ' lub wybierz tryb przywracania do lokalizacji.'; + } + } + + if ($restore_mode === 'location' && $target_full_path !== '') { + if (@file_exists($target_full_path) && !@is_dir($target_full_path)) { + $errors[] = 'Lokalizacja docelowa istnieje, ale nie jest katalogiem.'; + } elseif (@is_dir($target_full_path)) { + $entries = @scandir($target_full_path); + if ($entries === false) { + $errors[] = 'Nie można odczytać katalogu docelowego.'; + } elseif (count($entries) > 2) { + $errors[] = 'Lokalizacja docelowa nie jest pusta.'; + } + } + } + + if (empty($errors)) { + if (($restore_mode === 'overwrite' || $restore_mode === 'update') && !$confirm_overwrite) { + $confirm_overwrite_view = true; + } else { + $job_dir = '/usr/local/directadmin/plugins/borg-restore/data/jobs/pending'; + if (!is_dir($job_dir) && !@mkdir($job_dir, 0700, true) && !is_dir($job_dir)) { + $errors[] = 'Nie udało się utworzyć katalogu zadań.'; + } else { + $job_id = date('YmdHis') . '-' . bin2hex(random_bytes(4)); + $job_file = $job_dir . '/restore-' . $job_id . '.env'; + $job_content = "DA_USER=" . escapeshellarg($da_user) . "\n"; + $job_content .= "START_TS=" . escapeshellarg((string)time()) . "\n"; + $job_content .= "REPONAME=" . escapeshellarg($selected_archive) . "\n"; + $job_content .= "USERRESTOREPATH=" . escapeshellarg($restore_path) . "\n"; + $job_content .= "TARGETPATH=" . escapeshellarg($target_full_path) . "\n"; + $job_content .= "RESTOREMODE=" . escapeshellarg($restore_mode) . "\n"; + $job_content .= "RESTORETYPE=" . escapeshellarg($restore_type) . "\n"; + $job_content .= "RESTOREPATHMODE=" . escapeshellarg($restore_path_mode) . "\n"; + $job_content .= "USERLANG=" . escapeshellarg($lang_code) . "\n"; + + $old_umask = umask(0077); + $write_ok = @file_put_contents($job_file, $job_content, LOCK_EX); + if ($old_umask !== false) { + umask($old_umask); + } + if ($write_ok === false) { + $errors[] = 'Nie udało się zapisać pliku zadania.'; + } else { + @chmod($job_file, 0600); + $queued_job = 'restore-' . $job_id; + $confirm_replace_url = '/CMD_PLUGINS/borg-restore?queued=1&view=queued&job=' . rawurlencode($queued_job); + if ($ajax_requested) { + $toast_message = $queued_message; + $toast_job = $queued_job; + } elseif ($skin === 'evolution') { + $toast_message = $queued_message; + $toast_job = $queued_job; + } else { + $confirm_view = true; + } + } + } + } + } + } +} + +if (!empty($errors)) { + $remaining = []; + foreach ($errors as $err) { + if ($err === 'Nie wybrano backupu.' || strpos($err, 'Nie można przywrócić całego') === 0) { + $floating_errors[] = $err; + } else { + $remaining[] = $err; + } + } + $errors = $remaining; +} + +if ($action === 'restore' && $ajax_requested) { + $payload = [ + 'ok' => empty($errors) && !$confirm_overwrite_view, + 'errors' => $errors, + 'floating_errors' => $floating_errors, + ]; + if ($confirm_overwrite_view) { + $payload['confirm_html'] = render_confirm_overwrite_html($restore_path_raw, $selected_archive, $target_path_raw, $restore_mode, $restore_type, true, $csrf_token); + } elseif (empty($errors)) { + $payload['toast_message'] = $toast_message; + $payload['toast_job'] = $toast_job; + } + ajax_output($payload); +} + +if (!$ui_locked && empty($errors) && $security_ok) { + $env = $_ENV; + if (!empty($passphrase)) { + $env['BORG_PASSPHRASE'] = $passphrase; + } + if ($borg_ssh_key !== '' && is_readable($borg_ssh_key)) { + $env['BORG_RSH'] = 'ssh -i ' . escapeshellarg($borg_ssh_key); + } + + $command = $borg_bin . ' list ' . escapeshellarg((string)$repo); + $descriptors = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = @proc_open($command, $descriptors, $pipes, null, $env); + if (!is_resource($process)) { + $system_errors[] = 'Nie udało się uruchomić borg.'; + } else { + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exit_code = proc_close($process); + + if ($debug_mode) { + $system_debug_lines[] = '[borg list command] ' . $command; + $system_debug_lines[] = '[exit_code] ' . $exit_code; + $system_debug_lines[] = '[stdout]'; + $system_debug_lines[] = trim((string)$stdout) !== '' ? trim((string)$stdout) : '(pusty)'; + $system_debug_lines[] = '[stderr]'; + $system_debug_lines[] = trim((string)$stderr) !== '' ? trim((string)$stderr) : '(pusty)'; + } + + if ($exit_code !== 0) { + $msg = trim((string)$stderr); + if ($msg === '') { + $msg = 'Kod bledu: ' . $exit_code; + } + $system_errors[] = 'Błąd borg list: ' . $msg; + } else { + $lines = preg_split('/\r?\n/', trim((string)$stdout)); + foreach ($lines as $line) { + $line = trim($line); + if ($line === '') { + continue; + } + if (preg_match('/^(\\S+)\\s+(.+?)\\s+\\[([0-9a-fA-F]+)\\]\\s*$/', $line, $m)) { + $entries[] = [ + 'name' => $m[1], + 'date' => $m[2], + 'hash' => $m[3], + ]; + } else { + $entries[] = [ + 'name' => $line, + 'date' => '', + 'hash' => '', + ]; + } + } + } + } +} + +if ($debug_mode) { + $system_error_details_for_ui = array_values(array_unique(array_merge($system_errors, $system_debug_lines))); +} +if (!empty($system_errors)) { + $ui_locked = true; + if (!$ui_locked_by_feature_flags) { + $ui_locked_message = $ui_system_error_message; + } +} + +if ($action === 'clear_jobs' && $method === 'POST') { + $deleted = 0; + $owned_jobs = array_merge( + list_jobs($job_base . '/pending', 'pending', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/processing', 'processing', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'done', '*.ok', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'failed', '*.fail', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'canceled', '*.cancel', $log_dir, $pid_dir, $cancel_dir, $da_user) + ); + foreach ($owned_jobs as $job) { + if (delete_job_assets($job['file'], $job_base, $log_dir, $pid_dir, $cancel_dir)) { + $deleted++; + } + } + $message_status = $deleted > 0 ? 'Zadania zostały wyczyszczone.' : 'Brak zadań do wyczyszczenia.'; +} + +if ($action === 'delete_selected' && $method === 'POST') { + $to_delete = $params['jobs'] ?? []; + if (!is_array($to_delete)) { + $to_delete = [$to_delete]; + } + $deleted = 0; + foreach ($to_delete as $file) { + $file = basename((string)$file); + $job_id = job_id_from_file($file); + $job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user); + if ($job_file_path === '') { + continue; + } + if (delete_job_assets($file, $job_base, $log_dir, $pid_dir, $cancel_dir)) { + $deleted++; + } + } + $log_message = $deleted > 0 ? 'Wybrane zadania zostały usunięte.' : 'Nie wybrano żadnych zadań.'; +} + +if ($action === 'delete_job') { + $file = basename((string)($params['job'] ?? '')); + $job_id = job_id_from_file($file); + $job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user); + if ($job_file_path === '') { + $log_message = 'Brak dostępu do zadania.'; + } elseif (delete_job_assets($file, $job_base, $log_dir, $pid_dir, $cancel_dir)) { + $log_message = 'Zadanie zostało usunięte.'; + } else { + $log_message = 'Nie znaleziono zadania.'; + } +} + +if ($action === 'cancel_job') { + $file = basename((string)($params['job'] ?? '')); + $job_id = job_id_from_file($file); + $job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user); + $pending_file = $job_base . '/pending/' . $file; + $processing_file = $job_base . '/processing/' . $file; + $cancel_file = $cancel_dir . '/' . $job_id . '.flag'; + if ($job_id === '' || !preg_match('/^[-A-Za-z0-9_.]+$/', $file)) { + $log_message = 'Nieprawidłowe zadanie.'; + } elseif ($job_file_path === '') { + $log_message = 'Brak dostępu do zadania.'; + } elseif (is_file($pending_file)) { + $target = $job_base . '/done/' . $job_id . '.cancel'; + if (@rename($pending_file, $target)) { + $log_message = 'Zadanie zostało anulowane.'; + } else { + if (delete_job_assets($file, $job_base, $log_dir, $pid_dir, $cancel_dir)) { + $log_message = 'Zadanie zostało anulowane.'; + } else { + $log_message = 'Nie udało się anulować zadania.'; + } + } + } elseif (!is_file($processing_file)) { + $log_message = 'Zadanie nie jest w trakcie.'; + } else { + $pid = ''; + $pid_path = $pid_dir . '/' . $job_id . '.pid'; + if (is_readable($pid_path)) { + $pid = trim((string)file_get_contents($pid_path)); + } else { + $vars = load_job_vars($processing_file); + if (!empty($vars['PID'])) { + $pid = (string)$vars['PID']; + } + } + if ($pid === '' || !preg_match('/^[0-9]+$/', $pid)) { + $log_message = 'Nie znaleziono procesu do anulowania.'; + } elseif (!function_exists('posix_kill')) { + $log_message = 'Brak obsługi posix_kill na serwerze.'; + } else { + $ok = @posix_kill((int)$pid, SIGTERM); + usleep(300000); + if (@posix_kill((int)$pid, 0)) { + @posix_kill((int)$pid, SIGKILL); + usleep(300000); + } + if ($ok && !@posix_kill((int)$pid, 0)) { + @file_put_contents($cancel_file, "1"); + $log_message = 'Zadanie zostało anulowane.'; + } else { + $log_message = 'Nie udało się anulować zadania.'; + } + } + } +} + +if ($action === 'download_log') { + $file = basename((string)($params['job'] ?? '')); + $raw_download = (($params['raw'] ?? '') === '1'); + if (preg_match('/^[-A-Za-z0-9_.]+$/', $file)) { + $job_id = job_id_from_file($file); + $log_path = $log_dir . '/' . $job_id . '.log'; + $job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user); + $filename = $job_id . '.log'; + if ($job_file_path === '') { + header('HTTP/1.1 403 Forbidden'); + echo t("Brak dostępu do logu.\n"); + exit; + } + $content = build_log_download_content($job_id, $job_file_path, $log_path, $job_base); + if (!empty($lang_map)) { + $content = translate_text($content, $lang_map); + } + if ($raw_download) { + log_text_output($content); + } + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Disposition: attachment; filename="' . $filename . '"'); + echo $content; + } else { + header('Content-Type: text/plain; charset=utf-8'); + echo t("Nieprawidłowy identyfikator logu.\n"); + } + exit; +} + +if ($action === 'view_log') { + $file = basename((string)($params['job'] ?? '')); + if (preg_match('/^[-A-Za-z0-9_.]+$/', $file)) { + $job_id = job_id_from_file($file); + $job_file_path = find_job_file_for_user($job_base, $file, $job_id, $da_user); + $log_path = $log_dir . '/' . $job_id . '.log'; + if ($job_file_path === '') { + $log_message = 'Brak dostępu do zadania.'; + $log_status_value = 'Brak dostępu'; + $log_meta_start = '-'; + $log_meta_path = '-'; + $log_meta_mode = '-'; + $log_meta_type = '-'; + $log_meta_target = '-'; + $view_log_job = ''; + } else { + $view_log_job = $file; + $status = get_job_status($job_id, $job_base); + $log_status_value = status_label($status); + $vars = load_job_vars($job_file_path); + $start_ts = $vars['START_TS'] ?? ''; + $log_meta_start = format_start_time($start_ts, @filemtime($job_file_path) ?: 0); + $log_meta_path = display_restore_path($vars['USERRESTOREPATH'] ?? ''); + $log_meta_mode = restore_mode_label($vars['RESTOREMODE'] ?? ''); + $log_meta_type = restore_type_label($vars['RESTORETYPE'] ?? ''); + $log_meta_target = display_restore_path($vars['TARGETPATH'] ?? ''); + if ($log_meta_path === '') { + $log_meta_path = '-'; + } + if ($log_meta_type === '') { + $log_meta_type = '-'; + } + if ($log_meta_target === '') { + $log_meta_target = '-'; + } + if (is_file($log_path) && is_readable($log_path)) { + $log_message = normalize_log((string)file_get_contents($log_path)); + } else { + $log_message = 'Log nie istnieje.'; + } + $explanation = log_missing_include_explanation($log_message, $log_meta_path); + if ($explanation !== '') { + $log_message = rtrim($log_message) . "\n\n" . $explanation; + } + $log_last_line = last_non_empty_line($log_message); + $log_progress = extract_progress($log_last_line); + $display = display_status($status, $log_last_line); + $log_status_value = status_label($display); + if (in_array($display, ['done', 'failed', 'canceled'], true)) { + $log_progress = $display === 'done' ? 100 : null; + $log_last_line = status_label($display); + } + } + } else { + $log_message = 'Nieprawidłowy identyfikator logu.'; + } + $view_log_overlay = $enable_logs_overlay && (($params['overlay'] ?? '') === '1'); + if ($view_log_overlay) { + $overlay_html = render_log_view_html($view_log_job, $log_meta_start ?? '-', $log_meta_type ?? '-', $log_meta_path ?? '-', $log_meta_mode ?? '-', $log_meta_target ?? '-', $log_status_value ?? '-', $log_progress ?? null, $log_last_line ?? '', $log_message ?? '', $status ?? '', true); + echo translate_html_safe($overlay_html, $lang_map ?? []); + exit; + } +} + +if (($params['queued'] ?? '') === '1') { + $queued_job = isset($params['job']) ? trim((string)$params['job']) : ''; + $queued_label = $queued_message; + if ($queued_job !== '' && preg_match('/^restore-[A-Za-z0-9-]+$/', $queued_job)) { + $queued_label = $queued_message . ' ID: ' . $queued_job . '.'; + } + if ($skin === 'evolution') { + $toast_message = $queued_label; + $toast_job = $queued_job; + $confirm_view = false; + } elseif (($params['view'] ?? '') === 'queued') { + $confirm_view = true; + } else { + $message_status = $queued_label; + } +} + +$confirm_view = false; +$jobs = array_merge( + list_jobs($job_base . '/pending', 'pending', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/processing', 'processing', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'done', '*.ok', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'failed', '*.fail', $log_dir, $pid_dir, $cancel_dir, $da_user), + list_jobs($job_base . '/done', 'canceled', '*.cancel', $log_dir, $pid_dir, $cancel_dir, $da_user) +); +usort($jobs, function ($a, $b) { + return $b['time'] <=> $a['time']; +}); +if (count($jobs) > 50) { + $jobs = array_slice($jobs, 0, 50); +} +?> + + + + + + borg-restore + + + + + +
+
+ + + +
+
+
BorgBackup - Przywracanie Plików
+
+ +
+ +
+ + +
+ +
+ +
+ + +
+ +
+

Lista backupow

+ + +
+ +

Problem z konfiguracją środowiska backupowego - skontaktuj się z administratorem w celu uzyskania pomocy

+ + +

+ +
+ + + +
+ +

+ +
+ + + +
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
NazwaDataHash
+ +
+
+ +
+

Rodzaj przywracanych danych

+ + +
+ +
+ + +
+
+
+ + + +
+

Tryb przywracanych danych

+ +

Rodzaj przywracanych danych:

+
+ +
+ + +
+
+ + + + +
+ +
+ + +
+ Domyślna lokalizacja docelowa: +
+
+
+
+

Przykłady użycia

+

/home/uzytkownik/domains/uzytkownik.pl/public_html/

+

home/uzytkownik/domains/uzytkownik.pl/public_html

+

home/uzytkownik/domains/uzytkownik.pl/public_html/

+

Uwaga! Znaki / z początku i końca linii zostaną usunięte.

+
+

+ +

+
+ + +

Brak wpisów do wyświetlenia.

+ + +
+ +
+

Status zadań

+ +
+ +
+ + +

+ +
+ +
+
+ + + + + + + + +
+ + +