Files
2026-07-04 15:02:24 +02:00

4796 lines
183 KiB
HTML
Executable File

#!/usr/local/bin/php -nc/usr/local/directadmin/plugins/db-manager/php.ini
<?php
function parse_sh_value(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
if ($value[0] === "'" && substr($value, -1) === "'") {
return substr($value, 1, -1);
}
if ($value[0] === '"' && substr($value, -1) === '"') {
return stripcslashes(substr($value, 1, -1));
}
$value = preg_replace('/\s+#.*$/', '', $value);
return trim($value);
}
function parse_bool(string $value, bool $default = false): bool
{
$normalized = strtolower(trim($value));
if ($normalized === '') {
return $default;
}
if (in_array($normalized, ['1', 'true', 'yes', 'y', 'on'], true)) {
return true;
}
if (in_array($normalized, ['0', 'false', 'no', 'n', 'off'], true)) {
return false;
}
return $default;
}
function normalize_lang_code(string $value): string
{
$value = strtolower(trim($value));
if ($value === '') {
return 'en';
}
if (strpos($value, 'pl') === 0 || strpos($value, 'polish') !== false || strpos($value, 'polski') !== false) {
return 'pl';
}
if (strpos($value, 'en') === 0 || strpos($value, 'english') !== false) {
return 'en';
}
return 'en';
}
function load_user_language(string $user): string
{
$path = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
if (!is_readable($path)) {
return 'en';
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return 'en';
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^(?:lang|language|locale)=(.+)$/i', $line, $m)) {
return normalize_lang_code((string)$m[1]);
}
}
return 'en';
}
function load_conf_with_presence(string $path): array
{
$result = [
'values' => [],
'present' => [],
];
if (!is_readable($path)) {
return $result;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $result;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!preg_match('/^([A-Za-z0-9_]+)=(.*)$/', $line, $m)) {
continue;
}
$key = trim((string)$m[1]);
if ($key === '') {
continue;
}
$result['values'][$key] = parse_sh_value((string)$m[2]);
$result['present'][$key] = true;
}
return $result;
}
function parse_plugins_list(string $value): array
{
$value = trim($value);
if ($value === '') {
return [];
}
$out = [];
foreach (explode(':', $value) as $part) {
$part = strtolower(trim($part));
if ($part === '') {
continue;
}
$out[] = $part;
}
return array_values(array_unique($out));
}
function conf_explicit_bool(array $conf, array $keys, ?bool &$out): bool
{
$values = $conf['values'] ?? [];
$present = $conf['present'] ?? [];
$keys_lc = array_map('strtolower', $keys);
foreach ($present as $conf_key => $is_present) {
if (!$is_present) {
continue;
}
$conf_key_lc = strtolower((string)$conf_key);
if (!in_array($conf_key_lc, $keys_lc, true)) {
continue;
}
$out = parse_bool((string)($values[$conf_key] ?? ''), false);
return true;
}
return false;
}
function is_custom_package_item_enabled(array $conf): bool
{
$enabled = null;
if (conf_explicit_bool($conf, ['db_manager'], $enabled)) {
return (bool)$enabled;
}
return false;
}
function is_plugin_allowed_by_conf(array $conf, string $plugin_id): bool
{
$values = $conf['values'] ?? [];
$present = $conf['present'] ?? [];
$plugin_id = strtolower(trim($plugin_id));
if ($plugin_id === '') {
return false;
}
if (!empty($present['plugins_allow'])) {
$allow = parse_plugins_list((string)($values['plugins_allow'] ?? ''));
return in_array($plugin_id, $allow, true);
}
if (!empty($present['plugins_deny'])) {
$deny = parse_plugins_list((string)($values['plugins_deny'] ?? ''));
return !in_array($plugin_id, $deny, true);
}
return true;
}
function resolve_package_conf_path(array $user_conf, string $da_user): string
{
$values = $user_conf['values'] ?? [];
$package_name = trim((string)($values['package'] ?? $values['user_package'] ?? ''));
if ($package_name === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $package_name)) {
return '';
}
$owners = [];
foreach (['creator', 'owner', 'reseller', 'username'] as $key) {
$owner = trim((string)($values[$key] ?? ''));
if ($owner !== '' && preg_match('/^[A-Za-z0-9._-]+$/', $owner)) {
$owners[] = $owner;
}
}
if (preg_match('/^[A-Za-z0-9._-]+$/', $da_user)) {
$owners[] = $da_user;
}
$owners[] = 'admin';
$owners = array_values(array_unique($owners));
foreach ($owners as $owner) {
foreach (['', '.conf'] as $ext) {
$candidate = '/usr/local/directadmin/data/users/' . $owner . '/packages/' . $package_name . $ext;
if (is_readable($candidate)) {
return $candidate;
}
}
}
return '';
}
function is_plugin_enabled_for_user(string $da_user, string $plugin_id, array $settings = []): bool
{
$base = '/usr/local/directadmin/data/users/' . $da_user;
if (parse_bool((string)($settings['Always_enable'] ?? '0'), false)) {
return true;
}
$user_conf_path = $base . '/user.conf';
$user_conf = load_conf_with_presence($user_conf_path);
if (!is_custom_package_item_enabled($user_conf)) {
return false;
}
if (!is_plugin_allowed_by_conf($user_conf, $plugin_id)) {
return false;
}
$package_conf_path = resolve_package_conf_path($user_conf, $da_user);
if ($package_conf_path === '') {
return true;
}
$package_conf = load_conf_with_presence($package_conf_path);
if (!is_custom_package_item_enabled($package_conf)) {
return false;
}
return is_plugin_allowed_by_conf($package_conf, $plugin_id);
}
function load_lang_map(string $lang, string $plugin_dir): array
{
$lang = normalize_lang_code($lang);
$lang_dir = $plugin_dir . '/lang';
$fallback_path = $lang_dir . '/en.php';
$lang_path = $lang_dir . '/' . $lang . '.php';
$fallback = [];
if (is_readable($fallback_path)) {
$fallback_map = include $fallback_path;
if (is_array($fallback_map)) {
$fallback = $fallback_map;
}
}
if ($lang === 'en') {
return $fallback;
}
if (!is_readable($lang_path)) {
return $fallback;
}
$selected = include $lang_path;
if (!is_array($selected)) {
return $fallback;
}
return array_merge($fallback, $selected);
}
function translate_text(string $text, array $map): string
{
if (empty($map)) {
return $text;
}
uksort($map, function ($a, $b) {
return strlen((string)$b) <=> strlen((string)$a);
});
return str_replace(array_keys($map), array_values($map), $text);
}
function translate_html_safe(string $html, array $map): string
{
if (empty($map)) {
return $html;
}
$parts = preg_split('~(<script\\b[^>]*>.*?</script>)~is', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
if ($parts === false) {
return translate_text($html, $map);
}
$out = '';
foreach ($parts as $part) {
if (preg_match('~^<script\\b~i', $part)) {
$out .= $part;
} else {
$out .= translate_text($part, $map);
}
}
return $out;
}
function t(string $text): string
{
global $lang_map;
if (empty($lang_map)) {
return $text;
}
return $lang_map[$text] ?? $text;
}
function load_settings(string $path): array
{
$settings = [
'HITME_SQL_BACKUP_DIR' => '/home/admin/mysql_backups',
'USER_SQL_BACKUP_DIR' => '/home/da_user/mysql_backups/user_backups',
'ALLOW_DOWNLOAD_HITME_SQL_BACKUP' => 'true',
'ENABLE_USER_BACKUPS' => 'true',
'PRE_RESTORE_BACKUP_DIR' => '/home/da_user/mysql_backups/pre_restore_backups',
'PRE_RESTORE_USER_BACKUP_DIR' => '/home/da_user/mysql_backups/pre_restore_user_backups',
'ENABLE_PRE_RESTORE_BACKUP' => 'true',
'ENABLE_USER_PRE_RESTORE_BACKUP' => 'true',
'SECURE_BACKUP' => 'true',
'SECURE_RESTORE' => 'true',
'PREVENT_LOG_DELETION' => 'true',
'OWN_BACKUP_USER_RESTORE' => 'true',
'CHECK_DATABASE_COLLISIONS' => 'true',
'ENABLE_OPERATION_LIMITS' => 'true',
'RESTORE_LIMIT_5MIN' => '10',
'RESTORE_LIMIT_HOUR' => '30',
'RESTORE_LIMIT_DAY' => '100',
'BACKUP_LIMIT_5MIN' => '10',
'BACKUP_LIMIT_HOUR' => '30',
'BACKUP_LIMIT_DAY' => '100',
'ENABLE_LOGS_OVERLAY' => 'true',
'Always_enable' => '0',
];
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('/^(HITME_SQL_BACKUP_DIR|USER_SQL_BACKUP_DIR|ALLOW_DOWNLOAD_HITME_SQL_BACKUP|ENABLE_USER_BACKUPS|PRE_RESTORE_BACKUP_DIR|PRE_RESTORE_USER_BACKUP_DIR|ENABLE_PRE_RESTORE_BACKUP|ENABLE_USER_PRE_RESTORE_BACKUP|SECURE_BACKUP|SECURE_RESTORE|PREVENT_LOG_DELETION|OWN_BACKUP_USER_RESTORE|CHECK_DATABASE_COLLISIONS|ENABLE_OPERATION_LIMITS|RESTORE_LIMIT_5MIN|RESTORE_LIMIT_HOUR|RESTORE_LIMIT_DAY|BACKUP_LIMIT_5MIN|BACKUP_LIMIT_HOUR|BACKUP_LIMIT_DAY|ENABLE_LOGS_OVERLAY|Always_enable)=(.*)$/', $line, $m)) {
$key = (string)$m[1];
$value = parse_sh_value($m[2]);
$settings[$key] = $value;
}
}
return $settings;
}
function apply_da_user(string $value, string $da_user): string
{
$value = str_replace('DA_USER', $da_user, $value);
$value = str_replace('da_user', $da_user, $value);
return $value;
}
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' => '',
'DB_NAME' => '',
'SOURCE_DB_NAME' => '',
'JOB_TYPE' => '',
'BACKUP_SOURCE' => '',
'RESTORE_MODE' => '',
'SANITIZATION_REQUIRED' => '',
'DATE_DIR' => '',
'DUMP_FILE' => '',
'PRE_BACKUP_PATH' => '',
'START_TS' => '',
'PID' => '',
];
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|DB_NAME|SOURCE_DB_NAME|JOB_TYPE|BACKUP_SOURCE|RESTORE_MODE|SANITIZATION_REQUIRED|DATE_DIR|DUMP_FILE|PRE_BACKUP_PATH|START_TS|PID)=(.*)$/', $line, $m)) {
$vars[$m[1]] = parse_sh_value($m[2]);
}
}
return $vars;
}
function normalize_backup_source(array $vars): string
{
$source = strtolower(trim((string)($vars['BACKUP_SOURCE'] ?? '')));
return $source === 'user' ? 'user' : 'hosting';
}
function normalize_job_type(array $vars): string
{
$job_type = strtolower(trim((string)($vars['JOB_TYPE'] ?? '')));
return $job_type === 'backup' ? 'backup' : 'restore';
}
function normalize_restore_mode(array $vars): string
{
$mode = strtolower(trim((string)($vars['RESTORE_MODE'] ?? '')));
if ($mode === 'new_db' || $mode === 'overwrite') {
return $mode;
}
$db = trim((string)($vars['DB_NAME'] ?? ''));
$source_db = trim((string)($vars['SOURCE_DB_NAME'] ?? ''));
if ($db !== '' && $source_db !== '' && $db !== $source_db) {
return 'new_db';
}
return 'overwrite';
}
function sanitize_required_label(array $vars): string
{
$raw = strtolower(trim((string)($vars['SANITIZATION_REQUIRED'] ?? '')));
if (in_array($raw, ['tak', 'true', '1', 'yes', 'y', 'on'], true)) {
return 'Tak';
}
if (in_array($raw, ['nie', 'false', '0', 'no', 'n', 'off'], true)) {
return 'Nie';
}
$source = trim((string)($vars['SOURCE_DB_NAME'] ?? ''));
$target = trim((string)($vars['DB_NAME'] ?? ''));
if ($source !== '' && $target !== '' && $source !== $target) {
return 'Tak';
}
return 'Nie';
}
function backup_source_label(string $source): string
{
if ($source === 'user') {
return t('Kopia bazy uzytkownika');
}
return t('Kopia bazy udostepnianej przez HITME.PL');
}
function restore_mode_label(string $mode, string $job_type): string
{
if ($job_type === 'backup') {
return t('Tworzenie Kopii bazy danych');
}
if ($mode === 'new_db') {
return t('Przywracanie bazy danych - Do nowej bazy danych');
}
return t('Przywracanie bazy danych - Nadpisanie oryginalnej bazy danych');
}
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';
}
foreach ($candidates as $path) {
if (is_file($path)) {
return $path;
}
}
return '';
}
function job_belongs_to_user(string $job_id, string $job_base, string $da_user): bool
{
if ($job_id === '') {
return false;
}
$file = find_job_file($job_base, $job_id . '.env', $job_id);
if ($file === '') {
return false;
}
$vars = load_job_vars($file);
return ($vars['DA_USER'] ?? '') === $da_user;
}
function h(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
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 normalize_log(string $text): string
{
$text = str_replace("\r", "", $text);
return rtrim($text);
}
function log_text_output(string $text): void
{
header('Content-Type: text/plain; charset=utf-8');
echo '__DB_RESTORE_LOG_TEXT__';
echo $text;
echo '__DB_RESTORE_LOG_TEXT__';
exit;
}
function read_log_tail(string $path, int $max_lines = 200): string
{
if (!is_readable($path)) {
return '';
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return '';
}
if (count($lines) > $max_lines) {
$lines = array_slice($lines, -$max_lines);
}
return implode("\n", $lines);
}
function last_non_empty_line(string $text): string
{
$lines = preg_split('/\r?\n/', $text);
if ($lines === false) {
return '';
}
for ($i = count($lines) - 1; $i >= 0; $i--) {
$line = trim((string)$lines[$i]);
if ($line !== '') {
return $line;
}
}
return '';
}
function extract_progress(string $line): ?int
{
if (preg_match('/\b(\d{1,3})%\b/', $line, $m)) {
$progress = (int)$m[1];
if ($progress < 0) {
$progress = 0;
}
if ($progress > 100) {
$progress = 100;
}
return $progress;
}
return null;
}
function display_status(string $base_status, string $last_line): string
{
if ($base_status !== 'processing') {
return $base_status;
}
$line = strtolower(trim($last_line));
if ($line === '') {
return $base_status;
}
if (strpos($line, 'preparing') !== false || strpos($line, 'przygotow') !== false) {
return 'processing';
}
return $base_status;
}
function status_label(string $status): string
{
$map = [
'pending' => 'Oczekuje',
'processing' => 'W trakcie',
'done' => 'Zakonczone',
'failed' => 'Blad',
'canceled' => 'Anulowane',
'unknown' => 'Nieznany',
];
return t($map[$status] ?? 'Nieznany');
}
function get_job_status(string $job_id, string $job_base): string
{
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')) {
return 'done';
}
if (is_file($job_base . '/done/' . $job_id . '.fail')) {
return 'failed';
}
if (is_file($job_base . '/done/' . $job_id . '.cancel')) {
return 'canceled';
}
return 'unknown';
}
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_db = $vars['DB_NAME'] ?? '-';
$log_meta_date = format_backup_label((string)($vars['DATE_DIR'] ?? '-'));
$job_type = normalize_job_type($vars);
$backup_source = normalize_backup_source($vars);
$restore_mode = normalize_restore_mode($vars);
$log_meta_backup_kind = backup_source_label($backup_source);
$log_meta_restore_mode = restore_mode_label($restore_mode, $job_type);
$log_meta_sanitization = sanitize_required_label($vars);
$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);
$display = display_status(get_job_status($job_id, $job_base), $last_line);
$status = status_label($display);
if (in_array($display, ['done', 'failed', 'canceled'], true)) {
$progress_text = $display === 'done' ? '100%' : status_label($display);
} else {
$progress_text = $progress !== null ? ($progress . '%') : ($last_line !== '' ? $last_line : '-');
}
$header = "Data rozpoczecia: {$log_meta_start}\n";
$header .= "Baza danych: {$log_meta_db}\n";
$header .= "Backup (data katalogu): {$log_meta_date}\n";
$header .= "Rodzaj przywracanej bazy: {$log_meta_backup_kind}\n";
$header .= "Tryb przywracania: {$log_meta_restore_mode}\n";
$header .= "Koniecznosc Sanityzacji: {$log_meta_sanitization}\n";
$header .= "Status: {$status}\n";
$header .= "Postep: {$progress_text}\n";
$header .= "----- log -----\n";
$body = $log_text !== '' ? ($log_text . "\n") : "Log nie istnieje.\n";
return $header . $body;
}
function load_user_skin(string $user): string
{
$path = '/usr/local/directadmin/data/users/' . $user . '/user.conf';
if (!is_readable($path)) {
return 'enhanced';
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return 'enhanced';
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (preg_match('/^skin=(.+)$/', $line, $m)) {
$skin = strtolower(trim($m[1]));
if (strpos($skin, 'evolution') !== false) {
return 'evolution';
}
if (strpos($skin, 'enhanced') !== false) {
return 'enhanced';
}
return $skin;
}
}
return 'enhanced';
}
function list_backup_dates(string $base_dir): array
{
if (!is_dir($base_dir) || !is_readable($base_dir)) {
return [];
}
$base_real = realpath($base_dir);
if ($base_real === false) {
return [];
}
$items = [];
foreach (scandir($base_dir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
if (!preg_match('/^[A-Za-z0-9._-]+$/', $entry)) {
continue;
}
$path = $base_dir . '/' . $entry;
if (!is_dir($path) || is_link($path)) {
continue;
}
$path_real = realpath($path);
if ($path_real === false || strpos($path_real, $base_real . '/') !== 0) {
continue;
}
$items[] = [
'name' => $entry,
'mtime' => @filemtime($path) ?: 0,
];
}
usort($items, function ($a, $b) {
return $b['mtime'] <=> $a['mtime'];
});
return array_map(function ($item) {
return $item['name'];
}, $items);
}
function parse_backup_dir_timestamp(string $dir_name, int $fallback = 0): int
{
if (preg_match('/^(\d{2})[_-](\d{2})[_-](\d{2,4})(?:[-_](\d{2})[_-](\d{2})(?:[_-](\d{2}))?)?$/', $dir_name, $m)) {
$day = (int)$m[1];
$month = (int)$m[2];
$year = (int)$m[3];
if ($year < 100) {
$year += 2000;
}
$hour = isset($m[4]) ? (int)$m[4] : 0;
$minute = isset($m[5]) ? (int)$m[5] : 0;
$second = isset($m[6]) ? (int)$m[6] : 0;
if (checkdate($month, $day, $year)) {
return mktime($hour, $minute, $second, $month, $day, $year);
}
}
return $fallback > 0 ? $fallback : 0;
}
function format_backup_label(string $date_dir): string
{
$ts = parse_backup_dir_timestamp($date_dir, 0);
if ($ts > 0) {
return date('d-m-Y H:i:s', $ts);
}
return $date_dir;
}
function build_restore_confirmation_message(
string $source_type,
string $source_db,
string $target_db,
string $restore_mode,
string $backup_label = ''
): string {
$source_db = trim($source_db);
$target_db = trim($target_db);
if ($source_db === '') {
$source_db = '(nieznana baza)';
}
if ($target_db === '') {
$target_db = $source_db;
}
if ($source_type === 'user') {
$message = 'Czy na pewno chcesz przywrócić backup bazy danych z kopii zapasowej użytkownika';
if ($backup_label !== '') {
$message .= ' z dn. ' . $backup_label;
}
} else {
$message = 'Czy na pewno chcesz przywrócić backup bazy danych z kopii zapasowych HITME.PL';
}
if ($restore_mode === 'new_db') {
$message .= ' Nazwa bazy: ' . $source_db . ' i przywrócić kopię do nowej bazy danych - ' . $target_db . '.';
$message .= "\n" . 'Wskazana nowa baza danych ' . $target_db . ' musi istnieć. Utwórz ją ręcznie w DirectAdmin przed kontynuowaniem.';
$message .= "\n" . 'Docelowa baza danych ' . $target_db . ' nie może zawierać żadnych danych przed kontynuowaniem operacji.';
return $message;
}
$message .= ' Nazwa bazy: ' . $source_db . ' i nadpisać istniejącą bazę danych - ' . $target_db . '.';
return $message;
}
function format_month_label(string $month_value, string $lang_code): string
{
if (!preg_match('/^(\d{4})-(\d{2})$/', $month_value, $m)) {
return $month_value;
}
$year = $m[1];
$month = (int)$m[2];
$months_pl = [
1 => 'styczen', 2 => 'luty', 3 => 'marzec', 4 => 'kwiecien',
5 => 'maj', 6 => 'czerwiec', 7 => 'lipiec', 8 => 'sierpien',
9 => 'wrzesien', 10 => 'pazdziernik', 11 => 'listopad', 12 => 'grudzien',
];
$months_en = [
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April',
5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August',
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December',
];
$list = $lang_code === 'pl' ? $months_pl : $months_en;
$name = $list[$month] ?? $month_value;
return $name . ' ' . $year;
}
function month_nav_targets(array $available_months, string $selected_month): array
{
$prev = '';
$next = '';
$idx = array_search($selected_month, $available_months, true);
if ($idx !== false) {
if ($idx < count($available_months) - 1) {
$prev = $available_months[$idx + 1];
}
if ($idx > 0) {
$next = $available_months[$idx - 1];
}
}
return ['prev' => $prev, 'next' => $next];
}
function calendar_weekdays(string $lang_code): array
{
if ($lang_code === 'pl') {
return ['Pn', 'Wt', 'Sr', 'Cz', 'Pt', 'So', 'Nd'];
}
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
}
function render_hidden_inputs_from_params(array $params, array $exclude = []): string
{
$exclude_map = array_fill_keys($exclude, true);
$out = '';
foreach ($params as $key => $value) {
if (!is_string($key) || isset($exclude_map[$key])) {
continue;
}
$safe_key = htmlspecialchars($key, ENT_QUOTES, 'UTF-8');
if (is_array($value)) {
foreach ($value as $item) {
if (is_array($item) || is_object($item)) {
continue;
}
$safe_value = htmlspecialchars((string)$item, ENT_QUOTES, 'UTF-8');
$out .= '<input type="hidden" name="' . $safe_key . '[]" value="' . $safe_value . '" />' . "\n";
}
continue;
}
if (is_object($value)) {
continue;
}
$safe_value = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
$out .= '<input type="hidden" name="' . $safe_key . '" value="' . $safe_value . '" />' . "\n";
}
return $out;
}
function render_warning_overlay(string $title, string $message, array $params, string $base_url): string
{
$hidden = render_hidden_inputs_from_params($params, ['confirm_action']);
$lines = preg_split('/\r?\n/', $message) ?: [$message];
$printed = 0;
ob_start();
?>
<div class="br-overlay">
<div class="br-modal br-modal-danger">
<h3><?php echo h($title); ?></h3>
<?php foreach ($lines as $line) { ?>
<?php $line = trim((string)$line); if ($line === '') { continue; } ?>
<p><?php if ($printed === 0) { ?><strong>Uwaga:</strong> <?php } ?><?php echo h($line); ?></p>
<?php $printed++; ?>
<?php } ?>
<form method="post" action="<?php echo h($base_url); ?>">
<?php echo $hidden; ?>
<input type="hidden" name="confirm_action" value="1" />
<div class="br-modal-actions">
<a class="br-btn br-btn-ghost" href="<?php echo h($base_url); ?>">Nie</a>
<button class="br-btn br-btn-danger" type="submit">Tak, kontynuuj</button>
</div>
</form>
</div>
</div>
<?php
return trim((string)ob_get_clean());
}
function render_plugin_unavailable_page(string $inline_css, string $message): string
{
ob_start();
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>DB-Manager</title>
<?php if ($inline_css !== '') { ?>
<style><?php echo $inline_css; ?></style>
<?php } ?>
<style>
.br-disabled-shell {
max-width: 980px;
margin: 24px auto;
padding: 0 16px;
}
</style>
</head>
<body>
<div class="br-shell br-shell-full br-disabled-shell">
<h1>Manager kopii zapasowych baz danych</h1>
<div class="br-card">
<div class="br-alert br-alert-error"><?php echo h($message); ?></div>
</div>
</div>
</body>
</html>
<?php
return (string)ob_get_clean();
}
function db_name_from_dump_filename(string $filename): string
{
if (substr($filename, -7) === '.sql.gz') {
return substr($filename, 0, -7);
}
if (substr($filename, -3) === '.gz') {
return substr($filename, 0, -3);
}
if (substr($filename, -4) === '.sql') {
return substr($filename, 0, -4);
}
return '';
}
function collect_backup_slots(string $base_dir, string $da_user): array
{
$slots = [];
$days = [];
foreach (list_backup_dates($base_dir) as $dir_name) {
$dbs = list_user_databases($base_dir, $dir_name, $da_user);
if (empty($dbs)) {
continue;
}
$path = $base_dir . '/' . $dir_name;
$mtime = @filemtime($path) ?: 0;
$ts = parse_backup_dir_timestamp($dir_name, $mtime);
$day_key = $ts > 0 ? date('Y-m-d', $ts) : $dir_name;
$slot = [
'dir' => $dir_name,
'ts' => $ts,
'day_key' => $day_key,
'day_label' => $ts > 0 ? date('d-m-Y', $ts) : $day_key,
'time_label' => $ts > 0 ? date('H:i:s', $ts) : '-',
];
$slots[] = $slot;
if (!isset($days[$day_key])) {
$days[$day_key] = [
'day_key' => $day_key,
'day_label' => $slot['day_label'],
'ts' => $ts,
'slots' => [],
];
}
$days[$day_key]['slots'][] = $slot;
if ($ts > $days[$day_key]['ts']) {
$days[$day_key]['ts'] = $ts;
}
}
usort($slots, function ($a, $b) {
return $b['ts'] <=> $a['ts'];
});
foreach ($days as &$day_info) {
usort($day_info['slots'], function ($a, $b) {
return $b['ts'] <=> $a['ts'];
});
}
unset($day_info);
uasort($days, function ($a, $b) {
return $b['ts'] <=> $a['ts'];
});
return [
'slots' => $slots,
'days' => $days,
];
}
function is_trusted_user_backup_dir(string $path_real, string $da_user): bool
{
$marker = rtrim($path_real, '/') . '/.db-manager-owned';
if (is_readable($marker) && is_file($marker) && !is_link($marker)) {
$owner = '';
$user = '';
foreach (file($marker, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$line = trim((string)$line);
if ($line === '' || strpos($line, '=') === false) {
continue;
}
[$k, $v] = explode('=', $line, 2);
$k = trim((string)$k);
$v = trim((string)$v);
if ($k === 'OWNER') {
$owner = $v;
} elseif ($k === 'DA_USER') {
$user = $v;
}
}
if ($owner === 'db-manager' && $user === $da_user) {
return true;
}
}
$uid = @fileowner($path_real);
if ($uid === 0) {
return true;
}
return false;
}
function list_user_databases(string $base_dir, string $date_dir, string $da_user, bool $trusted_only = false): array
{
if ($date_dir === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $date_dir)) {
return [];
}
$base_real = realpath($base_dir);
$path = $base_dir . '/' . $date_dir;
$path_real = realpath($path);
if ($base_real === false || $path_real === false || strpos($path_real, $base_real . '/') !== 0) {
return [];
}
if (!is_dir($path_real) || !is_readable($path_real) || is_link($path_real)) {
return [];
}
if ($trusted_only && !is_trusted_user_backup_dir($path_real, $da_user)) {
return [];
}
$items = [];
foreach (scandir($path_real) ?: [] as $base) {
if ($base === '.' || $base === '..') {
continue;
}
$file = $path_real . '/' . $base;
if (!is_file($file) || is_link($file)) {
continue;
}
$file_real = realpath($file);
if ($file_real === false || strpos($file_real, $path_real . '/') !== 0) {
continue;
}
$db_name = db_name_from_dump_filename($base);
if ($db_name === '') {
continue;
}
if ($db_name !== $da_user && strpos($db_name, $da_user . '_') !== 0) {
continue;
}
$items[] = [
'name' => $db_name,
'file' => $file_real,
'size' => @filesize($file_real) ?: 0,
'mtime' => @filemtime($file_real) ?: 0,
];
}
usort($items, function ($a, $b) {
return strcmp($a['name'], $b['name']);
});
return $items;
}
function collect_backup_slots_filtered(string $base_dir, string $da_user, bool $trusted_only = false): array
{
$slots = [];
$days = [];
foreach (list_backup_dates($base_dir) as $dir_name) {
$dbs = list_user_databases($base_dir, $dir_name, $da_user, $trusted_only);
if (empty($dbs)) {
continue;
}
$path = $base_dir . '/' . $dir_name;
$mtime = @filemtime($path) ?: 0;
$ts = parse_backup_dir_timestamp($dir_name, $mtime);
$day_key = $ts > 0 ? date('Y-m-d', $ts) : $dir_name;
$slot = [
'dir' => $dir_name,
'ts' => $ts,
'day_key' => $day_key,
'day_label' => $ts > 0 ? date('d-m-Y', $ts) : $day_key,
'time_label' => $ts > 0 ? date('H:i:s', $ts) : '-',
];
$slots[] = $slot;
if (!isset($days[$day_key])) {
$days[$day_key] = [
'day_key' => $day_key,
'day_label' => $slot['day_label'],
'ts' => $ts,
'slots' => [],
];
}
$days[$day_key]['slots'][] = $slot;
if ($ts > $days[$day_key]['ts']) {
$days[$day_key]['ts'] = $ts;
}
}
usort($slots, function ($a, $b) {
return $b['ts'] <=> $a['ts'];
});
foreach ($days as &$day_info) {
usort($day_info['slots'], function ($a, $b) {
return $b['ts'] <=> $a['ts'];
});
}
unset($day_info);
uasort($days, function ($a, $b) {
return $b['ts'] <=> $a['ts'];
});
return [
'slots' => $slots,
'days' => $days,
];
}
function find_user_backup_entry(array $items, string $backup_file): array
{
if ($backup_file === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $backup_file)) {
return [];
}
foreach ($items as $item) {
$item_file = basename((string)($item['file'] ?? ''));
if ($item_file === $backup_file) {
return $item;
}
}
return [];
}
function csrf_token_path(string $data_dir, string $user): string
{
$safe = preg_replace('/[^A-Za-z0-9_.-]/', '_', $user);
return $data_dir . '/csrf/' . $safe . '.token';
}
function get_csrf_token(string $data_dir, string $user): string
{
$path = csrf_token_path($data_dir, $user);
$dir = dirname($path);
if (!is_dir($dir)) {
@mkdir($dir, 0700, true);
}
if (is_readable($path)) {
$mtime = @filemtime($path) ?: 0;
if ($mtime > 0 && $mtime > (time() - 12 * 3600)) {
$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 check_csrf(string $token, string $data_dir, string $user): bool
{
$path = csrf_token_path($data_dir, $user);
if (!is_readable($path)) {
return false;
}
$stored = trim((string)file_get_contents($path));
if ($stored === '' || $token === '') {
return false;
}
return hash_equals($stored, $token);
}
function is_path_within_dir(string $path, string $dir): bool
{
$real_path = realpath($path);
$real_dir = realpath($dir);
if ($real_path === false || $real_dir === false) {
return false;
}
if ($real_path === $real_dir) {
return true;
}
return strpos($real_path, $real_dir . '/') === 0;
}
function is_path_within_any_dir(string $path, array $dirs): bool
{
foreach ($dirs as $dir) {
if ($dir !== '' && is_path_within_dir($path, $dir)) {
return true;
}
}
return false;
}
function read_mysql_conf_values(string $path): array
{
$values = [
'user' => '',
'passwd' => '',
'host' => '',
'port' => '',
'socket' => '',
];
if (!is_readable($path)) {
return $values;
}
$lines = file($path, FILE_IGNORE_NEW_LINES);
if ($lines === false) {
return $values;
}
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (strpos($line, '=') === false) {
continue;
}
[$key, $val] = explode('=', $line, 2);
$key = trim($key);
$val = trim($val);
if (array_key_exists($key, $values)) {
$values[$key] = $val;
}
}
return $values;
}
function resolve_bin_path(array $names): string
{
$dirs = [
'/usr/local/mysql/bin',
'/usr/local/mariadb/bin',
'/usr/local/bin',
'/usr/bin',
'/bin',
];
$env_path = getenv('PATH');
if (is_string($env_path) && $env_path !== '') {
foreach (explode(':', $env_path) as $part) {
if ($part !== '') {
$dirs[] = $part;
}
}
}
$dirs = array_values(array_unique($dirs));
foreach ($names as $name) {
foreach ($dirs as $dir) {
$candidate = rtrim($dir, '/') . '/' . $name;
if (is_file($candidate) && is_executable($candidate)) {
return $candidate;
}
}
}
return '';
}
function list_owned_databases_via_mysql(string $da_user): array
{
$mysql_conf = read_mysql_conf_values('/usr/local/directadmin/conf/mysql.conf');
$mysql_bin = resolve_bin_path(['mysql', 'mariadb']);
if ($mysql_bin === '') {
return [];
}
$args = [$mysql_bin, '-N', '-B', '-e', 'SHOW DATABASES'];
if ($mysql_conf['user'] !== '') {
$args[] = '-u';
$args[] = $mysql_conf['user'];
}
if ($mysql_conf['passwd'] !== '') {
$args[] = '--password=' . $mysql_conf['passwd'];
}
if ($mysql_conf['host'] !== '') {
$args[] = '-h';
$args[] = $mysql_conf['host'];
}
if ($mysql_conf['port'] !== '' && $mysql_conf['port'] !== '0') {
$args[] = '-P';
$args[] = $mysql_conf['port'];
}
if ($mysql_conf['socket'] !== '') {
$args[] = '--socket=' . $mysql_conf['socket'];
}
$cmd = implode(' ', array_map('escapeshellarg', $args));
$stdout = [];
$exit_code = 1;
@exec($cmd, $stdout, $exit_code);
if ($exit_code !== 0) {
return [];
}
$dbs = [];
foreach ($stdout as $line) {
$db = trim((string)$line);
if ($db === '') {
continue;
}
if ($db === $da_user || strpos($db, $da_user . '_') === 0) {
$dbs[] = $db;
}
}
sort($dbs, SORT_STRING);
return array_values(array_unique($dbs));
}
function is_valid_target_db_name(string $db_name, string $da_user): bool
{
if (!preg_match('/^[A-Za-z0-9_]+$/', $db_name)) {
return false;
}
return strpos($db_name, $da_user . '_') === 0;
}
function database_has_tables(string $db_name): ?bool
{
$mysql_conf = read_mysql_conf_values('/usr/local/directadmin/conf/mysql.conf');
$mysql_bin = resolve_bin_path(['mysql', 'mariadb']);
if ($mysql_bin === '') {
@error_log('db-manager: database_has_tables: mysql client not found');
return null;
}
$db_sql = str_replace(['\\', "'"], ['\\\\', "\\'"], $db_name);
$query = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='{$db_sql}'";
$args = [$mysql_bin, '-N', '-B', '-e', $query];
if ($mysql_conf['user'] !== '') {
$args[] = '-u';
$args[] = $mysql_conf['user'];
}
if ($mysql_conf['passwd'] !== '') {
$args[] = '--password=' . $mysql_conf['passwd'];
}
if ($mysql_conf['host'] !== '') {
$args[] = '-h';
$args[] = $mysql_conf['host'];
}
if ($mysql_conf['port'] !== '' && $mysql_conf['port'] !== '0') {
$args[] = '-P';
$args[] = $mysql_conf['port'];
}
if ($mysql_conf['socket'] !== '') {
$args[] = '--socket=' . $mysql_conf['socket'];
}
$cmd = implode(' ', array_map('escapeshellarg', $args));
$stdout = [];
$exit_code = 1;
@exec($cmd, $stdout, $exit_code);
if ($exit_code !== 0) {
@error_log('db-manager: database_has_tables: mysql query failed for db=' . $db_name . ' exit=' . $exit_code);
return null;
}
$count = isset($stdout[0]) ? (int)trim((string)$stdout[0]) : 0;
if ($count < 0) {
$count = 0;
}
return $count > 0;
}
function render_restore_mode_hidden_fields(string $restore_mode, string $target_db, string $main_tab = ''): string
{
$html = '<input type="hidden" name="restore_mode" value="' . h($restore_mode) . '" class="js-restore-mode-field" />'
. '<input type="hidden" name="target_db" value="' . h($target_db) . '" class="js-target-db-field" />';
if ($main_tab === 'hitme' || $main_tab === 'user') {
$html .= '<input type="hidden" name="main_tab" value="' . h($main_tab) . '" class="js-main-tab-field" />';
}
return $html;
}
function trigger_worker(string $plugin_dir): void
{
$worker = $plugin_dir . '/scripts/worker.sh';
if (!is_file($worker) || !is_executable($worker)) {
return;
}
$cmd = escapeshellarg($worker) . ' >/dev/null 2>&1 &';
if (function_exists('exec')) {
@exec($cmd);
return;
}
if (function_exists('shell_exec')) {
@shell_exec($cmd);
}
}
function parse_non_negative_int($value, int $default): int
{
$value = trim((string)$value);
if ($value === '' || !preg_match('/^\d+$/', $value)) {
return $default;
}
$num = (int)$value;
return $num >= 0 ? $num : $default;
}
function format_wait_time_pl(int $seconds): string
{
if ($seconds < 1) {
return '1 s';
}
$seconds = (int)ceil($seconds);
$days = intdiv($seconds, 86400);
$seconds %= 86400;
$hours = intdiv($seconds, 3600);
$seconds %= 3600;
$minutes = intdiv($seconds, 60);
$seconds %= 60;
$parts = [];
if ($days > 0) {
$parts[] = $days . ' d';
}
if ($hours > 0) {
$parts[] = $hours . ' h';
}
if ($minutes > 0) {
$parts[] = $minutes . ' min';
}
if ($seconds > 0 || empty($parts)) {
$parts[] = $seconds . ' s';
}
return implode(' ', $parts);
}
function get_operation_limit_windows(array $settings, string $op_type): array
{
if ($op_type === 'backup') {
return [
['seconds' => 300, 'limit' => parse_non_negative_int($settings['BACKUP_LIMIT_5MIN'] ?? '10', 10), 'label' => '5 min'],
['seconds' => 3600, 'limit' => parse_non_negative_int($settings['BACKUP_LIMIT_HOUR'] ?? '30', 30), 'label' => '1 godzina'],
['seconds' => 86400, 'limit' => parse_non_negative_int($settings['BACKUP_LIMIT_DAY'] ?? '100', 100), 'label' => '24 godziny'],
];
}
return [
['seconds' => 300, 'limit' => parse_non_negative_int($settings['RESTORE_LIMIT_5MIN'] ?? '10', 10), 'label' => '5 min'],
['seconds' => 3600, 'limit' => parse_non_negative_int($settings['RESTORE_LIMIT_HOUR'] ?? '30', 30), 'label' => '1 godzina'],
['seconds' => 86400, 'limit' => parse_non_negative_int($settings['RESTORE_LIMIT_DAY'] ?? '100', 100), 'label' => '24 godziny'],
];
}
function reserve_operation_limit(
string $plugin_data_dir,
string $da_user,
string $op_type,
int $count,
array $settings,
string &$error_message
): bool {
if ($count < 1) {
return true;
}
$limits_enabled = parse_bool((string)($settings['ENABLE_OPERATION_LIMITS'] ?? 'true'), true);
if (!$limits_enabled) {
return true;
}
$windows = get_operation_limit_windows($settings, $op_type);
$has_any_limit = false;
foreach ($windows as $window) {
if (($window['limit'] ?? 0) > 0) {
$has_any_limit = true;
break;
}
}
if (!$has_any_limit) {
return true;
}
$rate_dir = $plugin_data_dir . '/rate_limits';
if (!is_dir($rate_dir) && !@mkdir($rate_dir, 0700, true) && !is_dir($rate_dir)) {
return true;
}
$rate_file = $rate_dir . '/' . preg_replace('/[^A-Za-z0-9_.-]/', '_', $da_user) . '.' . $op_type . '.log';
$fh = @fopen($rate_file, 'c+');
if ($fh === false) {
return true;
}
if (!@flock($fh, LOCK_EX)) {
@fclose($fh);
return true;
}
$now = time();
$max_window = 86400;
$timestamps = [];
rewind($fh);
while (($line = fgets($fh)) !== false) {
$line = trim($line);
if ($line === '' || !preg_match('/^\d+$/', $line)) {
continue;
}
$ts = (int)$line;
if ($ts >= ($now - $max_window)) {
$timestamps[] = $ts;
}
}
sort($timestamps, SORT_NUMERIC);
$max_wait = 0;
$limit_label = '';
foreach ($windows as $window) {
$limit = (int)$window['limit'];
$seconds = (int)$window['seconds'];
if ($limit <= 0) {
continue;
}
$recent = [];
foreach ($timestamps as $ts) {
if ($ts > ($now - $seconds)) {
$recent[] = $ts;
}
}
$current_count = count($recent);
if (($current_count + $count) > $limit) {
$needed = ($current_count + $count) - $limit;
$idx = $needed - 1;
$candidate_wait = $seconds;
if (isset($recent[$idx])) {
$candidate_wait = max(1, ($recent[$idx] + $seconds) - $now);
}
if ($candidate_wait > $max_wait) {
$max_wait = $candidate_wait;
$limit_label = (string)$window['label'];
}
}
}
if ($max_wait > 0) {
$op_label = $op_type === 'backup' ? 'tworzenia backupów' : 'przywracania';
$error_message = 'Przekroczono limit operacji ' . $op_label . ' (' . $limit_label . '). Możesz ponowić próbę za ' . format_wait_time_pl($max_wait) . '.';
@flock($fh, LOCK_UN);
@fclose($fh);
return false;
}
for ($i = 0; $i < $count; $i++) {
$timestamps[] = $now;
}
sort($timestamps, SORT_NUMERIC);
ftruncate($fh, 0);
rewind($fh);
foreach ($timestamps as $ts) {
fwrite($fh, (string)$ts . "\n");
}
@fflush($fh);
@flock($fh, LOCK_UN);
@fclose($fh);
return true;
}
function list_active_jobs_for_user(string $job_base, string $da_user): array
{
$items = [];
$maps = [
'pending' => 'pending',
'processing' => 'processing',
];
foreach ($maps as $subdir => $status) {
foreach (glob($job_base . '/' . $subdir . '/*.env') ?: [] as $path) {
if (!is_file($path)) {
continue;
}
$vars = load_job_vars($path);
if (($vars['DA_USER'] ?? '') !== $da_user) {
continue;
}
$items[] = [
'status' => $status,
'job_type' => normalize_job_type($vars),
'db_name' => trim((string)($vars['DB_NAME'] ?? '')),
'start_ts' => (int)trim((string)($vars['START_TS'] ?? '0')),
];
}
}
usort($items, function ($a, $b) {
return ($a['start_ts'] ?? 0) <=> ($b['start_ts'] ?? 0);
});
return $items;
}
function db_lock_dir(string $plugin_dir): string
{
return rtrim($plugin_dir, '/') . '/data/lock';
}
function db_lock_path(string $plugin_dir, string $db_name): string
{
$safe = preg_replace('/[^A-Za-z0-9_.-]/', '_', trim($db_name));
if ($safe === null || $safe === '') {
$safe = 'unknown_db';
}
return db_lock_dir($plugin_dir) . '/' . $safe . '.lock';
}
function acquire_db_lock(string $plugin_dir, string $db_name, string $job_id, string $job_type, string &$error_message): string
{
$error_message = '';
$db_name = trim($db_name);
if ($db_name === '') {
$error_message = t('Nie wybrano poprawnej bazy danych.');
return '';
}
$lock_dir = db_lock_dir($plugin_dir);
if (!is_dir($lock_dir) && !@mkdir($lock_dir, 0700, true) && !is_dir($lock_dir)) {
$error_message = t('Nie mozna zablokowac kolejki zadan. Sprobuj ponownie.');
return '';
}
$lock_path = db_lock_path($plugin_dir, $db_name);
$fh = @fopen($lock_path, 'x');
if ($fh === false) {
$existing_job_type = 'restore';
if (is_readable($lock_path)) {
foreach (file($lock_path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$line = trim((string)$line);
if (strpos($line, 'JOB_TYPE=') === 0) {
$existing_job_type = strtolower(trim(substr($line, 9)));
break;
}
}
}
$existing_job_type = $existing_job_type === 'backup' ? 'backup' : 'restore';
$error_message = sprintf(
t('Dla bazy %s istnieje juz aktywne zadanie (%s, %s). Zaczekaj na jego zakonczenie i ponow probe.'),
$db_name,
active_job_type_label($existing_job_type),
active_job_status_label('pending')
);
return '';
}
$job_type = strtolower(trim($job_type)) === 'backup' ? 'backup' : 'restore';
$content = "DB_NAME={$db_name}\nJOB_ID={$job_id}\nJOB_TYPE={$job_type}\nCREATED_TS=" . (string)time() . "\n";
@fwrite($fh, $content);
@fflush($fh);
@fclose($fh);
@chmod($lock_path, 0600);
return $lock_path;
}
function release_db_lock_file(string $path): void
{
if ($path !== '' && is_file($path)) {
@unlink($path);
}
}
function release_db_lock_for_job_path(string $job_path, string $plugin_dir): void
{
if (!is_file($job_path)) {
return;
}
$vars = load_job_vars($job_path);
$db_name = trim((string)($vars['DB_NAME'] ?? ''));
if ($db_name === '') {
return;
}
release_db_lock_file(db_lock_path($plugin_dir, $db_name));
}
function check_db_lock_present(string $plugin_dir, string $db_name, string &$error_message): bool
{
$error_message = '';
$db_name = trim($db_name);
if ($db_name === '') {
return true;
}
$lock_path = db_lock_path($plugin_dir, $db_name);
clearstatcache(true, $lock_path);
if (!is_file($lock_path)) {
return true;
}
$existing_job_type = 'restore';
if (is_readable($lock_path)) {
foreach (file($lock_path, FILE_IGNORE_NEW_LINES) ?: [] as $line) {
$line = trim((string)$line);
if (strpos($line, 'JOB_TYPE=') === 0) {
$existing_job_type = strtolower(trim(substr($line, 9)));
break;
}
}
}
$existing_job_type = $existing_job_type === 'backup' ? 'backup' : 'restore';
$error_message = sprintf(
t('Dla bazy %s istnieje juz aktywne zadanie (%s, %s). Zaczekaj na jego zakonczenie i ponow probe.'),
$db_name,
active_job_type_label($existing_job_type),
active_job_status_label('pending')
);
return false;
}
function acquire_jobs_queue_lock(string $job_base, string &$error_message)
{
$error_message = '';
if (!is_dir($job_base) && !@mkdir($job_base, 0700, true) && !is_dir($job_base)) {
$error_message = t('Nie mozna zablokowac kolejki zadan. Sprobuj ponownie.');
return null;
}
$lock_path = rtrim($job_base, '/') . '/.queue.lock';
$fh = @fopen($lock_path, 'c+');
if ($fh === false) {
$error_message = t('Nie mozna zablokowac kolejki zadan. Sprobuj ponownie.');
return null;
}
@chmod($lock_path, 0600);
if (!@flock($fh, LOCK_EX)) {
@fclose($fh);
$error_message = t('Nie mozna zablokowac kolejki zadan. Sprobuj ponownie.');
return null;
}
return $fh;
}
function release_jobs_queue_lock($fh): void
{
if (is_resource($fh)) {
@flock($fh, LOCK_UN);
@fclose($fh);
}
}
function active_job_type_label(string $job_type): string
{
return $job_type === 'backup' ? t('Tworzenie kopii zapasowej') : t('Przywracanie kopii zapasowej');
}
function active_job_status_label(string $status): string
{
return $status === 'processing' ? t('jest w trakcie') : t('oczekuje w kolejce');
}
function check_database_collisions(
array $settings,
string $plugin_dir,
string $job_base,
string $da_user,
string &$error_message,
string $db_name = ''
): bool
{
$collisions_enabled = parse_bool((string)($settings['CHECK_DATABASE_COLLISIONS'] ?? 'true'), true);
if (!$collisions_enabled) {
return true;
}
$db_name = trim($db_name);
if ($db_name === '') {
return true;
}
if (!check_db_lock_present($plugin_dir, $db_name, $error_message)) {
return false;
}
$active_jobs = list_active_jobs_for_user($job_base, $da_user);
foreach ($active_jobs as $job) {
$active_db_name = trim((string)($job['db_name'] ?? ''));
if ($active_db_name === '' || $active_db_name !== $db_name) {
continue;
}
$active_job_type = strtolower(trim((string)($job['job_type'] ?? '')));
$active_job_type = $active_job_type === 'backup' ? 'backup' : 'restore';
$job_type = active_job_type_label($active_job_type);
$status = active_job_status_label((string)($job['status'] ?? 'pending'));
$error_message = sprintf(
t('Dla bazy %s istnieje juz aktywne zadanie (%s, %s). Zaczekaj na jego zakonczenie i ponow probe.'),
$db_name,
$job_type,
$status
);
return false;
}
return true;
}
function list_jobs(string $dir, string $status, string $pattern, string $log_dir, string $pid_dir, string $cancel_dir, string $da_user, array $pre_restore_dirs): array
{
$items = [];
foreach (glob($dir . '/' . $pattern) ?: [] as $path) {
$vars = load_job_vars($path);
if (($vars['DA_USER'] ?? '') !== $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)) {
$last = last_non_empty_line(read_log_tail($log_path, 2000));
$display_status = display_status($status, $last);
$progress = extract_progress($last);
if ($cancel_requested) {
$display_status = 'canceled';
$progress_text = 'Anulowane';
} elseif ($progress !== null) {
$progress_text = $progress . '%';
} elseif ($last !== '') {
$progress_text = $last;
} else {
$progress_text = 'W trakcie';
}
} elseif ($status === 'done') {
$progress_text = '100%';
} elseif ($status === 'failed') {
$progress_text = 'Blad';
} elseif ($status === 'canceled') {
$progress_text = 'Anulowane';
}
$pre_backup_path = trim((string)($vars['PRE_BACKUP_PATH'] ?? ''));
$pre_backup_exists = $pre_backup_path !== '' && is_file($pre_backup_path) && is_path_within_any_dir($pre_backup_path, $pre_restore_dirs);
$job_type = normalize_job_type($vars);
$backup_source = normalize_backup_source($vars);
$restore_mode = normalize_restore_mode($vars);
$items[] = [
'status' => $status,
'display_status' => $display_status,
'file' => basename($path),
'time' => @filemtime($path) ?: 0,
'job_id' => $job_id,
'log_exists' => is_readable($log_path),
'progress_text' => $progress_text,
'cancel_requested' => $cancel_requested,
'db_name' => $vars['DB_NAME'] ?? '',
'date_dir' => $vars['DATE_DIR'] ?? '',
'start_ts' => $vars['START_TS'] ?? '',
'pid' => $pid,
'pre_backup_path' => $pre_backup_path,
'pre_backup_exists' => $pre_backup_exists,
'backup_source' => $backup_source,
'job_type' => $job_type,
'restore_mode' => $restore_mode,
'backup_kind_label' => backup_source_label($backup_source),
'restore_mode_label' => restore_mode_label($restore_mode, $job_type),
];
}
return $items;
}
function render_jobs_section(array $jobs, string $csrf_token, string $base_url, bool $enable_logs_overlay, bool $prevent_log_deletion): string
{
if (empty($jobs)) {
return '<p class="br-muted">Brak zadan do wyswietlenia.</p>';
}
ob_start();
?>
<table class="br-table" border="1" cellpadding="6" cellspacing="0">
<thead>
<tr>
<th>Data Rozpoczecia</th>
<th>Nazwa Bazy</th>
<th>Data Backupu</th>
<th>Rodzaj przywracanej bazy</th>
<th>Tryb Pracy</th>
<th>Status</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
<?php foreach ($jobs as $job) { ?>
<tr data-job-id="<?php echo h($job['job_id']); ?>" data-status="<?php echo h($job['display_status'] ?? $job['status']); ?>">
<td><?php echo h(format_datetime($job['start_ts'], (int)$job['time'])); ?></td>
<td><?php echo h($job['db_name']); ?></td>
<td><?php echo h(format_backup_label($job['date_dir'])); ?></td>
<td><?php echo h($job['backup_kind_label'] ?? '-'); ?></td>
<td><?php echo h($job['restore_mode_label'] ?? '-'); ?></td>
<td><?php echo h(status_label($job['display_status'] ?? $job['status'])); ?></td>
<td>
<?php
$actions = [];
if ($job['log_exists'] || $job['status'] === 'processing') {
if ($enable_logs_overlay) {
$safe_job = str_replace("'", "\\'", (string)$job['file']);
$actions[] = '<a href="' . h($base_url) . '?action=view_log&job=' . h((string)$job['file']) . '" onclick="if (typeof openLogOverlay === \'function\') { openLogOverlay(\'' . h($safe_job) . '\'); return false; }">Pokaż logi</a>';
} else {
$actions[] = '<a href="' . h($base_url) . '?action=view_log&job=' . h((string)$job['file']) . '">Pokaż logi</a>';
}
}
?>
<?php if (in_array($job['status'], ['processing', 'pending'], true)) { ?>
<?php
$actions[] = '<form method="post" action="' . h($base_url) . '" class="br-inline-form">'
. '<input type="hidden" name="action" value="cancel_job" />'
. '<input type="hidden" name="job" value="' . h((string)$job['file']) . '" />'
. '<input type="hidden" name="csrf_token" value="' . h($csrf_token) . '" />'
. '<button class="br-link-button" type="submit">Anuluj</button>'
. '</form>';
?>
<?php } ?>
<?php if (in_array($job['status'], ['done', 'failed', 'canceled'], true) && $job['log_exists']) { ?>
<?php
$actions[] = '<a href="#" onclick="downloadLog(\'' . h((string)$job['file']) . '\'); return false;">Zapisz log</a>';
?>
<?php } ?>
<?php if ($job['status'] === 'done' && $job['pre_backup_exists']) { ?>
<?php
$actions[] = '<form method="post" action="' . h($base_url) . '" class="br-inline-form">'
. '<input type="hidden" name="action" value="restore_pre_backup" />'
. '<input type="hidden" name="job" value="' . h((string)$job['file']) . '" />'
. '<input type="hidden" name="csrf_token" value="' . h($csrf_token) . '" />'
. '<button class="br-link-button br-link-success" type="submit">Przywróć poprzednią wersję bazy danych</button>'
. '</form>';
$actions[] = '<form method="post" action="' . h($base_url) . '" class="br-inline-form">'
. '<input type="hidden" name="action" value="delete_pre_backup" />'
. '<input type="hidden" name="job" value="' . h((string)$job['file']) . '" />'
. '<input type="hidden" name="csrf_token" value="' . h($csrf_token) . '" />'
. '<button class="br-link-button br-link-danger" type="submit">Usuń backup poprzedniej wersji bazy danych</button>'
. '</form>';
?>
<?php } ?>
<?php if (!$prevent_log_deletion && ($job['log_exists'] || $job['status'] === 'processing' || $job['status'] === 'pending' || $job['status'] === 'failed' || $job['status'] === 'canceled' || $job['status'] === 'done')) { ?>
<?php
$actions[] = '<form method="post" action="' . h($base_url) . '" class="br-inline-form">'
. '<input type="hidden" name="action" value="delete_job" />'
. '<input type="hidden" name="job" value="' . h((string)$job['file']) . '" />'
. '<input type="hidden" name="csrf_token" value="' . h($csrf_token) . '" />'
. '<button class="br-link-button br-link-danger" type="submit">Usun log</button>'
. '</form>';
?>
<?php } ?>
<?php
echo !empty($actions) ? implode(' | ', $actions) : '-';
?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?php
return ob_get_clean();
}
function render_log_view_html(
string $view_log_job,
string $log_meta_start,
string $log_meta_db,
string $log_meta_date,
string $log_meta_backup_kind,
string $log_meta_restore_mode,
string $log_meta_sanitization,
string $log_status_value,
?int $log_progress,
string $log_last_line,
string $log_message,
string $status,
bool $overlay,
string $base_url
): string {
ob_start();
if ($overlay) {
?>
<div class="br-overlay">
<div class="br-modal br-modal-log">
<?php
}
?>
<div class="br-log-header">
<h3>Log zadania</h3>
<div class="br-log-header-actions">
<button class="br-icon-btn" type="button" onclick="copyLogToClipboard();" title="Kopiuj do schowka" aria-label="Kopiuj do schowka">Kopiuj</button>
<?php if ($overlay) { ?>
<button class="br-icon-btn" type="button" data-close-overlay="1" title="Zamknij" aria-label="Zamknij">X</button>
<?php } else { ?>
<a class="br-icon-btn" href="<?php echo h($base_url); ?>" title="Zamknij" aria-label="Zamknij">X</a>
<?php } ?>
</div>
</div>
<div class="br-modal-scroll">
<p class="br-muted">Podglad na zywo (odswiezanie co 3s)</p>
<div class="br-log-meta">
<div><strong>Data rozpoczecia:</strong> <?php echo h($log_meta_start); ?></div>
<div><strong>Baza danych:</strong> <?php echo h($log_meta_db); ?></div>
<div><strong>Backup (data katalogu):</strong> <?php echo h($log_meta_date); ?></div>
<div><strong>Rodzaj przywracanej bazy:</strong> <?php echo h($log_meta_backup_kind); ?></div>
<div><strong>Tryb przywracania:</strong> <?php echo h($log_meta_restore_mode); ?></div>
<div><strong>Koniecznosc sanityzacji:</strong> <?php if ($log_meta_sanitization === 'Tak') { ?><span class="br-text-danger"><?php echo h($log_meta_sanitization); ?></span><?php } else { ?><?php echo h($log_meta_sanitization); ?><?php } ?></div>
</div>
<div class="br-status-line">Status: <strong id="log-status"><?php echo h($log_status_value); ?></strong></div>
<div class="br-status-line">Postep: <span id="log-progress-text"><?php echo h($log_progress !== null ? ($log_progress . '%') : $log_last_line); ?></span></div>
<div class="br-progress">
<div id="log-progress-bar" class="br-progress-bar" style="width: <?php echo h($log_progress !== null ? (string)$log_progress : '0'); ?>%;"></div>
</div>
<div class="br-log-actions">
<?php if ($view_log_job !== '' && in_array($status, ['done', 'failed', 'canceled'], true)) { ?>
<button class="br-btn br-btn-ghost" type="button" onclick="downloadLog('<?php echo h($view_log_job); ?>');">Zapisz log</button>
<?php } ?>
</div>
<pre id="log-content"><?php echo h($log_message); ?></pre>
</div>
<?php
if ($overlay) {
?>
</div>
</div>
<?php
}
return trim((string)ob_get_clean());
}
function render_user_backup_create_tab(array $owned_dbs, string $csrf_token, string $base_url, string $restore_mode, string $target_db, string $main_tab): string
{
if (empty($owned_dbs)) {
return '<p class="br-muted">Brak baz danych uzytkownika do backupu.</p>';
}
ob_start();
?>
<form method="post" action="<?php echo h($base_url); ?>" id="user-backup-form">
<input type="hidden" name="action" value="queue_user_backup" />
<input type="hidden" name="user_tab" value="backup_create" />
<input type="hidden" name="csrf_token" value="<?php echo h($csrf_token); ?>" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_db, $main_tab); ?>
<table class="br-table" border="1" cellpadding="6" cellspacing="0">
<thead>
<tr>
<th></th>
<th>Nazwa Bazy</th>
<th>Akcja</th>
</tr>
</thead>
<tbody>
<?php foreach ($owned_dbs as $db_name) { ?>
<tr>
<td><input type="checkbox" name="dbs[]" value="<?php echo h($db_name); ?>" class="js-user-db-check" /></td>
<td><?php echo h($db_name); ?></td>
<td>
<button class="br-btn br-btn-ghost" type="submit" name="single_db" value="<?php echo h($db_name); ?>">Wykonaj Backup</button>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<div class="br-actions-row">
<button class="br-btn" type="submit" name="bulk_backup" value="1" id="bulk-user-backup-btn" style="display:none;">Wykonaj backupy wszystkich zaznaczonych baz</button>
</div>
</form>
<?php
return (string)ob_get_clean();
}
function render_user_restore_tab(
string $base_url,
string $csrf_token,
string $selected_month,
string $selected_day,
string $selected_slot,
string $selected_db,
array $available_months,
array $days,
array $day_slots,
array $databases,
string $lang_code,
string $restore_mode,
string $target_db,
string $main_tab,
string $empty_state_message
): string {
$active_day_keys = array_keys($days);
$month_ts = strtotime($selected_month . '-01');
if ($month_ts === false) {
$month_ts = strtotime(date('Y-m-01'));
}
$calendar_days = (int)date('t', $month_ts);
$calendar_offset = (int)date('N', $month_ts);
$nav = month_nav_targets($available_months, $selected_month);
$weekdays = calendar_weekdays($lang_code);
if (empty($active_day_keys)) {
return '<p class="br-muted">' . h($empty_state_message) . '</p>';
}
ob_start();
?>
<div class="br-restore-layout">
<div class="br-restore-calendar">
<h4>Kalendarz backupow uzytkownika</h4>
<div class="br-month-nav">
<form method="get" action="<?php echo h($base_url); ?>">
<input type="hidden" name="action" value="user_tab" />
<input type="hidden" name="user_tab" value="restore_user" />
<input type="hidden" name="u_day" value="" />
<input type="hidden" name="u_slot" value="" />
<input type="hidden" name="u_month" value="<?php echo h($nav['prev']); ?>" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_db, $main_tab); ?>
<button class="br-month-btn" type="submit" <?php echo $nav['prev'] === '' ? 'disabled' : ''; ?> title="Poprzedni miesiac" aria-label="Poprzedni miesiac">◀</button>
</form>
<div class="br-month-label"><?php echo h(format_month_label($selected_month, $lang_code)); ?></div>
<form method="get" action="<?php echo h($base_url); ?>">
<input type="hidden" name="action" value="user_tab" />
<input type="hidden" name="user_tab" value="restore_user" />
<input type="hidden" name="u_day" value="" />
<input type="hidden" name="u_slot" value="" />
<input type="hidden" name="u_month" value="<?php echo h($nav['next']); ?>" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_db, $main_tab); ?>
<button class="br-month-btn" type="submit" <?php echo $nav['next'] === '' ? 'disabled' : ''; ?> title="Nastepny miesiac" aria-label="Nastepny miesiac">▶</button>
</form>
</div>
<form method="get" action="<?php echo h($base_url); ?>" class="br-calendar-grid-wrap">
<input type="hidden" name="action" value="user_tab" />
<input type="hidden" name="user_tab" value="restore_user" />
<input type="hidden" name="u_month" value="<?php echo h($selected_month); ?>" />
<input type="hidden" name="u_slot" value="" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_db, $main_tab); ?>
<table class="br-calendar-grid">
<thead>
<tr>
<?php foreach ($weekdays as $weekday) { ?>
<th><?php echo h($weekday); ?></th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php
$day_num = 1;
for ($row = 0; $row < 6; $row++) {
echo '<tr>';
for ($col = 1; $col <= 7; $col++) {
if (($row === 0 && $col < $calendar_offset) || $day_num > $calendar_days) {
echo '<td class="br-cal-empty"></td>';
continue;
}
$day_key = date('Y-m-', $month_ts) . str_pad((string)$day_num, 2, '0', STR_PAD_LEFT);
$is_active = isset($days[$day_key]);
$is_selected = $day_key === $selected_day;
echo '<td>';
if ($is_active) {
echo '<button class="br-cal-day' . ($is_selected ? ' is-selected' : '') . '" type="submit" name="u_day" value="' . h($day_key) . '">' . h((string)$day_num) . '</button>';
} else {
echo '<span class="br-cal-day-disabled">' . h((string)$day_num) . '</span>';
}
echo '</td>';
$day_num++;
}
echo '</tr>';
if ($day_num > $calendar_days) {
break;
}
}
?>
</tbody>
</table>
</form>
</div>
<div class="br-restore-picker">
<h4>Wybierz backup i baze</h4>
<?php if ($selected_day === '' || empty($day_slots)) { ?>
<p class="br-muted">Wybierz aktywna date w kalendarzu.</p>
<?php } else { ?>
<?php if (count($day_slots) > 1) { ?>
<p class="br-muted">Data rozpoczecia generowania backupow</p>
<form method="get" action="<?php echo h($base_url); ?>" class="br-time-picker">
<input type="hidden" name="action" value="user_tab" />
<input type="hidden" name="user_tab" value="restore_user" />
<input type="hidden" name="u_month" value="<?php echo h($selected_month); ?>" />
<input type="hidden" name="u_day" value="<?php echo h($selected_day); ?>" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_db, $main_tab); ?>
<div class="br-time-list">
<?php foreach ($day_slots as $slot) { ?>
<label class="br-time-item">
<input type="radio" name="u_slot" value="<?php echo h($slot['dir']); ?>" <?php echo $slot['dir'] === $selected_slot ? 'checked' : ''; ?> />
<span><?php echo h($slot['time_label']); ?></span>
</label>
<?php } ?>
</div>
</form>
<?php } ?>
<?php if ($selected_slot !== '' && !empty($databases)) { ?>
<form method="post" action="<?php echo h($base_url); ?>" id="user-restore-form">
<input type="hidden" name="action" value="restore_user" />
<input type="hidden" name="user_tab" value="restore_user" />
<input type="hidden" name="u_month" value="<?php echo h($selected_month); ?>" />
<input type="hidden" name="u_day" value="<?php echo h($selected_day); ?>" />
<input type="hidden" name="u_slot" value="<?php echo h($selected_slot); ?>" />
<input type="hidden" name="csrf_token" value="<?php echo h($csrf_token); ?>" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_db, $main_tab); ?>
<table class="br-table" border="1" cellpadding="6" cellspacing="0">
<thead>
<tr>
<th></th>
<th>Baza danych</th>
<th>Rozmiar</th>
<th>Data i godzina backupu</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
<?php foreach ($databases as $db) { ?>
<?php $backup_file = basename((string)$db['file']); ?>
<?php $is_selected_row = $db['name'] === $selected_db; ?>
<tr>
<td><input type="radio" name="u_db" value="<?php echo h($db['name']); ?>" <?php echo $db['name'] === $selected_db ? 'checked' : ''; ?> required /></td>
<td><?php echo h($db['name']); ?></td>
<td><?php echo h(number_format($db['size'] / 1024 / 1024, 2) . ' MB'); ?></td>
<td><?php echo h(format_datetime((string)$db['mtime'], (int)$db['mtime'])); ?></td>
<td>
<div class="br-actions-row">
<button
class="br-btn br-btn-small js-user-row-restore<?php echo $is_selected_row ? '' : ' br-btn-fogged'; ?>"
type="submit"
<?php echo $is_selected_row ? '' : 'disabled'; ?>
>
Przywróć kopię zapasową
</button>
<button class="br-btn br-btn-ghost br-btn-small js-download-user-backup" type="submit" name="download_user_backup" value="<?php echo h($backup_file); ?>" formnovalidate>Pobierz kopię zapasową</button>
<button class="br-btn br-btn-danger br-btn-small" type="submit" name="delete_user_backup" value="<?php echo h($backup_file); ?>" formnovalidate>Usun</button>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</form>
<?php } elseif ($selected_slot !== '') { ?>
<p class="br-muted">Brak baz dla wybranego backupu.</p>
<?php } else { ?>
<p class="br-muted">Wybierz godzine backupu, aby zobaczyc liste baz.</p>
<?php } ?>
<?php } ?>
</div>
</div>
<?php
return (string)ob_get_clean();
}
function render_user_tabs(
string $active_tab,
array $owned_dbs,
string $base_url,
string $csrf_token,
string $u_selected_month,
string $u_selected_day,
string $u_selected_slot,
string $u_selected_db,
array $u_available_months,
array $u_days,
array $u_day_slots,
array $u_databases,
string $lang_code,
string $restore_mode,
string $target_db,
string $main_tab,
string $user_restore_empty_message
): string {
if ($restore_mode === 'new_db' && $active_tab === 'backup_create') {
$active_tab = 'restore_user';
}
$content = $active_tab === 'restore_user'
? render_user_restore_tab($base_url, $csrf_token, $u_selected_month, $u_selected_day, $u_selected_slot, $u_selected_db, $u_available_months, $u_days, $u_day_slots, $u_databases, $lang_code, $restore_mode, $target_db, $main_tab, $user_restore_empty_message)
: render_user_backup_create_tab($owned_dbs, $csrf_token, $base_url, $restore_mode, $target_db, $main_tab);
ob_start();
?>
<div class="br-tab-header">
<?php if ($restore_mode !== 'new_db') { ?>
<button class="br-btn br-btn-ghost br-user-tab <?php echo $active_tab === 'backup_create' ? 'is-active' : ''; ?>" type="button" data-user-tab="backup_create">Wykonaj Backup Baz danych</button>
<?php } ?>
<button class="br-btn br-btn-ghost br-user-tab <?php echo $active_tab === 'restore_user' ? 'is-active' : ''; ?>" type="button" data-user-tab="restore_user">Przywróć kopię zapasową</button>
</div>
<div id="user-tab-content"><?php echo $content; ?></div>
<?php
return (string)ob_get_clean();
}
$params = read_params();
$action = $params['action'] ?? '';
$method = $_SERVER['REQUEST_METHOD'] ?? '';
if ($method === 'POST') {
$download_hitme_clicked = trim((string)($params['download_hitme_backup'] ?? ''));
$download_clicked = trim((string)($params['download_user_backup'] ?? ''));
$delete_clicked = trim((string)($params['delete_user_backup'] ?? ''));
if ($download_hitme_clicked !== '') {
$params['backup_file'] = $download_hitme_clicked;
$action = 'download_hitme_backup';
} elseif ($download_clicked !== '') {
$params['backup_file'] = $download_clicked;
$action = 'download_user_backup';
} elseif ($delete_clicked !== '') {
$params['backup_file'] = $delete_clicked;
$action = 'delete_user_backup';
}
}
$plugin_dir = realpath(__DIR__ . '/..') ?: '/usr/local/directadmin/plugins/db-manager';
$plugin_data_dir = $plugin_dir . '/data';
if (!is_dir($plugin_data_dir)) {
@mkdir($plugin_data_dir, 0700, true);
}
$plugin_log = $plugin_data_dir . '/plugin.log';
ini_set('log_errors', '1');
ini_set('error_log', $plugin_log);
$request_line = date('Y-m-d H:i:s') . ' ' . ($_SERVER['REQUEST_METHOD'] ?? '') . ' ' . ($_SERVER['REQUEST_URI'] ?? '');
$request_line .= ' action=' . ($action !== '' ? $action : '-');
$request_line .= ' qs=' . ($_SERVER['QUERY_STRING'] ?? '');
@file_put_contents($plugin_log, $request_line . "\n", FILE_APPEND);
$da_user = getenv('USERNAME');
if ($da_user === false || $da_user === '') {
$da_user = getenv('LOGNAME') ?: getenv('USER') ?: 'unknown';
}
$lang_code = load_user_language($da_user);
$lang_map = load_lang_map($lang_code, $plugin_dir);
$skin = load_user_skin($da_user);
$css_file = $skin === 'evolution' ? 'evolution.css' : 'enhanced.css';
$settings_path = $plugin_dir . '/plugin-settings.conf';
$settings = load_settings($settings_path);
if ($action === 'css') {
$skin_req = strtolower((string)($params['skin'] ?? ''));
if ($skin_req !== 'evolution' && $skin_req !== 'enhanced') {
$skin_req = 'enhanced';
}
$css_path = $plugin_dir . '/user/' . $skin_req . '.css';
if (!is_readable($css_path)) {
header('HTTP/1.1 404 Not Found');
exit;
}
header('Content-Type: text/css; charset=utf-8');
readfile($css_path);
exit;
}
$inline_css = '';
$css_path = $plugin_dir . '/user/' . $css_file;
if (is_readable($css_path)) {
$inline_css = (string)file_get_contents($css_path);
}
$plugin_enabled = is_plugin_enabled_for_user($da_user, 'db-manager', $settings);
if (!$plugin_enabled) {
header('Content-Type: text/html; charset=utf-8');
$disabled_message = t('Opcja zarzadzania bazami danych jest niedostepna - skontaktuj sie z supportem aby uzyskac wiecej informacji');
echo translate_html_safe(render_plugin_unavailable_page($inline_css, $disabled_message), $lang_map ?? []);
exit;
}
$errors = [];
$warnings = [];
$message_status = '';
$log_message = '';
$enable_logs_overlay = parse_bool((string)($settings['ENABLE_LOGS_OVERLAY'] ?? 'true'), true);
$hitme_dir = $settings['HITME_SQL_BACKUP_DIR'] ?? '/home/admin/mysql_backups';
$user_backup_dir = $settings['USER_SQL_BACKUP_DIR'] ?? '/home/da_user/mysql_backups/user_backups';
$pre_restore_dir = $settings['PRE_RESTORE_BACKUP_DIR'] ?? '/home/da_user/mysql_backups/pre_restore_backups';
$pre_restore_user_dir = $settings['PRE_RESTORE_USER_BACKUP_DIR'] ?? '/home/da_user/mysql_backups/pre_restore_user_backups';
$enable_pre_restore = strtolower(trim((string)($settings['ENABLE_PRE_RESTORE_BACKUP'] ?? 'true')));
$enable_user_pre_restore = strtolower(trim((string)($settings['ENABLE_USER_PRE_RESTORE_BACKUP'] ?? 'true')));
$secure_backup_mode = strtolower(trim((string)($settings['SECURE_BACKUP'] ?? 'true')));
$secure_restore_mode = strtolower(trim((string)($settings['SECURE_RESTORE'] ?? 'true')));
$allow_download_hitme_sql_backup = parse_bool((string)($settings['ALLOW_DOWNLOAD_HITME_SQL_BACKUP'] ?? 'true'), true);
$enable_user_backups = parse_bool((string)($settings['ENABLE_USER_BACKUPS'] ?? 'true'), true);
$prevent_log_deletion = parse_bool((string)($settings['PREVENT_LOG_DELETION'] ?? 'true'), true);
$own_backup_user_restore = parse_bool((string)($settings['OWN_BACKUP_USER_RESTORE'] ?? 'true'), true);
$user_backup_dir = apply_da_user($user_backup_dir, $da_user);
$pre_restore_dir = apply_da_user($pre_restore_dir, $da_user);
$pre_restore_user_dir = apply_da_user($pre_restore_user_dir, $da_user);
$user_backup_runtime_dir = $user_backup_dir;
if ($hitme_dir === '' || !is_dir($hitme_dir)) {
$errors[] = 'Brak katalogu HITME_SQL_BACKUP_DIR.';
}
if ($enable_pre_restore !== 'true' && $enable_pre_restore !== 'false') {
$warnings[] = 'ENABLE_PRE_RESTORE_BACKUP ma niepoprawna wartosc (uzywam true/false).';
}
if ($enable_user_pre_restore !== 'true' && $enable_user_pre_restore !== 'false') {
$warnings[] = 'ENABLE_USER_PRE_RESTORE_BACKUP ma niepoprawna wartosc (uzywam true/false).';
}
if ($secure_backup_mode !== 'true' && $secure_backup_mode !== 'false') {
$warnings[] = 'SECURE_BACKUP ma niepoprawna wartosc (uzywam true/false).';
}
if ($secure_restore_mode !== 'true' && $secure_restore_mode !== 'false') {
$warnings[] = 'SECURE_RESTORE ma niepoprawna wartosc (uzywam true/false).';
}
$csrf_token = get_csrf_token($plugin_data_dir, $da_user);
$base_url = '/CMD_PLUGINS/db-manager';
$job_base = $plugin_dir . '/data/jobs';
$log_dir = $plugin_dir . '/data/logs';
$pid_dir = $plugin_dir . '/data/pids';
$cancel_dir = $plugin_dir . '/data/cancel';
$selected_day = trim((string)($params['day'] ?? ''));
$selected_slot = trim((string)($params['slot'] ?? ''));
$selected_month = trim((string)($params['month'] ?? ''));
$selected_db = trim((string)($params['db'] ?? ''));
$restore_mode = trim((string)($params['restore_mode'] ?? 'overwrite'));
$target_restore_db = trim((string)($params['target_db'] ?? ''));
if ($restore_mode !== 'overwrite' && $restore_mode !== 'new_db') {
$restore_mode = 'overwrite';
}
if ($target_restore_db === '') {
$target_restore_db = $da_user . '_';
}
$slot_index = collect_backup_slots_filtered($hitme_dir, $da_user, false);
$days = $slot_index['days'];
$available_months = [];
foreach ($days as $day_info) {
$available_months[substr($day_info['day_key'], 0, 7)] = true;
}
$available_months = array_keys($available_months);
rsort($available_months, SORT_STRING);
if ($selected_month === '') {
$selected_month = !empty($available_months) ? $available_months[0] : date('Y-m');
}
if (!in_array($selected_month, $available_months, true) && !empty($available_months)) {
$selected_month = $available_months[0];
}
if ($selected_day === '' || !isset($days[$selected_day]) || substr($selected_day, 0, 7) !== $selected_month) {
$selected_day = '';
foreach ($days as $day_key => $day_info) {
if (substr($day_key, 0, 7) === $selected_month) {
$selected_day = $day_key;
break;
}
}
if ($selected_day === '' && !empty($days)) {
$selected_day = (string)array_key_first($days);
}
}
$day_slots = ($selected_day !== '' && isset($days[$selected_day])) ? $days[$selected_day]['slots'] : [];
$slot_names = array_map(function ($slot) {
return $slot['dir'];
}, $day_slots);
if ($selected_slot === '' && count($slot_names) === 1) {
$selected_slot = $slot_names[0];
}
if ($selected_slot !== '' && !in_array($selected_slot, $slot_names, true)) {
$selected_slot = '';
}
$databases = $selected_slot !== '' ? list_user_databases($hitme_dir, $selected_slot, $da_user, false) : [];
$active_user_tab = (string)($params['user_tab'] ?? 'backup_create');
if ($active_user_tab !== 'backup_create' && $active_user_tab !== 'restore_user') {
$active_user_tab = 'backup_create';
}
$main_tab_raw = trim((string)($params['main_tab'] ?? ''));
$main_tab_from_request = $main_tab_raw === 'hitme' || ($enable_user_backups && $main_tab_raw === 'user');
$main_tab = $main_tab_raw;
if (!$main_tab_from_request) {
$user_main_actions = ['user_tab', 'queue_user_backup', 'restore_user', 'download_user_backup', 'delete_user_backup'];
$main_tab = ($enable_user_backups && in_array((string)$action, $user_main_actions, true)) ? 'user' : 'hitme';
}
if (!$enable_user_backups) {
$main_tab = 'hitme';
$active_user_tab = 'restore_user';
}
$owned_user_dbs = list_owned_databases_via_mysql($da_user);
$u_selected_day = trim((string)($params['u_day'] ?? ''));
$u_selected_slot = trim((string)($params['u_slot'] ?? ''));
$u_selected_month = trim((string)($params['u_month'] ?? ''));
$u_selected_db = trim((string)($params['u_db'] ?? ''));
$u_slot_index = collect_backup_slots_filtered($user_backup_runtime_dir, $da_user, $own_backup_user_restore);
$u_days = $u_slot_index['days'];
$user_restore_empty_message = 'Brak kopii zapasowych użytkownika dostępnych do przywrócenia - najpierw utwórz kopię zapasową na zakładce Wykonaj Backup Baz danych';
$u_available_months = [];
foreach ($u_days as $day_info) {
$u_available_months[substr($day_info['day_key'], 0, 7)] = true;
}
$u_available_months = array_keys($u_available_months);
rsort($u_available_months, SORT_STRING);
if ($u_selected_month === '') {
$u_selected_month = !empty($u_available_months) ? $u_available_months[0] : date('Y-m');
}
if (!in_array($u_selected_month, $u_available_months, true) && !empty($u_available_months)) {
$u_selected_month = $u_available_months[0];
}
if ($u_selected_day === '' || !isset($u_days[$u_selected_day]) || substr($u_selected_day, 0, 7) !== $u_selected_month) {
$u_selected_day = '';
foreach ($u_days as $day_key => $day_info) {
if (substr($day_key, 0, 7) === $u_selected_month) {
$u_selected_day = $day_key;
break;
}
}
if ($u_selected_day === '' && !empty($u_days)) {
$u_selected_day = (string)array_key_first($u_days);
}
}
$u_day_slots = ($u_selected_day !== '' && isset($u_days[$u_selected_day])) ? $u_days[$u_selected_day]['slots'] : [];
$u_slot_names = array_map(function ($slot) {
return $slot['dir'];
}, $u_day_slots);
if ($u_selected_slot === '' && count($u_slot_names) === 1) {
$u_selected_slot = $u_slot_names[0];
}
if ($u_selected_slot !== '' && !in_array($u_selected_slot, $u_slot_names, true)) {
$u_selected_slot = '';
}
$u_databases = $u_selected_slot !== '' ? list_user_databases($user_backup_runtime_dir, $u_selected_slot, $da_user, $own_backup_user_restore) : [];
function require_csrf(string $token, string $data_dir, string $user, array &$errors): bool
{
if (!check_csrf($token, $data_dir, $user)) {
$errors[] = 'Nieprawidlowy token CSRF.';
return false;
}
return true;
}
$confirm_overlay_html = '';
if (!$enable_user_backups && in_array((string)$action, ['user_tab', 'queue_user_backup', 'restore_user', 'download_user_backup', 'delete_user_backup'], true)) {
$errors[] = 'Sekcja backupow uzytkownika jest wylaczona.';
$action = '';
}
if ($method === 'POST') {
$confirm_required_actions = ['restore', 'restore_user', 'restore_pre_backup', 'delete_pre_backup', 'delete_user_backup'];
if (!$prevent_log_deletion) {
$confirm_required_actions[] = 'delete_job';
}
$confirm_flag = (string)($params['confirm_action'] ?? '');
if ($confirm_flag !== '1' && in_array($action, ['restore', 'restore_user', 'restore_pre_backup'], true)) {
$target_for_lock_check = $target_restore_db !== '' ? $target_restore_db : ($da_user . '_');
$db_for_lock_check = '';
if ($action === 'restore') {
$restore_source_db = trim((string)($params['db'] ?? $selected_db));
$db_for_lock_check = $restore_mode === 'new_db'
? $target_for_lock_check
: ($restore_source_db !== '' ? $restore_source_db : $target_for_lock_check);
} elseif ($action === 'restore_user') {
$restore_user_source_db = trim((string)($params['u_db'] ?? $u_selected_db));
$db_for_lock_check = $restore_mode === 'new_db'
? $target_for_lock_check
: ($restore_user_source_db !== '' ? $restore_user_source_db : $target_for_lock_check);
} elseif ($action === 'restore_pre_backup') {
$job_file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($job_file);
if ($job_id !== '' && job_belongs_to_user($job_id, $job_base, $da_user)) {
$job_file_path = find_job_file($job_base, $job_file, $job_id);
$vars = $job_file_path !== '' ? load_job_vars($job_file_path) : [];
$db_for_lock_check = trim((string)($vars['DB_NAME'] ?? ''));
}
}
if ($db_for_lock_check !== '') {
$lock_error = '';
if (!check_database_collisions($settings, $plugin_dir, $job_base, $da_user, $lock_error, $db_for_lock_check)) {
$errors[] = $lock_error;
$action = '';
}
}
}
if (in_array($action, $confirm_required_actions, true) && $confirm_flag !== '1') {
$target_for_warning = $target_restore_db !== '' ? $target_restore_db : ($da_user . '_');
$restore_source_db = trim((string)($params['db'] ?? $selected_db));
$restore_user_source_db = trim((string)($params['u_db'] ?? $u_selected_db));
$restore_slot_user = trim((string)($params['u_slot'] ?? $u_selected_slot));
$restore_user_label = $restore_slot_user !== '' ? format_backup_label($restore_slot_user) : '';
$restore_target_db = $restore_mode === 'new_db'
? $target_for_warning
: ($restore_source_db !== '' ? $restore_source_db : $target_for_warning);
$restore_user_target_db = $restore_mode === 'new_db'
? $target_for_warning
: ($restore_user_source_db !== '' ? $restore_user_source_db : $target_for_warning);
$confirm_messages = [
'restore' => build_restore_confirmation_message('hitme', $restore_source_db, $restore_target_db, $restore_mode),
'restore_user' => build_restore_confirmation_message('user', $restore_user_source_db, $restore_user_target_db, $restore_mode, $restore_user_label),
'restore_pre_backup' => 'Przywrócenie backupu sprzed przywrócenia może nadpisać obecne dane. Czy kontynuować?',
'delete_pre_backup' => 'Usunięcie backupu sprzed przywrócenia jest nieodwracalne. Czy kontynuować?',
'delete_job' => 'Usuniesz log i wpis zadania. Tej operacji nie można cofnąć. Czy kontynuować?',
'delete_user_backup' => 'Usuniesz backup użytkownika. Tej operacji nie można cofnąć. Czy kontynuować?',
];
$confirm_overlay_html = render_warning_overlay(
'Potwierdzenie operacji',
$confirm_messages[$action] ?? 'Czy kontynuować operację?',
$params,
$base_url
);
$action = '';
}
}
if ($action === 'user_tab') {
header('Content-Type: text/html; charset=utf-8');
$html = render_user_tabs(
$active_user_tab,
$owned_user_dbs,
$base_url,
$csrf_token,
$u_selected_month,
$u_selected_day,
$u_selected_slot,
$u_selected_db,
$u_available_months,
$u_days,
$u_day_slots,
$u_databases,
$lang_code,
$restore_mode,
$target_restore_db,
$main_tab,
$user_restore_empty_message
);
echo translate_html_safe($html, $lang_map ?? []);
exit;
}
if ($action === 'restore' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
if ($selected_day === '' || !isset($days[$selected_day])) {
$errors[] = 'Nie wybrano poprawnej daty backupu.';
}
if ($selected_slot === '' || !in_array($selected_slot, $slot_names, true)) {
$errors[] = 'Nie wybrano poprawnej godziny backupu.';
}
$db_map = [];
foreach ($databases as $db) {
$db_map[$db['name']] = $db;
}
if ($selected_db === '' || !isset($db_map[$selected_db])) {
$errors[] = 'Nie wybrano poprawnej bazy danych.';
}
$target_db_for_restore = $selected_db;
if ($restore_mode === 'new_db') {
if ($target_restore_db === '' || !is_valid_target_db_name($target_restore_db, $da_user)) {
$errors[] = 'Nazwa docelowej bazy danych musi zaczynać się od prefiksu ' . $da_user . '_ (DA_USER_).';
} elseif ($target_restore_db === $selected_db) {
$errors[] = 'Docelowa baza danych musi byc inna niz zrodlowa baza z backupu.';
} elseif (!in_array($target_restore_db, $owned_user_dbs, true)) {
$errors[] = 'Wskazana docelowa baza danych nie istnieje. Utworz ja recznie w DirectAdmin.';
} else {
$has_tables = database_has_tables($target_restore_db);
if ($has_tables === null) {
$errors[] = 'Nie udalo sie sprawdzic, czy docelowa baza danych jest pusta.';
} elseif ($has_tables === true) {
$errors[] = 'Docelowa baza danych nie jest pusta. Usun dane przed kontynuowaniem.';
}
}
if (empty($errors)) {
$target_db_for_restore = $target_restore_db;
}
}
$queue_lock = null;
if (empty($errors)) {
$lock_error = '';
$queue_lock = acquire_jobs_queue_lock($job_base, $lock_error);
if ($queue_lock === null) {
$errors[] = $lock_error;
}
}
if (empty($errors)) {
$sim_error = '';
if (!check_database_collisions($settings, $plugin_dir, $job_base, $da_user, $sim_error, $target_db_for_restore)) {
$errors[] = $sim_error;
}
}
if (empty($errors)) {
$limit_error = '';
if (!reserve_operation_limit($plugin_data_dir, $da_user, 'restore', 1, $settings, $limit_error)) {
$errors[] = $limit_error;
}
}
if (empty($errors)) {
$job_dir = $job_base . '/pending';
if (!is_dir($job_dir) && !@mkdir($job_dir, 0700, true) && !is_dir($job_dir)) {
$errors[] = 'Nie mozna utworzyc katalogu zadan.';
} else {
$job_id = date('YmdHis') . '-' . bin2hex(random_bytes(4));
$job_file = $job_dir . '/restore-' . $job_id . '.env';
$dump_path = (string)($db_map[$selected_db]['file'] ?? '');
if ($dump_path === '' || !is_file($dump_path)) {
$errors[] = 'Nie znaleziono pliku dump dla wybranej bazy.';
}
}
$db_lock_path = '';
if (empty($errors)) {
$db_lock_error = '';
$db_lock_path = acquire_db_lock($plugin_dir, $target_db_for_restore, $job_id, 'restore', $db_lock_error);
if ($db_lock_path === '') {
$errors[] = $db_lock_error;
}
}
if (empty($errors)) {
$job_content = "DA_USER=" . escapeshellarg($da_user) . "\n";
$job_content .= "START_TS=" . escapeshellarg((string)time()) . "\n";
$job_content .= "DATE_DIR=" . escapeshellarg($selected_slot) . "\n";
$job_content .= "DB_NAME=" . escapeshellarg($target_db_for_restore) . "\n";
$job_content .= "SOURCE_DB_NAME=" . escapeshellarg($selected_db) . "\n";
$job_content .= "DUMP_FILE=" . escapeshellarg($dump_path) . "\n";
$job_content .= "DB_LOCK_FILE=" . escapeshellarg($db_lock_path) . "\n";
$job_content .= "BACKUP_SOURCE=" . escapeshellarg('hosting') . "\n";
$job_content .= "RESTORE_MODE=" . escapeshellarg($restore_mode) . "\n";
if (@file_put_contents($job_file, $job_content, LOCK_EX) === false) {
release_db_lock_file($db_lock_path);
$errors[] = 'Nie udalo sie zapisac pliku zadania.';
} else {
@chmod($job_file, 0600);
trigger_worker($plugin_dir);
$message_status = 'Zadanie przywracania bazy dodane do kolejki. Otrzymasz powiadomienie po zakonczeniu.';
}
}
}
release_jobs_queue_lock($queue_lock);
}
if ($action === 'queue_user_backup' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
if ($restore_mode === 'new_db') {
$errors[] = 'W trybie przywracania do nowej bazy tworzenie backupow uzytkownika jest ukryte.';
}
$selected_dbs = [];
$single_db = trim((string)($params['single_db'] ?? ''));
if ($single_db !== '') {
$selected_dbs[] = $single_db;
} else {
$raw = $params['dbs'] ?? [];
if (!is_array($raw)) {
$raw = [$raw];
}
foreach ($raw as $db) {
$db = trim((string)$db);
if ($db !== '') {
$selected_dbs[] = $db;
}
}
$selected_dbs = array_values(array_unique($selected_dbs));
if (!empty($params['bulk_backup']) && count($selected_dbs) < 2) {
$errors[] = 'Aby wykonac backup zbiorczy zaznacz co najmniej dwie bazy.';
}
}
if (empty($selected_dbs)) {
$errors[] = 'Nie wybrano baz do backupu.';
}
$allowed = array_fill_keys($owned_user_dbs, true);
foreach ($selected_dbs as $db) {
if (!isset($allowed[$db])) {
$errors[] = 'Nieprawidlowa baza danych: ' . $db;
break;
}
}
$queue_lock = null;
if (empty($errors)) {
$lock_error = '';
$queue_lock = acquire_jobs_queue_lock($job_base, $lock_error);
if ($queue_lock === null) {
$errors[] = $lock_error;
}
}
if (empty($errors)) {
$sim_error = '';
foreach ($selected_dbs as $db_name_to_backup) {
if (!check_database_collisions($settings, $plugin_dir, $job_base, $da_user, $sim_error, $db_name_to_backup)) {
$errors[] = $sim_error;
break;
}
}
}
if (empty($errors)) {
$limit_error = '';
if (!reserve_operation_limit($plugin_data_dir, $da_user, 'backup', count($selected_dbs), $settings, $limit_error)) {
$errors[] = $limit_error;
}
}
if (empty($errors)) {
$job_dir = $job_base . '/pending';
if (!is_dir($job_dir) && !@mkdir($job_dir, 0700, true) && !is_dir($job_dir)) {
$errors[] = 'Nie mozna utworzyc katalogu zadan.';
} else {
$date_dir = date('d-m-Y_H_i');
$target_dir = rtrim($user_backup_runtime_dir, '/') . '/' . $date_dir;
if (is_dir($target_dir)) {
do {
$date_dir = date('d-m-Y_H_i_s');
$target_dir = rtrim($user_backup_runtime_dir, '/') . '/' . $date_dir;
if (!is_dir($target_dir)) {
break;
}
usleep(250000);
} while (true);
}
while (is_dir($target_dir)) {
usleep(250000);
$date_dir = date('d-m-Y_H_i_s');
$target_dir = rtrim($user_backup_runtime_dir, '/') . '/' . $date_dir;
}
if (!is_dir($target_dir) && !@mkdir($target_dir, 0700, true) && !is_dir($target_dir)) {
$errors[] = 'Nie mozna utworzyc katalogu backupu uzytkownika.';
} else {
$queued = 0;
foreach ($selected_dbs as $db) {
$job_id = date('YmdHis') . '-' . bin2hex(random_bytes(4));
$job_file = $job_dir . '/backup-' . $job_id . '.env';
$db_lock_error = '';
$db_lock_path = acquire_db_lock($plugin_dir, $db, $job_id, 'backup', $db_lock_error);
if ($db_lock_path === '') {
$errors[] = $db_lock_error;
continue;
}
$job_content = "JOB_TYPE=" . escapeshellarg('backup') . "\n";
$job_content .= "DA_USER=" . escapeshellarg($da_user) . "\n";
$job_content .= "START_TS=" . escapeshellarg((string)time()) . "\n";
$job_content .= "DATE_DIR=" . escapeshellarg($date_dir) . "\n";
$job_content .= "DB_NAME=" . escapeshellarg($db) . "\n";
$job_content .= "DB_LOCK_FILE=" . escapeshellarg($db_lock_path) . "\n";
$job_content .= "BACKUP_SOURCE=" . escapeshellarg('user') . "\n";
$job_content .= "RESTORE_MODE=" . escapeshellarg('backup') . "\n";
if (@file_put_contents($job_file, $job_content, LOCK_EX) === false) {
release_db_lock_file($db_lock_path);
$errors[] = 'Nie udalo sie zapisac pliku zadania backupu dla bazy ' . $db . '.';
continue;
}
@chmod($job_file, 0600);
$queued++;
}
if ($queued > 0) {
trigger_worker($plugin_dir);
$message_status = $queued === 1
? 'Dodano zadanie backupu bazy danych.'
: sprintf('Dodano zadania backupu %d baz danych.', $queued);
}
}
}
}
release_jobs_queue_lock($queue_lock);
}
if ($action === 'download_hitme_backup' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
if (!$allow_download_hitme_sql_backup) {
$errors[] = 'Pobieranie backupow HITME.SQL jest wylaczone.';
}
$backup_file = basename((string)($params['backup_file'] ?? ''));
$download_slot = trim((string)($params['slot'] ?? ''));
if (empty($errors) && !preg_match('/^[A-Za-z0-9._-]+$/', $download_slot)) {
$errors[] = 'Nie wybrano poprawnej godziny backupu.';
}
if (empty($errors) && !preg_match('/^[A-Za-z0-9._-]+$/', $backup_file)) {
$errors[] = 'Nieprawidlowa nazwa pliku backupu.';
}
$slot_dir = '';
$slot_real = false;
if (empty($errors)) {
$slot_dir = rtrim($hitme_dir, '/') . '/' . $download_slot;
$slot_real = realpath($slot_dir);
if ($slot_real === false || !is_dir($slot_real) || !is_path_within_dir($slot_real, $hitme_dir)) {
$errors[] = 'Nieprawidlowa sciezka backupu.';
}
}
$backup_path = '';
$source_db_name = '';
if (empty($errors)) {
$source_db_name = db_name_from_dump_filename($backup_file);
if ($source_db_name === '') {
$errors[] = 'Nieprawidlowa nazwa pliku backupu.';
} elseif ($source_db_name !== $da_user && strpos($source_db_name, $da_user . '_') !== 0) {
$errors[] = 'Nieprawidlowa sciezka backupu.';
}
}
if (empty($errors)) {
$backup_path = (string)$slot_real . '/' . $backup_file;
if (!is_file($backup_path) || !is_path_within_dir($backup_path, $hitme_dir)) {
$errors[] = 'Nie znaleziono pliku dump dla wybranej bazy.';
}
}
if (empty($errors)) {
while (ob_get_level() > 0) {
@ob_end_clean();
}
if (headers_sent($hs_file, $hs_line)) {
$errors[] = 'Nie mozna pobrac pliku backupu (naglowki zostaly juz wyslane).';
@file_put_contents($plugin_log, date('Y-m-d H:i:s') . " download_hitme_backup headers_sent {$hs_file}:{$hs_line}\n", FILE_APPEND);
} else {
$download_name = basename($backup_path);
$content_type = 'application/octet-stream';
if (preg_match('/\.sql$/i', $download_name)) {
$content_type = 'application/sql';
} elseif (preg_match('/\.gz$/i', $download_name)) {
$content_type = 'application/gzip';
}
header('Content-Type: ' . $content_type);
header('Content-Disposition: attachment; filename="' . $download_name . '"');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-store, no-cache, must-revalidate');
$size = @filesize($backup_path);
if (is_int($size) && $size > 0) {
header('Content-Length: ' . $size);
}
readfile($backup_path);
exit;
}
}
if (!empty($errors)) {
@file_put_contents(
$plugin_log,
date('Y-m-d H:i:s') . ' download_hitme_backup error: ' . implode(' | ', $errors) . "\n",
FILE_APPEND
);
}
}
if ($action === 'download_user_backup' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
$backup_file = basename((string)($params['backup_file'] ?? ''));
$download_slot = trim((string)($params['u_slot'] ?? ''));
if (empty($errors) && !preg_match('/^[A-Za-z0-9._-]+$/', $download_slot)) {
$errors[] = 'Nie wybrano poprawnej godziny backupu uzytkownika.';
}
if (empty($errors) && !preg_match('/^[A-Za-z0-9._-]+$/', $backup_file)) {
$errors[] = 'Nieprawidlowa nazwa pliku backupu uzytkownika.';
}
$slot_dir = '';
$slot_real = false;
if (empty($errors)) {
$slot_dir = rtrim($user_backup_runtime_dir, '/') . '/' . $download_slot;
$slot_real = realpath($slot_dir);
if ($slot_real === false || !is_dir($slot_real) || !is_path_within_dir($slot_real, $user_backup_runtime_dir)) {
$errors[] = 'Nieprawidlowa sciezka backupu uzytkownika.';
}
}
$backup_path = '';
$source_db_name = '';
if (empty($errors)) {
$source_db_name = db_name_from_dump_filename($backup_file);
if ($source_db_name === '') {
$errors[] = 'Nieprawidlowa nazwa pliku backupu uzytkownika.';
} elseif ($source_db_name !== $da_user && strpos($source_db_name, $da_user . '_') !== 0) {
$errors[] = 'Nieprawidlowa sciezka backupu uzytkownika.';
}
}
if (empty($errors)) {
$backup_path = (string)$slot_real . '/' . $backup_file;
if (!is_file($backup_path) || !is_path_within_dir($backup_path, $user_backup_runtime_dir)) {
$errors[] = 'Nie znaleziono pliku dump dla wybranej bazy.';
}
}
if (empty($errors)) {
while (ob_get_level() > 0) {
@ob_end_clean();
}
if (headers_sent($hs_file, $hs_line)) {
$errors[] = 'Nie mozna pobrac pliku backupu (naglowki zostaly juz wyslane).';
@file_put_contents($plugin_log, date('Y-m-d H:i:s') . " download_user_backup headers_sent {$hs_file}:{$hs_line}\n", FILE_APPEND);
} else {
$download_name = basename($backup_path);
$content_type = 'application/octet-stream';
if (preg_match('/\.sql$/i', $download_name)) {
$content_type = 'application/sql';
} elseif (preg_match('/\.gz$/i', $download_name)) {
$content_type = 'application/gzip';
}
header('Content-Type: ' . $content_type);
header('Content-Disposition: attachment; filename="' . $download_name . '"');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-store, no-cache, must-revalidate');
$size = @filesize($backup_path);
if (is_int($size) && $size > 0) {
header('Content-Length: ' . $size);
}
readfile($backup_path);
exit;
}
}
if (!empty($errors)) {
@file_put_contents(
$plugin_log,
date('Y-m-d H:i:s') . ' download_user_backup error: ' . implode(' | ', $errors) . "\n",
FILE_APPEND
);
}
}
if ($action === 'delete_user_backup' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
$backup_file = basename((string)($params['backup_file'] ?? ''));
if ($u_selected_slot === '' || !in_array($u_selected_slot, $u_slot_names, true)) {
$errors[] = 'Nie wybrano poprawnej godziny backupu uzytkownika.';
}
$entry = empty($errors) ? find_user_backup_entry($u_databases, $backup_file) : [];
$backup_path = (string)($entry['file'] ?? '');
if (empty($errors) && ($backup_path === '' || !is_file($backup_path))) {
$errors[] = 'Nie znaleziono pliku dump dla wybranej bazy.';
}
if (empty($errors) && !is_path_within_dir($backup_path, $user_backup_runtime_dir)) {
$errors[] = 'Nieprawidlowa sciezka backupu uzytkownika.';
}
if (empty($errors) && !@unlink($backup_path)) {
$errors[] = 'Nie udalo sie usunac backupu uzytkownika.';
}
if (empty($errors)) {
$message_status = 'Backup uzytkownika zostal usuniety.';
$u_slot_index = collect_backup_slots_filtered($user_backup_runtime_dir, $da_user, $own_backup_user_restore);
$u_days = $u_slot_index['days'];
$u_available_months = [];
foreach ($u_days as $day_info) {
$u_available_months[substr($day_info['day_key'], 0, 7)] = true;
}
$u_available_months = array_keys($u_available_months);
rsort($u_available_months, SORT_STRING);
if (empty($u_days)) {
$u_selected_month = date('Y-m');
$u_selected_day = '';
$u_selected_slot = '';
$u_selected_db = '';
$u_day_slots = [];
$u_slot_names = [];
$u_databases = [];
$user_restore_empty_message = 'Brak kopii zapasowych użytkownika dostępnych do przywrócenia - najpierw utwórz kopię zapasową na zakładce Wykonaj Backup Baz danych';
} else {
// Po usunieciu backupu zawsze przechodzimy na najnowszy dostepny backup.
$u_selected_day = (string)array_key_first($u_days);
$u_selected_month = substr($u_selected_day, 0, 7);
$u_day_slots = $u_days[$u_selected_day]['slots'] ?? [];
$u_slot_names = array_map(function ($slot) {
return $slot['dir'];
}, $u_day_slots);
$u_selected_slot = !empty($u_slot_names) ? (string)$u_slot_names[0] : '';
$u_databases = $u_selected_slot !== '' ? list_user_databases($user_backup_runtime_dir, $u_selected_slot, $da_user, $own_backup_user_restore) : [];
$u_selected_db = !empty($u_databases) ? (string)($u_databases[0]['name'] ?? '') : '';
}
}
}
if ($action === 'restore_user' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
if ($u_selected_day === '' || !isset($u_days[$u_selected_day])) {
$errors[] = 'Nie wybrano poprawnej daty backupu uzytkownika.';
}
if ($u_selected_slot === '' || !in_array($u_selected_slot, $u_slot_names, true)) {
$errors[] = 'Nie wybrano poprawnej godziny backupu uzytkownika.';
}
$u_db_map = [];
foreach ($u_databases as $db) {
$u_db_map[$db['name']] = $db;
}
if ($u_selected_db === '' || !isset($u_db_map[$u_selected_db])) {
$errors[] = 'Nie wybrano poprawnej bazy danych.';
}
$target_db_for_restore = $u_selected_db;
if ($restore_mode === 'new_db') {
if ($target_restore_db === '' || !is_valid_target_db_name($target_restore_db, $da_user)) {
$errors[] = 'Nazwa docelowej bazy danych musi zaczynać się od prefiksu ' . $da_user . '_ (DA_USER_).';
} elseif ($target_restore_db === $u_selected_db) {
$errors[] = 'Docelowa baza danych musi byc inna niz zrodlowa baza z backupu.';
} elseif (!in_array($target_restore_db, $owned_user_dbs, true)) {
$errors[] = 'Wskazana docelowa baza danych nie istnieje. Utworz ja recznie w DirectAdmin.';
} else {
$has_tables = database_has_tables($target_restore_db);
if ($has_tables === null) {
$errors[] = 'Nie udalo sie sprawdzic, czy docelowa baza danych jest pusta.';
} elseif ($has_tables === true) {
$errors[] = 'Docelowa baza danych nie jest pusta. Usun dane przed kontynuowaniem.';
}
}
if (empty($errors)) {
$target_db_for_restore = $target_restore_db;
}
}
$queue_lock = null;
if (empty($errors)) {
$lock_error = '';
$queue_lock = acquire_jobs_queue_lock($job_base, $lock_error);
if ($queue_lock === null) {
$errors[] = $lock_error;
}
}
if (empty($errors)) {
$sim_error = '';
if (!check_database_collisions($settings, $plugin_dir, $job_base, $da_user, $sim_error, $target_db_for_restore)) {
$errors[] = $sim_error;
}
}
if (empty($errors)) {
$limit_error = '';
if (!reserve_operation_limit($plugin_data_dir, $da_user, 'restore', 1, $settings, $limit_error)) {
$errors[] = $limit_error;
}
}
if (empty($errors)) {
$job_dir = $job_base . '/pending';
if (!is_dir($job_dir) && !@mkdir($job_dir, 0700, true) && !is_dir($job_dir)) {
$errors[] = 'Nie mozna utworzyc katalogu zadan.';
} else {
$job_id = date('YmdHis') . '-' . bin2hex(random_bytes(4));
$job_file = $job_dir . '/restore-' . $job_id . '.env';
$dump_path = (string)($u_db_map[$u_selected_db]['file'] ?? '');
if ($dump_path === '' || !is_file($dump_path)) {
$errors[] = 'Nie znaleziono pliku dump dla wybranej bazy.';
} else {
$db_lock_error = '';
$db_lock_path = acquire_db_lock($plugin_dir, $target_db_for_restore, $job_id, 'restore', $db_lock_error);
if ($db_lock_path === '') {
$errors[] = $db_lock_error;
}
}
if (empty($errors)) {
$job_content = "DA_USER=" . escapeshellarg($da_user) . "\n";
$job_content .= "START_TS=" . escapeshellarg((string)time()) . "\n";
$job_content .= "DATE_DIR=" . escapeshellarg($u_selected_slot) . "\n";
$job_content .= "DB_NAME=" . escapeshellarg($target_db_for_restore) . "\n";
$job_content .= "SOURCE_DB_NAME=" . escapeshellarg($u_selected_db) . "\n";
$job_content .= "DUMP_FILE=" . escapeshellarg($dump_path) . "\n";
$job_content .= "DB_LOCK_FILE=" . escapeshellarg($db_lock_path) . "\n";
$job_content .= "BACKUP_SOURCE=" . escapeshellarg('user') . "\n";
$job_content .= "RESTORE_MODE=" . escapeshellarg($restore_mode) . "\n";
if (@file_put_contents($job_file, $job_content, LOCK_EX) === false) {
release_db_lock_file($db_lock_path);
$errors[] = 'Nie udalo sie zapisac pliku zadania.';
} else {
@chmod($job_file, 0600);
trigger_worker($plugin_dir);
$message_status = 'Zadanie przywracania backupu uzytkownika dodane do kolejki.';
}
}
}
}
release_jobs_queue_lock($queue_lock);
}
if ($action === 'restore_pre_backup' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
$file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($file);
if ($job_id === '' || !job_belongs_to_user($job_id, $job_base, $da_user)) {
$errors[] = 'Nie znaleziono zadania.';
} else {
$job_file_path = find_job_file($job_base, $file, $job_id);
$vars = $job_file_path !== '' ? load_job_vars($job_file_path) : [];
$db_name = trim((string)($vars['DB_NAME'] ?? ''));
$pre_backup_path = trim((string)($vars['PRE_BACKUP_PATH'] ?? ''));
if ($db_name === '') {
$errors[] = 'Brak nazwy bazy w zadaniu.';
} elseif ($pre_backup_path === '' || !is_file($pre_backup_path)) {
$errors[] = 'Backup przed przywroceniem nie istnieje.';
} elseif (!is_path_within_any_dir($pre_backup_path, [$pre_restore_dir, $pre_restore_user_dir])) {
$errors[] = 'Nieprawidlowa sciezka backupu przed przywroceniem.';
} else {
$queue_lock = null;
$lock_error = '';
$queue_lock = acquire_jobs_queue_lock($job_base, $lock_error);
if ($queue_lock === null) {
$errors[] = $lock_error;
}
if (empty($errors)) {
$sim_error = '';
if (!check_database_collisions($settings, $plugin_dir, $job_base, $da_user, $sim_error, $db_name)) {
$errors[] = $sim_error;
}
}
if (empty($errors)) {
$limit_error = '';
if (!reserve_operation_limit($plugin_data_dir, $da_user, 'restore', 1, $settings, $limit_error)) {
$errors[] = $limit_error;
}
}
if (empty($errors)) {
$job_dir = $job_base . '/pending';
if (!is_dir($job_dir) && !@mkdir($job_dir, 0700, true) && !is_dir($job_dir)) {
$errors[] = 'Nie mozna utworzyc katalogu zadan.';
} else {
$new_job_id = date('YmdHis') . '-' . bin2hex(random_bytes(4));
$new_job_file = $job_dir . '/restore-' . $new_job_id . '.env';
$db_lock_error = '';
$db_lock_path = acquire_db_lock($plugin_dir, $db_name, $new_job_id, 'restore', $db_lock_error);
if ($db_lock_path === '') {
$errors[] = $db_lock_error;
}
}
if (empty($errors)) {
$job_content = "DA_USER=" . escapeshellarg($da_user) . "\n";
$job_content .= "START_TS=" . escapeshellarg((string)time()) . "\n";
$job_content .= "DATE_DIR=" . escapeshellarg(basename($pre_backup_path)) . "\n";
$job_content .= "DB_NAME=" . escapeshellarg($db_name) . "\n";
$job_content .= "DUMP_FILE=" . escapeshellarg($pre_backup_path) . "\n";
$job_content .= "DB_LOCK_FILE=" . escapeshellarg($db_lock_path) . "\n";
$source = is_path_within_dir($pre_backup_path, $pre_restore_user_dir) ? 'user' : 'hosting';
$job_content .= "BACKUP_SOURCE=" . escapeshellarg($source) . "\n";
$job_content .= "RESTORE_MODE=" . escapeshellarg('overwrite') . "\n";
if (@file_put_contents($new_job_file, $job_content, LOCK_EX) === false) {
release_db_lock_file($db_lock_path);
$errors[] = 'Nie udalo sie zapisac pliku zadania.';
} else {
@chmod($new_job_file, 0600);
trigger_worker($plugin_dir);
$message_status = 'Dodano zadanie przywracania backupu przed przywroceniem.';
}
}
}
release_jobs_queue_lock($queue_lock);
}
}
}
if ($action === 'delete_pre_backup' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
$file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($file);
if ($job_id === '' || !job_belongs_to_user($job_id, $job_base, $da_user)) {
$errors[] = 'Nie znaleziono zadania.';
} else {
$job_file_path = find_job_file($job_base, $file, $job_id);
$vars = $job_file_path !== '' ? load_job_vars($job_file_path) : [];
$pre_backup_path = trim((string)($vars['PRE_BACKUP_PATH'] ?? ''));
if ($pre_backup_path === '' || !is_file($pre_backup_path)) {
$errors[] = 'Backup przed przywroceniem nie istnieje.';
} elseif (!is_path_within_any_dir($pre_backup_path, [$pre_restore_dir, $pre_restore_user_dir])) {
$errors[] = 'Nieprawidlowa sciezka backupu przed przywroceniem.';
} elseif (!@unlink($pre_backup_path)) {
$errors[] = 'Nie udalo sie usunac backupu przed przywroceniem.';
} else {
$message_status = 'Backup przed przywroceniem zostal usuniety.';
}
}
}
if ($action === 'cancel_job' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
$file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($file);
if ($job_id === '' || !job_belongs_to_user($job_id, $job_base, $da_user)) {
$errors[] = 'Nieprawidlowe zadanie.';
} elseif (!empty($errors)) {
} else {
$pending_file = $job_base . '/pending/' . $file;
$processing_file = $job_base . '/processing/' . $file;
$cancel_file = $cancel_dir . '/' . $job_id . '.flag';
if (is_file($pending_file)) {
release_db_lock_for_job_path($pending_file, $plugin_dir);
$target = $job_base . '/done/' . $job_id . '.cancel';
if (@rename($pending_file, $target)) {
$message_status = 'Zadanie zostalo anulowane.';
} else {
@unlink($pending_file);
$message_status = 'Zadanie zostalo anulowane.';
}
} elseif (!is_file($processing_file)) {
$errors[] = '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)) {
$errors[] = 'Nie znaleziono procesu do anulowania.';
} elseif (!function_exists('posix_kill')) {
$errors[] = 'Brak obsugi 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");
$message_status = 'Zadanie zostalo anulowane.';
} else {
$errors[] = 'Nie udalo sie anulowac zadania.';
}
}
}
}
}
if ($action === 'delete_selected' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
if ($prevent_log_deletion) {
$errors[] = 'Usuwanie logow i wpisow zadan jest zablokowane przez konfiguracje.';
} else {
$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);
if ($job_id === '' || !job_belongs_to_user($job_id, $job_base, $da_user)) {
continue;
}
foreach (['pending', 'processing', 'done'] as $dir) {
foreach (glob($job_base . '/' . $dir . '/' . $job_id . '*') ?: [] as $path) {
if (is_file($path)) {
if ($dir !== 'processing') {
release_db_lock_for_job_path($path, $plugin_dir);
}
@unlink($path);
}
}
}
@unlink($log_dir . '/' . $job_id . '.log');
@unlink($pid_dir . '/' . $job_id . '.pid');
@unlink($cancel_dir . '/' . $job_id . '.flag');
$deleted++;
}
$message_status = $deleted > 0 ? 'Wybrane zadania zostaly usuniete.' : 'Nie wybrano zadnych zadan.';
}
}
if ($action === 'delete_job' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
require_csrf($token, $plugin_data_dir, $da_user, $errors);
if ($prevent_log_deletion) {
$errors[] = 'Usuwanie logow i wpisow zadan jest zablokowane przez konfiguracje.';
} else {
$file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($file);
if ($job_id === '' || !job_belongs_to_user($job_id, $job_base, $da_user)) {
$errors[] = 'Nie znaleziono zadania.';
} else {
foreach (['pending', 'processing', 'done'] as $dir) {
foreach (glob($job_base . '/' . $dir . '/' . $job_id . '*') ?: [] as $path) {
if (is_file($path)) {
if ($dir !== 'processing') {
release_db_lock_for_job_path($path, $plugin_dir);
}
@unlink($path);
}
}
}
@unlink($log_dir . '/' . $job_id . '.log');
@unlink($pid_dir . '/' . $job_id . '.pid');
@unlink($cancel_dir . '/' . $job_id . '.flag');
$message_status = 'Zadanie zostalo usuniete.';
}
}
}
if ($action === 'download_log' && $method === 'POST') {
$token = (string)($params['csrf_token'] ?? '');
if (!check_csrf($token, $plugin_data_dir, $da_user)) {
header('HTTP/1.1 403 Forbidden');
exit;
}
$file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($file);
if ($job_id === '' || !job_belongs_to_user($job_id, $job_base, $da_user)) {
header('HTTP/1.1 404 Not Found');
exit;
}
$job_file_path = find_job_file($job_base, $job_id . '.env', $job_id);
$log_path = $log_dir . '/' . $job_id . '.log';
$content = build_log_download_content($job_id, $job_file_path, $log_path, $job_base);
if (!empty($lang_map)) {
$content = translate_text($content, $lang_map);
}
$raw = trim((string)($params['raw'] ?? '')) === '1';
if ($raw) {
log_text_output($content);
}
header('Content-Type: text/plain; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $job_id . '.log"');
echo $content;
exit;
}
$view_log_job = '';
$log_meta_start = '';
$log_meta_db = '';
$log_meta_date = '';
$log_meta_backup_kind = '';
$log_meta_restore_mode = '';
$log_meta_sanitization = 'Nie';
$log_status_value = '';
$log_message_view = '';
$log_progress = null;
$log_last_line = '-';
$view_log_status = '';
if ($action === 'view_log') {
$file = basename((string)($params['job'] ?? ''));
$job_id = job_id_from_file($file);
if ($job_id === '' || !job_belongs_to_user($job_id, $job_base, $da_user)) {
$errors[] = 'Nie znaleziono logu.';
} else {
$job_file_path = find_job_file($job_base, $job_id . '.env', $job_id);
$vars = load_job_vars($job_file_path);
$log_meta_start = format_datetime($vars['START_TS'] ?? '', time());
$log_meta_db = $vars['DB_NAME'] ?? '-';
$log_meta_date = format_backup_label((string)($vars['DATE_DIR'] ?? '-'));
$log_meta_backup_kind = backup_source_label(normalize_backup_source($vars));
$log_meta_restore_mode = restore_mode_label(normalize_restore_mode($vars), normalize_job_type($vars));
$log_meta_sanitization = sanitize_required_label($vars);
$status = get_job_status($job_id, $job_base);
$log_path = $log_dir . '/' . $job_id . '.log';
$log_message_view = normalize_log(read_log_tail($log_path, 2000));
$log_last_line = last_non_empty_line($log_message_view);
$display = display_status($status, $log_last_line);
$log_status_value = status_label($display);
$log_progress = extract_progress($log_last_line);
if (in_array($display, ['done', 'failed', 'canceled'], true)) {
$log_progress = $display === 'done' ? 100 : 0;
$log_last_line = status_label($display);
} elseif ($log_last_line === '') {
$log_last_line = '-';
}
$view_log_status = $status;
$view_log_job = $file;
}
$view_log_overlay = $enable_logs_overlay && (($params['overlay'] ?? '') === '1');
if ($view_log_overlay) {
header('Content-Type: text/html; charset=utf-8');
if ($view_log_job === '') {
echo translate_html_safe('<div class="br-overlay"><div class="br-modal"><div class="br-alert br-alert-error">Nie znaleziono logu.</div><div class="br-modal-actions"><button class="br-btn br-btn-ghost" type="button" data-close-overlay="1">Zamknij</button></div></div></div>', $lang_map ?? []);
} else {
$overlay_html = render_log_view_html(
$view_log_job,
$log_meta_start,
$log_meta_db,
$log_meta_date,
$log_meta_backup_kind,
$log_meta_restore_mode,
$log_meta_sanitization,
$log_status_value,
$log_progress,
$log_last_line,
$log_message_view,
$view_log_status,
true,
$base_url
);
echo translate_html_safe($overlay_html, $lang_map ?? []);
}
exit;
}
}
$jobs = array_merge(
list_jobs($job_base . '/pending', 'pending', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user, [$pre_restore_dir, $pre_restore_user_dir]),
list_jobs($job_base . '/processing', 'processing', '*.env', $log_dir, $pid_dir, $cancel_dir, $da_user, [$pre_restore_dir, $pre_restore_user_dir]),
list_jobs($job_base . '/done', 'done', '*.ok', $log_dir, $pid_dir, $cancel_dir, $da_user, [$pre_restore_dir, $pre_restore_user_dir]),
list_jobs($job_base . '/done', 'failed', '*.fail', $log_dir, $pid_dir, $cancel_dir, $da_user, [$pre_restore_dir, $pre_restore_user_dir]),
list_jobs($job_base . '/done', 'canceled', '*.cancel', $log_dir, $pid_dir, $cancel_dir, $da_user, [$pre_restore_dir, $pre_restore_user_dir])
);
usort($jobs, function ($a, $b) {
return $b['time'] <=> $a['time'];
});
if (count($jobs) > 50) {
$jobs = array_slice($jobs, 0, 50);
}
if ($action === 'status') {
header('Content-Type: text/html; charset=utf-8');
$status_html = render_jobs_section($jobs, $csrf_token, $base_url, $enable_logs_overlay, $prevent_log_deletion);
echo translate_html_safe($status_html, $lang_map ?? []);
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);
if ($job_id === '' || !job_belongs_to_user($job_id, $job_base, $da_user)) {
header('HTTP/1.1 403 Forbidden');
exit;
}
$log_path = $log_dir . '/' . $job_id . '.log';
$log_text = normalize_log(read_log_tail($log_path, 2000));
$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 . '%') : ($last_line !== '' ? $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;
}
$active_day_keys = array_keys($days);
$month_ts = strtotime($selected_month . '-01');
if ($month_ts === false) {
$month_ts = strtotime(date('Y-m-01'));
}
$calendar_days = (int)date('t', $month_ts);
$calendar_offset = (int)date('N', $month_ts);
$hosting_month_nav = month_nav_targets($available_months, $selected_month);
$hosting_weekdays = calendar_weekdays($lang_code);
ob_start();
?>
<html>
<head>
<meta charset="utf-8" />
<title>Manager kopii zapasowych baz danych</title>
<?php if ($inline_css !== '') { ?>
<style><?php echo $inline_css; ?></style>
<?php } ?>
</head>
<body>
<div class="br-container">
<div id="br-overlay-container"></div>
<?php if ($confirm_overlay_html !== '') { ?>
<?php echo $confirm_overlay_html; ?>
<?php } ?>
<div class="br-header">
<div class="br-title">Manager kopii zapasowych baz danych</div>
</div>
<div class="br-card">
<div class="br-main-tabs" id="main-backup-tabs">
<button
class="br-btn br-btn-ghost br-main-tab <?php echo $main_tab === 'hitme' ? 'is-active' : ''; ?>"
type="button"
data-main-tab="hitme"
>
Kopie zapasowe udostepniane przez HITME.PL
</button>
<?php if ($enable_user_backups) { ?>
<button
class="br-btn br-btn-ghost br-main-tab <?php echo $main_tab === 'user' ? 'is-active' : ''; ?>"
type="button"
data-main-tab="user"
>
Kopie zapasowe uzytkownika
</button>
<?php } ?>
</div>
</div>
<div class="br-card">
<h3>Tryb przywracania baz danych</h3>
<div class="br-actions-row" id="restore-mode-group">
<label><input type="radio" name="restore_mode_global" value="overwrite" <?php echo $restore_mode === 'overwrite' ? 'checked' : ''; ?> /> Nadpisanie istniejacej bazy danych (obecne dzialanie)</label>
<label><input type="radio" name="restore_mode_global" value="new_db" <?php echo $restore_mode === 'new_db' ? 'checked' : ''; ?> /> Przywrocenie wskazanej bazy do nowej bazy danych</label>
</div>
<div id="restore-mode-target-wrap" style="<?php echo $restore_mode === 'new_db' ? '' : 'display:none;'; ?>">
<label for="restore-mode-target-input"><strong>Nazwa docelowej bazy danych</strong></label>
<input type="text" id="restore-mode-target-input" value="<?php echo h($target_restore_db); ?>" placeholder="<?php echo h($da_user . '_mojabaza'); ?>" autocomplete="off" />
<div id="restore-mode-error" class="br-alert br-alert-error" style="display:none;"></div>
</div>
</div>
<?php if (!empty($errors) || !empty($warnings) || $message_status !== '' || ($action === 'view_log' && $view_log_job !== '')) { ?>
<div class="br-card">
<?php if (!empty($errors)) { ?>
<div class="br-alert br-alert-error">
<?php foreach ($errors as $err) { ?>
<p><?php echo h($err); ?></p>
<?php } ?>
</div>
<?php } ?>
<?php if (!empty($warnings)) { ?>
<div class="br-alert br-alert-warn">
<?php foreach ($warnings as $warn) { ?>
<p><?php echo h($warn); ?></p>
<?php } ?>
</div>
<?php } ?>
<?php if ($message_status !== '') { ?>
<div class="br-alert br-alert-success"><?php echo h($message_status); ?></div>
<?php } ?>
<?php if ($action === 'view_log' && $view_log_job !== '') { ?>
<?php echo render_log_view_html(
$view_log_job,
$log_meta_start,
$log_meta_db,
$log_meta_date,
$log_meta_backup_kind,
$log_meta_restore_mode,
$log_meta_sanitization,
$log_status_value,
$log_progress,
$log_last_line,
$log_message_view,
$view_log_status,
false,
$base_url
); ?>
<?php } ?>
</div>
<?php } ?>
<div class="br-main-sections" id="br-main-sections">
<div class="br-card br-main-section <?php echo $main_tab !== 'hitme' ? 'is-hidden' : ''; ?>" id="br-main-section-hitme" data-main-section="hitme">
<h3>Backupy udostepniane przez HITME.PL</h3>
<div class="br-restore-layout">
<div class="br-restore-calendar">
<h4>Kalendarz backupow</h4>
<?php if (empty($active_day_keys)) { ?>
<p class="br-muted">Brak aktywnych dat backupu dla tego uzytkownika.</p>
<?php } else { ?>
<div class="br-month-nav">
<form method="get" action="<?php echo h($base_url); ?>">
<input type="hidden" name="month" value="<?php echo h($hosting_month_nav['prev']); ?>" />
<input type="hidden" name="day" value="" />
<input type="hidden" name="slot" value="" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_restore_db, $main_tab); ?>
<button class="br-month-btn" type="submit" <?php echo $hosting_month_nav['prev'] === '' ? 'disabled' : ''; ?> title="Poprzedni miesiac" aria-label="Poprzedni miesiac">◀</button>
</form>
<div class="br-month-label"><?php echo h(format_month_label($selected_month, $lang_code)); ?></div>
<form method="get" action="<?php echo h($base_url); ?>">
<input type="hidden" name="month" value="<?php echo h($hosting_month_nav['next']); ?>" />
<input type="hidden" name="day" value="" />
<input type="hidden" name="slot" value="" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_restore_db, $main_tab); ?>
<button class="br-month-btn" type="submit" <?php echo $hosting_month_nav['next'] === '' ? 'disabled' : ''; ?> title="Nastepny miesiac" aria-label="Nastepny miesiac">▶</button>
</form>
</div>
<form method="get" action="<?php echo h($base_url); ?>" class="br-calendar-grid-wrap">
<input type="hidden" name="month" value="<?php echo h($selected_month); ?>" />
<input type="hidden" name="slot" value="" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_restore_db, $main_tab); ?>
<table class="br-calendar-grid">
<thead>
<tr>
<?php foreach ($hosting_weekdays as $weekday) { ?>
<th><?php echo h($weekday); ?></th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php
$day_num = 1;
for ($row = 0; $row < 6; $row++) {
echo '<tr>';
for ($col = 1; $col <= 7; $col++) {
if (($row === 0 && $col < $calendar_offset) || $day_num > $calendar_days) {
echo '<td class="br-cal-empty"></td>';
continue;
}
$day_key = date('Y-m-', $month_ts) . str_pad((string)$day_num, 2, '0', STR_PAD_LEFT);
$is_active = isset($days[$day_key]);
$is_selected = $day_key === $selected_day;
echo '<td>';
if ($is_active) {
echo '<button class="br-cal-day' . ($is_selected ? ' is-selected' : '') . '" type="submit" name="day" value="' . h($day_key) . '">' . h((string)$day_num) . '</button>';
} else {
echo '<span class="br-cal-day-disabled">' . h((string)$day_num) . '</span>';
}
echo '</td>';
$day_num++;
}
echo '</tr>';
if ($day_num > $calendar_days) {
break;
}
}
?>
</tbody>
</table>
</form>
<?php } ?>
</div>
<div class="br-restore-picker">
<h4>Wybierz backup i baze</h4>
<?php if ($selected_day === '' || empty($day_slots)) { ?>
<p class="br-muted">Wybierz aktywna date w kalendarzu.</p>
<?php } else { ?>
<?php if (count($day_slots) > 1) { ?>
<p class="br-muted">Data rozpoczecia generowania backupow</p>
<form method="get" action="<?php echo h($base_url); ?>" class="br-time-picker">
<input type="hidden" name="month" value="<?php echo h($selected_month); ?>" />
<input type="hidden" name="day" value="<?php echo h($selected_day); ?>" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_restore_db, $main_tab); ?>
<div class="br-time-list">
<?php foreach ($day_slots as $slot) { ?>
<label class="br-time-item">
<input type="radio" name="slot" value="<?php echo h($slot['dir']); ?>" <?php echo $slot['dir'] === $selected_slot ? 'checked' : ''; ?> onchange="this.form.submit();" />
<span><?php echo h($slot['time_label']); ?></span>
</label>
<?php } ?>
</div>
</form>
<?php } ?>
<?php if ($selected_slot !== '' && !empty($databases)) { ?>
<form method="post" action="<?php echo h($base_url); ?>" id="restore-form">
<input type="hidden" name="action" value="restore" />
<input type="hidden" name="month" value="<?php echo h($selected_month); ?>" />
<input type="hidden" name="day" value="<?php echo h($selected_day); ?>" />
<input type="hidden" name="slot" value="<?php echo h($selected_slot); ?>" />
<input type="hidden" name="csrf_token" value="<?php echo h($csrf_token); ?>" />
<?php echo render_restore_mode_hidden_fields($restore_mode, $target_restore_db, $main_tab); ?>
<table class="br-table" border="1" cellpadding="6" cellspacing="0">
<thead>
<tr>
<th></th>
<th>Baza danych</th>
<th>Rozmiar</th>
<th>Data i godzina backupu</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
<?php foreach ($databases as $db) { ?>
<?php $is_selected_row = $db['name'] === $selected_db; ?>
<?php $backup_file = basename((string)$db['file']); ?>
<tr>
<td>
<input type="radio" name="db" value="<?php echo h($db['name']); ?>" <?php echo $db['name'] === $selected_db ? 'checked' : ''; ?> required />
</td>
<td><?php echo h($db['name']); ?></td>
<td><?php echo h(number_format($db['size'] / 1024 / 1024, 2) . ' MB'); ?></td>
<td><?php echo h(format_datetime((string)$db['mtime'], (int)$db['mtime'])); ?></td>
<td>
<div class="br-actions-row">
<button
class="br-btn br-btn-small js-hitme-row-restore<?php echo $is_selected_row ? '' : ' br-btn-fogged'; ?>"
type="submit"
<?php echo $is_selected_row ? '' : 'disabled'; ?>
>
Przywróć kopię zapasową
</button>
<?php if ($allow_download_hitme_sql_backup) { ?>
<button class="br-btn br-btn-ghost br-btn-small js-download-hitme-backup" type="submit" name="download_hitme_backup" value="<?php echo h($backup_file); ?>" formnovalidate>Pobierz kopię zapasową</button>
<?php } ?>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</form>
<?php } elseif ($selected_slot !== '') { ?>
<p class="br-muted">Brak baz dla wybranego backupu.</p>
<?php } else { ?>
<p class="br-muted">Wybierz godzine backupu, aby zobaczyc liste baz.</p>
<?php } ?>
<?php } ?>
</div>
</div>
</div>
<?php if ($enable_user_backups) { ?>
<div class="br-card br-main-section <?php echo $main_tab !== 'user' ? 'is-hidden' : ''; ?>" id="br-main-section-user" data-main-section="user">
<h3>Backupy Uzytkownika</h3>
<div id="user-tabs">
<?php echo render_user_tabs(
$active_user_tab,
$owned_user_dbs,
$base_url,
$csrf_token,
$u_selected_month,
$u_selected_day,
$u_selected_slot,
$u_selected_db,
$u_available_months,
$u_days,
$u_day_slots,
$u_databases,
$lang_code,
$restore_mode,
$target_restore_db,
$main_tab,
$user_restore_empty_message
); ?>
</div>
</div>
<?php } ?>
</div>
<div class="br-card">
<h3>Status zadan</h3>
<div id="job-status">
<?php echo render_jobs_section($jobs, $csrf_token, $base_url, $enable_logs_overlay, $prevent_log_deletion); ?>
</div>
</div>
<div class="br-card">
<h3>Katalogi kopii zapasowych</h3>
<div class="br-path-legend">
<p class="br-muted"><strong>Backup przed przywroceniem (HITME.PL):</strong> <?php echo h($pre_restore_dir); ?></p>
<?php if ($enable_user_backups) { ?>
<p class="br-muted"><strong>Backupy uzytkownika:</strong> <?php echo h($user_backup_runtime_dir); ?></p>
<p class="br-muted"><strong>Backup przed przywroceniem (backupy uzytkownika):</strong> <?php echo h($pre_restore_user_dir); ?></p>
<?php } ?>
</div>
</div>
</div>
<script>
(function () {
var baseUrl = <?php echo json_encode($base_url, JSON_UNESCAPED_SLASHES); ?>;
var csrfToken = <?php echo json_encode($csrf_token); ?>;
var logsOverlayEnabled = <?php echo $enable_logs_overlay ? 'true' : 'false'; ?>;
var logOverlayJob = '';
var logOverlayTimer = null;
var pageLogJob = <?php echo json_encode($view_log_job); ?>;
var pageLogTimer = null;
var activeMainTab = <?php echo json_encode($main_tab); ?>;
var mainTabAnimTimer = null;
var userBackupsEnabled = <?php echo $enable_user_backups ? 'true' : 'false'; ?>;
function widenLayoutForEvolution() {
var root = document.querySelector('.br-container');
if (!root) { return; }
root.style.maxWidth = 'none';
root.style.width = '100%';
root.style.margin = '0';
root.style.padding = '16px 20px';
var parent = root.parentElement;
var hops = 0;
while (parent && parent !== document.body && hops < 12) {
parent.style.maxWidth = 'none';
parent.style.width = '100%';
var cs = window.getComputedStyle(parent);
if (cs.marginLeft === 'auto') { parent.style.marginLeft = '0'; }
if (cs.marginRight === 'auto') { parent.style.marginRight = '0'; }
hops++;
parent = parent.parentElement;
}
}
function normalizeJobId(job) {
if (!job) { return ''; }
var id = String(job).replace(/\.(ok|fail|cancel)$/i, '');
id = id.replace(/\.env$/i, '');
id = id.replace(/\.env\.(ok|fail|cancel)$/i, '');
return id;
}
function extractLogText(text) {
var marker = '__DB_RESTORE_LOG_TEXT__';
var start = text.indexOf(marker);
if (start === -1) { return null; }
start += marker.length;
var end = text.indexOf(marker, start);
if (end === -1) { return null; }
return text.substring(start, end);
}
function doCopyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(function () {});
return;
}
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.top = '-1000px';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
}
function normalizeMainTab(tab) {
if (tab === 'user' && userBackupsEnabled) {
return 'user';
}
return 'hitme';
}
function mainTabOrder(tab) {
return tab === 'user' ? 1 : 0;
}
function syncMainTabFields(tab) {
var forms = document.querySelectorAll('form');
for (var i = 0; i < forms.length; i++) {
var form = forms[i];
var field = form.querySelector('input[name="main_tab"]');
if (!field) {
field = document.createElement('input');
field.type = 'hidden';
field.name = 'main_tab';
field.className = 'js-main-tab-field';
form.appendChild(field);
}
field.value = tab;
}
}
function clearMainTabAnimation() {
if (mainTabAnimTimer) {
clearTimeout(mainTabAnimTimer);
mainTabAnimTimer = null;
}
var wrap = document.getElementById('br-main-sections');
if (wrap) {
wrap.classList.remove('is-animating');
wrap.style.minHeight = '';
}
var sections = document.querySelectorAll('[data-main-section]');
for (var i = 0; i < sections.length; i++) {
sections[i].classList.remove('br-anim-enter-from-left', 'br-anim-enter-from-right', 'br-anim-leave-to-left', 'br-anim-leave-to-right');
}
}
function setMainTabButtons(tab) {
var buttons = document.querySelectorAll('.br-main-tab');
for (var j = 0; j < buttons.length; j++) {
var btn = buttons[j];
btn.classList.toggle('is-active', btn.getAttribute('data-main-tab') === tab);
}
}
function showMainTabSection(tab) {
var sections = document.querySelectorAll('[data-main-section]');
for (var i = 0; i < sections.length; i++) {
var section = sections[i];
var sectionTab = section.getAttribute('data-main-section');
var hidden = sectionTab !== tab;
section.classList.toggle('is-hidden', hidden);
}
}
function applyMainTab(tab, persist, animate) {
tab = normalizeMainTab(tab);
var previousTab = normalizeMainTab(activeMainTab);
activeMainTab = tab;
clearMainTabAnimation();
setMainTabButtons(tab);
syncMainTabFields(tab);
if (!animate || previousTab === tab) {
showMainTabSection(tab);
return;
}
var fromSection = document.querySelector('[data-main-section="' + previousTab + '"]');
var toSection = document.querySelector('[data-main-section="' + tab + '"]');
if (!fromSection || !toSection) {
showMainTabSection(tab);
return;
}
if (fromSection === toSection) {
showMainTabSection(tab);
return;
}
var wrap = document.getElementById('br-main-sections');
if (wrap) {
wrap.classList.add('is-animating');
var nextHeight = Math.max(fromSection.offsetHeight, toSection.offsetHeight);
if (nextHeight > 0) {
wrap.style.minHeight = String(nextHeight) + 'px';
}
}
fromSection.classList.remove('is-hidden');
toSection.classList.remove('is-hidden');
var isForward = mainTabOrder(tab) > mainTabOrder(previousTab);
toSection.classList.add(isForward ? 'br-anim-enter-from-right' : 'br-anim-enter-from-left');
fromSection.classList.add(isForward ? 'br-anim-leave-to-left' : 'br-anim-leave-to-right');
mainTabAnimTimer = window.setTimeout(function () {
fromSection.classList.add('is-hidden');
clearMainTabAnimation();
}, 240);
}
function initMainTabs() {
var buttons = document.querySelectorAll('.br-main-tab');
if (!buttons.length) {
return;
}
var initial = normalizeMainTab(activeMainTab);
applyMainTab(initial, false, false);
for (var i = 0; i < buttons.length; i++) {
if (buttons[i].dataset.bound === '1') { continue; }
buttons[i].dataset.bound = '1';
buttons[i].addEventListener('click', function () {
applyMainTab(this.getAttribute('data-main-tab') || 'hitme', true, true);
});
}
}
function parseFilenameFromDisposition(disposition, fallback) {
var safeFallback = fallback || 'backup.sql.gz';
if (!disposition) { return safeFallback; }
var utf8 = disposition.match(/filename\\*=UTF-8''([^;]+)/i);
if (utf8 && utf8[1]) {
try {
return decodeURIComponent(utf8[1]);
} catch (e) {}
}
var plain = disposition.match(/filename=\"?([^\";]+)\"?/i);
if (plain && plain[1]) {
return plain[1];
}
return safeFallback;
}
function saveBlob(blob, filename) {
var link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename || 'backup.sql.gz';
document.body.appendChild(link);
link.click();
setTimeout(function () {
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
}, 0);
}
function bindHitmeBackupDownloadButtons() {
var form = document.getElementById('restore-form');
if (!form) { return; }
var buttons = form.querySelectorAll('.js-download-hitme-backup');
for (var i = 0; i < buttons.length; i++) {
if (buttons[i].dataset.boundDownload === '1') { continue; }
buttons[i].dataset.boundDownload = '1';
buttons[i].addEventListener('click', function (e) {
if (!window.fetch || !window.FormData || !window.URL || !window.URL.createObjectURL) {
return;
}
e.preventDefault();
var backupFile = String(this.value || '');
if (!backupFile) { return; }
var btn = this;
btn.disabled = true;
var data = new FormData(form);
data.set('action', 'download_hitme_backup');
data.set('backup_file', backupFile);
fetch(baseUrl, { method: 'POST', body: data, credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) {
throw new Error('HTTP ' + res.status);
}
var disposition = String(res.headers.get('Content-Disposition') || '');
return res.blob().then(function (blob) {
return { blob: blob, disposition: disposition };
});
})
.then(function (payload) {
if (!payload.blob || payload.blob.size === 0) {
throw new Error('Pusty plik odpowiedzi');
}
var filename = parseFilenameFromDisposition(payload.disposition, backupFile);
saveBlob(payload.blob, filename);
})
.catch(function (err) {
alert('Nie udało się pobrać backupu: ' + (err && err.message ? err.message : 'nieznany błąd'));
})
.finally(function () {
btn.disabled = false;
});
});
}
}
function bindOverlay(container) {
if (!container) { return; }
var overlays = container.querySelectorAll('.br-overlay');
for (var i = 0; i < overlays.length; i++) {
overlays[i].addEventListener('click', function (e) {
if (e.target === this) {
closeOverlay();
}
});
}
var closeButtons = container.querySelectorAll('[data-close-overlay]');
for (var j = 0; j < closeButtons.length; j++) {
closeButtons[j].addEventListener('click', function () {
closeOverlay();
});
}
}
function closeOverlay() {
var container = document.getElementById('br-overlay-container');
if (!container) { return; }
container.innerHTML = '';
if (logOverlayTimer) {
clearInterval(logOverlayTimer);
logOverlayTimer = null;
}
logOverlayJob = '';
}
function showOverlay(html) {
var container = document.getElementById('br-overlay-container');
if (!container) { return; }
container.innerHTML = html;
bindOverlay(container);
}
function refreshLogCommon(job, onUpdate, onComplete) {
if (!job) { return; }
fetch(baseUrl + '?action=log_stream&job=' + encodeURIComponent(job), { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.json(); })
.then(function (data) {
if (typeof onUpdate === 'function') {
onUpdate(data || {});
}
if (data && data.status && data.status !== 'processing' && typeof onComplete === 'function') {
onComplete();
}
})
.catch(function () {});
}
function refreshLogOverlay() {
if (!logOverlayJob) { return; }
refreshLogCommon(logOverlayJob, function (data) {
var el = document.getElementById('log-content');
if (el && typeof data.log === 'string') {
el.textContent = data.log;
el.scrollTop = el.scrollHeight;
}
var statusEl = document.getElementById('log-status');
if (statusEl && data.display_label) {
statusEl.textContent = data.display_label;
}
var progressTextEl = document.getElementById('log-progress-text');
if (progressTextEl) {
if (typeof data.progress_text === 'string') {
progressTextEl.textContent = data.progress_text;
} else if (typeof data.last_line === 'string') {
progressTextEl.textContent = data.last_line;
}
}
var bar = document.getElementById('log-progress-bar');
if (bar) {
var p = (typeof data.progress === 'number') ? data.progress : 0;
bar.style.width = p + '%';
}
}, function () {
if (logOverlayTimer) {
clearInterval(logOverlayTimer);
logOverlayTimer = null;
}
});
}
function refreshPageLog() {
if (!pageLogJob) { return; }
refreshLogCommon(pageLogJob, function (data) {
var el = document.getElementById('log-content');
if (el && typeof data.log === 'string') {
el.textContent = data.log;
el.scrollTop = el.scrollHeight;
}
var statusEl = document.getElementById('log-status');
if (statusEl && data.display_label) {
statusEl.textContent = data.display_label;
}
var progressTextEl = document.getElementById('log-progress-text');
if (progressTextEl) {
if (typeof data.progress_text === 'string') {
progressTextEl.textContent = data.progress_text;
} else if (typeof data.last_line === 'string') {
progressTextEl.textContent = data.last_line;
}
}
var bar = document.getElementById('log-progress-bar');
if (bar) {
var p = (typeof data.progress === 'number') ? data.progress : 0;
bar.style.width = p + '%';
}
}, function () {
if (pageLogTimer) {
clearInterval(pageLogTimer);
pageLogTimer = null;
}
});
}
window.downloadLog = function (job) {
if (!job) { return; }
var id = normalizeJobId(job);
var formData = new FormData();
formData.append('action', 'download_log');
formData.append('raw', '1');
formData.append('job', job);
formData.append('csrf_token', csrfToken || '');
fetch(baseUrl, { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var content = extractLogText(text || '');
if (content === null) {
content = text || '';
}
var blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = (id || 'log') + '.log';
document.body.appendChild(a);
a.click();
setTimeout(function () {
URL.revokeObjectURL(a.href);
document.body.removeChild(a);
}, 0);
})
.catch(function () {});
};
window.copyLogToClipboard = function () {
var job = logOverlayJob || pageLogJob || '';
var el = document.getElementById('log-content');
if (!job) {
if (!el) { return; }
doCopyText(el.textContent || '');
return;
}
var formData = new FormData();
formData.append('action', 'download_log');
formData.append('raw', '1');
formData.append('job', job);
formData.append('csrf_token', csrfToken || '');
fetch(baseUrl, { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function (r) { return r.text(); })
.then(function (text) {
var content = extractLogText(text || '');
if (content === null) {
content = text || '';
}
doCopyText(content);
})
.catch(function () {
if (el) { doCopyText(el.textContent || ''); }
});
};
window.openLogOverlay = function (job) {
if (!job) { return; }
if (!logsOverlayEnabled) {
window.location.href = baseUrl + '?action=view_log&job=' + encodeURIComponent(job);
return;
}
logOverlayJob = job;
fetch(baseUrl + '?action=view_log&overlay=1&job=' + encodeURIComponent(job), { credentials: 'same-origin', cache: 'no-store' })
.then(function (r) { return r.text(); })
.then(function (html) {
showOverlay(html);
refreshLogOverlay();
if (logOverlayTimer) {
clearInterval(logOverlayTimer);
}
logOverlayTimer = setInterval(refreshLogOverlay, 3000);
})
.catch(function () {});
};
var statusEl = document.getElementById('job-status');
var busy = false;
function refreshStatus() {
if (!statusEl) { return; }
if (busy) { return; }
busy = true;
fetch(baseUrl + '?action=status', { credentials: 'same-origin', cache: 'no-store' })
.then(function (res) {
if (!res.ok) { return ''; }
return res.text();
})
.then(function (html) {
if (html) {
statusEl.innerHTML = html;
syncMainTabFields(activeMainTab);
}
})
.catch(function () {})
.finally(function () { busy = false; });
}
bindOverlay(document.getElementById('br-overlay-container'));
initMainTabs();
bindHitmeBackupDownloadButtons();
widenLayoutForEvolution();
window.addEventListener('dbrestore:tab-updated', function () {
syncMainTabFields(activeMainTab);
});
if (pageLogJob) {
pageLogTimer = setInterval(refreshPageLog, 3000);
}
setInterval(refreshStatus, 3000);
window.addEventListener('resize', widenLayoutForEvolution);
})();
(function () {
var daUserPrefix = <?php echo json_encode($da_user . '_'); ?>;
var modeRadios = document.querySelectorAll('input[name="restore_mode_global"]');
var targetWrap = document.getElementById('restore-mode-target-wrap');
var targetInput = document.getElementById('restore-mode-target-input');
var errorBox = document.getElementById('restore-mode-error');
function currentMode() {
var checked = document.querySelector('input[name="restore_mode_global"]:checked');
return checked ? checked.value : 'overwrite';
}
function currentTargetDb() {
return targetInput ? String(targetInput.value || '').trim() : '';
}
function ensureTargetPrefix() {
if (!targetInput) { return; }
var value = String(targetInput.value || '').trim();
if (!value) {
targetInput.value = daUserPrefix;
return;
}
if (value.indexOf(daUserPrefix) !== 0) {
targetInput.value = daUserPrefix + value.replace(/^[^_]*_?/, '');
}
}
function targetNameValid(name) {
if (!name) { return false; }
var escapedPrefix = daUserPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
var re = new RegExp('^' + escapedPrefix + '[A-Za-z0-9_]+$');
return re.test(name);
}
function syncHiddenFields() {
var mode = currentMode();
var target = currentTargetDb();
var modeFields = document.querySelectorAll('.js-restore-mode-field');
for (var i = 0; i < modeFields.length; i++) {
modeFields[i].value = mode;
}
var targetFields = document.querySelectorAll('.js-target-db-field');
for (var j = 0; j < targetFields.length; j++) {
targetFields[j].value = target;
}
}
function validateModeSection(showMessage, sourceDb) {
if (errorBox) {
errorBox.style.display = 'none';
errorBox.textContent = '';
}
if (currentMode() !== 'new_db') {
return true;
}
var val = currentTargetDb();
if (!targetNameValid(val)) {
if (showMessage && errorBox) {
errorBox.textContent = 'Nazwa docelowej bazy danych musi zaczynać się od prefiksu ' + daUserPrefix + ' (DA_USER_).';
errorBox.style.display = 'block';
}
return false;
}
if (sourceDb && val === sourceDb) {
if (showMessage && errorBox) {
errorBox.textContent = 'Docelowa baza danych musi być inna niż źródłowa baza z backupu.';
errorBox.style.display = 'block';
}
return false;
}
return true;
}
function enforceModeUi() {
var mode = currentMode();
if (targetWrap) {
targetWrap.style.display = mode === 'new_db' ? '' : 'none';
}
if (mode === 'new_db') {
ensureTargetPrefix();
}
var backupTabBtn = document.querySelector('.br-user-tab[data-user-tab="backup_create"]');
if (backupTabBtn) {
backupTabBtn.style.display = mode === 'new_db' ? 'none' : '';
}
if (mode === 'new_db') {
var activeBackup = document.querySelector('.br-user-tab.is-active[data-user-tab="backup_create"]');
var restoreTabBtn = document.querySelector('.br-user-tab[data-user-tab="restore_user"]');
if (activeBackup && restoreTabBtn) {
restoreTabBtn.click();
}
}
syncHiddenFields();
}
for (var i = 0; i < modeRadios.length; i++) {
modeRadios[i].addEventListener('change', function () {
enforceModeUi();
validateModeSection(false);
});
}
if (targetInput) {
targetInput.addEventListener('input', function () {
syncHiddenFields();
validateModeSection(false);
});
targetInput.addEventListener('blur', function () {
ensureTargetPrefix();
syncHiddenFields();
});
}
document.addEventListener('submit', function (e) {
var form = e.target;
if (!form || form.tagName !== 'FORM') { return; }
syncHiddenFields();
var submitterName = '';
if (e.submitter && e.submitter.name) {
submitterName = String(e.submitter.name);
}
if (submitterName === 'download_user_backup' || submitterName === 'download_hitme_backup' || submitterName === 'delete_user_backup') {
return;
}
var actionField = form.querySelector('input[name="action"]');
var actionValue = actionField ? String(actionField.value || '') : '';
if (actionValue === 'restore' || actionValue === 'restore_user') {
var sourceDb = '';
if (actionValue === 'restore') {
var src = form.querySelector('input[name="db"]:checked');
sourceDb = src ? String(src.value || '') : '';
} else if (actionValue === 'restore_user') {
var usrc = form.querySelector('input[name="u_db"]:checked');
sourceDb = usrc ? String(usrc.value || '') : '';
}
if (!validateModeSection(true, sourceDb)) {
e.preventDefault();
}
}
}, true);
window.__dbRestoreMode = {
getMode: currentMode,
getTargetDb: currentTargetDb,
sync: syncHiddenFields,
validate: validateModeSection
};
enforceModeUi();
})();
(function () {
function bindRowRestoreActions(formId, radioName, buttonSelector) {
var form = document.getElementById(formId);
if (!form) { return; }
var buttons = form.querySelectorAll(buttonSelector);
if (!buttons.length) { return; }
function setButtonState(btn, isActive) {
btn.disabled = !isActive;
if (isActive) {
btn.classList.remove('br-btn-fogged');
btn.setAttribute('aria-disabled', 'false');
} else {
if (!btn.classList.contains('br-btn-fogged')) {
btn.classList.add('br-btn-fogged');
}
btn.setAttribute('aria-disabled', 'true');
}
}
function updateButtons() {
var checked = form.querySelector('input[name="' + radioName + '"]:checked');
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
var row = btn.closest('tr');
var rowRadio = row ? row.querySelector('input[name="' + radioName + '"]') : null;
var isActive = !!(checked && rowRadio && checked === rowRadio);
setButtonState(btn, isActive);
}
}
if (form.dataset.rowRestoreBound === '1') {
if (typeof form.__rowRestoreUpdate === 'function') {
form.__rowRestoreUpdate();
}
return;
}
form.dataset.rowRestoreBound = '1';
form.__rowRestoreUpdate = updateButtons;
form.addEventListener('change', updateButtons);
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function (e) {
if (this.disabled) {
e.preventDefault();
return;
}
var row = this.closest('tr');
var rowRadio = row ? row.querySelector('input[name="' + radioName + '"]') : null;
if (rowRadio) {
rowRadio.checked = true;
}
updateButtons();
});
}
updateButtons();
}
bindRowRestoreActions('restore-form', 'db', '.js-hitme-row-restore');
bindRowRestoreActions('user-restore-form', 'u_db', '.js-user-row-restore');
window.addEventListener('dbrestore:tab-updated', function () {
bindRowRestoreActions('restore-form', 'db', '.js-hitme-row-restore');
bindRowRestoreActions('user-restore-form', 'u_db', '.js-user-row-restore');
});
})();
(function () {
var baseUrl = <?php echo json_encode($base_url, JSON_UNESCAPED_SLASHES); ?>;
var userTabsEl = document.getElementById('user-tabs');
if (!userTabsEl) { return; }
function withRestoreMode(query) {
var params = new URLSearchParams(query);
if (window.__dbRestoreMode) {
params.set('restore_mode', window.__dbRestoreMode.getMode());
params.set('target_db', window.__dbRestoreMode.getTargetDb());
}
return params.toString();
}
function parseFilenameFromDisposition(disposition, fallback) {
var safeFallback = fallback || 'backup.sql.gz';
if (!disposition) { return safeFallback; }
var utf8 = disposition.match(/filename\\*=UTF-8''([^;]+)/i);
if (utf8 && utf8[1]) {
try {
return decodeURIComponent(utf8[1]);
} catch (e) {}
}
var plain = disposition.match(/filename=\"?([^\";]+)\"?/i);
if (plain && plain[1]) {
return plain[1];
}
return safeFallback;
}
function saveBlob(blob, filename) {
var link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename || 'backup.sql.gz';
document.body.appendChild(link);
link.click();
setTimeout(function () {
URL.revokeObjectURL(link.href);
document.body.removeChild(link);
}, 0);
}
function bindBackupSelection() {
var form = userTabsEl.querySelector('#user-backup-form');
if (!form) { return; }
var checks = form.querySelectorAll('.js-user-db-check');
var bulkBtn = form.querySelector('#bulk-user-backup-btn');
if (!bulkBtn) { return; }
function updateBulkButton() {
var checked = 0;
for (var i = 0; i < checks.length; i++) {
if (checks[i].checked) { checked++; }
}
bulkBtn.style.display = checked >= 2 ? 'inline-block' : 'none';
}
for (var i = 0; i < checks.length; i++) {
checks[i].addEventListener('change', updateBulkButton);
}
updateBulkButton();
}
function bindUserBackupDownloadButtons() {
var form = userTabsEl.querySelector('#user-restore-form');
if (!form) { return; }
var buttons = form.querySelectorAll('.js-download-user-backup');
for (var i = 0; i < buttons.length; i++) {
if (buttons[i].dataset.boundDownload === '1') { continue; }
buttons[i].dataset.boundDownload = '1';
buttons[i].addEventListener('click', function (e) {
if (!window.fetch || !window.FormData || !window.URL || !window.URL.createObjectURL) {
return;
}
e.preventDefault();
var backupFile = String(this.value || '');
if (!backupFile) { return; }
var btn = this;
btn.disabled = true;
var data = new FormData(form);
data.set('action', 'download_user_backup');
data.set('backup_file', backupFile);
fetch(baseUrl, { method: 'POST', body: data, credentials: 'same-origin' })
.then(function (res) {
if (!res.ok) {
throw new Error('HTTP ' + res.status);
}
var disposition = String(res.headers.get('Content-Disposition') || '');
return res.blob().then(function (blob) {
return { blob: blob, disposition: disposition };
});
})
.then(function (payload) {
if (!payload.blob || payload.blob.size === 0) {
throw new Error('Pusty plik odpowiedzi');
}
var filename = parseFilenameFromDisposition(payload.disposition, backupFile);
saveBlob(payload.blob, filename);
})
.catch(function (err) {
alert('Nie udało się pobrać backupu: ' + (err && err.message ? err.message : 'nieznany błąd'));
})
.finally(function () {
btn.disabled = false;
});
});
}
}
function bindUserRestoreSlotPicker() {
var radios = userTabsEl.querySelectorAll('form.br-time-picker input[type="radio"][name="u_slot"]');
for (var i = 0; i < radios.length; i++) {
if (radios[i].dataset.bound === '1') { continue; }
radios[i].dataset.bound = '1';
radios[i].addEventListener('change', function () {
if (!this.checked) { return; }
var form = this.form;
if (!form) { return; }
var params = new URLSearchParams(new FormData(form));
loadUserTab(withRestoreMode(params.toString()), 0);
});
}
}
function userTabOrder(tab) {
return tab === 'restore_user' ? 1 : 0;
}
function getActiveUserTab() {
var active = userTabsEl.querySelector('.br-user-tab.is-active');
if (!active) {
return 'backup_create';
}
var tab = active.getAttribute('data-user-tab') || 'backup_create';
return tab === 'restore_user' ? 'restore_user' : 'backup_create';
}
function animateUserTabPanel(direction) {
if (!direction) { return; }
var panel = userTabsEl.querySelector('#user-tab-content');
if (!panel) { return; }
panel.classList.remove('br-anim-enter-from-left', 'br-anim-enter-from-right');
void panel.offsetWidth;
panel.classList.add(direction > 0 ? 'br-anim-enter-from-right' : 'br-anim-enter-from-left');
window.setTimeout(function () {
panel.classList.remove('br-anim-enter-from-left', 'br-anim-enter-from-right');
}, 240);
}
function bindUserTabButtons() {
var buttons = userTabsEl.querySelectorAll('.br-user-tab');
for (var i = 0; i < buttons.length; i++) {
if (buttons[i].dataset.bound === '1') { continue; }
buttons[i].dataset.bound = '1';
buttons[i].addEventListener('click', function () {
var tab = this.getAttribute('data-user-tab') || 'backup_create';
var currentTab = getActiveUserTab();
var direction = userTabOrder(tab) - userTabOrder(currentTab);
loadUserTab(withRestoreMode('action=user_tab&user_tab=' + encodeURIComponent(tab)), direction);
});
}
}
function loadUserTab(query, direction) {
var animDirection = (typeof direction === 'number') ? direction : 0;
fetch(baseUrl + '?' + query, { credentials: 'same-origin', cache: 'no-store' })
.then(function (res) {
if (!res.ok) { return ''; }
return res.text();
})
.then(function (html) {
if (!html) { return; }
userTabsEl.innerHTML = html;
if (window.__dbRestoreMode) {
window.__dbRestoreMode.sync();
}
bindUserTabButtons();
bindBackupSelection();
bindUserBackupDownloadButtons();
bindUserRestoreSlotPicker();
animateUserTabPanel(animDirection);
var evt = new Event('dbrestore:tab-updated');
window.dispatchEvent(evt);
})
.catch(function () {});
}
userTabsEl.addEventListener('submit', function (e) {
var form = e.target;
if (!form || form.tagName !== 'FORM') { return; }
var actionField = form.querySelector('input[name="action"]');
if (!actionField || actionField.value !== 'user_tab') { return; }
e.preventDefault();
var params = new URLSearchParams(new FormData(form));
loadUserTab(withRestoreMode(params.toString()), 0);
});
bindUserTabButtons();
bindBackupSelection();
bindUserBackupDownloadButtons();
bindUserRestoreSlotPicker();
})();
</script>
</body>
</html>
<?php
$page = (string)ob_get_clean();
echo translate_html_safe($page, $lang_map ?? []);