#!/usr/local/cpanel/3rdparty/bin/perl package Cpanel::AdminBin::Script::Call::Gniza4cp::Restore; use strict; use warnings; use parent 'Cpanel::AdminBin::Script::Call'; use IPC::Open3; use Symbol 'gensym'; use POSIX qw(setsid); my $GNIZA4CP4CP_BIN = '/usr/local/bin/gniza4cp'; my $MAIN_CONFIG = '/etc/gniza4cp/gniza4cp.conf'; my $REMOTES_DIR = '/etc/gniza4cp/remotes.d'; # Argument validation patterns (mirrors Gniza4cp4cpWHM::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_gniza4cp { my (@args) = @_; my $err_fh = gensym; my ($in, $out); my $pid = eval { open3($in, $out, $err_fh, $GNIZA4CP4CP_BIN, @args) }; unless ($pid) { return (0, '', "Failed to execute gniza4cp: $@"); } 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 ─────────────────────────────────────────── # ── Per-user activity logging ───────────────────────────────── my $ACTIVITY_ENTRY_RE = qr/^[0-9]+$/; sub _get_log_dir { my $log_dir = '/var/log/gniza4cp'; if (open my $fh, '<', $MAIN_CONFIG) { while (my $line = <$fh>) { if ($line =~ /^LOG_DIR=(?:"([^"]*)"|'([^']*)'|(\S*))$/) { my $val = defined $1 ? $1 : (defined $2 ? $2 : ($3 // '')); $log_dir = $val if $val ne ''; } } close $fh; } return $log_dir; } sub _activity_log_path { my ($user) = @_; my $log_dir = _get_log_dir(); return "$log_dir/cpanel-$user.log"; } my %ACTION_LABELS = ( RESTORE_ACCOUNT => 'Full Account', RESTORE_FILES => 'Home Directory', RESTORE_DATABASE => 'Database', RESTORE_MAILBOX => 'Email', RESTORE_CRON => 'Cron Jobs', RESTORE_DBUSERS => 'DB Users', RESTORE_DOMAINS => 'Domains', RESTORE_SSL => 'SSL Certificates', ); sub _log_activity { my ($user, $action, $details, $status, $output) = @_; my $log_file = _activity_log_path($user); my $log_dir = _get_log_dir(); mkdir $log_dir, 0700 unless -d $log_dir; my @t = gmtime(time); my $ts = sprintf('%04d-%02d-%02d %02d:%02d:%02d', $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]); my $label = $ACTION_LABELS{$action} // $action; if (open my $fh, '>>', $log_file) { print $fh "--- ENTRY ---\n"; print $fh "Date: $ts\n"; print $fh "Action: $label\n"; print $fh "Details: $details\n"; print $fh "Status: $status\n"; print $fh $output if defined $output && $output ne ''; print $fh "--- END ---\n"; close $fh; } } sub _actions { return qw( LIST_ALLOWED_REMOTES LIST_SNAPSHOTS LIST_DATABASES LIST_MAILBOXES LIST_FILES LIST_DBUSERS LIST_CRON LIST_DNS LIST_SSL LIST_LOGS GET_LOG START_RESTORE RESTORE_ACCOUNT RESTORE_FILES RESTORE_DATABASE RESTORE_MAILBOX RESTORE_CRON RESTORE_DBUSERS RESTORE_DOMAINS RESTORE_SSL ); } sub LIST_LOGS { my ($self) = @_; my $user = $self->get_caller_username() // ''; return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE; my $log_file = _activity_log_path($user); return '' unless -f $log_file && !-l $log_file; # Parse entries from the activity log (newest first) my @entries; if (open my $fh, '<', $log_file) { my $in_entry = 0; my %cur; while (my $line = <$fh>) { chomp $line; if ($line eq '--- ENTRY ---') { $in_entry = 1; %cur = (); } elsif ($line eq '--- END ---' && $in_entry) { push @entries, { %cur } if $cur{date}; $in_entry = 0; } elsif ($in_entry) { if ($line =~ /^Date:\s+(.+)$/) { $cur{date} = $1; } elsif ($line =~ /^Action:\s+(.+)$/) { $cur{action} = $1; } elsif ($line =~ /^Details:\s+(.+)$/) { $cur{details} = $1; } elsif ($line =~ /^Status:\s+(.+)$/) { $cur{status} = $1; } } } close $fh; } # Return newest first, one line per entry: index\tdate\taction\tdetails\tstatus my @lines; for my $i (reverse 0 .. $#entries) { my $e = $entries[$i]; push @lines, join("\t", $i, $e->{date} // '', $e->{action} // '', $e->{details} // '', $e->{status} // ''); } return join("\n", @lines); } sub GET_LOG { my ($self, $entry_idx) = @_; my $user = $self->get_caller_username() // ''; return "ERROR: Invalid user" unless $user =~ $ACCOUNT_RE; return "ERROR: Invalid entry" unless defined $entry_idx && $entry_idx =~ $ACTIVITY_ENTRY_RE; my $log_file = _activity_log_path($user); return "ERROR: No activity log" unless -f $log_file && !-l $log_file; # Parse the Nth entry my $idx = int($entry_idx); my @entries; if (open my $fh, '<', $log_file) { my $in_entry = 0; my @cur_lines; while (my $line = <$fh>) { chomp $line; if ($line eq '--- ENTRY ---') { $in_entry = 1; @cur_lines = (); } elsif ($line eq '--- END ---' && $in_entry) { push @entries, join("\n", @cur_lines); $in_entry = 0; } elsif ($in_entry) { push @cur_lines, $line; } } close $fh; } return "ERROR: Entry not found" if $idx < 0 || $idx > $#entries; return $entries[$idx]; } sub LIST_ALLOWED_REMOTES { my ($self) = @_; my @remotes = _get_filtered_remotes(); return join("\n", @remotes); } sub LIST_SNAPSHOTS { my ($self, $remote) = @_; my $user = $self->get_caller_username() // ''; 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_gniza4cp('list', "--remote=$remote", "--account=$user"); return $ok ? $stdout : "ERROR: $stderr"; } sub LIST_DATABASES { my ($self, $remote, $timestamp) = @_; my $user = $self->get_caller_username() // ''; 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_gniza4cp('restore', 'list-databases', $user, "--remote=$remote", "--timestamp=$timestamp"); return $ok ? $stdout : "ERROR: $stderr"; } sub LIST_MAILBOXES { my ($self, $remote, $timestamp) = @_; my $user = $self->get_caller_username() // ''; 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_gniza4cp('restore', 'list-mailboxes', $user, "--remote=$remote", "--timestamp=$timestamp"); return $ok ? $stdout : "ERROR: $stderr"; } sub LIST_FILES { my ($self, $remote, $timestamp, $path) = @_; my $user = $self->get_caller_username() // ''; 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_gniza4cp('restore', 'list-files', $user, @opts); return $ok ? $stdout : "ERROR: $stderr"; } sub LIST_DBUSERS { my ($self, $remote, $timestamp) = @_; my $user = $self->get_caller_username() // ''; 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_gniza4cp('restore', 'list-dbusers', $user, "--remote=$remote", "--timestamp=$timestamp"); return $ok ? $stdout : "ERROR: $stderr"; } sub LIST_CRON { my ($self, $remote, $timestamp) = @_; my $user = $self->get_caller_username() // ''; 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_gniza4cp('restore', 'list-cron', $user, "--remote=$remote", "--timestamp=$timestamp"); return $ok ? $stdout : "ERROR: $stderr"; } sub LIST_DNS { my ($self, $remote, $timestamp) = @_; my $user = $self->get_caller_username() // ''; 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_gniza4cp('restore', 'list-dns', $user, "--remote=$remote", "--timestamp=$timestamp"); return $ok ? $stdout : "ERROR: $stderr"; } sub LIST_SSL { my ($self, $remote, $timestamp) = @_; my $user = $self->get_caller_username() // ''; 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_gniza4cp('restore', 'list-ssl', $user, "--remote=$remote", "--timestamp=$timestamp"); return $ok ? $stdout : "ERROR: $stderr"; } # ── Background restore ──────────────────────────────────────── # # START_RESTORE($remote, $timestamp, $types_str, $path, $exclude_paths) # # $types_str encodes selected types and items, semicolon-separated: # account;files;database:db1,db2;mailbox:a@b.com;cron;dbusers:u1;domains:d1;ssl:d1 # # Validates all inputs, forks a detached child that runs each gniza4cp # restore command and logs results via _log_activity(), then returns # immediately with "OK". my %TYPE_ITEM_RE = ( account => undef, # no items files => undef, # uses $path arg instead cron => undef, # no items database => $DBNAME_RE, dbusers => $DBNAME_RE, mailbox => $EMAIL_RE, domains => $DOMAIN_RE, ssl => $DOMAIN_RE, ); # Map type + item to gniza4cp CLI arguments (excluding --remote/--timestamp which are always added) sub _build_restore_args { my ($type, $user, $item, $path, $exclude) = @_; if ($type eq 'account') { my @args = ('restore', 'account', $user); push @args, "--exclude=$exclude" if defined $exclude && $exclude ne ''; return @args; } elsif ($type eq 'files') { my @args = ('restore', 'files', $user); push @args, "--path=$path" if defined $path && $path ne ''; push @args, "--exclude=$exclude" if defined $exclude && $exclude ne ''; return @args; } elsif ($type eq 'cron') { return ('restore', 'cron', $user); } elsif ($type eq 'database') { my @args = ('restore', 'database', $user); push @args, $item if defined $item && $item ne ''; return @args; } elsif ($type eq 'dbusers') { my @args = ('restore', 'dbusers', $user); push @args, $item if defined $item && $item ne ''; return @args; } elsif ($type eq 'mailbox') { my @args = ('restore', 'mailbox', $user); push @args, $item if defined $item && $item ne ''; return @args; } elsif ($type eq 'domains') { my @args = ('restore', 'domains', $user); push @args, $item if defined $item && $item ne ''; return @args; } elsif ($type eq 'ssl') { my @args = ('restore', 'ssl', $user); push @args, $item if defined $item && $item ne ''; return @args; } return (); } # Map type to RESTORE_* action name for _log_activity my %TYPE_ACTION_MAP = ( account => 'RESTORE_ACCOUNT', files => 'RESTORE_FILES', database => 'RESTORE_DATABASE', dbusers => 'RESTORE_DBUSERS', mailbox => 'RESTORE_MAILBOX', cron => 'RESTORE_CRON', domains => 'RESTORE_DOMAINS', ssl => 'RESTORE_SSL', ); sub START_RESTORE { my ($self, $remote, $timestamp, $types_str, $path, $exclude_paths) = @_; my $user = $self->get_caller_username() // ''; # ── Validate common args ── 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); if (defined $path && $path ne '') { return "ERROR: Invalid path" unless $path =~ $OPT_PATTERNS{path}; } if (defined $exclude_paths && $exclude_paths ne '') { return "ERROR: Invalid exclude" unless $exclude_paths =~ $OPT_PATTERNS{exclude}; } # ── Parse and validate types_str ── $types_str //= ''; return "ERROR: No restore types specified" if $types_str eq ''; my @jobs; # [ { type => ..., items => [...] }, ... ] for my $part (split /;/, $types_str) { next if $part eq ''; my ($type, $items_str) = split /:/, $part, 2; return "ERROR: Invalid restore type: $type" unless exists $TYPE_ITEM_RE{$type}; my $item_re = $TYPE_ITEM_RE{$type}; my @items; if (defined $items_str && $items_str ne '') { return "ERROR: Type '$type' does not accept items" unless defined $item_re; for my $item (split /,/, $items_str) { next if $item eq ''; return "ERROR: Invalid item for $type: $item" unless $item =~ $item_re; push @items, $item; return "ERROR: Too many items for $type (max 100)" if @items > 100; } } push @jobs, { type => $type, items => \@items }; } return "ERROR: No valid restore types parsed" unless @jobs; # ── Pre-build all command arg lists to validate before forking ── my @cmd_list; # [ { args => [...], action => ..., details => ... }, ... ] for my $job (@jobs) { my $type = $job->{type}; my @items = @{$job->{items}}; if (@items) { # One command per item for my $item (@items) { my @args = _build_restore_args($type, $user, $item, $path, $exclude_paths); return "ERROR: Failed to build command for $type" unless @args; my $details = "remote=$remote snapshot=$timestamp"; if ($type eq 'database') { $details .= " database=$item"; } elsif ($type eq 'dbusers') { $details .= " dbuser=$item"; } elsif ($type eq 'mailbox') { $details .= " email=$item"; } elsif ($type eq 'domains') { $details .= " domain=$item"; } elsif ($type eq 'ssl') { $details .= " domain=$item"; } push @cmd_list, { args => \@args, action => $TYPE_ACTION_MAP{$type}, details => $details }; } } else { # Single command for this type my @args = _build_restore_args($type, $user, '', $path, $exclude_paths); return "ERROR: Failed to build command for $type" unless @args; my $details = "remote=$remote snapshot=$timestamp"; $details .= " path=$path" if $type eq 'files' && defined $path && $path ne ''; $details .= " exclude=$exclude_paths" if defined $exclude_paths && $exclude_paths ne ''; push @cmd_list, { args => \@args, action => $TYPE_ACTION_MAP{$type}, details => $details }; } } # ── Fork detached child ── local $SIG{CHLD} = 'IGNORE'; my $pid = fork(); return "ERROR: Fork failed: $!" unless defined $pid; if ($pid == 0) { # Child: detach from parent completely eval { setsid(); open STDIN, '<', '/dev/null'; open STDOUT, '>', '/dev/null'; open STDERR, '>', '/dev/null'; for my $cmd (@cmd_list) { my @full_args = (@{$cmd->{args}}, "--remote=$remote", "--timestamp=$timestamp"); my ($ok, $stdout, $stderr) = _run_gniza4cp(@full_args); _log_activity($user, $cmd->{action}, $cmd->{details}, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); } }; # Ensure child exits even on error POSIX::_exit(0); } # Parent: return immediately return "OK"; } sub RESTORE_ACCOUNT { my ($self, $remote, $timestamp, $exclude) = @_; my $user = $self->get_caller_username() // ''; 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 $details = "remote=$remote snapshot=$timestamp"; $details .= " exclude=$exclude" if defined $exclude && $exclude ne ''; my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'account', $user, @opts); _log_activity($user, 'RESTORE_ACCOUNT', $details, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); return $ok ? "OK\n$stdout" : "ERROR: $stderr"; } sub RESTORE_FILES { my ($self, $remote, $timestamp, $path, $exclude) = @_; my $user = $self->get_caller_username() // ''; 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 $details = "remote=$remote snapshot=$timestamp"; $details .= " path=$path" if defined $path && $path ne ''; $details .= " exclude=$exclude" if defined $exclude && $exclude ne ''; my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'files', $user, @opts); _log_activity($user, 'RESTORE_FILES', $details, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); return $ok ? "OK\n$stdout" : "ERROR: $stderr"; } sub RESTORE_DATABASE { my ($self, $remote, $timestamp, $dbname) = @_; my $user = $self->get_caller_username() // ''; 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 $details = "remote=$remote snapshot=$timestamp"; $details .= " database=$dbname" if defined $dbname && $dbname ne ''; my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'database', @args, "--remote=$remote", "--timestamp=$timestamp"); _log_activity($user, 'RESTORE_DATABASE', $details, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); return $ok ? "OK\n$stdout" : "ERROR: $stderr"; } sub RESTORE_MAILBOX { my ($self, $remote, $timestamp, $email) = @_; my $user = $self->get_caller_username() // ''; 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 $details = "remote=$remote snapshot=$timestamp"; $details .= " email=$email" if defined $email && $email ne ''; my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'mailbox', @args, "--remote=$remote", "--timestamp=$timestamp"); _log_activity($user, 'RESTORE_MAILBOX', $details, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); return $ok ? "OK\n$stdout" : "ERROR: $stderr"; } sub RESTORE_CRON { my ($self, $remote, $timestamp) = @_; my $user = $self->get_caller_username() // ''; 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 $details = "remote=$remote snapshot=$timestamp"; my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'cron', $user, "--remote=$remote", "--timestamp=$timestamp"); _log_activity($user, 'RESTORE_CRON', $details, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); return $ok ? "OK\n$stdout" : "ERROR: $stderr"; } sub RESTORE_DBUSERS { my ($self, $remote, $timestamp, $dbuser) = @_; my $user = $self->get_caller_username() // ''; 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 $details = "remote=$remote snapshot=$timestamp"; $details .= " dbuser=$dbuser" if defined $dbuser && $dbuser ne ''; my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'dbusers', @args, "--remote=$remote", "--timestamp=$timestamp"); _log_activity($user, 'RESTORE_DBUSERS', $details, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); return $ok ? "OK\n$stdout" : "ERROR: $stderr"; } sub RESTORE_DOMAINS { my ($self, $remote, $timestamp, $domain) = @_; my $user = $self->get_caller_username() // ''; 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 $details = "remote=$remote snapshot=$timestamp"; $details .= " domain=$domain" if defined $domain && $domain ne ''; my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'domains', @args, "--remote=$remote", "--timestamp=$timestamp"); _log_activity($user, 'RESTORE_DOMAINS', $details, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); return $ok ? "OK\n$stdout" : "ERROR: $stderr"; } sub RESTORE_SSL { my ($self, $remote, $timestamp, $domain) = @_; my $user = $self->get_caller_username() // ''; 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 $details = "remote=$remote snapshot=$timestamp"; $details .= " domain=$domain" if defined $domain && $domain ne ''; my ($ok, $stdout, $stderr) = _run_gniza4cp('restore', 'ssl', @args, "--remote=$remote", "--timestamp=$timestamp"); _log_activity($user, 'RESTORE_SSL', $details, $ok ? 'OK' : 'Error', $ok ? $stdout : $stderr); return $ok ? "OK\n$stdout" : "ERROR: $stderr"; } __PACKAGE__->run() if !caller; 1;