#!/usr/local/cpanel/3rdparty/bin/perl # gniza WHM Plugin — Setup Wizard # 3-step wizard: SSH Key → Remote Destination → Schedule use strict; use warnings; use lib '/usr/local/cpanel/whostmgr/docroot/cgi/gniza-whm/lib'; use Whostmgr::HTMLInterface (); use Cpanel::Form (); use File::Copy (); use GnizaWHM::Config; use GnizaWHM::Validator; use GnizaWHM::Cron; use GnizaWHM::UI; my $form = Cpanel::Form::parseform(); my $method = $ENV{'REQUEST_METHOD'} // 'GET'; my $step = $form->{'step'} // '1'; if ($step eq 'test') { handle_test_connection() } elsif ($step eq '2') { handle_step2() } elsif ($step eq '3') { handle_step3() } else { handle_step1() } exit; # ── Test Connection (JSON) ──────────────────────────────────── sub handle_test_connection { print "Content-Type: application/json\r\n\r\n"; my $host = $form->{'host'} // ''; my $port = $form->{'port'} || '22'; my $user = $form->{'user'} || 'root'; my $key = $form->{'key'} // ''; if ($host eq '' || $key eq '') { print qq({"success":false,"message":"Host and SSH key path are required."}); exit; } my ($ok, $err) = GnizaWHM::UI::test_ssh_connection($host, $port, $user, $key); if ($ok) { print qq({"success":true,"message":"SSH connection successful."}); } 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"}); } exit; } # ── Step 1: SSH Key ────────────────────────────────────────── sub handle_step1 { print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('gniza Setup Wizard', '', '/cgi/gniza-whm/setup.cgi'); print GnizaWHM::UI::page_header('gniza Setup Wizard'); render_steps_indicator(1); my $keys = GnizaWHM::UI::detect_ssh_keys(); print qq{
\n
\n}; print qq{

Step 1: SSH Key

\n}; print qq{

gniza uses SSH keys to connect to remote backup destinations. An SSH key must be set up before adding a remote.

\n}; if (@$keys) { print qq{
\n}; print qq{

Existing keys found:

\n}; print qq{\n}; print qq{\n}; print qq{\n}; my $first = 1; for my $k (@$keys) { my $checked = $first ? ' checked' : ''; my $pub = $k->{has_pub} ? 'Available' : 'Missing'; my $esc_path = GnizaWHM::UI::esc($k->{path}); my $esc_type = GnizaWHM::UI::esc($k->{type}); print qq{}; print qq{}; print qq{}; print qq{}; print qq{}; print qq{\n}; $first = 0; } print qq{\n
TypePathPublic Key
$esc_type$esc_path$pub
\n}; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; print qq{
\n}; print qq{\n}; print qq{
\n}; print qq{ \n}; print qq{ Cancel\n}; print qq{
\n}; print qq{
\n}; } else { print qq{
No SSH keys found in /root/.ssh/. You need to create one first.
\n}; } # Always show key generation instructions print qq{
\n}; print qq{

Generate a new SSH key (if needed):

\n}; print qq{
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""
\n}; print qq{

Copy the public key to the remote server:

\n}; print qq{
ssh-copy-id -i /root/.ssh/id_ed25519.pub user\@host
\n}; print qq{

Run these commands in WHM → Server Configuration → Terminal, or via SSH.

\n}; print qq{
\n}; unless (@$keys) { print qq{
\n}; print qq{\n}; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; print qq{
\n}; print qq{ \n}; print qq{ Cancel\n}; print qq{
\n}; print qq{
\n}; } print qq{
\n
\n}; # JS to resolve selected key into key_path param print <<'JS'; JS print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Step 2: Remote Destination ─────────────────────────────── sub handle_step2 { my @errors; my $key_path = $form->{'key_path'} // '/root/.ssh/id_ed25519'; if ($method eq 'POST') { unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) { push @errors, 'Invalid or expired form token. Please try again.'; } my $name = $form->{'remote_name'} // ''; my $name_err = GnizaWHM::Validator::validate_remote_name($name); push @errors, $name_err if $name_err; if (!@errors && -f GnizaWHM::UI::remote_conf_path($name)) { push @errors, "A remote named '$name' already exists."; } my %data; for my $key (@GnizaWHM::Config::REMOTE_KEYS) { $data{$key} = $form->{$key} // ''; } if (!@errors) { my $validation_errors = GnizaWHM::Validator::validate_remote_config(\%data); push @errors, @$validation_errors; } if (!@errors) { my ($ssh_ok, $ssh_err) = GnizaWHM::UI::test_ssh_connection( $data{REMOTE_HOST}, $data{REMOTE_PORT} || '22', $data{REMOTE_USER} || 'root', $data{REMOTE_KEY}, ); push @errors, "SSH connection test failed: $ssh_err" unless $ssh_ok; } if (!@errors) { my $dest = GnizaWHM::UI::remote_conf_path($name); my $example = GnizaWHM::UI::remote_example_path(); if (-f $example) { File::Copy::copy($example, $dest) or do { push @errors, "Failed to create remote file: $!"; goto RENDER_STEP2; }; } my ($ok, $err) = GnizaWHM::Config::write($dest, \%data, \@GnizaWHM::Config::REMOTE_KEYS); if ($ok) { print "Status: 302 Found\r\n"; print "Location: setup.cgi?step=3&remote_name=" . _uri_escape($name) . "\r\n\r\n"; exit; } else { push @errors, "Failed to save remote config: $err"; } } } RENDER_STEP2: print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('gniza Setup Wizard', '', '/cgi/gniza-whm/setup.cgi'); print GnizaWHM::UI::page_header('gniza Setup Wizard'); render_steps_indicator(2); if (@errors) { print GnizaWHM::UI::render_errors(\@errors); } my $conf = {}; my $name_val = ''; if ($method eq 'POST') { for my $key (@GnizaWHM::Config::REMOTE_KEYS) { $conf->{$key} = $form->{$key} // ''; } $name_val = GnizaWHM::UI::esc($form->{'remote_name'} // ''); } else { $conf->{REMOTE_KEY} = $key_path; $conf->{REMOTE_PORT} = '22'; $conf->{REMOTE_USER} = 'root'; $conf->{REMOTE_BASE} = '/backups'; $conf->{RETENTION_COUNT} = '30'; $conf->{BWLIMIT} = '0'; } print qq{
\n}; print qq{\n}; print qq{\n}; print qq{\n}; print GnizaWHM::UI::csrf_hidden_field(); print qq{
\n
\n}; print qq{

Step 2: Remote Destination

\n}; print qq{

Configure the remote server where backups will be stored.

\n}; _wiz_field('remote_name', $name_val, 'Remote Name', 'Letters, digits, hyphens, underscores'); _wiz_field_conf($conf, 'REMOTE_HOST', 'Hostname / IP', 'Required'); _wiz_field_conf($conf, 'REMOTE_PORT', 'SSH Port', 'Default: 22'); _wiz_field_conf($conf, 'REMOTE_USER', 'SSH User', 'Default: root'); _wiz_field_conf($conf, 'REMOTE_KEY', 'SSH Private Key', 'Absolute path, required'); _wiz_field_conf($conf, 'REMOTE_BASE', 'Remote Base Dir', 'Default: /backups'); _wiz_field_conf($conf, 'BWLIMIT', 'Bandwidth Limit', 'KB/s, 0 = unlimited'); _wiz_field_conf($conf, 'RETENTION_COUNT', 'Snapshots to Keep', 'Default: 30'); _wiz_field_conf($conf, 'RSYNC_EXTRA_OPTS', 'Extra rsync Options', 'Optional'); print qq{
\n
\n}; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{ Back\n}; print qq{
\n}; print qq{
\n}; print qq{
\n}; print <<'JS'; JS print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Step 3: Schedule (writes to schedules.d/) ──────────────── sub handle_step3 { my $remote_name = $form->{'remote_name'} // ''; my @errors; my $name_err = GnizaWHM::Validator::validate_remote_name($remote_name); if ($name_err) { GnizaWHM::UI::set_flash('error', 'Invalid remote name. Please start the wizard again.'); print "Status: 302 Found\r\n"; print "Location: setup.cgi\r\n\r\n"; exit; } my $remote_conf_path = GnizaWHM::UI::remote_conf_path($remote_name); unless (-f $remote_conf_path) { GnizaWHM::UI::set_flash('error', "Remote '$remote_name' not found. Please start the wizard again."); print "Status: 302 Found\r\n"; print "Location: setup.cgi\r\n\r\n"; exit; } if ($method eq 'POST') { unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) { push @errors, 'Invalid or expired form token. Please try again.'; } my $schedule = $form->{SCHEDULE} // ''; if ($schedule ne '' && !@errors) { # Create a schedule config in schedules.d/ targeting this remote my $sched_name = $remote_name; my %data = ( SCHEDULE => $schedule, SCHEDULE_TIME => $form->{SCHEDULE_TIME} // '02:00', SCHEDULE_DAY => $form->{SCHEDULE_DAY} // '', SCHEDULE_CRON => $form->{SCHEDULE_CRON} // '', REMOTES => $remote_name, ); my $validation_errors = GnizaWHM::Validator::validate_schedule_config(\%data); push @errors, @$validation_errors; if (!@errors) { my $sched_path = GnizaWHM::UI::schedule_conf_path($sched_name); my $example = GnizaWHM::UI::schedule_example_path(); if (-f $example) { File::Copy::copy($example, $sched_path) or do { push @errors, "Failed to create schedule file: $!"; }; } if (!@errors) { my ($ok, $err) = GnizaWHM::Config::write($sched_path, \%data, \@GnizaWHM::Config::SCHEDULE_KEYS); push @errors, "Failed to save schedule: $err" unless $ok; } } if (!@errors) { my ($ok, $stdout, $stderr) = GnizaWHM::Cron::install_schedules(); if (!$ok) { push @errors, "Schedule saved but cron install failed: $stderr"; } } } if (!@errors) { GnizaWHM::UI::set_flash('success', 'Setup complete! Your first remote destination is configured.'); print "Status: 302 Found\r\n"; print "Location: index.cgi\r\n\r\n"; exit; } } print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('gniza Setup Wizard', '', '/cgi/gniza-whm/setup.cgi'); print GnizaWHM::UI::page_header('gniza Setup Wizard'); render_steps_indicator(3); if (@errors) { print GnizaWHM::UI::render_errors(\@errors); } my $esc_name = GnizaWHM::UI::esc($remote_name); # Load existing schedule data from schedules.d/ if it exists, else from POST my $conf = {}; if (@errors && $method eq 'POST') { for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) { $conf->{$key} = $form->{$key} // ''; } } elsif (-f GnizaWHM::UI::schedule_conf_path($remote_name)) { $conf = GnizaWHM::Config::parse(GnizaWHM::UI::schedule_conf_path($remote_name), 'schedule'); } print qq{
\n}; print qq{\n}; print qq{\n}; print GnizaWHM::UI::csrf_hidden_field(); print qq{
\n
\n}; print qq{

Step 3: Backup Schedule

\n}; print qq{

Set up an automatic backup schedule for remote $esc_name. You can skip this and configure it later.

\n}; my $sched = $conf->{SCHEDULE} // ''; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; _wiz_field_conf($conf, 'SCHEDULE_TIME', 'Time (HH:MM)', 'Default: 02:00'); print qq{
\n}; _wiz_field_conf($conf, 'SCHEDULE_DAY', 'Day', 'Day-of-week 0-6 (weekly) or day-of-month 1-28 (monthly)'); print qq{
\n}; print qq{
\n}; _wiz_field_conf($conf, 'SCHEDULE_CRON', 'Cron Expression', '5-field cron (for custom only)'); print qq{
\n}; print qq{
\n
\n}; print qq{
\n}; print qq{ \n}; print qq{ Skip\n}; print qq{
\n}; print qq{
\n}; print <<'JS'; JS print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Shared Helpers ─────────────────────────────────────────── sub render_steps_indicator { my ($current) = @_; my @labels = ('SSH Key', 'Remote', 'Schedule'); print qq{\n}; } sub _wiz_field { my ($name, $val, $label, $hint) = @_; $val = GnizaWHM::UI::esc($val // ''); my $hint_html = $hint ? qq{ $hint} : ''; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{ $hint_html\n} if $hint; print qq{
\n}; } sub _wiz_field_conf { my ($conf, $key, $label, $hint) = @_; _wiz_field($key, $conf->{$key}, $label, $hint); } sub _uri_escape { my ($str) = @_; $str =~ s/([^A-Za-z0-9._~-])/sprintf("%%%02X", ord($1))/ge; return $str; }