#!/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'} // '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};
}
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{| Account | $user |
\n};
print qq{| Remote | $esc_remote |
\n};
print qq{| Snapshot | $esc_timestamp |
\n};
print qq{| Restore Type | $type_label |
\n};
if ($type eq 'files') {
my $path_display = $path ne '' ? GnizaCPanel::UI::esc($path) : 'All files';
print qq{| Path | $path_display |
\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{| Items | $items_display |
\n};
}
print qq{
\n};
print qq{
\n
\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};
}
}