#!/usr/local/cpanel/3rdparty/bin/perl # gniza4cp WHM Plugin — Activity Logs # View backup and cron log files (read-only) use strict; use warnings; use lib '/usr/local/cpanel/whostmgr/docroot/cgi/gniza4cp-whm/lib'; use Whostmgr::HTMLInterface (); use Cpanel::Form (); use Gniza4cpWHM::Config; use Gniza4cpWHM::UI; my $form = Cpanel::Form::parseform(); # Determine log directory from config my $log_dir = '/var/log/gniza4cp'; my $main_conf = '/etc/gniza4cp/gniza4cp.conf'; if (-f $main_conf) { my $cfg = Gniza4cpWHM::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('gniza4cp — Logs'); print Gniza4cpWHM::UI::page_header('GNIZA4CP Backup Manager'); print Gniza4cpWHM::UI::render_nav('logs.cgi'); print Gniza4cpWHM::UI::render_flash(); unless (-d $log_dir) { print qq{
No log directory found at } . Gniza4cpWHM::UI::esc($log_dir) . qq{.
\n}; print Gniza4cpWHM::UI::page_footer(); return; } # Read log files my @files; if (opendir my $dh, $log_dir) { while (my $entry = readdir $dh) { next unless $entry =~ /^gniza4cp-\d{8}-\d{6}\.log$/ || $entry =~ /^cpanel-[a-z][a-z0-9_-]*\.log$/; my $path = "$log_dir/$entry"; next unless -f $path; my @stat = stat($path); my ($type, $status, $owner); if ($entry =~ /^cpanel-([a-z][a-z0-9_-]*)\.log$/) { $type = 'User'; $owner = $1; $status = _detect_activity_status($path); } else { $type = _detect_log_type($path); $status = _detect_log_status($path); $owner = ''; } push @files, { name => $entry, size => $stat[7] // 0, mtime => $stat[9] // 0, type => $type, status => $status, owner => $owner, }; } closedir $dh; } # Sort by mtime descending @files = sort { $b->{mtime} <=> $a->{mtime} } @files; if (!@files) { print qq{
No log files found.
\n}; print Gniza4cpWHM::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{\n}; print qq{\n}; for my $f (@page_files) { my $esc_name = Gniza4cpWHM::UI::esc($f->{name}); my $badge = $f->{type} eq 'Cron' ? 'badge-neutral' : $f->{type} eq 'System' ? 'badge-warning' : $f->{type} eq 'User' ? 'badge-secondary' : '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{ \n}; print qq{ \n}; my $esc_owner = Gniza4cpWHM::UI::esc($f->{owner} // ''); print qq{ \n}; my $status_badge = $f->{status} eq 'Error' ? 'badge-error' : $f->{status} eq 'Warning' ? 'badge-warning' : 'badge-success'; print qq{ \n}; print qq{ \n}; print qq{ \n}; print qq{ \n}; print qq{\n}; } print qq{\n
FilenameTypeOwnerStatusDateSize
$esc_name$f->{type}$esc_owner$f->{status}$date$size
\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 gniza4cp is running print qq{\n}; print Gniza4cpWHM::UI::page_footer(); } # ── File View ───────────────────────────────────────────────── sub show_file { my ($filename) = @_; print "Content-Type: text/html\r\n\r\n"; Whostmgr::HTMLInterface::defheader('gniza4cp — Log Viewer'); print Gniza4cpWHM::UI::page_header('GNIZA4CP Backup Manager'); print Gniza4cpWHM::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 Gniza4cpWHM::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 Gniza4cpWHM::UI::page_footer(); return; } # Detect activity log vs standard log if ($filename =~ /^cpanel-([a-z][a-z0-9_-]*)\.log$/) { _show_activity_file($filename, $filepath, $1); 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 = Gniza4cpWHM::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 = Gniza4cpWHM::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 gniza4cp is running print qq{\n}; print Gniza4cpWHM::UI::page_footer(); } # ── Activity Log Viewer (cpanel-*.log) ─────────────────────── sub _show_activity_file { my ($filename, $filepath, $owner) = @_; my $esc_name = Gniza4cpWHM::UI::esc($filename); my $esc_owner = Gniza4cpWHM::UI::esc($owner); my @stat = stat($filepath); my $file_size = _human_size($stat[7] // 0); my $file_date = _format_time($stat[9] // 0); # Back link print qq{

← Back to logs

\n}; # File info card print qq{
\n}; print qq{
\n}; print qq{
\n}; print qq{ User: $esc_owner\n}; print qq{ File: $esc_name\n}; print qq{ Size: $file_size\n}; print qq{ Last Modified: $file_date\n}; print qq{
\n
\n
\n}; # Parse activity entries my @entries; if (open my $fh, '<', $filepath) { my $in_entry = 0; my (%cur, @cur_output); while (my $line = <$fh>) { chomp $line; if ($line eq '--- ENTRY ---') { $in_entry = 1; %cur = (); @cur_output = (); } elsif ($line eq '--- END ---' && $in_entry) { $cur{output} = join("\n", @cur_output) if @cur_output; push @entries, { %cur } if $cur{date}; $in_entry = 0; } elsif ($in_entry) { if ($line =~ /^Date:\s+(.+)$/) { $cur{date} = $1; } elsif ($line =~ /^Action:\s+(.+)$/) { $cur{action} = $1; } elsif ($line =~ /^Details:\s+(.+)$/) { $cur{details} = $1; } elsif ($line =~ /^Status:\s+(.+)$/) { $cur{status} = $1; } else { push @cur_output, $line; } } } close $fh; } # Show newest first @entries = reverse @entries; if (!@entries) { print qq{
No activity entries found.
\n}; print Gniza4cpWHM::UI::page_footer(); return; } # Pagination my $per_page = 25; my $total = scalar @entries; 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 = $#entries if $end > $#entries; my @page_entries = @entries[$start .. $end]; for my $e (@page_entries) { my $status_badge = ($e->{status} // '') eq 'Error' ? 'badge-error' : 'badge-success'; my $esc_date = Gniza4cpWHM::UI::esc($e->{date} // ''); my $esc_action = Gniza4cpWHM::UI::esc($e->{action} // ''); my $esc_details = Gniza4cpWHM::UI::esc($e->{details} // ''); my $esc_status = Gniza4cpWHM::UI::esc($e->{status} // ''); print qq{
\n}; print qq{
\n}; print qq{
\n}; print qq{ $esc_date\n}; print qq{ $esc_action\n}; print qq{ $esc_status\n}; print qq{
\n}; print qq{
$esc_details
\n}; if ($e->{output} && $e->{output} ne '') { print qq{
\n}; print qq{Command Output\n}; print qq{
};
            for my $oline (split /\n/, $e->{output}) {
                my $esc = Gniza4cpWHM::UI::esc($oline);
                if ($oline =~ /\[ERROR\]/) {
                    print qq{$esc\n};
                } elsif ($oline =~ /\[WARN\]/) {
                    print qq{$esc\n};
                } else {
                    print "$esc\n";
                }
            }
            print qq{
\n}; print qq{
\n}; } print qq{
\n
\n}; } # Pagination controls if ($total_pages > 1) { my $base = 'logs.cgi?file=' . _uri_escape($filename); print qq{
\n}; if ($page > 1) { my $prev = $page - 1; print qq{ \n}; } print qq{ Page $page of $total_pages ($total entries)\n}; if ($page < $total_pages) { my $next = $page + 1; print qq{ \n}; } print qq{
\n}; } print Gniza4cpWHM::UI::page_footer(); } # ── Helpers ─────────────────────────────────────────────────── sub _valid_log_filename { my ($name) = @_; return 0 unless defined $name && $name ne ''; return 1 if $name =~ /^gniza4cp-\d{8}-\d{6}\.log$/; return 1 if $name =~ /^cron-[a-zA-Z0-9_-]+\.log$/; return 1 if $name =~ /^cpanel-[a-z][a-z0-9_-]*\.log$/; return 0; } sub _detect_activity_status { my ($path) = @_; return 'Success' unless -f $path; my $has_error = 0; if (open my $fh, '<', $path) { while (my $line = <$fh>) { if ($line =~ /^Status:\s+Error$/) { $has_error = 1; last; } } close $fh; } return $has_error ? 'Error' : 'Success'; } sub _detect_log_type { my ($path) = @_; if (open my $fh, '<', $path) { while (my $line = <$fh>) { if ($line =~ /\[TYPE:SYSBACKUP\]/) { close $fh; return 'System'; } last if $. > 5; } close $fh; } return 'Backup'; } sub _detect_log_status { my ($path) = @_; return 'Success' unless -f $path; my $has_warn = 0; if (open my $fh, '<', $path) { while (my $line = <$fh>) { if ($line =~ /\[ERROR\]/) { close $fh; return 'Error'; } $has_warn = 1 if !$has_warn && $line =~ /\[WARN\]/; } close $fh; } return $has_warn ? 'Warning' : 'Success'; } 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('%02d/%02d/%04d %02d:%02d:%02d', $t[3], $t[4]+1, $t[5]+1900, $t[2], $t[1], $t[0]); } 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;