Add request logging, bot blocking, and admin log viewer
This commit is contained in:
@@ -15,4 +15,5 @@ return [
|
|||||||
'assets_dir' => __DIR__ . '/../../content/-assets/',
|
'assets_dir' => __DIR__ . '/../../content/-assets/',
|
||||||
'admin_config' => __DIR__ . '/admin.json',
|
'admin_config' => __DIR__ . '/admin.json',
|
||||||
'log_file' => __DIR__ . '/../storage/logs/admin.log',
|
'log_file' => __DIR__ . '/../storage/logs/admin.log',
|
||||||
|
'request_log' => __DIR__ . '/../storage/logs/requests.log',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ $layoutSidebarColor = $layoutThemeConfig['header_color'] ?? '#0a369d';
|
|||||||
<i class="bi bi-book"></i> Handleiding
|
<i class="bi bi-book"></i> Handleiding
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= ($route ?? '') === 'logs' ? 'active' : '' ?>" href="/admin/logs">
|
||||||
|
<i class="bi bi-journal-text"></i> Logs
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item mt-3">
|
<li class="nav-item mt-3">
|
||||||
<a class="nav-link" href="/" target="_blank">
|
<a class="nav-link" href="/" target="_blank">
|
||||||
<i class="bi bi-box-arrow-up-right"></i> Website bekijken
|
<i class="bi bi-box-arrow-up-right"></i> Website bekijken
|
||||||
@@ -160,6 +165,9 @@ $layoutSidebarColor = $layoutThemeConfig['header_color'] ?? '#0a369d';
|
|||||||
case 'guide':
|
case 'guide':
|
||||||
require __DIR__ . '/pages/guide.php';
|
require __DIR__ . '/pages/guide.php';
|
||||||
break;
|
break;
|
||||||
|
case 'logs':
|
||||||
|
require __DIR__ . '/pages/logs.php';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<h2 class="mb-4"><i class="bi bi-journal-text"></i> Logs</h2>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs mb-3" id="logTabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link <?= $activeTab === 'admin' ? 'active' : '' ?>" id="admin-log-tab" data-bs-toggle="tab" data-bs-target="#admin-log" type="button" role="tab">Activiteitenlog</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link <?= $activeTab === 'requests' ? 'active' : '' ?>" id="request-log-tab" data-bs-toggle="tab" data-bs-target="#request-log" type="button" role="tab">Requests log</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- Admin activity log -->
|
||||||
|
<div class="tab-pane fade <?= $activeTab === 'admin' ? 'show active' : '' ?>" id="admin-log" role="tabpanel">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<small class="text-muted"><?= count($adminLogs) ?> regels</small>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/logs?tab=admin&download=1" class="btn btn-sm btn-outline-secondary"><i class="bi bi-download"></i> Download</a>
|
||||||
|
<a href="/admin/logs?tab=admin&clear=1" class="btn btn-sm btn-outline-danger" onclick="return confirm('Activiteitenlog wissen?')"><i class="bi bi-trash"></i> Wissen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body p-0" style="max-height: 500px; overflow-y: auto;">
|
||||||
|
<?php if (empty($adminLogs)): ?>
|
||||||
|
<p class="text-muted p-3 mb-0">Geen activiteiten geregistreerd.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Tijd</th>
|
||||||
|
<th>Niveau</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Bericht</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($adminLogs as $log): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap small text-muted"><?= htmlspecialchars($log['time']) ?></td>
|
||||||
|
<td><span class="badge bg-<?= $log['level'] === 'warning' ? 'warning text-dark' : ($log['level'] === 'error' ? 'danger' : 'info') ?>"><?= htmlspecialchars($log['level']) ?></span></td>
|
||||||
|
<td><code class="small"><?= htmlspecialchars($log['ip']) ?></code></td>
|
||||||
|
<td><?= htmlspecialchars($log['message']) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Request log -->
|
||||||
|
<div class="tab-pane fade <?= $activeTab === 'requests' ? 'show active' : '' ?>" id="request-log" role="tabpanel">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<small class="text-muted"><?= count($requestLogs) ?> regels</small>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/logs?tab=requests&download=1" class="btn btn-sm btn-outline-secondary"><i class="bi bi-download"></i> Download</a>
|
||||||
|
<a href="/admin/logs?tab=requests&clear=1" class="btn btn-sm btn-outline-danger" onclick="return confirm('Requestlog wissen?')"><i class="bi bi-trash"></i> Wissen</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body p-0" style="max-height: 500px; overflow-y: auto;">
|
||||||
|
<?php if (empty($requestLogs)): ?>
|
||||||
|
<p class="text-muted p-3 mb-0">Geen requests geregistreerd.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Tijd</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Pagina</th>
|
||||||
|
<th>Domein</th>
|
||||||
|
<th>Taal</th>
|
||||||
|
<th>User agent</th>
|
||||||
|
<th>Referrer</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($requestLogs as $log): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap small text-muted"><?= htmlspecialchars($log['time']) ?></td>
|
||||||
|
<td><code class="small"><?= htmlspecialchars($log['ip']) ?></code></td>
|
||||||
|
<td><?= htmlspecialchars($log['page']) ?></td>
|
||||||
|
<td><?= htmlspecialchars($log['host']) ?></td>
|
||||||
|
<td><?= htmlspecialchars($log['lang']) ?></td>
|
||||||
|
<td class="small text-muted" title="<?= htmlspecialchars($log['ua']) ?>"><?= htmlspecialchars(substr($log['ua'], 0, 60)) ?><?= strlen($log['ua']) > 60 ? '…' : '' ?></td>
|
||||||
|
<td class="small text-muted" title="<?= htmlspecialchars($log['referrer']) ?>"><?= htmlspecialchars(substr($log['referrer'], 0, 40)) ?><?= strlen($log['referrer']) > 40 ? '…' : '' ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class RequestLogger
|
||||||
|
{
|
||||||
|
private string $logFile;
|
||||||
|
|
||||||
|
public function __construct(string $logFile)
|
||||||
|
{
|
||||||
|
$this->logFile = $logFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function log(string $page, string $ip, string $userAgent, string $referrer, string $host, string $acceptLanguage): void
|
||||||
|
{
|
||||||
|
$dir = dirname($this->logFile);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
@mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
$ua = substr(preg_replace('/[[:cntrl:]]/', '', $userAgent), 0, 500);
|
||||||
|
$ref = substr(preg_replace('/[[:cntrl:]]/', '', $referrer), 0, 500);
|
||||||
|
$line = "[{$timestamp}] [{$ip}] [{$host}] [{$acceptLanguage}] [{$page}] [{$ua}] [{$ref}]\n";
|
||||||
|
@file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function detectBot(): ?string
|
||||||
|
{
|
||||||
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||||
|
if (empty($ua)) return null;
|
||||||
|
|
||||||
|
$bots = [
|
||||||
|
'AI' => [
|
||||||
|
'GPTBot', 'ChatGPT-User', 'Claude-Web', 'ClaudeBot',
|
||||||
|
'anthropic-ai', 'Google-Extended', 'CCBot',
|
||||||
|
'PerplexityBot', 'Amazonbot', 'cohere-ai',
|
||||||
|
'OAI-SearchBot', 'Bytespider', 'FacebookBot',
|
||||||
|
'Applebot-Extended',
|
||||||
|
],
|
||||||
|
'Search' => [
|
||||||
|
'Googlebot', 'Bingbot', 'BingPreview', 'Slurp',
|
||||||
|
'DuckDuckBot', 'Baiduspider', 'YandexBot',
|
||||||
|
'Sogou', 'Exabot', 'facebot',
|
||||||
|
],
|
||||||
|
'Scraper' => [
|
||||||
|
'HTTrack', 'wget', 'curl', 'python-requests',
|
||||||
|
'python-httpx', 'Go-http-client', 'okhttp',
|
||||||
|
'Scrapy', 'Apache-HttpClient', 'Java/',
|
||||||
|
'ruby', 'PhantomJS', 'HeadlessChrome',
|
||||||
|
],
|
||||||
|
'Aggregator' => [
|
||||||
|
'Feedly', 'Feedbin', 'FeedReader', 'Inoreader',
|
||||||
|
'Flipboard', 'Pocket', 'Pinterest',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($bots as $category => $patterns) {
|
||||||
|
foreach ($patterns as $pattern) {
|
||||||
|
if (stripos($ua, $pattern) !== false) {
|
||||||
|
return $category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLogs(int $lines = 100): array
|
||||||
|
{
|
||||||
|
if (!file_exists($this->logFile)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file($this->logFile);
|
||||||
|
$content = array_slice($content, -$lines);
|
||||||
|
$logs = [];
|
||||||
|
|
||||||
|
foreach ($content as $line) {
|
||||||
|
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]*)\]$/', trim($line), $m)) {
|
||||||
|
$logs[] = [
|
||||||
|
'time' => $m[1],
|
||||||
|
'ip' => $m[2],
|
||||||
|
'host' => $m[3],
|
||||||
|
'lang' => $m[4],
|
||||||
|
'page' => $m[5],
|
||||||
|
'ua' => $m[6],
|
||||||
|
'referrer' => $m[7],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_reverse($logs);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,6 +131,10 @@ switch ($route) {
|
|||||||
handleGuide($auth, $appConfig);
|
handleGuide($auth, $appConfig);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'logs':
|
||||||
|
handleLogs($auth, $appConfig);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'users':
|
case 'users':
|
||||||
handleUsers($auth, $appConfig);
|
handleUsers($auth, $appConfig);
|
||||||
break;
|
break;
|
||||||
@@ -1451,6 +1455,66 @@ function handleGuide(AdminAuth $auth, array $config): void
|
|||||||
require __DIR__ . '/../admin/templates/layout.php';
|
require __DIR__ . '/../admin/templates/layout.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleLogs(AdminAuth $auth, array $config): void
|
||||||
|
{
|
||||||
|
$user = $auth->getCurrentUser();
|
||||||
|
$csrf = $auth->getCsrfToken();
|
||||||
|
|
||||||
|
$activeTab = $_GET['tab'] ?? 'admin';
|
||||||
|
if (!in_array($activeTab, ['admin', 'requests'])) $activeTab = 'admin';
|
||||||
|
|
||||||
|
$adminLogFile = $config['log_file'];
|
||||||
|
$requestLogFile = $config['request_log'];
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
if (isset($_GET['clear'])) {
|
||||||
|
$target = $activeTab === 'admin' ? $adminLogFile : $requestLogFile;
|
||||||
|
@file_put_contents($target, '');
|
||||||
|
$message = $activeTab === 'admin' ? 'Activiteitenlog gewist.' : 'Requestlog gewist.';
|
||||||
|
header('Location: /admin/logs?tab=' . $activeTab);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download
|
||||||
|
if (isset($_GET['download'])) {
|
||||||
|
$target = $activeTab === 'admin' ? $adminLogFile : $requestLogFile;
|
||||||
|
$filename = $activeTab === 'admin' ? 'admin.log' : 'requests.log';
|
||||||
|
if (file_exists($target)) {
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||||
|
header('Content-Length: ' . filesize($target));
|
||||||
|
readfile($target);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read admin log
|
||||||
|
$adminLogs = [];
|
||||||
|
if (file_exists($adminLogFile)) {
|
||||||
|
$lines = file($adminLogFile);
|
||||||
|
$lines = array_slice($lines, -200);
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] (.+)$/', trim($line), $m)) {
|
||||||
|
$adminLogs[] = [
|
||||||
|
'time' => $m[1],
|
||||||
|
'level' => strtolower($m[2]),
|
||||||
|
'ip' => $m[3],
|
||||||
|
'message' => $m[4],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$adminLogs = array_reverse($adminLogs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read request log
|
||||||
|
require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
|
||||||
|
$requestLogger = new RequestLogger($requestLogFile);
|
||||||
|
$requestLogs = $requestLogger->getLogs(200);
|
||||||
|
|
||||||
|
$route = 'logs';
|
||||||
|
require __DIR__ . '/../admin/templates/layout.php';
|
||||||
|
}
|
||||||
|
|
||||||
// --- Frontmatter helpers ---
|
// --- Frontmatter helpers ---
|
||||||
|
|
||||||
function parseFrontmatterField(string $content, string $key, string $default = ''): string
|
function parseFrontmatterField(string $content, string $key, string $default = ''): string
|
||||||
|
|||||||
@@ -89,5 +89,27 @@ if (strpos($path, '/content/') === 0) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bot detection — block known bots/AI scrapers early
|
||||||
|
if (RequestLogger::detectBot() !== null) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>403 Forbidden</title><meta name="robots" content="noindex,nofollow"></head><body><h1>403 Forbidden</h1><p>Access denied.</p></body></html>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$cms = new CodePressCMS($config);
|
$cms = new CodePressCMS($config);
|
||||||
|
|
||||||
|
// Log page view (not for media/assets)
|
||||||
|
if (!str_starts_with($path, '/-media/') && !str_starts_with($path, '/-assets/')) {
|
||||||
|
$requestLogFile = dirname(__DIR__) . '/admin/storage/logs/requests.log';
|
||||||
|
$logger = new RequestLogger($requestLogFile);
|
||||||
|
$logger->log(
|
||||||
|
$_GET['page'] ?? $config['default_page'] ?? 'index',
|
||||||
|
$_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? 'cli',
|
||||||
|
$_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||||
|
$_SERVER['HTTP_REFERER'] ?? '',
|
||||||
|
$_SERVER['HTTP_HOST'] ?? '',
|
||||||
|
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$cms->render();
|
$cms->render();
|
||||||
Reference in New Issue
Block a user