From ad884665cdf87b8c2fa643bfa332a603ddd90454 Mon Sep 17 00:00:00 2001 From: shuki Date: Wed, 4 Mar 2026 03:30:44 +0200 Subject: [PATCH] Refactor setup wizard to reuse remotes.cgi and schedules.cgi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setup wizard step 1 (SSH key) stays as-is, then redirects to remotes.cgi?action=add&wizard=1 and schedules.cgi?action=add&wizard=1 instead of reimplementing its own forms. - Remove ~200 lines of duplicated form/handler code from setup.cgi - Wizard gets full feature parity (S3/GDrive, password auth, etc.) - key_path pre-fills REMOTE_KEY, remote_name pre-fills REMOTES - wizard=1 flag chains the flow: remote add → schedule add → index Co-Authored-By: Claude Opus 4.6 --- whm/gniza-whm/remotes.cgi | 23 +- whm/gniza-whm/schedules.cgi | 15 +- whm/gniza-whm/setup.cgi | 427 +----------------------------------- 3 files changed, 42 insertions(+), 423 deletions(-) diff --git a/whm/gniza-whm/remotes.cgi b/whm/gniza-whm/remotes.cgi index c2e129a..ab1b936 100644 --- a/whm/gniza-whm/remotes.cgi +++ b/whm/gniza-whm/remotes.cgi @@ -267,6 +267,12 @@ sub handle_add { } my ($ok, $err) = GnizaWHM::Config::write($dest, \%data, \@GnizaWHM::Config::REMOTE_KEYS); if ($ok) { + if ($form->{'wizard'}) { + GnizaWHM::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; + } GnizaWHM::UI::set_flash('success', "Remote '$name' created successfully."); print "Status: 302 Found\r\n"; print "Location: remotes.cgi\r\n\r\n"; @@ -289,16 +295,18 @@ sub handle_add { print GnizaWHM::UI::render_errors(\@errors); } - # Pre-populate from POST if validation failed, else empty + # Pre-populate from POST if validation failed, else defaults my $conf = {}; if ($method eq 'POST') { for my $key (@GnizaWHM::Config::REMOTE_KEYS) { $conf->{$key} = $form->{$key} // ''; } + } elsif ($form->{'key_path'}) { + $conf->{REMOTE_KEY} = $form->{'key_path'}; } my $name_val = GnizaWHM::UI::esc($form->{'remote_name'} // ''); - render_remote_form($conf, $name_val, 0); + render_remote_form($conf, $name_val, 0, $form->{'wizard'} ? 1 : 0); print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); @@ -453,13 +461,16 @@ sub handle_delete { # ── Shared Form Renderer ──────────────────────────────────── sub render_remote_form { - my ($conf, $name_val, $is_edit) = @_; + my ($conf, $name_val, $is_edit, $wizard) = @_; my $action_val = $is_edit ? 'edit' : 'add'; my $remote_type = $conf->{REMOTE_TYPE} // 'ssh'; print qq{
\n}; print qq{\n}; + if ($wizard) { + print qq{\n}; + } print GnizaWHM::UI::csrf_hidden_field(); if ($is_edit) { @@ -750,3 +761,9 @@ sub _password_field { 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; +} diff --git a/whm/gniza-whm/schedules.cgi b/whm/gniza-whm/schedules.cgi index ddd16db..d3d8285 100644 --- a/whm/gniza-whm/schedules.cgi +++ b/whm/gniza-whm/schedules.cgi @@ -154,6 +154,12 @@ sub handle_add { my ($ok, $err) = GnizaWHM::Config::write($dest, \%data, \@GnizaWHM::Config::SCHEDULE_KEYS); if ($ok) { my ($cron_ok, $cron_err) = GnizaWHM::Cron::install_schedule($name); + if ($form->{'wizard'}) { + GnizaWHM::UI::set_flash('success', 'Setup complete! Your remote and schedule are configured.'); + print "Status: 302 Found\r\n"; + print "Location: index.cgi\r\n\r\n"; + exit; + } if ($cron_ok) { GnizaWHM::UI::set_flash('success', "Schedule '$name' created and activated."); } else { @@ -185,10 +191,12 @@ sub handle_add { for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) { $conf->{$key} = $form->{$key} // ''; } + } elsif ($form->{'remote_name'}) { + $conf->{REMOTES} = $form->{'remote_name'}; } my $name_val = GnizaWHM::UI::esc($form->{'schedule_name'} // ''); - render_schedule_form($conf, $name_val, 0); + render_schedule_form($conf, $name_val, 0, $form->{'wizard'} ? 1 : 0); print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); @@ -371,11 +379,14 @@ sub _json_response { # ── Shared Form Renderer ──────────────────────────────────── sub render_schedule_form { - my ($conf, $name_val, $is_edit) = @_; + my ($conf, $name_val, $is_edit, $wizard) = @_; my $action_val = $is_edit ? 'edit' : 'add'; print qq{\n}; print qq{\n}; + if ($wizard) { + print qq{\n}; + } print GnizaWHM::UI::csrf_hidden_field(); if ($is_edit) { diff --git a/whm/gniza-whm/setup.cgi b/whm/gniza-whm/setup.cgi index 6d127e1..ff3ebfd 100644 --- a/whm/gniza-whm/setup.cgi +++ b/whm/gniza-whm/setup.cgi @@ -1,6 +1,6 @@ #!/usr/local/cpanel/3rdparty/bin/perl # gniza WHM Plugin — Setup Wizard -# 3-step wizard: SSH Key → Remote Destination → Schedule +# Step 1: SSH Key selection, then redirects to remotes.cgi and schedules.cgi use strict; use warnings; @@ -8,54 +8,13 @@ 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() } +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 { @@ -63,13 +22,12 @@ sub handle_step1 { 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}; + print qq{

gniza uses SSH keys to connect to remote backup destinations. Select an existing key or create one first.

\n}; if (@$keys) { print qq{
\n}; @@ -100,8 +58,9 @@ sub handle_step1 { 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}; @@ -121,8 +80,9 @@ sub handle_step1 { 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}; @@ -161,372 +121,3 @@ 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}; - for my $i (1..3) { - my $class = 'step'; - $class .= ' step-primary' if $i <= $current; - print qq{
  • $labels[$i-1]
  • \n}; - } - 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; -}