Files
gniza4cp/cpanel/admin/Gniza/Restore
shuki 065f83d691 Fix AdminBin: add shebang and __PACKAGE__->run()
cPanel's adminbin framework requires the module to be directly
executable with a shebang line, and Script::Call modules need
__PACKAGE__->run() to bootstrap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 01:57:56 +02:00

414 lines
15 KiB
Perl

#!/usr/local/cpanel/3rdparty/bin/perl
package Cpanel::AdminBin::Script::Call::Gniza::Restore;
use strict;
use warnings;
use parent 'Cpanel::AdminBin::Script::Call';
use IPC::Open3;
use Symbol 'gensym';
__PACKAGE__->run() if !caller;
my $GNIZA_BIN = '/usr/local/bin/gniza';
my $MAIN_CONFIG = '/etc/gniza/gniza.conf';
my $REMOTES_DIR = '/etc/gniza/remotes.d';
# Argument validation patterns (mirrors GnizaWHM::Runner)
my %OPT_PATTERNS = (
remote => qr/^[a-zA-Z0-9_,-]+$/,
timestamp => qr/^\d{4}-\d{2}-\d{2}T\d{6}$/,
path => qr/^(?!.*\.\.)[a-zA-Z0-9_.\/@ -]+$/,
exclude => qr/^[a-zA-Z0-9_.,\/@ *?\[\]-]+$/,
);
my $ACCOUNT_RE = qr/^[a-z][a-z0-9_-]*$/;
my $REMOTE_RE = qr/^[a-zA-Z0-9_-]+$/;
my $DBNAME_RE = qr/^[a-zA-Z0-9_]+$/;
my $EMAIL_RE = qr/^[a-zA-Z0-9._+-]+\@[a-zA-Z0-9._-]+$/;
my $DOMAIN_RE = qr/^[a-zA-Z0-9._-]+$/;
my $TS_RE = qr/^\d{4}-\d{2}-\d{2}T\d{6}$/;
# ── Allowed remotes for user restore ──────────────────────────
sub _get_allowed_remotes {
my $setting = '';
if (open my $fh, '<', $MAIN_CONFIG) {
while (my $line = <$fh>) {
if ($line =~ /^USER_RESTORE_REMOTES=(?:"([^"]*)"|'([^']*)'|(\S*))$/) {
$setting = defined $1 ? $1 : (defined $2 ? $2 : ($3 // ''));
}
}
close $fh;
}
# Default to "all" if not set
$setting = 'all' if !defined $setting || $setting eq '';
return $setting;
}
sub _list_all_remotes {
my @remotes;
if (-d $REMOTES_DIR && opendir my $dh, $REMOTES_DIR) {
while (my $entry = readdir $dh) {
if ($entry =~ /^([a-zA-Z0-9_-]+)\.conf$/) {
push @remotes, $1;
}
}
closedir $dh;
}
return sort @remotes;
}
sub _is_remote_allowed {
my ($remote) = @_;
my $setting = _get_allowed_remotes();
return 0 if $setting eq ''; # disabled
if ($setting eq 'all') {
# Check it actually exists
return -f "$REMOTES_DIR/$remote.conf" ? 1 : 0;
}
my %allowed = map { $_ => 1 } split /,/, $setting;
return $allowed{$remote} ? 1 : 0;
}
sub _get_filtered_remotes {
my $setting = _get_allowed_remotes();
return () if $setting eq '';
my @all = _list_all_remotes();
return @all if $setting eq 'all';
my %allowed = map { $_ => 1 } split /,/, $setting;
return grep { $allowed{$_} } @all;
}
# ── Command execution ─────────────────────────────────────────
sub _run_gniza {
my (@args) = @_;
my $err_fh = gensym;
my ($in, $out);
my $pid = eval { open3($in, $out, $err_fh, $GNIZA_BIN, @args) };
unless ($pid) {
return (0, '', "Failed to execute gniza: $@");
}
close $in if $in;
my $stdout = do { local $/; <$out> } // '';
my $stderr = do { local $/; <$err_fh> } // '';
close $out;
close $err_fh;
waitpid($pid, 0);
my $exit_code = $? >> 8;
return ($exit_code == 0, $stdout, $stderr);
}
# ── Action dispatch ───────────────────────────────────────────
sub _actions {
return qw(
LIST_ALLOWED_REMOTES
LIST_SNAPSHOTS
LIST_DATABASES
LIST_MAILBOXES
LIST_FILES
LIST_DBUSERS
LIST_CRON
LIST_DNS
LIST_SSL
RESTORE_ACCOUNT
RESTORE_FILES
RESTORE_DATABASE
RESTORE_MAILBOX
RESTORE_CRON
RESTORE_DBUSERS
RESTORE_DOMAINS
RESTORE_SSL
);
}
sub LIST_ALLOWED_REMOTES {
my ($self) = @_;
my @remotes = _get_filtered_remotes();
return join("\n", @remotes);
}
sub LIST_SNAPSHOTS {
my ($self, $remote) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('list', "--remote=$remote", "--account=$user");
return $ok ? $stdout : "ERROR: $stderr";
}
sub LIST_DATABASES {
my ($self, $remote, $timestamp) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-databases', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
sub LIST_MAILBOXES {
my ($self, $remote, $timestamp) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-mailboxes', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
sub LIST_FILES {
my ($self, $remote, $timestamp, $path) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
if (defined $path && $path ne '') {
return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path};
push @opts, "--path=$path";
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-files', $user, @opts);
return $ok ? $stdout : "ERROR: $stderr";
}
sub LIST_DBUSERS {
my ($self, $remote, $timestamp) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-dbusers', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
sub LIST_CRON {
my ($self, $remote, $timestamp) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-cron', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
sub LIST_DNS {
my ($self, $remote, $timestamp) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-dns', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
sub LIST_SSL {
my ($self, $remote, $timestamp) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'list-ssl', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? $stdout : "ERROR: $stderr";
}
sub RESTORE_ACCOUNT {
my ($self, $remote, $timestamp, $exclude) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
# NOTE: --terminate is NEVER passed for user restore
if (defined $exclude && $exclude ne '') {
return "ERROR: Invalid exclude" unless $exclude =~ $OPT_PATTERNS{exclude};
push @opts, "--exclude=$exclude";
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'account', $user, @opts);
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
}
sub RESTORE_FILES {
my ($self, $remote, $timestamp, $path, $exclude) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my @opts = ("--remote=$remote", "--timestamp=$timestamp");
if (defined $path && $path ne '') {
return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path};
push @opts, "--path=$path";
}
if (defined $exclude && $exclude ne '') {
return "ERROR: Invalid exclude" unless $exclude =~ $OPT_PATTERNS{exclude};
push @opts, "--exclude=$exclude";
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'files', $user, @opts);
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
}
sub RESTORE_DATABASE {
my ($self, $remote, $timestamp, $dbname) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my @args = ($user);
if (defined $dbname && $dbname ne '') {
return "ERROR: Invalid database name" unless $dbname =~ $DBNAME_RE;
push @args, $dbname;
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'database', @args,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
}
sub RESTORE_MAILBOX {
my ($self, $remote, $timestamp, $email) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my @args = ($user);
if (defined $email && $email ne '') {
return "ERROR: Invalid email" unless $email =~ $EMAIL_RE;
push @args, $email;
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'mailbox', @args,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
}
sub RESTORE_CRON {
my ($self, $remote, $timestamp) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'cron', $user,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
}
sub RESTORE_DBUSERS {
my ($self, $remote, $timestamp, $dbuser) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my @args = ($user);
if (defined $dbuser && $dbuser ne '') {
return "ERROR: Invalid database user" unless $dbuser =~ $DBNAME_RE;
push @args, $dbuser;
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'dbusers', @args,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
}
sub RESTORE_DOMAINS {
my ($self, $remote, $timestamp, $domain) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my @args = ($user);
if (defined $domain && $domain ne '') {
return "ERROR: Invalid domain" unless $domain =~ $DOMAIN_RE;
push @args, $domain;
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'domains', @args,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
}
sub RESTORE_SSL {
my ($self, $remote, $timestamp, $domain) = @_;
my $user = $ENV{'REMOTE_USER'} // '';
return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE;
return "ERROR: Invalid remote" unless defined $remote && $remote =~ $REMOTE_RE;
return "ERROR: Invalid timestamp" unless defined $timestamp && $timestamp =~ $TS_RE;
return "ERROR: Remote not allowed" unless _is_remote_allowed($remote);
my @args = ($user);
if (defined $domain && $domain ne '') {
return "ERROR: Invalid domain" unless $domain =~ $DOMAIN_RE;
push @args, $domain;
}
my ($ok, $stdout, $stderr) = _run_gniza('restore', 'ssl', @args,
"--remote=$remote", "--timestamp=$timestamp");
return $ok ? "OK\n$stdout" : "ERROR: $stderr";
}
1;