adminlog.php

main 238 lines · 8.2 KB Raw
Alex Alex Commit Initial commit 01/10/2026 09:20
1<?php
2/**
3 * Title: Admin Log
4 * Icon: fa-history
5 * Group: Overview
6 * Order: 30
7 * Description: Who did what from the admin panel, and when.
8 */
9
10if (!defined('ACP_ROOT')) {
11 http_response_code(403);
12 die('Direct access denied.');
13}
14
15function acp_log_action_label(string $action): string {
16 $key = 'acp.log.action.' . $action;
17 $label = t($key);
18 if ($label !== $key) {
19 return $label;
20 }
21 $parts = explode('.', $action, 2);
22 $words = str_replace(array('_', '-'), ' ', end($parts));
23 return $words !== '' ? ucfirst($words) : $action;
24}
25
26$hasTable = acp_log_table_exists();
27
28$days = (string)($_GET['days'] ?? 'all');
29if (!in_array($days, array('1', '7', '30', '90', 'all'), true)) {
30 $days = 'all';
31}
32
33$target = trim((string)($_GET['target'] ?? ''));
34$q = $target !== '' ? $target : trim((string)($_GET['q'] ?? ''));
35$action = trim((string)($_GET['action'] ?? ''));
36$page = max(1, intv($_GET['page'] ?? 1));
37$perPage = 40;
38
39$where = array();
40$params = array();
41if ($hasTable) {
42 if ($days !== 'all') {
43 $where[] = "`created` >= ?";
44 $params[] = time() - ((int)$days * 86400);
45 }
46 if ($target !== '') {
47 $where[] = "`target` = ?";
48 $params[] = $target;
49 } elseif ($q !== '') {
50 $like = '%' . $q . '%';
51 $where[] = "(`target` LIKE ? OR `admin_name` LIKE ? OR `action` LIKE ? OR `details` LIKE ?)";
52 $params[] = $like;
53 $params[] = $like;
54 $params[] = $like;
55 $params[] = $like;
56 }
57 if ($action !== '') {
58 $where[] = "`action` = ?";
59 $params[] = $action;
60 }
61}
62$whereSql = $where ? ('WHERE ' . implode(' AND ', $where)) : '';
63
64$total = $hasTable ? acp_count("SELECT COUNT(*) AS `c` FROM `znote_admin_log` {$whereSql};", $params) : 0;
65$totalPages = max(1, (int)ceil($total / $perPage));
66$page = min($page, $totalPages);
67$offset = ($page - 1) * $perPage;
68
69$rows = array();
70if ($hasTable) {
71 $rows = db()->fetchAll("
72 SELECT `id`, `admin_id`, `admin_name`, `action`, `target`, `details`, `ip`, `created`
73 FROM `znote_admin_log`
74 {$whereSql}
75 ORDER BY `id` DESC
76 LIMIT {$offset}, {$perPage};
77 ", $params);
78 $rows = is_array($rows) ? $rows : array();
79}
80
81$actionOptions = array();
82if ($hasTable) {
83 $actionRows = db()->fetchAll("SELECT DISTINCT `action` FROM `znote_admin_log` ORDER BY `action` ASC;");
84 if (is_array($actionRows)) {
85 foreach ($actionRows as $row) {
86 $actionOptions[] = (string)$row['action'];
87 }
88 }
89}
90
91$eventsToday = $hasTable ? acp_count("SELECT COUNT(*) AS `c` FROM `znote_admin_log` WHERE `created` >= ?;", [time() - 86400]) : 0;
92$eventsAll = $hasTable ? acp_count("SELECT COUNT(*) AS `c` FROM `znote_admin_log`;") : 0;
93$adminsActive = $hasTable ? acp_count("SELECT COUNT(DISTINCT `admin_id`) AS `c` FROM `znote_admin_log` WHERE `created` >= ?;", [time() - 30 * 86400]) : 0;
94
95function acp_log_query(array $overrides = array()): array {
96 global $q, $action, $days;
97 return array_filter(array_merge(
98 array('q' => $q, 'action' => $action, 'days' => $days === 'all' ? null : $days),
99 $overrides
100 ), static fn($v) => $v !== null && $v !== '');
101}
102?>
103
104<?php if (!$hasTable): ?>
105 <div class="acp-flash acp-flash--error">
106 <i class="fa fa-exclamation-triangle"></i>
107 <span>
108 <?= t('acp.log.table_missing', [
109 'table' => '<code>znote_admin_log</code>',
110 'file' => '<code>SQL/migrations/2.0.0_admin_log.sql</code>',
111 ]) ?>
112 </span>
113 </div>
114<?php endif; ?>
115
116<div class="acp-stats">
117 <?php
118 acp_stat(t('acp.log.stat_today'), $eventsToday, 'fa-clock-o', null, 'blue');
119 acp_stat(t('acp.log.stat_total'), $eventsAll, 'fa-database', null, 'purple');
120 acp_stat(t('acp.log.stat_admins'), $adminsActive, 'fa-users', null, 'teal');
121 ?>
122</div>
123
124<section class="acp-card">
125 <header class="acp-card-head">
126 <h2><?= t('acp.log.title') ?></h2>
127 <p><?= t('acp.log.sub') ?></p>
128 </header>
129 <div class="acp-card-body">
130 <form method="get" class="acp-row">
131 <input type="hidden" name="p" value="adminlog">
132 <div class="acp-field" style="flex:2;min-width:220px;">
133 <label class="acp-label" for="q"><?= t('acp.log.search_label') ?></label>
134 <input class="acp-input" id="q" name="q" value="<?= h($q) ?>" placeholder="<?= h(t('acp.log.search_placeholder')) ?>">
135 </div>
136 <div class="acp-field">
137 <label class="acp-label" for="action"><?= t('acp.log.action_label') ?></label>
138 <select class="acp-select" id="action" name="action">
139 <option value=""><?= t('acp.log.all_actions') ?></option>
140 <?php foreach ($actionOptions as $opt): ?>
141 <option value="<?= h($opt) ?>" <?= $opt === $action ? 'selected' : '' ?>><?= h(acp_log_action_label($opt)) ?></option>
142 <?php endforeach; ?>
143 </select>
144 </div>
145 <div class="acp-field">
146 <label class="acp-label" for="days"><?= t('acp.log.period_label') ?></label>
147 <select class="acp-select" id="days" name="days">
148 <?php foreach (array('1' => t('acp.vis.today'), '7' => t('acp.vis.7days'), '30' => t('acp.vis.30days'), '90' => t('acp.vis.90days'), 'all' => t('acp.log.all_time')) as $val => $label): ?>
149 <option value="<?= h($val) ?>" <?= $val === $days ? 'selected' : '' ?>><?= h($label) ?></option>
150 <?php endforeach; ?>
151 </select>
152 </div>
153 <div class="acp-actions">
154 <button class="acp-btn" type="submit"><i class="fa fa-filter"></i> <?= t('acp.log.filter') ?></button>
155 <a class="acp-btn acp-btn--ghost" href="<?= h(acp_url('adminlog')) ?>"><?= t('acp.log.clear') ?></a>
156 </div>
157 </form>
158 </div>
159</section>
160
161<section class="acp-card">
162 <div class="acp-card-body is-flush">
163 <?php if ($rows): ?>
164 <div class="acp-table-wrap">
165 <table class="acp-table" data-sortable>
166 <thead>
167 <tr>
168 <th><?= t('acp.log.col_date') ?></th>
169 <th><?= t('acp.log.col_admin') ?></th>
170 <th><?= t('acp.log.col_action') ?></th>
171 <th><?= t('acp.log.col_target') ?></th>
172 <th><?= t('acp.log.col_details') ?></th>
173 <th><?= t('acp.log.col_ip') ?></th>
174 </tr>
175 </thead>
176 <tbody>
177 <?php foreach ($rows as $row):
178 $details = array();
179 if ((string)$row['details'] !== '') {
180 $decoded = json_decode((string)$row['details'], true);
181 if (is_array($decoded)) {
182 $details = $decoded;
183 }
184 }
185 ?>
186 <tr>
187 <td class="is-nowrap"><?= h(getClock((int)$row['created'], true, true)) ?></td>
188 <td class="is-nowrap">
189 <?php if ((int)$row['admin_id'] > 0): ?>
190 <a href="<?= h(acp_url('accounts', array('id' => (int)$row['admin_id']))) ?>"><?= h((string)$row['admin_name']) ?></a>
191 <?php else: ?>
192 <span class="is-muted"><?= h((string)$row['admin_name'] !== '' ? (string)$row['admin_name'] : '—') ?></span>
193 <?php endif; ?>
194 </td>
195 <td><span class="acp-pill acp-pill--blue"><?= h(acp_log_action_label((string)$row['action'])) ?></span></td>
196 <td><?= $row['target'] !== '' ? '<strong>' . h((string)$row['target']) . '</strong>' : '<span class="is-muted">—</span>' ?></td>
197 <td class="is-muted">
198 <?php if ($details): ?>
199 <?php
200 $pairs = array();
201 foreach ($details as $k => $v) {
202 if (is_array($v)) {
203 $v = implode(', ', array_map('strval', $v));
204 }
205 $pairs[] = '<code>' . h((string)$k) . '</code>: ' . h((string)$v);
206 }
207 echo implode('<br>', $pairs);
208 ?>
209 <?php else: ?>
210 &mdash;
211 <?php endif; ?>
212 </td>
213 <td class="is-nowrap is-muted"><code><?= h((string)$row['ip']) ?></code></td>
214 </tr>
215 <?php endforeach; ?>
216 </tbody>
217 </table>
218 </div>
219
220 <?php if ($totalPages > 1): ?>
221 <div class="acp-toolbar">
222 <span class="is-muted"><?= t('acp.log.page_of', ['page' => $page, 'total' => $totalPages, 'n' => number_format($total)]) ?></span>
223 <div class="acp-actions is-tight">
224 <?php if ($page > 1): ?>
225 <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('adminlog', acp_log_query(array('page' => (string)($page - 1))))) ?>"><?= t('common.previous') ?></a>
226 <?php endif; ?>
227 <?php if ($page < $totalPages): ?>
228 <a class="acp-btn acp-btn--ghost acp-btn--sm" href="<?= h(acp_url('adminlog', acp_log_query(array('page' => (string)($page + 1))))) ?>"><?= t('common.next') ?></a>
229 <?php endif; ?>
230 </div>
231 </div>
232 <?php endif; ?>
233 <?php else: ?>
234 <?php acp_empty(t('acp.log.empty'), 'fa-history'); ?>
235 <?php endif; ?>
236 </div>
237</section>
238
Top