#!/usr/local/cpanel/3rdparty/bin/perl # gniza cPanel Plugin — Restore Workflow # Multi-step restore with dynamic dropdowns via AdminBin use strict; use warnings; BEGIN { my $base; if ($0 =~ m{^(.*)/}) { $base = $1; } else { $base = '.'; } unshift @INC, "$base/lib"; } use Cpanel::LiveAPI (); use Cpanel::AdminBin::Call (); use Cpanel::Form (); use GnizaCPanel::UI; my $cpanel = Cpanel::LiveAPI->new(); END { $cpanel->end() if $cpanel } my $form = Cpanel::Form::parseform(); my $method = $ENV{'REQUEST_METHOD'} // 'GET'; my $step = $form->{'step'} // ''; my %TYPE_LABELS = ( account => 'Full Backup', files => 'Home Directory', database => 'Databases', mailbox => 'Email Accounts', cron => 'Cron Jobs', dbusers => 'Database Users', domains => 'Domains', ssl => 'SSL Certificates', ); # JSON endpoints if ($step eq 'fetch_snapshots') { handle_fetch_snapshots() } elsif ($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 { # Default: redirect to index print "Status: 302 Found\r\n"; print "Location: index.live.cgi\r\n\r\n"; } exit; # ── Helpers ─────────────────────────────────────────────────── sub _json_escape { my ($str) = @_; $str //= ''; $str =~ s/\\/\\\\/g; $str =~ s/"/\\"/g; $str =~ s/\n/\\n/g; $str =~ s/\r/\\r/g; $str =~ s/\t/\\t/g; $str =~ s/[\x00-\x1f]//g; return $str; } sub _adminbin_call { my ($action, @args) = @_; my $result = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', $action, @args) }; if ($@) { return (0, '', "AdminBin call failed: $@"); } $result //= ''; if ($result =~ /^ERROR:\s*(.*)/) { return (0, '', $1); } return (1, $result, ''); } sub _uri_escape { my $str = shift // ''; $str =~ s/([^A-Za-z0-9\-._~])/sprintf("%%%02X", ord($1))/ge; return $str; } # ── JSON: fetch snapshots ───────────────────────────────────── sub handle_fetch_snapshots { my $remote = $form->{'remote'} // ''; print "Content-Type: application/json\r\n\r\n"; if ($remote eq '') { print qq({"error":"Remote is required"}); return; } my ($ok, $stdout, $err) = _adminbin_call('LIST_SNAPSHOTS', $remote); unless ($ok) { my $msg = _json_escape($err || 'Failed to list snapshots'); print qq({"error":"$msg"}); return; } my @snapshots; for my $line (split /\n/, $stdout) { if ($line =~ /^\s+(\d{4}-\d{2}-\d{2}T\d{6})/) { push @snapshots, $1; } } my $json_arr = join(',', map { qq("$_") } reverse sort @snapshots); print qq({"snapshots":[$json_arr]}); } # ── JSON: fetch item options ────────────────────────────────── sub handle_fetch_options { my $remote = $form->{'remote'} // ''; my $timestamp = $form->{'timestamp'} // ''; my $type = $form->{'type'} // ''; print "Content-Type: application/json\r\n\r\n"; if ($remote eq '' || $timestamp eq '' || $type eq '') { print qq({"error":"Missing required parameters"}); return; } my %action_map = ( database => 'LIST_DATABASES', mailbox => 'LIST_MAILBOXES', files => 'LIST_FILES', dbusers => 'LIST_DBUSERS', cron => 'LIST_CRON', domains => 'LIST_DNS', ssl => 'LIST_SSL', ); my $action = $action_map{$type}; unless ($action) { print qq({"error":"Invalid type"}); return; } my @call_args = ($remote, $timestamp); if ($type eq 'files') { my $path = $form->{'path'} // ''; push @call_args, $path if $path ne ''; } my ($ok, $stdout, $err) = _adminbin_call($action, @call_args); unless ($ok) { my $msg = _json_escape($err || 'Failed to list options'); 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; } my $json_arr = join(',', map { my $v = $_; $v =~ s/\\/\\\\/g; $v =~ s/"/\\"/g; qq("$v") } @options); print qq({"options":[$json_arr]}); } # ── Step 2: Choose Restore Options ─────────────────────────── sub handle_step2 { my $remote = $form->{'remote'} // ''; my $timestamp = $form->{'timestamp'} // ''; if ($remote eq '' || $timestamp eq '') { GnizaCPanel::UI::set_flash('error', 'Remote and snapshot are required.'); print "Status: 302 Found\r\n"; print "Location: index.live.cgi\r\n\r\n"; exit; } # Fetch snapshots server-side via AdminBin my ($ok, $stdout, $err) = _adminbin_call('LIST_SNAPSHOTS', $remote); my @snapshots; if ($ok) { for my $line (split /\n/, $stdout) { if ($line =~ /^\s+(\d{4}-\d{2}-\d{2}T\d{6})/) { push @snapshots, $1; } } } print "Content-Type: text/html\r\n\r\n"; print $cpanel->header('GNIZA Backups'); print GnizaCPanel::UI::page_header('Restore Options'); print GnizaCPanel::UI::render_flash(); my $esc_remote = GnizaCPanel::UI::esc($remote); my $esc_timestamp = GnizaCPanel::UI::esc($timestamp); print qq{
\n}; print qq{\n}; print qq{\n}; print qq{
\n
\n}; print qq{

Step 2: Choose Restore Options

\n}; print qq{

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 print qq{
\n}; print qq{ \n}; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; # Exclude paths section 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 for account type in full mode print qq{\n}; # Selective type buttons (hidden by default) my @selective_types = ( ['files', 'Files'], ['database', 'Database'], ['dbusers', 'Database Users'], ['mailbox', 'Mailbox'], ['cron', 'Cron'], ['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{ Back\n}; print qq{
\n}; } else { print qq{Back\n}; } print qq{
\n}; # JavaScript for dynamic dropdowns and interactive elements _print_step2_js($esc_remote); print GnizaCPanel::UI::page_footer(); print $cpanel->footer(); } sub _print_step2_js { my ($esc_remote) = @_; print <<"END_JS"; END_JS } # ── Step 3: Confirmation ───────────────────────────────────── sub handle_step3 { my $remote = $form->{'remote'} // ''; 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 $exclude_paths = $form->{'exclude_paths'} // ''; # Collect selected types from type_* checkboxes my @all_type_keys = qw(account files database mailbox cron dbusers domains ssl); my @selected_types; for my $t (@all_type_keys) { push @selected_types, $t if ($form->{"type_$t"} // '') eq '1'; } if ($remote eq '' || $timestamp eq '') { GnizaCPanel::UI::set_flash('error', 'Remote and snapshot are required.'); print "Status: 302 Found\r\n"; print "Location: index.live.cgi\r\n\r\n"; exit; } unless (@selected_types) { GnizaCPanel::UI::set_flash('error', 'Please select at least one restore type.'); print "Status: 302 Found\r\n"; print "Location: restore.live.cgi?step=2&remote=" . _uri_escape($remote) . "×tamp=" . _uri_escape($timestamp) . "\r\n\r\n"; exit; } print "Content-Type: text/html\r\n\r\n"; print $cpanel->header('GNIZA Backups'); print GnizaCPanel::UI::page_header('Restore: Confirm'); print GnizaCPanel::UI::render_flash(); my $esc_remote = GnizaCPanel::UI::esc($remote); my $esc_timestamp = GnizaCPanel::UI::esc($timestamp); my $user = GnizaCPanel::UI::esc(GnizaCPanel::UI::get_current_user()); my $types_display = join(', ', map { GnizaCPanel::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}; # Show sub-field details for applicable types if (grep { $_ eq 'files' } @selected_types) { my $path_display = $path ne '' ? GnizaCPanel::UI::esc($path) : 'All files'; print qq{\n}; } if (grep { $_ eq 'database' } @selected_types) { my $db_display = ($dbnames eq '' || $dbnames eq '__ALL__') ? 'All databases' : GnizaCPanel::UI::esc($dbnames); $db_display =~ s/,/, /g; print qq{\n}; } if (grep { $_ eq 'dbusers' } @selected_types) { my $dbu_display = ($dbuser_names eq '' || $dbuser_names eq '__ALL__') ? 'All database users' : GnizaCPanel::UI::esc($dbuser_names); $dbu_display =~ s/,/, /g; print qq{\n}; } if (grep { $_ eq 'mailbox' } @selected_types) { my $mb_display = ($emails eq '' || $emails eq '__ALL__') ? 'All mailboxes' : GnizaCPanel::UI::esc($emails); $mb_display =~ s/,/, /g; print qq{\n}; } if (grep { $_ eq 'domains' } @selected_types) { my $dom_display = ($domain_names eq '' || $domain_names eq '__ALL__') ? 'All domains' : GnizaCPanel::UI::esc($domain_names); $dom_display =~ s/,/, /g; print qq{\n}; } if (grep { $_ eq 'ssl' } @selected_types) { my $ssl_display = ($ssl_names eq '' || $ssl_names eq '__ALL__') ? 'All certificates' : GnizaCPanel::UI::esc($ssl_names); $ssl_display =~ s/,/, /g; print qq{\n}; } if ($exclude_paths ne '') { my $exclude_display = GnizaCPanel::UI::esc($exclude_paths); $exclude_display =~ s/,/, /g; print qq{\n}; } print qq{
Account$user
Remote$esc_remote
Snapshot$esc_timestamp
Restore Types$types_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}; 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 GnizaCPanel::UI::csrf_hidden_field(); print qq{
\n}; print qq{ \n}; print qq{ Cancel\n}; print qq{
\n}; print qq{
\n}; print GnizaCPanel::UI::page_footer(); print $cpanel->footer(); } # ── Step 4: Execute ─────────────────────────────────────────── sub handle_step4 { unless ($method eq 'POST' && GnizaCPanel::UI::verify_csrf_token($form->{'gniza_csrf'})) { GnizaCPanel::UI::set_flash('error', 'Invalid or expired form token.'); print "Status: 302 Found\r\n"; print "Location: index.live.cgi\r\n\r\n"; exit; } my $remote = $form->{'remote'} // ''; 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 $exclude_paths = $form->{'exclude_paths'} // ''; # Collect selected types my @all_type_keys = qw(account files database mailbox cron dbusers domains ssl); my @selected_types; for my $t (@all_type_keys) { push @selected_types, $t if ($form->{"type_$t"} // '') eq '1'; } unless (@selected_types) { GnizaCPanel::UI::set_flash('error', 'No restore types selected.'); print "Status: 302 Found\r\n"; print "Location: index.live.cgi\r\n\r\n"; exit; } print "Content-Type: text/html\r\n\r\n"; print $cpanel->header('GNIZA Backups'); print GnizaCPanel::UI::page_header('Restore Results'); print qq{
\n
\n}; print qq{

Restore Results

\n}; my @results; for my $type (@selected_types) { my $type_label = $TYPE_LABELS{$type} // $type; if ($type eq 'account') { my ($ok, $stdout, $err) = _adminbin_call('RESTORE_ACCOUNT', $remote, $timestamp, $exclude_paths); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } elsif ($type eq 'files') { my ($ok, $stdout, $err) = _adminbin_call('RESTORE_FILES', $remote, $timestamp, $path, $exclude_paths); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } elsif ($type eq 'cron') { my ($ok, $stdout, $err) = _adminbin_call('RESTORE_CRON', $remote, $timestamp); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } elsif ($type eq 'database') { if ($dbnames eq '' || $dbnames eq '__ALL__') { my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DATABASE', $remote, $timestamp, ''); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } else { for my $item (split /,/, $dbnames) { next if $item eq ''; my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DATABASE', $remote, $timestamp, $item); push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err }; } } } elsif ($type eq 'dbusers') { if ($dbuser_names eq '' || $dbuser_names eq '__ALL__') { my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DBUSERS', $remote, $timestamp, ''); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } else { for my $item (split /,/, $dbuser_names) { next if $item eq ''; my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DBUSERS', $remote, $timestamp, $item); push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err }; } } } elsif ($type eq 'mailbox') { if ($emails eq '' || $emails eq '__ALL__') { my ($ok, $stdout, $err) = _adminbin_call('RESTORE_MAILBOX', $remote, $timestamp, ''); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } else { for my $item (split /,/, $emails) { next if $item eq ''; my ($ok, $stdout, $err) = _adminbin_call('RESTORE_MAILBOX', $remote, $timestamp, $item); push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err }; } } } elsif ($type eq 'domains') { if ($domain_names eq '' || $domain_names eq '__ALL__') { my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DOMAINS', $remote, $timestamp, ''); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } else { for my $item (split /,/, $domain_names) { next if $item eq ''; my ($ok, $stdout, $err) = _adminbin_call('RESTORE_DOMAINS', $remote, $timestamp, $item); push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err }; } } } elsif ($type eq 'ssl') { if ($ssl_names eq '' || $ssl_names eq '__ALL__') { my ($ok, $stdout, $err) = _adminbin_call('RESTORE_SSL', $remote, $timestamp, ''); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } else { for my $item (split /,/, $ssl_names) { next if $item eq ''; my ($ok, $stdout, $err) = _adminbin_call('RESTORE_SSL', $remote, $timestamp, $item); push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err }; } } } } _render_results(\@results); print qq{
\n
\n}; print qq{Back to Home\n}; print GnizaCPanel::UI::page_footer(); print $cpanel->footer(); } sub _render_results { my ($results) = @_; for my $r (@$results) { my $icon_class = $r->{ok} ? 'text-success' : 'text-error'; my $icon = $r->{ok} ? '✓' : '✗'; my $label = GnizaCPanel::UI::esc($r->{label}); my $msg = GnizaCPanel::UI::esc($r->{msg} // ''); # Clean up the "OK\n" prefix from successful results $msg =~ s/^OK\s*//; print qq{
\n}; print qq{ $icon\n}; print qq{
\n}; print qq{
$label
\n}; if ($msg ne '') { print qq{
$msg
\n}; } print qq{
\n}; print qq{
\n}; } }