#!/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(''); print GnizaCPanel::UI::page_header('Restore Options'); print GnizaCPanel::UI::render_nav('restore.live.cgi'); print GnizaCPanel::UI::render_flash(); my $esc_remote = GnizaCPanel::UI::esc($remote); my $esc_timestamp = GnizaCPanel::UI::esc($timestamp); 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(''); print GnizaCPanel::UI::page_header('Restore: Confirm'); print GnizaCPanel::UI::render_nav('restore.live.cgi'); 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{| 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 |
$msg\n}; } print qq{