#!/usr/local/cpanel/3rdparty/bin/perl # gniza WHM Plugin — Schedule CRUD 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 POSIX (); use GnizaWHM::Config; use GnizaWHM::Validator; use GnizaWHM::Cron; use GnizaWHM::UI; my $form = Cpanel::Form::parseform(); my $method = $ENV{'REQUEST_METHOD'} // 'GET'; my $action = $form->{'action'} // 'list'; # Route to handler if ($action eq 'add') { handle_add() } elsif ($action eq 'edit') { handle_edit() } elsif ($action eq 'delete') { handle_delete() } elsif ($action eq 'toggle_cron') { handle_toggle_cron() } elsif ($action eq 'run_now') { handle_run_now() } else { handle_list() } exit; # ── List ───────────────────────────────────────────────────── sub handle_list { print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Schedules', '', '/cgi/gniza-whm/schedules.cgi'); print GnizaWHM::UI::page_header('Schedule Management'); print GnizaWHM::UI::render_nav('schedules.cgi'); print GnizaWHM::UI::render_flash(); # Configured schedules my @schedules = GnizaWHM::UI::list_schedules(); my $cron_schedules = GnizaWHM::Cron::get_current_schedules(); print qq{
\n
\n}; print qq{

Configured Schedules

\n}; if (@schedules) { print qq{
\n}; print qq{\n}; print qq{\n}; for my $name (@schedules) { my $conf = GnizaWHM::Config::parse(GnizaWHM::UI::schedule_conf_path($name), 'schedule'); my $esc_name = GnizaWHM::UI::esc($name); my $esc_sched = GnizaWHM::UI::esc($conf->{SCHEDULE} // ''); my $esc_time = GnizaWHM::UI::esc($conf->{SCHEDULE_TIME} // '02:00'); my $esc_day = GnizaWHM::UI::esc($conf->{SCHEDULE_DAY} // '-'); my $esc_remotes = GnizaWHM::UI::esc($conf->{REMOTES} // '(all)'); $esc_remotes = '(all)' if $esc_remotes eq ''; my $in_cron = exists $cron_schedules->{$name}; my $checked = $in_cron ? ' checked' : ''; print qq{}; print qq{}; print qq{}; print qq{}; print qq{}; print qq{\n}; } print qq{\n
NameTypeTimeDayRemotesActiveActions
$esc_name$esc_sched$esc_time$esc_day$esc_remotes}; print qq{}; print qq{}; print qq{
}; print qq{
}; print qq{}; print qq{}; print GnizaWHM::UI::csrf_hidden_field(); print qq{}; print qq{
}; print qq{Edit}; print qq{
}; print qq{}; print qq{}; print GnizaWHM::UI::csrf_hidden_field(); print qq{}; print qq{
}; print qq{
}; print qq{
\n}; } else { print qq{

No schedules configured. Add a schedule to define when backups run.

\n}; } print qq{
\n
\n}; # CSRF token + AJAX toggle script my $csrf_token = GnizaWHM::UI::generate_csrf_token(); print qq{\n}; # Action buttons print qq{
\n}; print qq{ Add Schedule\n}; print qq{
\n}; print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Add ────────────────────────────────────────────────────── sub handle_add { my @errors; 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->{'schedule_name'} // ''; my $name_err = GnizaWHM::Validator::validate_schedule_name($name); push @errors, $name_err if $name_err; if (!@errors && -f GnizaWHM::UI::schedule_conf_path($name)) { push @errors, "A schedule named '$name' already exists."; } my %data; for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) { $data{$key} = $form->{$key} // ''; } if (!@errors) { my $validation_errors = GnizaWHM::Validator::validate_schedule_config(\%data); push @errors, @$validation_errors; } if (!@errors) { my $dest = GnizaWHM::UI::schedule_conf_path($name); my $example = GnizaWHM::UI::schedule_example_path(); if (-f $example) { File::Copy::copy($example, $dest) or do { push @errors, "Failed to create schedule file: $!"; goto RENDER_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 { GnizaWHM::UI::set_flash('warning', "Schedule '$name' created but cron activation failed: $cron_err"); } print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } else { push @errors, "Failed to save schedule config: $err"; } } } RENDER_ADD: print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Add Schedule', '', '/cgi/gniza-whm/schedules.cgi'); print GnizaWHM::UI::page_header('Add Schedule'); print GnizaWHM::UI::render_nav('schedules.cgi'); if (@errors) { print GnizaWHM::UI::render_errors(\@errors); } my $conf = {}; if ($method eq 'POST') { 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, $form->{'wizard'} ? 1 : 0); print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Edit ───────────────────────────────────────────────────── sub handle_edit { my $name = $form->{'name'} // ''; my @errors; my $name_err = GnizaWHM::Validator::validate_schedule_name($name); if ($name_err) { GnizaWHM::UI::set_flash('error', "Invalid schedule name."); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } my $conf_path = GnizaWHM::UI::schedule_conf_path($name); unless (-f $conf_path) { GnizaWHM::UI::set_flash('error', "Schedule '$name' not found."); print "Status: 302 Found\r\n"; print "Location: schedules.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 %data; for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) { $data{$key} = $form->{$key} // ''; } if (!@errors) { my $validation_errors = GnizaWHM::Validator::validate_schedule_config(\%data); push @errors, @$validation_errors; } if (!@errors) { my ($ok, $err) = GnizaWHM::Config::write($conf_path, \%data, \@GnizaWHM::Config::SCHEDULE_KEYS); if ($ok) { # Re-install cron if it was active, so timing/remote changes take effect my $cron_schedules = GnizaWHM::Cron::get_current_schedules(); if (exists $cron_schedules->{$name}) { GnizaWHM::Cron::install_schedule($name); } GnizaWHM::UI::set_flash('success', "Schedule '$name' updated successfully."); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } else { push @errors, "Failed to save schedule config: $err"; } } } print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('gniza Backup Manager — Edit Schedule', '', '/cgi/gniza-whm/schedules.cgi'); print GnizaWHM::UI::page_header("Edit Schedule: " . GnizaWHM::UI::esc($name)); print GnizaWHM::UI::render_nav('schedules.cgi'); if (@errors) { print GnizaWHM::UI::render_errors(\@errors); } my $conf; if (@errors && $method eq 'POST') { $conf = {}; for my $key (@GnizaWHM::Config::SCHEDULE_KEYS) { $conf->{$key} = $form->{$key} // ''; } } else { $conf = GnizaWHM::Config::parse($conf_path, 'schedule'); } render_schedule_form($conf, GnizaWHM::UI::esc($name), 1); print GnizaWHM::UI::page_footer(); Whostmgr::HTMLInterface::footer(); } # ── Delete ─────────────────────────────────────────────────── sub handle_delete { if ($method ne 'POST') { print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) { GnizaWHM::UI::set_flash('error', 'Invalid or expired form token.'); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } my $name = $form->{'name'} // ''; my $name_err = GnizaWHM::Validator::validate_schedule_name($name); if ($name_err) { GnizaWHM::UI::set_flash('error', 'Invalid schedule name.'); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } my $conf_path = GnizaWHM::UI::schedule_conf_path($name); if (-f $conf_path) { GnizaWHM::Cron::remove_schedule($name); unlink $conf_path; GnizaWHM::UI::set_flash('success', "Schedule '$name' deleted."); } else { GnizaWHM::UI::set_flash('error', "Schedule '$name' not found."); } print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } # ── Run Now ────────────────────────────────────────────────── sub handle_run_now { if ($method ne 'POST') { print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) { GnizaWHM::UI::set_flash('error', 'Invalid or expired form token.'); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } my $name = $form->{'name'} // ''; my $name_err = GnizaWHM::Validator::validate_schedule_name($name); if ($name_err) { GnizaWHM::UI::set_flash('error', 'Invalid schedule name.'); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } my $conf_path = GnizaWHM::UI::schedule_conf_path($name); unless (-f $conf_path) { GnizaWHM::UI::set_flash('error', "Schedule '$name' not found."); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } my $conf = GnizaWHM::Config::parse($conf_path, 'schedule'); my $remotes = $conf->{REMOTES} // ''; $remotes =~ s/^\s+|\s+$//g; # Build command my @cmd = ('/usr/local/bin/gniza', 'backup'); if ($remotes ne '') { push @cmd, "--remote=$remotes"; } my $log_file = "/var/log/gniza/cron-${name}.log"; # Double-fork to fully detach from CGI process group my $pid = fork(); if (!defined $pid) { GnizaWHM::UI::set_flash('error', "Failed to fork backup process: $!"); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } if ($pid == 0) { # First child: new session, then fork again POSIX::setsid(); my $pid2 = fork(); exit 0 if $pid2; # first child exits immediately # Grandchild: fully detached daemon close STDIN; open STDIN, '<', '/dev/null'; close STDOUT; open STDOUT, '>>', $log_file; close STDERR; open STDERR, '>&', \*STDOUT; exec @cmd; exit 1; } waitpid($pid, 0); # Reap first child (exits immediately) # Parent: redirect GnizaWHM::UI::set_flash('success', "Backup started for schedule '$name'. Check Logs for progress."); print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } # ── Toggle Cron ────────────────────────────────────────────── sub handle_toggle_cron { if ($method ne 'POST') { print "Status: 302 Found\r\n"; print "Location: schedules.cgi\r\n\r\n"; exit; } unless (GnizaWHM::UI::verify_csrf_token($form->{'gniza_csrf'})) { my $new_csrf = GnizaWHM::UI::generate_csrf_token(); _json_response(0, 0, 'Invalid or expired form token.', $new_csrf); } my $new_csrf = GnizaWHM::UI::generate_csrf_token(); my $name = $form->{'name'} // ''; my $name_err = GnizaWHM::Validator::validate_schedule_name($name); if ($name_err) { _json_response(0, 0, 'Invalid schedule name.', $new_csrf); } my $cron_schedules = GnizaWHM::Cron::get_current_schedules(); my $is_active = exists $cron_schedules->{$name}; if ($is_active) { my ($ok, $err) = GnizaWHM::Cron::remove_schedule($name); if ($ok) { _json_response(1, 0, "Cron disabled for '$name'.", $new_csrf); } else { _json_response(0, 1, "Failed to remove cron: $err", $new_csrf); } } else { my ($ok, $err) = GnizaWHM::Cron::install_schedule($name); if ($ok) { _json_response(1, 1, "Cron enabled for '$name'.", $new_csrf); } else { _json_response(0, 0, "Failed to install cron: $err", $new_csrf); } } } sub _json_response { my ($ok, $active, $message, $csrf) = @_; # Escape for JSON $message =~ s/\\/\\\\/g; $message =~ s/"/\\"/g; my $active_str = $active ? 'true' : 'false'; my $ok_str = $ok ? 'true' : 'false'; print "Content-Type: application/json\r\n\r\n"; print qq({"ok":$ok_str,"active":$active_str,"message":"$message","csrf":"$csrf"}); exit; } # ── Shared Form Renderer ──────────────────────────────────── sub render_schedule_form { 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) { print qq{\n}; } # Schedule name print qq{
\n
\n}; print qq{

Schedule Identity

\n}; print qq{
\n}; print qq{ \n}; if ($is_edit) { print qq{ \n}; } else { print qq{ \n}; print qq{ Letters, digits, hyphens, underscores\n}; } print qq{
\n}; print qq{
\n
\n}; # Schedule settings print qq{
\n
\n}; print qq{

Schedule Settings

\n}; my $sched = $conf->{SCHEDULE} // ''; print qq{
\n}; print qq{ \n}; print qq{ \n}; print qq{
\n}; _sched_field($conf, 'SCHEDULE_TIME', 'Time (HH:MM)', 'Default: 02:00'); print qq{
\n}; _sched_field($conf, 'SCHEDULE_DAY', 'Day', 'Day-of-week 0-6 (weekly) or day-of-month 1-28 (monthly)'); print qq{
\n}; print qq{
\n}; _sched_field($conf, 'SCHEDULE_CRON', 'Cron Expression', '5-field cron (for custom only)'); print qq{
\n}; print qq{
\n
\n}; # Target remotes print qq{
\n
\n}; print qq{

Target Remotes

\n}; print qq{

Select which remotes this schedule targets. Leave all unchecked to target all remotes.

\n}; my @remotes = GnizaWHM::UI::list_remotes(); my %selected_remotes; my $remotes_str = $conf->{REMOTES} // ''; for my $r (split /,/, $remotes_str) { $r =~ s/^\s+|\s+$//g; $selected_remotes{$r} = 1 if $r ne ''; } if (@remotes) { for my $rname (@remotes) { my $esc_name = GnizaWHM::UI::esc($rname); my $checked = $selected_remotes{$rname} ? ' checked' : ''; print qq{\n}; } # Hidden field to collect selected remotes via JS print qq{\n}; } else { print qq{

No remotes configured. Add a remote first.

\n}; } print qq{
\n
\n}; # Submit print qq{
\n}; my $btn_label = $is_edit ? 'Save Changes' : 'Create Schedule'; print qq{ \n}; print qq{ Cancel\n}; print qq{
\n}; print qq{
\n}; # JS for schedule field visibility and remote collection print <<'JS'; JS } sub _sched_field { my ($conf, $key, $label, $hint) = @_; my $val = GnizaWHM::UI::esc($conf->{$key} // ''); 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}; }