#!/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::AdminBin::Call (); use Cpanel::Form (); use GnizaCPanel::UI; my $form = Cpanel::Form::parseform(); my $method = $ENV{'REQUEST_METHOD'} // 'GET'; my $step = $form->{'step'} // '1'; 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', ); my %SIMPLE_TYPES = map { $_ => 1 } qw(account cron); # 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 { handle_step1() } 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, ''); } # ── 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 1: Select Remote + Snapshot ────────────────────────── sub handle_step1 { my $type = $form->{'type'} // 'account'; unless (exists $TYPE_LABELS{$type}) { $type = 'account'; } print "Content-Type: text/html\r\n\r\n"; print GnizaCPanel::UI::page_header('Restore: ' . ($TYPE_LABELS{$type} // $type)); print GnizaCPanel::UI::render_flash(); # Get allowed remotes my $remotes_raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_ALLOWED_REMOTES') } // ''; my @remotes = grep { $_ ne '' } split /\n/, $remotes_raw; unless (@remotes) { print qq{
No backup remotes are available. Please contact your server administrator.
\n}; print qq{Back\n}; print GnizaCPanel::UI::page_footer(); return; } my $esc_type = GnizaCPanel::UI::esc($type); my $type_label = GnizaCPanel::UI::esc($TYPE_LABELS{$type} // $type); print qq{
\n
\n}; print qq{

Step 1: Select Source

\n}; print qq{

Restore type: $type_label

\n}; # Remote dropdown print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; # Snapshot dropdown (populated via AJAX) print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{
\n
\n}; # For simple types, go directly to step 3 (confirm); others go to step 2 my $next_step = $SIMPLE_TYPES{$type} ? '3' : '2'; print qq{
\n}; print qq{ \n}; print qq{ Back\n}; print qq{
\n}; # JavaScript for snapshot loading and navigation _print_step1_js($esc_type, $next_step); print GnizaCPanel::UI::page_footer(); } sub _print_step1_js { my ($esc_type, $next_step) = @_; print qq{\n}; } # ── Step 2: Select specific item ───────────────────────────── sub handle_step2 { my $type = $form->{'type'} // 'account'; my $remote = $form->{'remote'} // ''; my $timestamp = $form->{'timestamp'} // ''; $type = 'account' unless exists $TYPE_LABELS{$type}; if ($remote eq '' || $timestamp eq '') { GnizaCPanel::UI::set_flash('error', 'Remote and snapshot are required.'); print "Status: 302 Found\r\n"; print "Location: restore.live.cgi?type=$type\r\n\r\n"; exit; } print "Content-Type: text/html\r\n\r\n"; print GnizaCPanel::UI::page_header('Restore: ' . ($TYPE_LABELS{$type} // $type)); print GnizaCPanel::UI::render_flash(); my $esc_type = GnizaCPanel::UI::esc($type); my $esc_remote = GnizaCPanel::UI::esc($remote); my $esc_timestamp = GnizaCPanel::UI::esc($timestamp); print qq{
\n
\n}; print qq{

Step 2: Select Items

\n}; print qq{

Remote: $esc_remote · Snapshot: $esc_timestamp

\n}; if ($type eq 'files') { _render_file_picker(); } elsif ($type eq 'database' || $type eq 'dbusers' || $type eq 'mailbox' || $type eq 'domains' || $type eq 'ssl') { my $item_label = { database => 'Databases', dbusers => 'Database Users', mailbox => 'Mailboxes', domains => 'Domains', ssl => 'SSL Certificates', }->{$type}; print qq{

$item_label

\n}; print qq{\n}; print qq{
\n}; print qq{ Loading...\n}; print qq{
\n}; } elsif ($type eq 'cron') { print qq{

Cron Jobs Preview

\n}; print qq{
\n}; print qq{ Loading...\n}; print qq{
\n}; } print qq{
\n
\n}; print qq{
\n}; print qq{ \n}; print qq{ Back\n}; print qq{
\n}; _print_step2_js($esc_type, $esc_remote, $esc_timestamp); print GnizaCPanel::UI::page_footer(); } sub _render_file_picker { print qq{
\n}; print qq{ \n}; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; print qq{

Leave empty to restore all files.

\n}; # File browser modal print qq{\n}; print qq{\n}; print qq{\n}; print qq{\n}; } sub _print_step2_js { my ($esc_type, $esc_remote, $esc_timestamp) = @_; print qq{\n}; } # ── Step 3: Confirmation ───────────────────────────────────── sub handle_step3 { my $type = $form->{'type'} // 'account'; my $remote = $form->{'remote'} // ''; my $timestamp = $form->{'timestamp'} // ''; my $path = $form->{'path'} // ''; my $items = $form->{'items'} // ''; $type = 'account' unless exists $TYPE_LABELS{$type}; if ($remote eq '' || $timestamp eq '') { GnizaCPanel::UI::set_flash('error', 'Remote and snapshot are required.'); print "Status: 302 Found\r\n"; print "Location: restore.live.cgi?type=$type\r\n\r\n"; exit; } print "Content-Type: text/html\r\n\r\n"; print GnizaCPanel::UI::page_header('Restore: Confirm'); print GnizaCPanel::UI::render_flash(); my $esc_type = GnizaCPanel::UI::esc($type); my $esc_remote = GnizaCPanel::UI::esc($remote); my $esc_timestamp = GnizaCPanel::UI::esc($timestamp); my $type_label = GnizaCPanel::UI::esc($TYPE_LABELS{$type} // $type); my $user = GnizaCPanel::UI::esc(GnizaCPanel::UI::get_current_user()); 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 ($type eq 'files') { my $path_display = $path ne '' ? GnizaCPanel::UI::esc($path) : 'All files'; print qq{\n}; } elsif ($type ne 'account' && $type ne 'cron' && $items ne '') { my $items_display = $items eq '__ALL__' ? 'All' : GnizaCPanel::UI::esc($items); $items_display =~ s/,/, /g; print qq{\n}; } print qq{
Account$user
Remote$esc_remote
Snapshot$esc_timestamp
Restore Type$type_label
Path$path_display
Items$items_display
\n}; print qq{
\n
\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(); } # ── 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 $type = $form->{'type'} // 'account'; my $remote = $form->{'remote'} // ''; my $timestamp = $form->{'timestamp'} // ''; my $path = $form->{'path'} // ''; my $items = $form->{'items'} // ''; $type = 'account' unless exists $TYPE_LABELS{$type}; print "Content-Type: text/html\r\n\r\n"; print GnizaCPanel::UI::page_header('Restore: Results'); my $type_label = GnizaCPanel::UI::esc($TYPE_LABELS{$type}); print qq{
\n
\n}; print qq{

Restore Results: $type_label

\n}; my @results; my %action_map = ( account => 'RESTORE_ACCOUNT', files => 'RESTORE_FILES', database => 'RESTORE_DATABASE', mailbox => 'RESTORE_MAILBOX', cron => 'RESTORE_CRON', dbusers => 'RESTORE_DBUSERS', domains => 'RESTORE_DOMAINS', ssl => 'RESTORE_SSL', ); my $action = $action_map{$type}; unless ($action) { push @results, { ok => 0, label => $type, msg => 'Unknown restore type' }; _render_results(\@results); print qq{
\n
\n}; print qq{Back to Categories\n}; print GnizaCPanel::UI::page_footer(); return; } if ($type eq 'account') { my ($ok, $stdout, $err) = _adminbin_call($action, $remote, $timestamp, ''); push @results, { ok => $ok, label => 'Full Account', msg => $ok ? $stdout : $err }; } elsif ($type eq 'cron') { my ($ok, $stdout, $err) = _adminbin_call($action, $remote, $timestamp); push @results, { ok => $ok, label => 'Cron Jobs', msg => $ok ? $stdout : $err }; } elsif ($type eq 'files') { my ($ok, $stdout, $err) = _adminbin_call($action, $remote, $timestamp, $path, ''); push @results, { ok => $ok, label => 'Files', msg => $ok ? $stdout : $err }; } elsif ($type eq 'database' || $type eq 'dbusers' || $type eq 'mailbox' || $type eq 'domains' || $type eq 'ssl') { if ($items eq '' || $items eq '__ALL__') { my ($ok, $stdout, $err) = _adminbin_call($action, $remote, $timestamp, ''); push @results, { ok => $ok, label => $type_label, msg => $ok ? $stdout : $err }; } else { for my $item (split /,/, $items) { next if $item eq ''; my ($ok, $stdout, $err) = _adminbin_call($action, $remote, $timestamp, $item); push @results, { ok => $ok, label => $item, msg => $ok ? $stdout : $err }; } } } _render_results(\@results); print qq{\n\n}; print qq{Back to Categories\n}; print GnizaCPanel::UI::page_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}; } }