Files
gniza4cp/whm/gniza-whm/lib/GnizaWHM/Config.pm
shuki 3547b00ead Add sysbackup/sysrestore CLI commands and schedule integration
- Add lib/sysbackup.sh and lib/sysrestore.sh for system-level
  backup and restore of WHM/cPanel config, packages, and cron jobs
- Wire cmd_sysbackup and cmd_sysrestore into bin/gniza
- Add --sysbackup flag to cmd_backup: runs system backup after all
  account backups complete
- Add SYSBACKUP schedule config key so cron jobs can include
  --sysbackup automatically via build_cron_line()
- Add "Include system backup" toggle to WHM schedule form
- Revert sysbackup toggle from remotes.cgi (belongs in schedules)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 17:30:10 +02:00

144 lines
4.3 KiB
Perl

package GnizaWHM::Config;
# Pure Perl config file parser/writer for bash-style KEY="value" files.
# No shell calls — reads/writes via Perl file I/O only.
use strict;
use warnings;
use Fcntl qw(:flock);
our @MAIN_KEYS = qw(
TEMP_DIR INCLUDE_ACCOUNTS EXCLUDE_ACCOUNTS
RSYNC_EXTRA_OPTS LOG_DIR LOG_LEVEL LOG_RETAIN NOTIFY_EMAIL NOTIFY_ON
SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASSWORD SMTP_FROM SMTP_SECURITY
LOCK_FILE SSH_TIMEOUT SSH_RETRIES
);
our @REMOTE_KEYS = qw(
REMOTE_TYPE REMOTE_HOST REMOTE_PORT REMOTE_USER REMOTE_AUTH_METHOD REMOTE_KEY
REMOTE_PASSWORD REMOTE_BASE BWLIMIT RETENTION_COUNT RSYNC_EXTRA_OPTS
S3_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY S3_REGION S3_ENDPOINT S3_BUCKET
GDRIVE_SERVICE_ACCOUNT_FILE GDRIVE_ROOT_FOLDER_ID
);
our @SCHEDULE_KEYS = qw(
SCHEDULE SCHEDULE_TIME SCHEDULE_DAY SCHEDULE_CRON REMOTES SYSBACKUP
);
my %MAIN_KEY_SET = map { $_ => 1 } @MAIN_KEYS;
my %REMOTE_KEY_SET = map { $_ => 1 } @REMOTE_KEYS;
my %SCHEDULE_KEY_SET = map { $_ => 1 } @SCHEDULE_KEYS;
# parse($filepath, $type)
# $type: 'main', 'remote', or 'schedule' — determines which keys are allowed.
# Returns hashref of KEY => value.
sub parse {
my ($filepath, $type) = @_;
$type //= 'main';
my $allowed = ($type eq 'schedule') ? \%SCHEDULE_KEY_SET
: ($type eq 'remote') ? \%REMOTE_KEY_SET
: \%MAIN_KEY_SET;
my %config;
open my $fh, '<', $filepath or return \%config;
while (my $line = <$fh>) {
chomp $line;
# Skip blank lines and comments
next if $line =~ /^\s*$/;
next if $line =~ /^\s*#/;
# Match KEY="value", KEY='value', or KEY=value
if ($line =~ /^([A-Z_]+)=(?:"([^"]*)"|'([^']*)'|(\S*))$/) {
my $key = $1;
my $val = defined $2 ? $2 : (defined $3 ? $3 : ($4 // ''));
if ($allowed->{$key}) {
$config{$key} = $val;
}
}
}
close $fh;
return \%config;
}
# escape_value($string)
# Strips everything except safe characters for bash config values.
sub escape_value {
my ($val) = @_;
$val //= '';
$val =~ s/[^a-zA-Z0-9\@._\/: ,=+\-]//g;
return $val;
}
# Keys whose values are written with single quotes (preserves special chars).
my %SINGLE_QUOTE_KEYS = (REMOTE_PASSWORD => 1, S3_SECRET_ACCESS_KEY => 1, SMTP_PASSWORD => 1);
# escape_password($string)
# For single-quoted bash values: only strip single quotes (can't appear in single-quoted strings).
sub escape_password {
my ($val) = @_;
$val //= '';
$val =~ s/'//g;
return $val;
}
# write($filepath, \%values, \@allowed_keys)
# Updates a config file preserving comments and structure.
# Keys not in @allowed_keys are ignored. Values are escaped.
# Uses flock for concurrency safety.
sub write {
my ($filepath, $values, $allowed_keys) = @_;
my %allowed = map { $_ => 1 } @$allowed_keys;
my %to_write;
for my $key (keys %$values) {
if ($allowed{$key}) {
$to_write{$key} = $SINGLE_QUOTE_KEYS{$key}
? escape_password($values->{$key})
: escape_value($values->{$key});
}
}
# Read existing file
my @lines;
if (-f $filepath) {
open my $rfh, '<', $filepath or return (0, "Cannot read $filepath: $!");
@lines = <$rfh>;
close $rfh;
}
# Track which keys we've updated in-place
my %written;
my @output;
for my $line (@lines) {
if ($line =~ /^([A-Z_]+)=/) {
my $key = $1;
if (exists $to_write{$key}) {
my $val = $to_write{$key};
my $q = $SINGLE_QUOTE_KEYS{$key} ? "'" : '"';
push @output, "$key=$q$val$q\n";
$written{$key} = 1;
next;
}
}
push @output, $line;
}
# Append any new keys not already in the file
for my $key (@$allowed_keys) {
next unless exists $to_write{$key};
next if $written{$key};
my $val = $to_write{$key};
my $q = $SINGLE_QUOTE_KEYS{$key} ? "'" : '"';
push @output, "$key=$q$val$q\n";
}
# Write with flock
open my $wfh, '>', $filepath or return (0, "Cannot write $filepath: $!");
flock($wfh, LOCK_EX) or return (0, "Cannot lock $filepath: $!");
print $wfh @output;
flock($wfh, Fcntl::LOCK_UN);
close $wfh;
return (1, undef);
}
1;