#!/usr/local/cpanel/3rdparty/bin/perl
# gniza WHM Plugin — Activity Logs
# View backup and cron log files (read-only)
use strict;
use warnings;
use lib '/usr/local/cpanel/whostmgr/docroot/cgi/gniza-whm/lib';
use Whostmgr::HTMLInterface ();
use Cpanel::Form ();
use GnizaWHM::Config;
use GnizaWHM::UI;
my $form = Cpanel::Form::parseform();
# Determine log directory from config
my $log_dir = '/var/log/gniza';
my $main_conf = '/etc/gniza/gniza.conf';
if (-f $main_conf) {
my $cfg = GnizaWHM::Config::parse($main_conf, 'main');
$log_dir = $cfg->{LOG_DIR} if $cfg->{LOG_DIR} && $cfg->{LOG_DIR} ne '';
}
my $file = $form->{'file'} // '';
if ($file ne '') {
show_file($file);
} else {
show_list();
}
exit;
# ── File List View ────────────────────────────────────────────
sub show_list {
print "Content-Type: text/html\r\n\r\n";
Whostmgr::HTMLInterface::defheader('gniza — Logs');
print GnizaWHM::UI::page_header('GNIZA Backup Manager');
print GnizaWHM::UI::render_nav('logs.cgi');
print GnizaWHM::UI::render_flash();
unless (-d $log_dir) {
print qq{
No log directory found at }
. GnizaWHM::UI::esc($log_dir)
. qq{.
\n};
print GnizaWHM::UI::page_footer();
return;
}
# Read log files
my @files;
if (opendir my $dh, $log_dir) {
while (my $entry = readdir $dh) {
next unless _valid_log_filename($entry);
my $path = "$log_dir/$entry";
next unless -f $path;
my @stat = stat($path);
push @files, {
name => $entry,
size => $stat[7] // 0,
mtime => $stat[9] // 0,
type => ($entry =~ /^cron-/) ? 'Cron' : 'Backup',
};
}
closedir $dh;
}
# Sort by mtime descending
@files = sort { $b->{mtime} <=> $a->{mtime} } @files;
if (!@files) {
print qq{No log files found.
\n};
print GnizaWHM::UI::page_footer();
return;
}
# Pagination
my $per_page = 25;
my $total = scalar @files;
my $page = int($form->{'page'} // 1);
$page = 1 if $page < 1;
my $total_pages = int(($total + $per_page - 1) / $per_page);
$page = $total_pages if $page > $total_pages;
my $start = ($page - 1) * $per_page;
my $end = $start + $per_page - 1;
$end = $#files if $end > $#files;
my @page_files = @files[$start .. $end];
print qq{\n};
print qq{
\n};
print qq{| Filename | Type | Date | Size | |
\n};
print qq{\n};
for my $f (@page_files) {
my $esc_name = GnizaWHM::UI::esc($f->{name});
my $badge = $f->{type} eq 'Cron' ? 'badge-neutral' : 'badge-info';
my $date = _format_time($f->{mtime});
my $size = _human_size($f->{size});
my $href = 'logs.cgi?file=' . _uri_escape($f->{name});
print qq{\n};
print qq{ $esc_name | \n};
print qq{ $f->{type} | \n};
print qq{ $date | \n};
print qq{ $size | \n};
print qq{ | \n};
print qq{
\n};
}
print qq{\n
\n
\n};
# Pagination controls
if ($total_pages > 1) {
print qq{\n};
if ($page > 1) {
my $prev = $page - 1;
print qq{ \n};
}
print qq{ Page $page of $total_pages ($total logs)\n};
if ($page < $total_pages) {
my $next = $page + 1;
print qq{ \n};
}
print qq{
\n};
}
# Auto-refresh while gniza is running
print qq{\n};
print GnizaWHM::UI::page_footer();
}
# ── File View ─────────────────────────────────────────────────
sub show_file {
my ($filename) = @_;
print "Content-Type: text/html\r\n\r\n";
Whostmgr::HTMLInterface::defheader('gniza — Log Viewer');
print GnizaWHM::UI::page_header('GNIZA Backup Manager');
print GnizaWHM::UI::render_nav('logs.cgi');
# Validate filename (prevents path traversal)
unless (_valid_log_filename($filename)) {
print qq{Invalid log filename.
\n};
print qq{← Back to logs
\n};
print GnizaWHM::UI::page_footer();
return;
}
my $filepath = "$log_dir/$filename";
unless (-f $filepath) {
print qq{Log file not found.
\n};
print qq{← Back to logs
\n};
print GnizaWHM::UI::page_footer();
return;
}
# Read file
my @lines;
if (open my $fh, '<', $filepath) {
@lines = <$fh>;
close $fh;
}
chomp @lines;
my $total_lines = scalar @lines;
my $is_cron = ($filename =~ /^cron-/);
my $show_all = ($form->{'all'} // '') eq '1';
my $truncated = 0;
my $max_lines = 10_000;
my $cron_default = 500;
# Apply line limits
if ($is_cron && !$show_all && $total_lines > $cron_default) {
@lines = @lines[-$cron_default .. -1];
$truncated = 1;
}
if (@lines > $max_lines) {
@lines = @lines[-$max_lines .. -1];
$truncated = 1;
}
# Level filter
my $level_filter = $form->{'level'} // '';
$level_filter =~ s/\s+//g;
$level_filter = '' unless $level_filter =~ /^(error|warn|info|debug)$/i;
$level_filter = uc($level_filter) if $level_filter;
# File info
my @stat = stat($filepath);
my $file_size = _human_size($stat[7] // 0);
my $file_date = _format_time($stat[9] // 0);
my $esc_name = GnizaWHM::UI::esc($filename);
# Back link
print qq{← Back to logs
\n};
# File info card
print qq{\n};
print qq{
\n};
print qq{
\n};
print qq{ File: $esc_name\n};
print qq{ Size: $file_size\n};
print qq{ Lines: $total_lines\n};
print qq{ Date: $file_date\n};
print qq{
\n
\n
\n};
# Truncation alert
if ($truncated) {
my $all_href = 'logs.cgi?file=' . _uri_escape($filename) . '&all=1';
$all_href .= '&level=' . lc($level_filter) if $level_filter;
print qq{Showing last }
. scalar(@lines)
. qq{ of $total_lines lines. }
. qq{
View all \n};
}
# Level filter buttons
my $base_href = 'logs.cgi?file=' . _uri_escape($filename);
$base_href .= '&all=1' if $show_all;
print qq{\n};
my @levels = ('', 'error', 'warn', 'info', 'debug');
my %level_labels = ('' => 'All', error => 'Error', warn => 'Warn', info => 'Info', debug => 'Debug');
for my $lv (@levels) {
my $active = '';
if ($lv eq '' && $level_filter eq '') {
$active = ' btn-active';
} elsif ($lv ne '' && uc($lv) eq $level_filter) {
$active = ' btn-active';
}
my $href = $base_href;
$href .= "&level=$lv" if $lv ne '';
my $label = $level_labels{$lv};
print qq{ \n};
}
print qq{
\n};
# Filter lines by level
my @display_lines;
if ($level_filter) {
@display_lines = grep { /\[$level_filter\]/ } @lines;
} else {
@display_lines = @lines;
}
# Render log content
if (!@display_lines) {
print qq{No matching log entries.
\n};
} else {
print qq{};
for my $line (@display_lines) {
my $esc = GnizaWHM::UI::esc($line);
if ($line =~ /\[ERROR\]/) {
print qq{$esc\n};
} elsif ($line =~ /\[WARN\]/) {
print qq{$esc\n};
} elsif ($line =~ /\[DEBUG\]/) {
print qq{$esc\n};
} else {
print "$esc\n";
}
}
print qq{\n};
}
# Auto-refresh + auto-scroll while gniza is running
print qq{\n};
print GnizaWHM::UI::page_footer();
}
# ── Helpers ───────────────────────────────────────────────────
sub _valid_log_filename {
my ($name) = @_;
return 0 unless defined $name && $name ne '';
return 1 if $name =~ /^gniza-\d{8}-\d{6}\.log$/;
return 1 if $name =~ /^cron-[a-zA-Z0-9_-]+\.log$/;
return 0;
}
sub _uri_escape {
my $str = shift // '';
$str =~ s/([^A-Za-z0-9\-._~])/sprintf("%%%02X", ord($1))/ge;
return $str;
}
sub _format_time {
my ($epoch) = @_;
return '' unless $epoch;
my @t = localtime($epoch);
return sprintf('%04d-%02d-%02d %02d:%02d', $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1]);
}
sub _human_size {
my ($bytes) = @_;
return '0 B' unless $bytes;
my @units = ('B', 'KB', 'MB', 'GB');
my $i = 0;
my $size = $bytes;
while ($size >= 1024 && $i < $#units) {
$size /= 1024;
$i++;
}
return ($i == 0) ? "$size B" : sprintf('%.1f %s', $size, $units[$i]);
}
1;