#!/usr/local/cpanel/3rdparty/bin/perl # gniza WHM Plugin — Restore # Step-by-step restore workflow with dynamic dropdowns use strict; use warnings; use lib '/usr/local/cpanel/whostmgr/docroot/cgi/gniza-whm/lib'; use Whostmgr::HTMLInterface (); use Cpanel::Form (); use GnizaWHM::Config; use GnizaWHM::Runner; use GnizaWHM::UI; my $form = Cpanel::Form::parseform(); my $method = $ENV{'REQUEST_METHOD'} // 'GET'; my $step = $form->{'restore_step'} // '1'; my %TYPE_LABELS = ( account => 'Full Account', files => 'Files', database => 'Database', mailbox => 'Mailbox', cron => 'Cron Jobs', dbusers => 'Database Users & Grants', cpconfig => 'Panel Config', domains => 'Domains', ssl => 'SSL Certificates', ); my %SIMPLE_TYPES = map { $_ => 1 } qw(account cron cpconfig); if ($step eq 'fetch_options') { handle_fetch_options() } elsif ($step eq '2') { handle_step2() } elsif ($step eq '3') { handle_step3() } elsif ($step eq '4') { handle_step4() } else { handle_step1() } exit; # ── Helpers ─────────────────────────────────────────────────── sub _uri_escape { my $str = shift // ''; $str =~ s/([^A-Za-z0-9\-._~])/sprintf("%%%02X", ord($1))/ge; return $str; } # ── JSON endpoint: fetch database/mailbox options ───────────── sub handle_fetch_options { my $remote = $form->{'remote'} // ''; my $account = $form->{'account'} // ''; my $timestamp = $form->{'timestamp'} // ''; my $type = $form->{'type'} // ''; print "Content-Type: application/json\r\n\r\n"; if ($remote eq '' || $account eq '' || $timestamp eq '' || $type eq '') { print qq({"error":"Missing required parameters"}); return; } my $subcmd; my %extra_opts; if ($type eq 'database') { $subcmd = 'list-databases' } elsif ($type eq 'mailbox') { $subcmd = 'list-mailboxes' } elsif ($type eq 'dbusers') { $subcmd = 'list-dbusers' } elsif ($type eq 'cron') { $subcmd = 'list-cron' } elsif ($type eq 'domains') { $subcmd = 'list-dns' } elsif ($type eq 'ssl') { $subcmd = 'list-ssl' } elsif ($type eq 'files') { $subcmd = 'list-files'; my $path = $form->{'path'} // ''; $extra_opts{path} = $path if $path ne ''; } else { print qq({"error":"Invalid type"}); return; } my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run( 'restore', $subcmd, [$account], { remote => $remote, timestamp => $timestamp, %extra_opts } ); unless ($ok) { my $msg = $stderr // 'Failed to list options'; $msg =~ s/\\/\\\\/g; $msg =~ s/"/\\"/g; $msg =~ s/\n/\\n/g; $msg =~ s/\r/\\r/g; print qq({"error":"$msg"}); return; } my @options; for my $line (split /\n/, $stdout) { $line =~ s/^\s+|\s+$//g; next unless $line ne ''; push @options, $line; } # Build JSON array manually (no JSON module dependency) my $json_arr = join(',', map { my $v = $_; $v =~ s/\\/\\\\/g; $v =~ s/"/\\"/g; qq("$v") } @options); print qq({"options":[$json_arr]}); } # ── Step 1: Select Account + Remote ───────────────────────── sub handle_step1 { print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Restore', '', '/cgi/gniza-whm/restore.cgi'); print GnizaWHM::UI::page_header('Restore from Backup'); print GnizaWHM::UI::render_nav('restore.cgi'); print GnizaWHM::UI::render_flash(); my @remotes = GnizaWHM::UI::list_remotes(); unless (@remotes) { print qq{
No remotes configured. Add a remote first.
\n}; print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); return; } print qq{
\n}; print qq{\n}; print qq{
\n
\n}; print qq{

Step 1: Select Source

\n}; # Remote dropdown print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; # Account input my @accounts = GnizaWHM::UI::get_cpanel_accounts(); print qq{
\n}; print qq{ \n}; if (@accounts) { print qq{ \n}; } else { print qq{ \n}; } print qq{
\n}; print qq{
\n
\n}; print qq{
\n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Step 2: Select Snapshot + Restore Type ─────────────────── sub handle_step2 { my $remote = $form->{'remote'} // ''; my $account = $form->{'account'} // ''; if ($remote eq '' || $account eq '') { GnizaWHM::UI::set_flash('error', 'Remote and account are required.'); print "Status: 302 Found\r\n"; print "Location: restore.cgi\r\n\r\n"; exit; } # Fetch snapshots via Runner my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('list', undef, [], { remote => $remote, account => $account }); print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Restore', '', '/cgi/gniza-whm/restore.cgi'); print GnizaWHM::UI::page_header('Restore from Backup'); print GnizaWHM::UI::render_nav('restore.cgi'); print GnizaWHM::UI::render_flash(); my $esc_remote = GnizaWHM::UI::esc($remote); my $esc_account = GnizaWHM::UI::esc($account); unless ($ok) { my $msg = GnizaWHM::UI::esc($stderr || 'Failed to list snapshots'); print qq{
$msg
\n}; print qq{\n}; print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); return; } # Parse snapshot timestamps from output my @snapshots; for my $line (split /\n/, $stdout) { if ($line =~ /^\s+(\d{4}-\d{2}-\d{2}T\d{6})/) { push @snapshots, $1; } } print qq{
\n}; print qq{\n}; print qq{\n}; print qq{\n}; print qq{
\n
\n}; print qq{

Step 2: Choose Restore Options

\n}; print qq{

Account: $esc_account on remote $esc_remote

\n}; # Snapshot dropdown print qq{
\n}; print qq{ \n}; if (@snapshots) { print qq{ \n}; } else { print qq{ No snapshots found\n}; } print qq{
\n}; # Restore mode toggle: Full Account vs Selective print qq{
\n}; print qq{ \n}; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; # Restore strategy (visible only for Full Account mode) print qq{
\n}; print qq{ \n}; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; # Exclude paths (visible in both Full Account and Selective modes) print qq{
\n}; print qq{
\n}; print qq{

Directories and Files to Exclude

\n}; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{

Exclude files and directories from restoration

\n}; print qq{
\n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; # Exclude modal print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; # Hidden field that always carries account type when Full is selected print qq{\n}; # Selective type buttons (hidden by default) my @selective_types = ( ['files', 'Files'], ['database', 'Database'], ['dbusers', 'Database Users'], ['mailbox', 'Mailbox'], ['cron', 'Cron'], ['cpconfig', 'Config'], ['domains', 'Domains'], ['ssl', 'SSL'], ); print qq{\n}; # File browser modal print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; print qq{
\n
\n}; if (@snapshots) { print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; } else { print qq{\n}; } print qq{
\n}; # JavaScript for dynamic dropdowns print < var gnizaCache = {}; var gnizaRemote = '$esc_remote'; var gnizaAccount = '$esc_account'; var fbCache = {}; var fbSelected = ''; function gnizaSnapshotChange() { gnizaCache = {}; fbCache = {}; gnizaModeChanged(); } function gnizaModeChanged() { var mode = document.querySelector('input[name="restore_mode"]:checked').value; var selective = mode === 'selective'; document.getElementById('selective-panel').hidden = !selective; document.getElementById('strategy-panel').hidden = selective; document.getElementById('type_account_hidden').disabled = selective; if (selective) { gnizaTypesChanged(); } else { var panels = ['field-path','field-dbname','field-email','field-dbusers','field-cron','field-domains','field-ssl']; for (var i = 0; i < panels.length; i++) { document.getElementById(panels[i]).hidden = true; } } } function gnizaTypesChanged() { var types = { files: 'field-path', database: 'field-dbname', mailbox: 'field-email', dbusers: 'field-dbusers', cron: 'field-cron', domains: 'field-domains', ssl: 'field-ssl' }; for (var t in types) { var el = document.querySelector('input[name="type_' + t + '"]'); document.getElementById(types[t]).hidden = !(el && el.checked); } if (document.querySelector('input[name="type_database"]').checked) { gnizaLoadOptions('database', 'dbname-list', 'dbnames'); } if (document.querySelector('input[name="type_mailbox"]').checked) { gnizaLoadOptions('mailbox', 'email-list', 'emails'); } if (document.querySelector('input[name="type_dbusers"]').checked) { gnizaLoadOptions('dbusers', 'dbusers-list', 'dbuser_names'); } if (document.querySelector('input[name="type_cron"]').checked) { gnizaLoadPreview('cron', 'cron-list'); } if (document.querySelector('input[name="type_domains"]').checked) { gnizaLoadOptions('domains', 'domains-list', 'domain_names'); } if (document.querySelector('input[name="type_ssl"]').checked) { gnizaLoadOptions('ssl', 'ssl-list', 'ssl_names'); } } function gnizaLoadOptions(type, containerId, hiddenId) { var ts = document.getElementById('timestamp').value; var cacheKey = type + ':' + ts; if (gnizaCache[cacheKey]) { gnizaPopulateChecklist(containerId, hiddenId, gnizaCache[cacheKey]); return; } var container = document.getElementById(containerId); container.innerHTML = ' Loading...'; document.getElementById(hiddenId).value = ''; var url = 'restore.cgi?restore_step=fetch_options' + '&remote=' + encodeURIComponent(gnizaRemote) + '&account=' + encodeURIComponent(gnizaAccount) + '×tamp=' + encodeURIComponent(ts) + '&type=' + encodeURIComponent(type); var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; if (xhr.status === 200) { try { var data = JSON.parse(xhr.responseText); if (data.error) { container.innerHTML = 'Error: ' + data.error + ''; } else { gnizaCache[cacheKey] = data.options; gnizaPopulateChecklist(containerId, hiddenId, data.options); } } catch(e) { container.innerHTML = 'Failed to parse response'; } } else { container.innerHTML = 'Request failed'; } }; xhr.send(); } function gnizaPopulateChecklist(containerId, hiddenId, options) { var container = document.getElementById(containerId); var hidden = document.getElementById(hiddenId); hidden.value = ''; if (!options || options.length === 0) { container.innerHTML = '(none found)'; return; } var allLabels = {'dbname-list':'All Databases','dbusers-list':'All Database Users','email-list':'All Mailboxes','domains-list':'All Domains','ssl-list':'All Certificates'}; var allLabel = allLabels[containerId] || 'All'; var html = ''; for (var i = 0; i < options.length; i++) { var v = options[i].replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); html += ''; } container.innerHTML = html; } function gnizaToggleAll(containerId, hiddenId, checked) { var container = document.getElementById(containerId); var hidden = document.getElementById(hiddenId); var items = container.querySelectorAll('input[data-item]'); for (var i = 0; i < items.length; i++) { items[i].disabled = checked; if (checked) items[i].checked = false; } hidden.value = checked ? '__ALL__' : ''; } function gnizaSyncHidden(containerId, hiddenId) { var container = document.getElementById(containerId); var hidden = document.getElementById(hiddenId); var items = container.querySelectorAll('input[data-item]:checked'); var vals = []; for (var i = 0; i < items.length; i++) { vals.push(items[i].value); } hidden.value = vals.join(','); } function gnizaLoadPreview(type, containerId) { var ts = document.getElementById('timestamp').value; var cacheKey = type + ':' + ts; if (gnizaCache[cacheKey]) { gnizaPopulatePreview(containerId, gnizaCache[cacheKey], type); return; } var container = document.getElementById(containerId); container.innerHTML = ' Loading...'; var url = 'restore.cgi?restore_step=fetch_options' + '&remote=' + encodeURIComponent(gnizaRemote) + '&account=' + encodeURIComponent(gnizaAccount) + '×tamp=' + encodeURIComponent(ts) + '&type=' + encodeURIComponent(type); var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; if (xhr.status === 200) { try { var data = JSON.parse(xhr.responseText); if (data.error) { container.innerHTML = 'Error: ' + data.error + ''; } else { gnizaCache[cacheKey] = data.options; gnizaPopulatePreview(containerId, data.options, type); } } catch(e) { container.innerHTML = 'Failed to parse response'; } } else { container.innerHTML = 'Request failed'; } }; xhr.send(); } function gnizaPopulatePreview(containerId, options, type) { var container = document.getElementById(containerId); if (!options || options.length === 0) { container.innerHTML = '(none found)'; return; } if (type === 'cron') { var html = '
';
        for (var i = 0; i < options.length; i++) {
            html += options[i].replace(/&/g,'&').replace(//g,'>') + '\\n';
        }
        html += '
'; container.innerHTML = html; } else { var html = ''; container.innerHTML = html; } } function gnizaAddExclude() { var input = document.getElementById('exclude-input'); var val = input.value.trim(); if (!val) return; gnizaAddExcludeTag(val); input.value = ''; gnizaUpdateExcludeField(); } function gnizaAddExcludeTag(text) { var container = document.getElementById('exclude-tags'); // Skip duplicates var existing = container.querySelectorAll('.badge span'); for (var i = 0; i < existing.length; i++) { if (existing[i].textContent === text) return; } var badge = document.createElement('span'); badge.className = 'badge badge-warning gap-1'; var span = document.createElement('span'); span.textContent = text; badge.appendChild(span); var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-xs btn-ghost btn-circle'; btn.innerHTML = '\\u2715'; btn.onclick = function() { badge.remove(); gnizaUpdateExcludeField(); }; badge.appendChild(btn); container.appendChild(badge); } function gnizaUpdateExcludeField() { var tags = document.getElementById('exclude-tags').querySelectorAll('.badge span'); var vals = []; for (var i = 0; i < tags.length; i++) { vals.push(tags[i].textContent); } document.getElementById('exclude_paths').value = vals.join(','); } function gnizaOpenExcludeModal() { document.getElementById('exclude-textarea').value = ''; document.getElementById('exclude-modal').showModal(); } function gnizaExcludeModalOk() { var text = document.getElementById('exclude-textarea').value; var lines = text.split('\\n'); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (line) gnizaAddExcludeTag(line); } gnizaUpdateExcludeField(); document.getElementById('exclude-modal').close(); } gnizaModeChanged(); function gnizaOpenFileBrowser() { fbSelected = ''; document.getElementById('fb-select-btn').disabled = true; document.getElementById('fb-modal').showModal(); gnizaLoadDir(''); } function gnizaLoadDir(path) { var ts = document.getElementById('timestamp').value; var cacheKey = 'fb:' + ts + ':' + path; if (fbCache[cacheKey]) { gnizaRenderFileList(path, fbCache[cacheKey]); return; } document.getElementById('fb-loading').hidden = false; document.getElementById('fb-error').hidden = true; document.getElementById('fb-tbody').innerHTML = ''; var url = 'restore.cgi?restore_step=fetch_options' + '&remote=' + encodeURIComponent(gnizaRemote) + '&account=' + encodeURIComponent(gnizaAccount) + '×tamp=' + encodeURIComponent(ts) + '&type=files' + (path ? '&path=' + encodeURIComponent(path) : ''); var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; document.getElementById('fb-loading').hidden = true; if (xhr.status === 200) { try { var data = JSON.parse(xhr.responseText); if (data.error) { document.getElementById('fb-error').textContent = data.error; document.getElementById('fb-error').hidden = false; } else { fbCache[cacheKey] = data.options; gnizaRenderFileList(path, data.options); } } catch(e) { document.getElementById('fb-error').textContent = 'Failed to parse response'; document.getElementById('fb-error').hidden = false; } } else { document.getElementById('fb-error').textContent = 'Request failed'; document.getElementById('fb-error').hidden = false; } }; xhr.send(); } function gnizaRenderBreadcrumbs(path) { var ul = document.createElement('ul'); var li = document.createElement('li'); var a = document.createElement('a'); a.textContent = 'homedir'; a.href = '#'; a.onclick = function(e) { e.preventDefault(); gnizaLoadDir(''); }; li.appendChild(a); ul.appendChild(li); if (path) { var parts = path.replace(/\\/\$/, '').split('/'); var built = ''; for (var i = 0; i < parts.length; i++) { built += (i > 0 ? '/' : '') + parts[i]; li = document.createElement('li'); if (i < parts.length - 1) { a = document.createElement('a'); a.textContent = parts[i]; a.href = '#'; (function(p) { a.onclick = function(e) { e.preventDefault(); gnizaLoadDir(p); }; })(built); li.appendChild(a); } else { li.textContent = parts[i]; } ul.appendChild(li); } } var bc = document.getElementById('fb-breadcrumbs'); bc.innerHTML = ''; bc.appendChild(ul); } function gnizaRenderFileList(currentPath, entries) { gnizaRenderBreadcrumbs(currentPath); fbSelected = ''; document.getElementById('fb-select-btn').disabled = true; var tbody = document.getElementById('fb-tbody'); tbody.innerHTML = ''; if (!entries || entries.length === 0) { tbody.innerHTML = '(empty directory)'; return; } for (var i = 0; i < entries.length; i++) { var entry = entries[i]; var isDir = entry.endsWith('/'); var displayName = entry; var fullPath = currentPath ? currentPath.replace(/\\/\$/, '') + '/' + entry : entry; var tr = document.createElement('tr'); tr.className = 'cursor-pointer hover'; tr.setAttribute('data-path', fullPath); var td = document.createElement('td'); td.className = 'py-1'; var icon = isDir ? '\\uD83D\\uDCC1 ' : '\\uD83D\\uDCC4 '; td.textContent = icon + displayName; tr.appendChild(td); (function(row, path, dir) { row.onclick = function() { gnizaHighlight(row, path); }; if (dir) { row.ondblclick = function() { gnizaLoadDir(path.replace(/\\/\$/, '')); }; } })(tr, fullPath, isDir); tbody.appendChild(tr); } } function gnizaHighlight(row, path) { var rows = document.getElementById('fb-tbody').querySelectorAll('tr'); for (var i = 0; i < rows.length; i++) { rows[i].classList.remove('bg-primary/10'); } row.classList.add('bg-primary/10'); fbSelected = path; document.getElementById('fb-select-btn').disabled = false; } function gnizaSelectPath() { if (fbSelected) { document.getElementById('path').value = fbSelected; } document.getElementById('fb-modal').close(); } JS print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Step 3: Summary + Confirm ──────────────────────────────── sub handle_step3 { my $remote = $form->{'remote'} // ''; my $account = $form->{'account'} // ''; my $timestamp = $form->{'timestamp'} // ''; my $path = $form->{'path'} // ''; my $dbnames = $form->{'dbnames'} // ''; my $dbuser_names = $form->{'dbuser_names'} // ''; my $emails = $form->{'emails'} // ''; my $domain_names = $form->{'domain_names'} // ''; my $ssl_names = $form->{'ssl_names'} // ''; my $strategy = $form->{'strategy'} // ''; my $exclude_paths = $form->{'exclude_paths'} // ''; # Collect selected types from type_* checkboxes my @all_type_keys = qw(account files database mailbox cron dbusers cpconfig domains ssl); my @selected_types; for my $t (@all_type_keys) { push @selected_types, $t if ($form->{"type_$t"} // '') eq '1'; } unless (@selected_types) { GnizaWHM::UI::set_flash('error', 'Please select at least one restore type.'); print "Status: 302 Found\r\n"; print "Location: restore.cgi?restore_step=2&remote=" . _uri_escape($remote) . "&account=" . _uri_escape($account) . "\r\n\r\n"; exit; } print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Restore', '', '/cgi/gniza-whm/restore.cgi'); print GnizaWHM::UI::page_header('Restore from Backup'); print GnizaWHM::UI::render_nav('restore.cgi'); my $esc_remote = GnizaWHM::UI::esc($remote); my $esc_account = GnizaWHM::UI::esc($account); my $esc_timestamp = GnizaWHM::UI::esc($timestamp); my $types_display = join(', ', map { GnizaWHM::UI::esc($TYPE_LABELS{$_} // $_) } @selected_types); print qq{
\n
\n}; print qq{

Step 3: Confirm Restore

\n}; print qq{
\n}; print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; if (grep { $_ eq 'account' } @selected_types) { my %strategy_labels = (merge => 'Overwrite (merge)', terminate => 'Terminate & re-create'); my $strategy_display = GnizaWHM::UI::esc($strategy_labels{$strategy} // $strategy); print qq{\n}; } # Show sub-field details for applicable types if (grep { $_ eq 'files' } @selected_types) { my $path_display = $path ne '' ? GnizaWHM::UI::esc($path) : 'All files'; print qq{\n}; } if (grep { $_ eq 'database' } @selected_types) { my $db_display = ($dbnames eq '' || $dbnames eq '__ALL__') ? 'All databases' : GnizaWHM::UI::esc($dbnames); print qq{\n}; } if (grep { $_ eq 'dbusers' } @selected_types) { my $dbu_display = ($dbuser_names eq '' || $dbuser_names eq '__ALL__') ? 'All database users' : GnizaWHM::UI::esc($dbuser_names); print qq{\n}; } if (grep { $_ eq 'mailbox' } @selected_types) { my $mb_display = ($emails eq '' || $emails eq '__ALL__') ? 'All mailboxes' : GnizaWHM::UI::esc($emails); print qq{\n}; } if (grep { $_ eq 'domains' } @selected_types) { my $dom_display = ($domain_names eq '' || $domain_names eq '__ALL__') ? 'All domains' : GnizaWHM::UI::esc($domain_names); print qq{\n}; } if (grep { $_ eq 'ssl' } @selected_types) { my $ssl_display = ($ssl_names eq '' || $ssl_names eq '__ALL__') ? 'All certificates' : GnizaWHM::UI::esc($ssl_names); print qq{\n}; } if ($exclude_paths ne '') { my $exclude_display = GnizaWHM::UI::esc($exclude_paths); $exclude_display =~ s/,/, /g; print qq{\n}; } print qq{
Remote$esc_remote
Account$esc_account
Snapshot$esc_timestamp
Restore Types$types_display
Strategy$strategy_display
Path$path_display
Database$db_display
Database Users$dbu_display
Mailbox$mb_display
Domains$dom_display
SSL$ssl_display
Exclude$exclude_display
\n}; print qq{
\n
\n}; print qq{
\n}; print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; for my $t (@selected_types) { print qq{\n}; } print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; print GnizaWHM::UI::csrf_hidden_field(); print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Step 4: Execute + Show Output ──────────────────────────── sub handle_step4 { unless ($method eq 'POST' && GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) { GnizaWHM::UI::set_flash('error', 'Invalid or expired form token.'); print "Status: 302 Found\r\n"; print "Location: restore.cgi\r\n\r\n"; exit; } my $remote = $form->{'remote'} // ''; my $account = $form->{'account'} // ''; my $timestamp = $form->{'timestamp'} // ''; my $path = $form->{'path'} // ''; my $dbnames = $form->{'dbnames'} // ''; my $dbuser_names = $form->{'dbuser_names'} // ''; my $emails = $form->{'emails'} // ''; my $domain_names = $form->{'domain_names'} // ''; my $ssl_names = $form->{'ssl_names'} // ''; my $strategy = $form->{'strategy'} // ''; my $exclude_paths = $form->{'exclude_paths'} // ''; # Collect selected types my @all_type_keys = qw(account files database mailbox cron dbusers cpconfig domains ssl); my @selected_types; for my $t (@all_type_keys) { push @selected_types, $t if ($form->{"type_$t"} // '') eq '1'; } unless (@selected_types) { GnizaWHM::UI::set_flash('error', 'No restore types selected.'); print "Status: 302 Found\r\n"; print "Location: restore.cgi\r\n\r\n"; exit; } # Build commands for async execution my @commands; for my $type (@selected_types) { my %opts = (remote => $remote); $opts{timestamp} = $timestamp if $timestamp ne ''; $opts{strategy} = $strategy if $strategy ne '' && $type eq 'account'; if ($SIMPLE_TYPES{$type}) { $opts{exclude} = $exclude_paths if $exclude_paths ne '' && $type eq 'account'; push @commands, ['restore', $type, [$account], {%opts}]; } elsif ($type eq 'files') { $opts{path} = $path if $path ne ''; $opts{exclude} = $exclude_paths if $exclude_paths ne ''; push @commands, ['restore', 'files', [$account], {%opts}]; } elsif ($type eq 'database') { my @dbs; if ($dbnames ne '' && $dbnames ne '__ALL__') { @dbs = split /,/, $dbnames; } if (@dbs) { for my $db (@dbs) { push @commands, ['restore', 'database', [$account, $db], {%opts}]; } } else { push @commands, ['restore', 'database', [$account], {%opts}]; } } elsif ($type eq 'dbusers') { my @dbus; if ($dbuser_names ne '' && $dbuser_names ne '__ALL__') { @dbus = split /,/, $dbuser_names; } if (@dbus) { for my $dbu (@dbus) { push @commands, ['restore', 'dbusers', [$account, $dbu], {%opts}]; } } else { push @commands, ['restore', 'dbusers', [$account], {%opts}]; } } elsif ($type eq 'mailbox') { my @mbs; if ($emails ne '' && $emails ne '__ALL__') { @mbs = split /,/, $emails; } if (@mbs) { for my $mb (@mbs) { push @commands, ['restore', 'mailbox', [$account, $mb], {%opts}]; } } else { push @commands, ['restore', 'mailbox', [$account], {%opts}]; } } elsif ($type eq 'domains') { my @doms; if ($domain_names ne '' && $domain_names ne '__ALL__') { @doms = split /,/, $domain_names; } if (@doms) { for my $dom (@doms) { push @commands, ['restore', 'domains', [$account, $dom], {%opts}]; } } else { push @commands, ['restore', 'domains', [$account], {%opts}]; } } elsif ($type eq 'ssl') { my @certs; if ($ssl_names ne '' && $ssl_names ne '__ALL__') { @certs = split /,/, $ssl_names; } if (@certs) { for my $cert (@certs) { push @commands, ['restore', 'ssl', [$account, $cert], {%opts}]; } } else { push @commands, ['restore', 'ssl', [$account], {%opts}]; } } } my ($ok, $err) = GnizaWHM::Runner::run_async(\@commands); if ($ok) { GnizaWHM::UI::set_flash('success', 'Restore started in background. Watch progress below.'); print "Status: 302 Found\r\n"; print "Location: logs.cgi\r\n\r\n"; } else { GnizaWHM::UI::set_flash('error', "Failed to start restore: $err"); print "Status: 302 Found\r\n"; print "Location: restore.cgi\r\n\r\n"; } }