#!/usr/local/cpanel/3rdparty/bin/perl
# gniza4cp WHM Plugin — Remote Destination CRUD
use strict;
use warnings;
use lib '/usr/local/cpanel/whostmgr/docroot/cgi/gniza4cp-whm/lib';
use Whostmgr::HTMLInterface ();
use Cpanel::Form ();
use File::Copy ();
use IPC::Open3 ();
use Symbol qw(gensym);
use Gniza4cpWHM::Config;
use Gniza4cpWHM::Validator;
use Gniza4cpWHM::UI;
my $form = Cpanel::Form::parseform();
my $method = $ENV{'REQUEST_METHOD'} // 'GET';
my $action = $form->{'action'} // 'list';
# Route to handler
if ($action eq 'test') { handle_test_connection() }
elsif ($action eq 'disk_info') { handle_disk_info() }
elsif ($action eq 'add') { handle_add() }
elsif ($action eq 'edit') { handle_edit() }
elsif ($action eq 'delete') { handle_delete() }
else { handle_list() }
exit;
# ── Test Connection (JSON) ────────────────────────────────────
sub handle_test_connection {
print "Content-Type: application/json\r\n\r\n";
unless ($method eq 'POST' && Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
print qq({"success":false,"message":"Invalid or expired token. Please reload and try again."});
exit;
}
# Generate fresh token after consuming the old one (CSRF is single-use)
my $new_csrf = Gniza4cpWHM::UI::generate_csrf_token();
my $type = $form->{'remote_type'} || 'ssh';
if ($type eq 'ssh') {
my $host = $form->{'host'} // '';
my $port = $form->{'port'} || '22';
my $user = $form->{'user'} || 'root';
my $auth_method = $form->{'auth_method'} || 'key';
my $key = $form->{'key'} // '';
my $password = $form->{'password'} // '';
if ($host eq '') {
print qq({"success":false,"message":"Host is required.","csrf":"$new_csrf"});
exit;
}
if ($auth_method eq 'password') {
if ($password eq '') {
print qq({"success":false,"message":"Password is required.","csrf":"$new_csrf"});
exit;
}
} else {
if ($key eq '') {
print qq({"success":false,"message":"SSH key path is required.","csrf":"$new_csrf"});
exit;
}
}
my ($ok, $err) = Gniza4cpWHM::UI::test_ssh_connection(
host => $host,
port => $port,
user => $user,
auth_method => $auth_method,
key => $key,
password => $password,
);
if ($ok) {
print qq({"success":true,"message":"SSH connection successful.","csrf":"$new_csrf"});
} else {
$err //= 'Unknown error';
$err =~ s/\\/\\\\/g;
$err =~ s/"/\\"/g;
$err =~ s/\n/\\n/g;
$err =~ s/\r/\\r/g;
$err =~ s/\t/\\t/g;
$err =~ s/[\x00-\x1f]//g;
print qq({"success":false,"message":"SSH connection failed: $err","csrf":"$new_csrf"});
}
}
elsif ($type eq 's3' || $type eq 'gdrive') {
my %rclone_args = (type => $type);
if ($type eq 's3') {
$rclone_args{s3_access_key_id} = $form->{'S3_ACCESS_KEY_ID'} // '';
$rclone_args{s3_secret_access_key} = $form->{'S3_SECRET_ACCESS_KEY'} // '';
$rclone_args{s3_region} = $form->{'S3_REGION'} || 'us-east-1';
$rclone_args{s3_endpoint} = $form->{'S3_ENDPOINT'} // '';
$rclone_args{s3_bucket} = $form->{'S3_BUCKET'} // '';
if ($rclone_args{s3_access_key_id} eq '' || $rclone_args{s3_secret_access_key} eq '') {
print qq({"success":false,"message":"S3 access key and secret are required.","csrf":"$new_csrf"});
exit;
}
if ($rclone_args{s3_bucket} eq '') {
print qq({"success":false,"message":"S3 bucket is required.","csrf":"$new_csrf"});
exit;
}
} else {
$rclone_args{gdrive_service_account_file} = $form->{'GDRIVE_SERVICE_ACCOUNT_FILE'} // '';
$rclone_args{gdrive_root_folder_id} = $form->{'GDRIVE_ROOT_FOLDER_ID'} // '';
if ($rclone_args{gdrive_service_account_file} eq '') {
print qq({"success":false,"message":"Service account file path is required.","csrf":"$new_csrf"});
exit;
}
}
my ($ok, $err) = Gniza4cpWHM::UI::test_rclone_connection(%rclone_args);
if ($ok) {
my $label = $type eq 's3' ? 'S3' : 'Google Drive';
print qq({"success":true,"message":"$label connection successful.","csrf":"$new_csrf"});
} else {
$err //= 'Unknown error';
$err =~ s/\\/\\\\/g;
$err =~ s/"/\\"/g;
$err =~ s/\n/\\n/g;
$err =~ s/\r/\\r/g;
$err =~ s/\t/\\t/g;
$err =~ s/[\x00-\x1f]//g;
print qq({"success":false,"message":"Connection failed: $err","csrf":"$new_csrf"});
}
}
else {
print qq({"success":false,"message":"Unknown remote type.","csrf":"$new_csrf"});
}
exit;
}
# ── List ─────────────────────────────────────────────────────
sub handle_list {
print "Content-Type: text/html\r\n\r\n";
Whostmgr::HTMLInterface::defheader('GNIZA4CP Backup Manager — Remotes', '', '/cgi/gniza4cp-whm/remotes.cgi');
print Gniza4cpWHM::UI::page_header('Remote Destinations');
print Gniza4cpWHM::UI::render_nav('remotes.cgi');
print Gniza4cpWHM::UI::render_flash();
my @remotes = Gniza4cpWHM::UI::list_remotes();
# Build schedule map: remote_name => [schedule_names]
my %schedule_map;
for my $sname (Gniza4cpWHM::UI::list_schedules()) {
my $scfg = Gniza4cpWHM::Config::parse(Gniza4cpWHM::UI::schedule_conf_path($sname), 'schedule');
my $remotes_str = $scfg->{REMOTES} // '';
if ($remotes_str eq '') {
# Empty REMOTES = targets ALL remotes
for my $r (@remotes) { push @{$schedule_map{$r}}, $sname; }
} else {
for my $r (split /,/, $remotes_str) {
$r =~ s/^\s+|\s+$//g;
push @{$schedule_map{$r}}, $sname if $r ne '';
}
}
}
print qq{
\n
\n};
if (@remotes) {
print qq{
\n};
print qq{| Name | Type | Destination | Disk Usage | Schedules | Retention | Actions |
\n};
print qq{\n};
for my $name (@remotes) {
my $conf = Gniza4cpWHM::Config::parse(Gniza4cpWHM::UI::remote_conf_path($name), 'remote');
my $esc_name = Gniza4cpWHM::UI::esc($name);
my $type = $conf->{REMOTE_TYPE} // 'ssh';
my $retention_raw = $conf->{RETENTION_COUNT} // '';
my $retention = Gniza4cpWHM::UI::esc($retention_raw ne '' ? $retention_raw : '30');
my ($type_label, $dest);
if ($type eq 's3') {
$type_label = 'S3';
$dest = 's3://' . Gniza4cpWHM::UI::esc($conf->{S3_BUCKET} // '');
} elsif ($type eq 'gdrive') {
$type_label = 'GDrive';
my $sa = $conf->{GDRIVE_SERVICE_ACCOUNT_FILE} // '';
$sa =~ s{.*/}{};
$dest = 'gdrive:' . Gniza4cpWHM::UI::esc($sa);
} else {
$type_label = 'SSH';
my $host = Gniza4cpWHM::UI::esc($conf->{REMOTE_HOST} // '');
my $port = Gniza4cpWHM::UI::esc($conf->{REMOTE_PORT} // '22');
$dest = "$host:$port";
}
# Schedule count + tooltip
my @sched_names = @{$schedule_map{$name} // []};
my $sched_count = scalar @sched_names;
my $sched_html;
if ($sched_count > 0) {
my $sched_tip = Gniza4cpWHM::UI::esc(join(', ', @sched_names));
$sched_html = qq{$sched_count};
} else {
$sched_html = qq{0};
}
print qq{};
print qq{| $esc_name | };
print qq{$type_label | };
print qq{$dest | };
print qq{ | };
print qq{$sched_html | };
print qq{$retention | };
print qq{};
print qq{ };
print qq{};
print qq{};
print qq{ };
print qq{ | };
print qq{
\n};
}
print qq{\n
\n};
# JavaScript: fetch disk info for each remote via safe DOM methods
print <<'JS';
JS
} else {
print qq{
No remote destinations configured. Add a remote to enable multi-remote backups.
\n};
print qq{
Remote configs are stored in /etc/gniza4cp/remotes.d/.
\n};
}
print qq{
\n
\n};
print qq{\n};
print qq{ \n};
print qq{
\n};
print Gniza4cpWHM::UI::page_footer();
Whostmgr::HTMLInterface::footer();
}
# ── Disk Info (JSON, GET) ────────────────────────────────────
sub handle_disk_info {
print "Content-Type: application/json\r\n\r\n";
my $name = $form->{'name'} // '';
my $name_err = Gniza4cpWHM::Validator::validate_remote_name($name);
if ($name_err) {
print qq({"ok":false,"disk":"Invalid remote name"});
exit;
}
my $conf_path = Gniza4cpWHM::UI::remote_conf_path($name);
unless (-f $conf_path) {
print qq({"ok":false,"disk":"Remote not found"});
exit;
}
my $conf = Gniza4cpWHM::Config::parse($conf_path, 'remote');
my $type = $conf->{REMOTE_TYPE} // 'ssh';
my $base = $conf->{REMOTE_BASE} // '/backups';
my $disk_str = eval { _get_disk_info($conf, $type, $base) };
if ($@ || !defined $disk_str) {
my $err = $@ || 'Unknown error';
$err =~ s/[\r\n]/ /g;
$err =~ s/"/\\"/g;
print qq({"ok":false,"disk":"Connection failed"});
exit;
}
$disk_str =~ s/\\/\\\\/g;
$disk_str =~ s/"/\\"/g;
$disk_str =~ s/\n/\\n/g;
$disk_str =~ s/\r/\\r/g;
$disk_str =~ s/[\x00-\x1f]//g;
print qq({"ok":true,"disk":"$disk_str"});
exit;
}
sub _get_disk_info {
my ($conf, $type, $base) = @_;
if ($type eq 'ssh') {
return _get_disk_info_ssh($conf, $base);
} else {
return _get_disk_info_rclone($conf, $type, $base);
}
}
sub _get_disk_info_ssh {
my ($conf, $base) = @_;
my $host = $conf->{REMOTE_HOST} // '';
my $port = $conf->{REMOTE_PORT} || '22';
my $user = $conf->{REMOTE_USER} || 'root';
my $auth_method = $conf->{REMOTE_AUTH_METHOD} || 'key';
my $key = $conf->{REMOTE_KEY} // '';
my $password = $conf->{REMOTE_PASSWORD} // '';
my @ssh_args = (
'ssh', '-n',
'-p', $port,
'-o', 'StrictHostKeyChecking=accept-new',
'-o', 'ConnectTimeout=10',
'-o', 'BatchMode=yes',
);
# Build df command — quote $base for remote shell
(my $safe_base = $base) =~ s/'/'\\''/g;
my $remote_cmd = "df -B1 '$safe_base' 2>/dev/null | tail -1";
my @cmd;
if ($auth_method eq 'password') {
# Remove BatchMode for password auth
@ssh_args = grep { $_ ne 'BatchMode=yes' } @ssh_args;
push @ssh_args, "$user\@$host", $remote_cmd;
$ENV{SSHPASS} = $password;
@cmd = ('sshpass', '-e', @ssh_args);
} else {
push @ssh_args, '-i', $key;
push @ssh_args, "$user\@$host", $remote_cmd;
@cmd = @ssh_args;
}
my ($in, $out, $err_fh) = (undef, undef, gensym);
my $pid = eval { IPC::Open3::open3($in, $out, $err_fh, @cmd) };
delete $ENV{SSHPASS};
return undef unless $pid;
close $in if $in;
# Timeout: kill subprocess after 20 seconds
local $SIG{ALRM} = sub { kill 'TERM', $pid; };
alarm(20);
my $stdout = do { local $/; <$out> } // '';
close $out;
close $err_fh;
waitpid($pid, 0);
alarm(0);
return undef if ($? >> 8) != 0;
chomp $stdout;
return undef if $stdout eq '';
# df -B1 output: Filesystem 1B-blocks Used Available Use% Mounted
my @fields = split /\s+/, $stdout;
return undef if @fields < 4;
my ($total, $used, $avail) = ($fields[1], $fields[2], $fields[3]);
return undef unless $total =~ /^\d+$/ && $used =~ /^\d+$/ && $avail =~ /^\d+$/;
return _human_size($used) . ' used / ' . _human_size($total) . ' total (' . _human_size($avail) . ' free)';
}
sub _get_disk_info_rclone {
my ($conf, $type, $base) = @_;
# Build temp rclone config
my $tmpfile = "/tmp/gniza4cp-rclone-disk-$$.conf";
my $conf_content = '';
my $about_path = '';
if ($type eq 's3') {
my $key_id = $conf->{S3_ACCESS_KEY_ID} // '';
my $secret = $conf->{S3_SECRET_ACCESS_KEY} // '';
my $region = $conf->{S3_REGION} || 'us-east-1';
my $endpoint = $conf->{S3_ENDPOINT} // '';
my $bucket = $conf->{S3_BUCKET} // '';
$conf_content = "[remote]\ntype = s3\nprovider = AWS\naccess_key_id = $key_id\nsecret_access_key = $secret\nregion = $region\n";
$conf_content .= "endpoint = $endpoint\n" if $endpoint ne '';
$about_path = "remote:$bucket$base";
}
elsif ($type eq 'gdrive') {
my $sa_file = $conf->{GDRIVE_SERVICE_ACCOUNT_FILE} // '';
my $folder_id = $conf->{GDRIVE_ROOT_FOLDER_ID} // '';
$conf_content = "[remote]\ntype = drive\nscope = drive\nservice_account_file = $sa_file\n";
$conf_content .= "root_folder_id = $folder_id\n" if $folder_id ne '';
$about_path = "remote:$base";
}
else {
return undef;
}
if (open my $fh, '>', $tmpfile) {
print $fh $conf_content;
close $fh;
chmod 0600, $tmpfile;
} else {
return undef;
}
my @cmd = ('rclone', '--config', $tmpfile, '--timeout', '15s', 'about', '--json', $about_path);
my ($in, $out, $err_fh) = (undef, undef, gensym);
my $pid = eval { IPC::Open3::open3($in, $out, $err_fh, @cmd) };
unless ($pid) {
unlink $tmpfile;
return undef;
}
close $in if $in;
# Timeout: kill subprocess after 20 seconds
local $SIG{ALRM} = sub { kill 'TERM', $pid; };
alarm(20);
my $stdout = do { local $/; <$out> } // '';
close $out;
close $err_fh;
waitpid($pid, 0);
alarm(0);
my $exit_code = $? >> 8;
unlink $tmpfile;
return undef if $exit_code != 0;
# Parse JSON fields: used, total, free
my ($used) = ($stdout =~ /"used"\s*:\s*(\d+)/);
my ($total) = ($stdout =~ /"total"\s*:\s*(\d+)/);
my ($free) = ($stdout =~ /"free"\s*:\s*(\d+)/);
if (defined $used && defined $total && $total > 0) {
my $free_str = defined $free ? ' (' . _human_size($free) . ' free)' : '';
return _human_size($used) . ' used / ' . _human_size($total) . ' total' . $free_str;
} elsif (defined $used) {
return _human_size($used) . ' used';
}
return undef;
}
sub _human_size {
my ($bytes) = @_;
return '0 B' unless defined $bytes && $bytes >= 0;
my @units = ('B', 'KB', 'MB', 'GB', 'TB', 'PB');
my $i = 0;
my $size = $bytes + 0.0;
while ($size >= 1024 && $i < $#units) {
$size /= 1024;
$i++;
}
if ($i == 0) {
return sprintf('%d %s', $size, $units[$i]);
}
return sprintf('%.2f %s', $size, $units[$i]);
}
# ── Add ──────────────────────────────────────────────────────
sub handle_add {
my @errors;
if ($method eq 'POST') {
unless (Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
push @errors, 'Invalid or expired form token. Please try again.';
}
my $name = $form->{'remote_name'} // '';
my $name_err = Gniza4cpWHM::Validator::validate_remote_name($name);
push @errors, $name_err if $name_err;
if (!@errors && -f Gniza4cpWHM::UI::remote_conf_path($name)) {
push @errors, "A remote named '$name' already exists.";
}
my %data;
for my $key (@Gniza4cpWHM::Config::REMOTE_KEYS) {
$data{$key} = $form->{$key} // '';
}
if (!@errors) {
my $validation_errors = Gniza4cpWHM::Validator::validate_remote_config(\%data);
push @errors, @$validation_errors;
}
if (!@errors) {
my $type = $data{REMOTE_TYPE} || 'ssh';
my ($conn_ok, $conn_err);
if ($type eq 'ssh') {
($conn_ok, $conn_err) = Gniza4cpWHM::UI::test_ssh_connection(
host => $data{REMOTE_HOST},
port => $data{REMOTE_PORT} || '22',
user => $data{REMOTE_USER} || 'root',
auth_method => $data{REMOTE_AUTH_METHOD} || 'key',
key => $data{REMOTE_KEY},
password => $data{REMOTE_PASSWORD},
);
} else {
my %rclone_args = (type => $type);
if ($type eq 's3') {
$rclone_args{s3_access_key_id} = $data{S3_ACCESS_KEY_ID};
$rclone_args{s3_secret_access_key} = $data{S3_SECRET_ACCESS_KEY};
$rclone_args{s3_region} = $data{S3_REGION} || 'us-east-1';
$rclone_args{s3_endpoint} = $data{S3_ENDPOINT};
$rclone_args{s3_bucket} = $data{S3_BUCKET};
} else {
$rclone_args{gdrive_service_account_file} = $data{GDRIVE_SERVICE_ACCOUNT_FILE};
$rclone_args{gdrive_root_folder_id} = $data{GDRIVE_ROOT_FOLDER_ID};
}
($conn_ok, $conn_err) = Gniza4cpWHM::UI::test_rclone_connection(%rclone_args);
}
push @errors, "Connection test failed: $conn_err" unless $conn_ok;
}
if (!@errors) {
# Ensure main config exists (gniza4cp backup requires it)
_ensure_main_config();
# Copy example template then write values
my $dest = Gniza4cpWHM::UI::remote_conf_path($name);
my $example = Gniza4cpWHM::UI::remote_example_path();
if (-f $example) {
File::Copy::copy($example, $dest)
or do { push @errors, "Failed to create remote file: $!"; goto RENDER_ADD; };
}
my ($ok, $err) = Gniza4cpWHM::Config::save($dest, \%data, \@Gniza4cpWHM::Config::REMOTE_KEYS);
if ($ok) {
# Initialize remote directory structure
my $type = $data{REMOTE_TYPE} || 'ssh';
my %init_args = (
type => $type,
remote_base => $data{REMOTE_BASE} || '/backups',
);
if ($type eq 'ssh') {
$init_args{host} = $data{REMOTE_HOST};
$init_args{port} = $data{REMOTE_PORT} || '22';
$init_args{user} = $data{REMOTE_USER} || 'root';
$init_args{auth_method} = $data{REMOTE_AUTH_METHOD} || 'key';
$init_args{key} = $data{REMOTE_KEY};
$init_args{password} = $data{REMOTE_PASSWORD};
} elsif ($type eq 's3') {
$init_args{s3_access_key_id} = $data{S3_ACCESS_KEY_ID};
$init_args{s3_secret_access_key} = $data{S3_SECRET_ACCESS_KEY};
$init_args{s3_region} = $data{S3_REGION};
$init_args{s3_endpoint} = $data{S3_ENDPOINT};
$init_args{s3_bucket} = $data{S3_BUCKET};
} else {
$init_args{gdrive_service_account_file} = $data{GDRIVE_SERVICE_ACCOUNT_FILE};
$init_args{gdrive_root_folder_id} = $data{GDRIVE_ROOT_FOLDER_ID};
}
Gniza4cpWHM::UI::init_remote_dir(%init_args);
if ($form->{'wizard'}) {
Gniza4cpWHM::UI::set_flash('success', "Remote '$name' created. Now set up a schedule.");
print "Status: 302 Found\r\n";
print "Location: schedules.cgi?action=add&wizard=1&remote_name=" . _uri_escape($name) . "\r\n\r\n";
exit;
}
Gniza4cpWHM::UI::set_flash('success', "Remote '$name' created successfully.");
print "Status: 302 Found\r\n";
print "Location: remotes.cgi\r\n\r\n";
exit;
} else {
push @errors, "Failed to save remote config: $err";
}
}
}
RENDER_ADD:
print "Content-Type: text/html\r\n\r\n";
Whostmgr::HTMLInterface::defheader('GNIZA4CP Backup Manager — Add Remote', '', '/cgi/gniza4cp-whm/remotes.cgi');
print Gniza4cpWHM::UI::page_header('Add Remote Destination');
print Gniza4cpWHM::UI::render_nav('remotes.cgi');
if (@errors) {
print Gniza4cpWHM::UI::render_errors(\@errors);
}
# Pre-populate from POST if validation failed, else defaults
my $conf = {};
if ($method eq 'POST') {
for my $key (@Gniza4cpWHM::Config::REMOTE_KEYS) {
$conf->{$key} = $form->{$key} // '';
}
} elsif ($form->{'key_path'}) {
$conf->{REMOTE_KEY} = $form->{'key_path'};
}
my $name_val = Gniza4cpWHM::UI::esc($form->{'remote_name'} // '');
render_remote_form($conf, $name_val, 0, $form->{'wizard'} ? 1 : 0);
print Gniza4cpWHM::UI::page_footer();
Whostmgr::HTMLInterface::footer();
}
# ── Edit ─────────────────────────────────────────────────────
sub handle_edit {
my $name = $form->{'name'} // '';
my @errors;
# Validate name
my $name_err = Gniza4cpWHM::Validator::validate_remote_name($name);
if ($name_err) {
Gniza4cpWHM::UI::set_flash('error', "Invalid remote name.");
print "Status: 302 Found\r\n";
print "Location: remotes.cgi\r\n\r\n";
exit;
}
my $conf_path = Gniza4cpWHM::UI::remote_conf_path($name);
unless (-f $conf_path) {
Gniza4cpWHM::UI::set_flash('error', "Remote '$name' not found.");
print "Status: 302 Found\r\n";
print "Location: remotes.cgi\r\n\r\n";
exit;
}
if ($method eq 'POST') {
unless (Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
push @errors, 'Invalid or expired form token. Please try again.';
}
my %data;
for my $key (@Gniza4cpWHM::Config::REMOTE_KEYS) {
$data{$key} = $form->{$key} // '';
}
if (!@errors) {
my $validation_errors = Gniza4cpWHM::Validator::validate_remote_config(\%data);
push @errors, @$validation_errors;
}
if (!@errors) {
my $type = $data{REMOTE_TYPE} || 'ssh';
my ($conn_ok, $conn_err);
if ($type eq 'ssh') {
($conn_ok, $conn_err) = Gniza4cpWHM::UI::test_ssh_connection(
host => $data{REMOTE_HOST},
port => $data{REMOTE_PORT} || '22',
user => $data{REMOTE_USER} || 'root',
auth_method => $data{REMOTE_AUTH_METHOD} || 'key',
key => $data{REMOTE_KEY},
password => $data{REMOTE_PASSWORD},
);
} else {
my %rclone_args = (type => $type);
if ($type eq 's3') {
$rclone_args{s3_access_key_id} = $data{S3_ACCESS_KEY_ID};
$rclone_args{s3_secret_access_key} = $data{S3_SECRET_ACCESS_KEY};
$rclone_args{s3_region} = $data{S3_REGION} || 'us-east-1';
$rclone_args{s3_endpoint} = $data{S3_ENDPOINT};
$rclone_args{s3_bucket} = $data{S3_BUCKET};
} else {
$rclone_args{gdrive_service_account_file} = $data{GDRIVE_SERVICE_ACCOUNT_FILE};
$rclone_args{gdrive_root_folder_id} = $data{GDRIVE_ROOT_FOLDER_ID};
}
($conn_ok, $conn_err) = Gniza4cpWHM::UI::test_rclone_connection(%rclone_args);
}
push @errors, "Connection test failed: $conn_err" unless $conn_ok;
}
if (!@errors) {
my ($ok, $err) = Gniza4cpWHM::Config::save($conf_path, \%data, \@Gniza4cpWHM::Config::REMOTE_KEYS);
if ($ok) {
Gniza4cpWHM::UI::set_flash('success', "Remote '$name' updated successfully.");
print "Status: 302 Found\r\n";
print "Location: remotes.cgi\r\n\r\n";
exit;
} else {
push @errors, "Failed to save remote config: $err";
}
}
}
print "Content-Type: text/html\r\n\r\n";
Whostmgr::HTMLInterface::defheader('GNIZA4CP Backup Manager — Edit Remote', '', '/cgi/gniza4cp-whm/remotes.cgi');
print Gniza4cpWHM::UI::page_header("Edit Remote: " . Gniza4cpWHM::UI::esc($name));
print Gniza4cpWHM::UI::render_nav('remotes.cgi');
if (@errors) {
print Gniza4cpWHM::UI::render_errors(\@errors);
}
# Load config (or re-use POST data on error)
my $conf;
if (@errors && $method eq 'POST') {
$conf = {};
for my $key (@Gniza4cpWHM::Config::REMOTE_KEYS) {
$conf->{$key} = $form->{$key} // '';
}
} else {
$conf = Gniza4cpWHM::Config::parse($conf_path, 'remote');
}
render_remote_form($conf, Gniza4cpWHM::UI::esc($name), 1);
print Gniza4cpWHM::UI::page_footer();
Whostmgr::HTMLInterface::footer();
}
# ── Delete ───────────────────────────────────────────────────
sub handle_delete {
if ($method ne 'POST') {
print "Status: 302 Found\r\n";
print "Location: remotes.cgi\r\n\r\n";
exit;
}
unless (Gniza4cpWHM::UI::verify_csrf_token($form->{'gniza4cp_csrf'})) {
Gniza4cpWHM::UI::set_flash('error', 'Invalid or expired form token.');
print "Status: 302 Found\r\n";
print "Location: remotes.cgi\r\n\r\n";
exit;
}
my $name = $form->{'name'} // '';
my $name_err = Gniza4cpWHM::Validator::validate_remote_name($name);
if ($name_err) {
Gniza4cpWHM::UI::set_flash('error', 'Invalid remote name.');
print "Status: 302 Found\r\n";
print "Location: remotes.cgi\r\n\r\n";
exit;
}
my $conf_path = Gniza4cpWHM::UI::remote_conf_path($name);
if (-f $conf_path) {
unlink $conf_path;
Gniza4cpWHM::UI::set_flash('success', "Remote '$name' deleted.");
} else {
Gniza4cpWHM::UI::set_flash('error', "Remote '$name' not found.");
}
print "Status: 302 Found\r\n";
print "Location: remotes.cgi\r\n\r\n";
exit;
}
# ── Shared Form Renderer ────────────────────────────────────
sub render_remote_form {
my ($conf, $name_val, $is_edit, $wizard) = @_;
my $action_val = $is_edit ? 'edit' : 'add';
my $remote_type = $conf->{REMOTE_TYPE} // 'ssh';
print qq{\n};
my $js_csrf = Gniza4cpWHM::UI::esc(Gniza4cpWHM::UI::generate_csrf_token());
print qq{
JS
}
sub _field {
my ($conf, $key, $label, $hint, $tip) = @_;
my $val = Gniza4cpWHM::UI::esc($conf->{$key} // '');
my $hint_html = $hint ? qq{ } . Gniza4cpWHM::UI::esc($hint) . qq{} : '';
my $tip_html = $tip ? qq{ ⓘ} : '';
print qq{\n};
print qq{ \n};
print qq{ \n};
print qq{ $hint_html\n} if $hint;
print qq{
\n};
}
sub _password_field {
my ($conf, $key, $label, $hint) = @_;
my $val = Gniza4cpWHM::UI::esc($conf->{$key} // '');
my $hint_html = $hint ? qq{ } . Gniza4cpWHM::UI::esc($hint) . qq{} : '';
print qq{\n};
print qq{ \n};
print qq{ \n};
print qq{ $hint_html\n} if $hint;
print qq{
\n};
}
sub _uri_escape {
my ($str) = @_;
$str =~ s/([^A-Za-z0-9._~-])/sprintf("%%%02X", ord($1))/ge;
return $str;
}
sub _ensure_main_config {
my $config_file = '/etc/gniza4cp/gniza4cp.conf';
return if -f $config_file;
# Create dirs
for my $dir ('/etc/gniza4cp', '/etc/gniza4cp/remotes.d', '/etc/gniza4cp/schedules.d', '/var/log/gniza4cp') {
mkdir $dir unless -d $dir;
}
# Copy example or write defaults
my $example = '/usr/local/gniza4cp/etc/gniza4cp.conf.example';
if (-f $example) {
File::Copy::copy($example, $config_file);
} else {
if (open my $fh, '>', $config_file) {
print $fh qq{# gniza4cp configuration — auto-created by WHM plugin\n};
print $fh qq{TEMP_DIR="/usr/local/gniza4cp/workdir"\n};
print $fh qq{INCLUDE_ACCOUNTS=""\n};
print $fh qq{EXCLUDE_ACCOUNTS="nobody"\n};
print $fh qq{LOG_DIR="/var/log/gniza4cp"\n};
print $fh qq{LOG_LEVEL="info"\n};
print $fh qq{LOG_RETAIN=90\n};
print $fh qq{NOTIFY_EMAIL=""\n};
print $fh qq{NOTIFY_ON="failure"\n};
print $fh qq{LOCK_FILE="/var/run/gniza4cp.lock"\n};
print $fh qq{SSH_TIMEOUT=30\n};
print $fh qq{SSH_RETRIES=3\n};
print $fh qq{RSYNC_EXTRA_OPTS=""\n};
close $fh;
}
}
}