Files
gniza4cp/cpanel/gniza/logs.live.cgi
shuki c20c019048 Add cPanel user activity logs page and WHM user log visibility
- Add per-user activity logging to AdminBin: every RESTORE_* action
  writes to /var/log/gniza/cpanel-<user>.log with action details and
  gniza command output
- New logs.live.cgi CGI with paginated activity list and detail view
- WHM logs.cgi now shows cpanel-*.log files with Owner column and
  structured activity entry viewer with expandable command output
- Add Logs nav item to cPanel plugin, update install.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 19:37:08 +02:00

224 lines
8.5 KiB
Perl

#!/usr/local/cpanel/3rdparty/bin/perl
# gniza cPanel Plugin — Activity Logs
# Shows user-initiated restore actions and their results
use strict;
use warnings;
BEGIN {
my $base;
if ($0 =~ m{^(.*)/}) {
$base = $1;
} else {
$base = '.';
}
unshift @INC, "$base/lib";
}
use Cpanel::LiveAPI ();
use Cpanel::AdminBin::Call ();
use Cpanel::Form ();
use GnizaCPanel::UI;
my $cpanel = Cpanel::LiveAPI->new();
END { $cpanel->end() if $cpanel }
my $form = Cpanel::Form::parseform();
my $entry = $form->{'entry'} // '';
if ($entry ne '') {
show_entry($entry);
} else {
show_list();
}
exit;
# ── List View ────────────────────────────────────────────────
sub show_list {
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
print GnizaCPanel::UI::page_header('GNIZA Activity Log');
print GnizaCPanel::UI::render_nav('logs.live.cgi');
print GnizaCPanel::UI::render_flash();
my $raw = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'LIST_LOGS') } // '';
if ($raw =~ /^ERROR: (.*)/) {
print qq{<div class="alert alert-error mb-4">} . GnizaCPanel::UI::esc($1) . qq{</div>\n};
print GnizaCPanel::UI::page_footer();
print $cpanel->footer();
return;
}
my @entries;
for my $line (split /\n/, $raw) {
next if $line eq '';
my ($idx, $date, $action, $details, $status) = split /\t/, $line;
push @entries, {
idx => $idx // '',
date => $date // '',
action => $action // '',
details => $details // '',
status => $status // '',
};
}
if (!@entries) {
print qq{<div class="alert alert-info mb-4">No restore activity yet. Actions you perform in the Restore section will appear here.</div>\n};
print GnizaCPanel::UI::page_footer();
print $cpanel->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];
print qq{<div class="overflow-x-auto rounded-box border border-base-content/5 bg-base-100">\n};
print qq{<table class="table">\n};
print qq{<thead><tr><th>Date (UTC)</th><th>Action</th><th>Details</th><th>Status</th><th></th></tr></thead>\n};
print qq{<tbody>\n};
for my $e (@page_entries) {
my $esc_date = GnizaCPanel::UI::esc($e->{date});
my $esc_action = GnizaCPanel::UI::esc($e->{action});
my $esc_details = GnizaCPanel::UI::esc($e->{details});
my $esc_status = GnizaCPanel::UI::esc($e->{status});
my $esc_idx = GnizaCPanel::UI::esc($e->{idx});
my $status_badge = $e->{status} eq 'Error' ? 'badge-error' : 'badge-success';
my $href = 'logs.live.cgi?entry=' . _uri_escape($e->{idx});
print qq{<tr>\n};
print qq{ <td class="whitespace-nowrap">$esc_date</td>\n};
print qq{ <td><span class="badge badge-info badge-sm">$esc_action</span></td>\n};
print qq{ <td class="text-sm">$esc_details</td>\n};
print qq{ <td><span class="badge $status_badge badge-sm">$esc_status</span></td>\n};
print qq{ <td><button type="button" class="btn btn-secondary btn-sm" onclick="location.href='$href'">View</button></td>\n};
print qq{</tr>\n};
}
print qq{</tbody>\n</table>\n</div>\n};
# Pagination controls
if ($total_pages > 1) {
print qq{<div class="flex items-center justify-center gap-2 mt-4">\n};
if ($page > 1) {
my $prev = $page - 1;
print qq{ <button type="button" class="btn btn-sm" onclick="location.href='logs.live.cgi?page=$prev'">&laquo; Prev</button>\n};
}
print qq{ <span class="text-sm">Page $page of $total_pages ($total entries)</span>\n};
if ($page < $total_pages) {
my $next = $page + 1;
print qq{ <button type="button" class="btn btn-sm" onclick="location.href='logs.live.cgi?page=$next'">Next &raquo;</button>\n};
}
print qq{</div>\n};
}
print GnizaCPanel::UI::page_footer();
print $cpanel->footer();
}
# ── Entry Detail View ────────────────────────────────────────
sub show_entry {
my ($entry_idx) = @_;
print "Content-Type: text/html\r\n\r\n";
print $cpanel->header('');
print GnizaCPanel::UI::page_header('GNIZA Activity Detail');
print GnizaCPanel::UI::render_nav('logs.live.cgi');
# Validate entry index (numeric only)
unless ($entry_idx =~ /^[0-9]+$/) {
print qq{<div class="alert alert-error mb-4">Invalid entry.</div>\n};
print qq{<p><a href="logs.live.cgi" class="link">&larr; Back to activity log</a></p>\n};
print GnizaCPanel::UI::page_footer();
print $cpanel->footer();
return;
}
my $content = eval { Cpanel::AdminBin::Call::call('Gniza', 'Restore', 'GET_LOG', $entry_idx) } // '';
if ($content =~ /^ERROR: (.*)/) {
print qq{<div class="alert alert-error mb-4">} . GnizaCPanel::UI::esc($1) . qq{</div>\n};
print qq{<p><a href="logs.live.cgi" class="link">&larr; Back to activity log</a></p>\n};
print GnizaCPanel::UI::page_footer();
print $cpanel->footer();
return;
}
# Parse header fields and output from entry content
my ($date, $action, $details, $status, $output) = ('', '', '', '', '');
my @lines = split /\n/, $content;
my @output_lines;
my $in_output = 0;
for my $line (@lines) {
if (!$in_output) {
if ($line =~ /^Date:\s+(.+)$/) { $date = $1; }
elsif ($line =~ /^Action:\s+(.+)$/) { $action = $1; }
elsif ($line =~ /^Details:\s+(.+)$/) { $details = $1; }
elsif ($line =~ /^Status:\s+(.+)$/) { $status = $1; $in_output = 1; }
} else {
push @output_lines, $line;
}
}
# Back link
print qq{<p class="mb-4"><a href="logs.live.cgi" class="link">&larr; Back to activity log</a></p>\n};
# Entry info card
my $status_badge = $status eq 'Error' ? 'badge-error' : 'badge-success';
print qq{<div class="card bg-base-200 shadow-sm border border-base-300 mb-4">\n};
print qq{<div class="card-body py-3 px-4">\n};
print qq{<div class="flex flex-wrap gap-4 items-center text-sm">\n};
print qq{ <span><strong>Date:</strong> } . GnizaCPanel::UI::esc($date) . qq{</span>\n};
print qq{ <span><strong>Action:</strong> <span class="badge badge-info badge-sm">} . GnizaCPanel::UI::esc($action) . qq{</span></span>\n};
print qq{ <span><strong>Status:</strong> <span class="badge $status_badge badge-sm">} . GnizaCPanel::UI::esc($status) . qq{</span></span>\n};
print qq{</div>\n};
print qq{<div class="text-sm mt-2"><strong>Details:</strong> } . GnizaCPanel::UI::esc($details) . qq{</div>\n};
print qq{</div>\n</div>\n};
# Output section
if (@output_lines) {
print qq{<h3 class="text-sm font-bold mb-2">Command Output</h3>\n};
print qq{<pre class="bg-base-100 border border-base-300 rounded-lg p-4 text-sm font-mono overflow-x-auto leading-relaxed">};
for my $line (@output_lines) {
my $esc = GnizaCPanel::UI::esc($line);
if ($line =~ /\[ERROR\]/) {
print qq{<span class="text-error font-bold">$esc</span>\n};
} elsif ($line =~ /\[WARN\]/) {
print qq{<span class="text-warning">$esc</span>\n};
} elsif ($line =~ /\[DEBUG\]/) {
print qq{<span class="text-base-content/60">$esc</span>\n};
} else {
print "$esc\n";
}
}
print qq{</pre>\n};
} else {
print qq{<div class="alert alert-info">No output recorded for this action.</div>\n};
}
print GnizaCPanel::UI::page_footer();
print $cpanel->footer();
}
# ── Helpers ───────────────────────────────────────────────────
sub _uri_escape {
my $str = shift // '';
$str =~ s/([^A-Za-z0-9\-._~])/sprintf("%%%02X", ord($1))/ge;
return $str;
}
1;