Fix newest homepage detection, file creation dates, real IP, and request log visitor type
- Fix detectNewestPage() to search subdirectories recursively and handle language prefixes - Fix getFileInfo() to preserve frontmatter created date or ctime instead of overwriting with mtime - Automatically store created date in frontmatter when creating/editing files - Add RequestLogger::getClientIp() with proxy and Cloudflare header support - Replace domain column in request log with visitor/bot type badges (Human, AI, Search, Scraper) - Update 'Activiteitenlog' to 'Activiteiten log' in UI and guide
This commit is contained in:
@@ -304,7 +304,14 @@ class AdminAuth
|
|||||||
mkdir($dir, 0755, true);
|
mkdir($dir, 0755, true);
|
||||||
}
|
}
|
||||||
$timestamp = date('Y-m-d H:i:s');
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'cli';
|
if (class_exists('RequestLogger')) {
|
||||||
|
$ip = RequestLogger::getClientIp();
|
||||||
|
} else {
|
||||||
|
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'cli';
|
||||||
|
if (($commaPos = strpos($ip, ',')) !== false) {
|
||||||
|
$ip = trim(substr($ip, 0, $commaPos));
|
||||||
|
}
|
||||||
|
}
|
||||||
file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND);
|
file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,12 +100,16 @@
|
|||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<ul class="list-unstyled mb-0">
|
<ul class="list-unstyled mb-0">
|
||||||
<?php foreach ($recentRequests as $log): ?>
|
<?php foreach ($recentRequests as $log): ?>
|
||||||
<li class="mb-2 pb-2 border-bottom small">
|
<li class="mb-2 pb-2 border-bottom small d-flex justify-content-between align-items-center">
|
||||||
<span class="text-muted"><?= htmlspecialchars($log['time']) ?></span>
|
<div>
|
||||||
<code class="text-muted"><?= htmlspecialchars($log['ip']) ?></code>
|
<span class="text-muted me-1"><?= htmlspecialchars($log['time']) ?></span>
|
||||||
<span class="text-truncate d-inline-block" style="max-width: 200px; vertical-align: bottom;"><?= htmlspecialchars($log['page']) ?></span>
|
<code class="text-muted me-1"><?= htmlspecialchars($log['ip']) ?></code>
|
||||||
<?php if ($log['ua']): ?>
|
<span class="fw-bold"><?= htmlspecialchars($log['page']) ?></span>
|
||||||
<span class="badge bg-secondary" title="<?= htmlspecialchars($log['ua']) ?>">bot?</span>
|
</div>
|
||||||
|
<?php if (!empty($log['visitor_info'])): ?>
|
||||||
|
<span class="badge bg-<?= $log['visitor_info']['badge'] ?>" title="<?= htmlspecialchars($log['ua']) ?>">
|
||||||
|
<i class="bi <?= $log['visitor_info']['icon'] ?>"></i> <?= $log['visitor_info']['label'] ?>
|
||||||
|
</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<ul class="nav nav-tabs mb-3" id="logTabs" role="tablist">
|
<ul class="nav nav-tabs mb-3" id="logTabs" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<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>
|
<button class="nav-link <?= $activeTab === 'admin' ? 'active' : '' ?>" id="admin-log-tab" data-bs-toggle="tab" data-bs-target="#admin-log" type="button" role="tab">Activiteiten log</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<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>
|
<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>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<small class="text-muted"><?= count($adminLogs) ?> regels</small>
|
<small class="text-muted"><?= count($adminLogs) ?> regels</small>
|
||||||
<div>
|
<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&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>
|
<a href="/admin/logs?tab=admin&clear=1" class="btn btn-sm btn-outline-danger" onclick="return confirm('Activiteiten log wissen?')"><i class="bi bi-trash"></i> Wissen</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
<th>Tijd</th>
|
<th>Tijd</th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
<th>Pagina</th>
|
<th>Pagina</th>
|
||||||
<th>Domein</th>
|
<th>Type / Gebruiker</th>
|
||||||
<th>Taal</th>
|
<th>Taal</th>
|
||||||
<th>User agent</th>
|
<th>User agent</th>
|
||||||
<th>Referrer</th>
|
<th>Referrer</th>
|
||||||
@@ -81,7 +81,11 @@
|
|||||||
<td class="text-nowrap small text-muted"><?= htmlspecialchars($log['time']) ?></td>
|
<td class="text-nowrap small text-muted"><?= htmlspecialchars($log['time']) ?></td>
|
||||||
<td><code class="small"><?= htmlspecialchars($log['ip']) ?></code></td>
|
<td><code class="small"><?= htmlspecialchars($log['ip']) ?></code></td>
|
||||||
<td><?= htmlspecialchars($log['page']) ?></td>
|
<td><?= htmlspecialchars($log['page']) ?></td>
|
||||||
<td><?= htmlspecialchars($log['host']) ?></td>
|
<td>
|
||||||
|
<span class="badge bg-<?= $log['visitor_info']['badge'] ?>">
|
||||||
|
<i class="bi <?= $log['visitor_info']['icon'] ?>"></i> <?= $log['visitor_info']['label'] ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td><?= htmlspecialchars($log['lang']) ?></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['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>
|
<td class="small text-muted" title="<?= htmlspecialchars($log['referrer']) ?>"><?= htmlspecialchars(substr($log['referrer'], 0, 40)) ?><?= strlen($log['referrer']) > 40 ? '…' : '' ?></td>
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ class CodePressCMS {
|
|||||||
{
|
{
|
||||||
if ($this->effectiveDefaultPage === null) {
|
if ($this->effectiveDefaultPage === null) {
|
||||||
$page = $this->config['default_page'] ?? 'auto';
|
$page = $this->config['default_page'] ?? 'auto';
|
||||||
$this->effectiveDefaultPage = ($page === 'auto') ? $this->detectDefaultPage() : $page;
|
$this->effectiveDefaultPage = match ($page) {
|
||||||
|
'auto' => $this->detectDefaultPage(),
|
||||||
|
'newest' => $this->detectNewestPage(),
|
||||||
|
default => $page,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return $this->effectiveDefaultPage;
|
return $this->effectiveDefaultPage;
|
||||||
}
|
}
|
||||||
@@ -438,7 +442,7 @@ class CodePressCMS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isset($result) && $actualFilePath) {
|
if (isset($result) && $actualFilePath) {
|
||||||
$result['file_info'] = $this->getFileInfo($actualFilePath);
|
$result['file_info'] = $this->getFileInfo($actualFilePath, $result['metadata'] ?? []);
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,19 +453,31 @@ class CodePressCMS {
|
|||||||
* Get file information including creation and modification dates
|
* Get file information including creation and modification dates
|
||||||
*
|
*
|
||||||
* @param string $filePath Path to the file
|
* @param string $filePath Path to the file
|
||||||
|
* @param array $metadata Optional frontmatter metadata
|
||||||
* @return array|null File information or null if file doesn't exist
|
* @return array|null File information or null if file doesn't exist
|
||||||
*/
|
*/
|
||||||
private function getFileInfo($filePath) {
|
private function getFileInfo($filePath, array $metadata = []) {
|
||||||
if (!file_exists($filePath)) {
|
if (!file_exists($filePath)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stats = stat($filePath);
|
$stats = stat($filePath);
|
||||||
// Use birthtime if available (macOS/BSD), fall back to mtime on Linux where ctime is inode change time
|
|
||||||
$createdTimestamp = $stats['birthtime'] ?? $stats['mtime'];
|
|
||||||
$created = date('d-m-Y H:i', $createdTimestamp);
|
|
||||||
$modified = date('d-m-Y H:i', $stats['mtime']);
|
$modified = date('d-m-Y H:i', $stats['mtime']);
|
||||||
|
|
||||||
|
// 1. Check if created date is present in frontmatter metadata
|
||||||
|
$metaCreated = $metadata['created'] ?? $metadata['date'] ?? $metadata['date_created'] ?? $metadata['created_at'] ?? null;
|
||||||
|
if ($metaCreated) {
|
||||||
|
if (is_numeric($metaCreated)) {
|
||||||
|
$created = date('d-m-Y H:i', (int)$metaCreated);
|
||||||
|
} else {
|
||||||
|
$created = (string)$metaCreated;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2. Fallback to ctime (inode change time / creation time) or mtime
|
||||||
|
$createdTimestamp = $stats['ctime'] ?? $stats['mtime'];
|
||||||
|
$created = date('d-m-Y H:i', $createdTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'created' => $created,
|
'created' => $created,
|
||||||
'modified' => $modified,
|
'modified' => $modified,
|
||||||
@@ -1366,6 +1382,83 @@ class CodePressCMS {
|
|||||||
return $candidates[0]['name'];
|
return $candidates[0]['name'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect the newest modified page (last modified timestamp)
|
||||||
|
*
|
||||||
|
* @return string Page key that was most recently modified or created
|
||||||
|
*/
|
||||||
|
private function detectNewestPage(): string
|
||||||
|
{
|
||||||
|
$contentDir = $this->config['content_dir'];
|
||||||
|
$realContentDir = realpath($contentDir);
|
||||||
|
if (!$realContentDir || !is_dir($realContentDir)) return 'index';
|
||||||
|
|
||||||
|
$availableLangs = array_keys($this->getAvailableLanguages());
|
||||||
|
if (empty($availableLangs)) {
|
||||||
|
$availableLangs = ['nl', 'en'];
|
||||||
|
}
|
||||||
|
$langRegex = implode('|', array_map('preg_quote', $availableLangs));
|
||||||
|
|
||||||
|
$candidates = [];
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($realContentDir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $fileInfo) {
|
||||||
|
if (!$fileInfo->isFile()) continue;
|
||||||
|
|
||||||
|
$ext = strtolower($fileInfo->getExtension());
|
||||||
|
if (!in_array($ext, ['md', 'php', 'html'])) continue;
|
||||||
|
|
||||||
|
$filePath = $fileInfo->getRealPath();
|
||||||
|
$relative = substr($filePath, strlen($realContentDir) + 1);
|
||||||
|
|
||||||
|
// Skip hidden files/directories (starting with . or -)
|
||||||
|
$parts = explode('/', str_replace('\\', '/', $relative));
|
||||||
|
$skip = false;
|
||||||
|
foreach ($parts as $p) {
|
||||||
|
if ($p !== '' && ($p[0] === '.' || $p[0] === '-')) {
|
||||||
|
$skip = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($skip) continue;
|
||||||
|
|
||||||
|
// Strip extension
|
||||||
|
$base = preg_replace('/\.(md|php|html)$/i', '', $relative);
|
||||||
|
|
||||||
|
// Handle language prefix e.g. nl.test -> test, or dir/en.page -> dir/page
|
||||||
|
$filename = basename($base);
|
||||||
|
$dirPrefix = dirname($base);
|
||||||
|
$dirPrefix = ($dirPrefix === '.' || $dirPrefix === '') ? '' : $dirPrefix . '/';
|
||||||
|
|
||||||
|
if (preg_match('/^(' . $langRegex . ')\.(.+)$/i', $filename, $m)) {
|
||||||
|
$pageKey = $dirPrefix . $m[2];
|
||||||
|
} else {
|
||||||
|
$pageKey = $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it ends with /index, e.g. folder/index -> folder
|
||||||
|
if (str_ends_with($pageKey, '/index')) {
|
||||||
|
$pageKey = substr($pageKey, 0, -6);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mtime = $fileInfo->getMTime();
|
||||||
|
$candidates[] = [
|
||||||
|
'key' => $pageKey ?: 'index',
|
||||||
|
'mtime' => $mtime
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($candidates)) return 'index';
|
||||||
|
|
||||||
|
usort($candidates, function ($a, $b) {
|
||||||
|
return $b['mtime'] <=> $a['mtime'];
|
||||||
|
});
|
||||||
|
|
||||||
|
return $candidates[0]['key'];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get homepage title
|
* Get homepage title
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -23,6 +23,114 @@ class RequestLogger
|
|||||||
@file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
|
@file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getClientIp(): string
|
||||||
|
{
|
||||||
|
$headers = [
|
||||||
|
'HTTP_CF_CONNECTING_IP',
|
||||||
|
'HTTP_X_REAL_IP',
|
||||||
|
'HTTP_CLIENT_IP',
|
||||||
|
'HTTP_X_FORWARDED_FOR',
|
||||||
|
'REMOTE_ADDR',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
if (!empty($_SERVER[$header])) {
|
||||||
|
$rawIp = $_SERVER[$header];
|
||||||
|
if (str_contains($rawIp, ',')) {
|
||||||
|
$ips = explode(',', $rawIp);
|
||||||
|
$rawIp = trim($ips[0]);
|
||||||
|
}
|
||||||
|
$rawIp = trim($rawIp);
|
||||||
|
if (filter_var($rawIp, FILTER_VALIDATE_IP) !== false) {
|
||||||
|
return $rawIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_SERVER['REMOTE_ADDR'] ?? 'cli';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function detectVisitorInfo(string $ua, string $user = ''): array
|
||||||
|
{
|
||||||
|
if (empty($ua)) {
|
||||||
|
$userLabel = ($user && $user !== 'Gast') ? $user : 'Onbekend';
|
||||||
|
return ['type' => 'unknown', 'label' => $userLabel, 'badge' => 'secondary', 'icon' => 'bi-question-circle'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bots = [
|
||||||
|
'AI' => [
|
||||||
|
'GPTBot' => 'AI (GPTBot)',
|
||||||
|
'ChatGPT-User' => 'AI (ChatGPT)',
|
||||||
|
'Claude-Web' => 'AI (Claude)',
|
||||||
|
'ClaudeBot' => 'AI (ClaudeBot)',
|
||||||
|
'anthropic-ai' => 'AI (Anthropic)',
|
||||||
|
'Google-Extended' => 'AI (Gemini/Google)',
|
||||||
|
'CCBot' => 'AI (CommonCrawl)',
|
||||||
|
'PerplexityBot' => 'AI (Perplexity)',
|
||||||
|
'Amazonbot' => 'AI (Amazon)',
|
||||||
|
'cohere-ai' => 'AI (Cohere)',
|
||||||
|
'OAI-SearchBot' => 'AI (OpenAI)',
|
||||||
|
'Bytespider' => 'AI (ByteDance)',
|
||||||
|
'FacebookBot' => 'AI (Meta/FB)',
|
||||||
|
'Applebot-Extended' => 'AI (Apple)',
|
||||||
|
],
|
||||||
|
'Search' => [
|
||||||
|
'Googlebot' => 'Zoekmachine (Google)',
|
||||||
|
'Bingbot' => 'Zoekmachine (Bing)',
|
||||||
|
'BingPreview' => 'Zoekmachine (Bing)',
|
||||||
|
'Slurp' => 'Zoekmachine (Yahoo)',
|
||||||
|
'DuckDuckBot' => 'Zoekmachine (DuckDuckGo)',
|
||||||
|
'Baiduspider' => 'Zoekmachine (Baidu)',
|
||||||
|
'YandexBot' => 'Zoekmachine (Yandex)',
|
||||||
|
'Sogou' => 'Zoekmachine (Sogou)',
|
||||||
|
'Exabot' => 'Zoekmachine (Exabot)',
|
||||||
|
'facebot' => 'Zoekmachine (Facebook)',
|
||||||
|
],
|
||||||
|
'Scraper' => [
|
||||||
|
'HTTrack' => 'Scraper (HTTrack)',
|
||||||
|
'Scrapy' => 'Scraper (Scrapy)',
|
||||||
|
'PhantomJS' => 'Scraper (PhantomJS)',
|
||||||
|
'HeadlessChrome' => 'Scraper (Headless)',
|
||||||
|
'curl' => 'Scraper (cURL)',
|
||||||
|
'wget' => 'Scraper (Wget)',
|
||||||
|
'python' => 'Scraper (Python)',
|
||||||
|
'Postman' => 'Scraper (Postman)',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($bots['AI'] as $pattern => $label) {
|
||||||
|
if (stripos($ua, $pattern) !== false) {
|
||||||
|
return ['type' => 'ai', 'label' => $label, 'badge' => 'danger', 'icon' => 'bi-robot'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($bots['Search'] as $pattern => $label) {
|
||||||
|
if (stripos($ua, $pattern) !== false) {
|
||||||
|
return ['type' => 'search', 'label' => $label, 'badge' => 'primary', 'icon' => 'bi-search'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($bots['Scraper'] as $pattern => $label) {
|
||||||
|
if (stripos($ua, $pattern) !== false) {
|
||||||
|
return ['type' => 'scraper', 'label' => $label, 'badge' => 'warning text-dark', 'icon' => 'bi-bug'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match('/(bot|crawler|spider|slurp)/i', $ua)) {
|
||||||
|
return ['type' => 'bot', 'label' => 'Bot', 'badge' => 'secondary', 'icon' => 'bi-robot'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$userPrefix = ($user && $user !== 'Gast') ? htmlspecialchars($user) . ' (' : '';
|
||||||
|
$userSuffix = ($user && $user !== 'Gast') ? ')' : '';
|
||||||
|
|
||||||
|
return [
|
||||||
|
'type' => 'human',
|
||||||
|
'label' => $userPrefix . 'Mens' . $userSuffix,
|
||||||
|
'badge' => 'success',
|
||||||
|
'icon' => 'bi-person-check',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public static function detectBot(): ?string
|
public static function detectBot(): ?string
|
||||||
{
|
{
|
||||||
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||||
@@ -69,10 +177,17 @@ class RequestLogger
|
|||||||
|
|
||||||
foreach ($content as $line) {
|
foreach ($content as $line) {
|
||||||
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]*)\]$/', trim($line), $m)) {
|
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]*)\]$/', trim($line), $m)) {
|
||||||
|
$user = $m[3];
|
||||||
|
// Handle backwards compatibility where host (e.g. localhost:8080 or domain.com) was logged
|
||||||
|
if (str_contains($user, '.') || str_contains($user, ':') || $user === 'cli') {
|
||||||
|
$user = 'Gast';
|
||||||
|
}
|
||||||
|
$visitorInfo = self::detectVisitorInfo($m[6], $user);
|
||||||
$logs[] = [
|
$logs[] = [
|
||||||
'time' => $m[1],
|
'time' => $m[1],
|
||||||
'ip' => $m[2],
|
'ip' => $m[2],
|
||||||
'host' => $m[3],
|
'user' => $user,
|
||||||
|
'visitor_info' => $visitorInfo,
|
||||||
'lang' => $m[4],
|
'lang' => $m[4],
|
||||||
'page' => $m[5],
|
'page' => $m[5],
|
||||||
'ua' => $m[6],
|
'ua' => $m[6],
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"content_dir": "content",
|
"content_dir": "content",
|
||||||
"templates_dir": "cms\/templates",
|
"templates_dir": "cms\/templates",
|
||||||
"active_theme": "default",
|
"active_theme": "default",
|
||||||
"default_page": "auto",
|
"default_page": "newest",
|
||||||
"language": {
|
"language": {
|
||||||
"default": "nl",
|
"default": "nl",
|
||||||
"available": [
|
"available": [
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ CodePress CMS is een lichtgewicht, file-based content management systeem gebouwd
|
|||||||
- **Media beheer** - Uploaden en verwijderen van mediabestanden in `content/-assets/`
|
- **Media beheer** - Uploaden en verwijderen van mediabestanden in `content/-assets/`
|
||||||
- **Gebruikersbeheer** - Gebruikers toevoegen, verwijderen, wachtwoorden wijzigen
|
- **Gebruikersbeheer** - Gebruikers toevoegen, verwijderen, wachtwoorden wijzigen
|
||||||
- **Handleiding** - Ingebouwde documentatie met API referentie
|
- **Handleiding** - Ingebouwde documentatie met API referentie
|
||||||
- **Logging** - Activiteitenlog en requests log met IP, pagina, domein; te bekijken en downloaden via `/admin/logs`
|
- **Logging** - Activiteiten log en requests log met IP, pagina, domein; te bekijken en downloaden via `/admin/logs`
|
||||||
- **Bot & AI blokkering** - Automatische 403 voor bekende bots en AI-crawlers
|
- **Bot & AI blokkering** - Automatische 403 voor bekende bots en AI-crawlers
|
||||||
- Session-based authenticatie met bcrypt hashing
|
- Session-based authenticatie met bcrypt hashing
|
||||||
- CSRF-bescherming, brute-force lockout (5 pogingen, 15 min)
|
- CSRF-bescherming, brute-force lockout (5 pogingen, 15 min)
|
||||||
@@ -333,7 +333,7 @@ De content editor (`content-edit`, `content-new`) bevat:
|
|||||||
|
|
||||||
De admin console houdt twee logs bij:
|
De admin console houdt twee logs bij:
|
||||||
|
|
||||||
- **Activiteitenlog** (`admin/storage/logs/admin.log`) — admin acties zoals pagina's aanmaken, bewerken, verwijderen, plugin in/uitschakelen, configuratie wijzigen.
|
- **Activiteiten log** (`admin/storage/logs/admin.log`) — admin acties zoals pagina's aanmaken, bewerken, verwijderen, plugin in/uitschakelen, configuratie wijzigen.
|
||||||
- **Requests log** (`admin/storage/logs/requests.log`) — elke pageview op de website, met IP, pagina, domein, taal, user-agent en referrer.
|
- **Requests log** (`admin/storage/logs/requests.log`) — elke pageview op de website, met IP, pagina, domein, taal, user-agent en referrer.
|
||||||
|
|
||||||
Beide logs zijn te bekijken en te downloaden via **`/admin/logs`** of via de sidebar link "Logs". Het dashboard toont de laatste 20 entries van elk log. Klik "Bekijk alle →" voor de volledige lijst, waar je ook kunt downloaden of wissen.
|
Beide logs zijn te bekijken en te downloaden via **`/admin/logs`** of via de sidebar link "Logs". Het dashboard toont de laatste 20 entries van elk log. Klik "Bekijk alle →" voor de volledige lijst, waar je ook kunt downloaden of wissen.
|
||||||
|
|||||||
+5
-21
@@ -21,6 +21,7 @@ if (file_exists($autoloader)) {
|
|||||||
// Load admin components
|
// Load admin components
|
||||||
$appConfig = require __DIR__ . '/../admin/config/app.php';
|
$appConfig = require __DIR__ . '/../admin/config/app.php';
|
||||||
require_once __DIR__ . '/../admin/src/AdminAuth.php';
|
require_once __DIR__ . '/../admin/src/AdminAuth.php';
|
||||||
|
require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
|
||||||
|
|
||||||
$auth = new AdminAuth($appConfig);
|
$auth = new AdminAuth($appConfig);
|
||||||
|
|
||||||
@@ -220,22 +221,8 @@ function handleDashboard(AdminAuth $auth, array $config): void
|
|||||||
|
|
||||||
// Load recent request log
|
// Load recent request log
|
||||||
$requestLogFile = $config['request_log'];
|
$requestLogFile = $config['request_log'];
|
||||||
$recentRequests = [];
|
$requestLogger = new RequestLogger($requestLogFile);
|
||||||
if (file_exists($requestLogFile)) {
|
$recentRequests = $requestLogger->getLogs(20);
|
||||||
$lines = file($requestLogFile);
|
|
||||||
$lines = array_slice($lines, -20);
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]+)\] \[([^\]]*)\] \[([^\]]*)\]$/', trim($line), $m)) {
|
|
||||||
$recentRequests[] = [
|
|
||||||
'time' => $m[1],
|
|
||||||
'ip' => $m[2],
|
|
||||||
'page' => $m[5],
|
|
||||||
'ua' => $m[6],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$recentRequests = array_reverse($recentRequests);
|
|
||||||
}
|
|
||||||
|
|
||||||
require __DIR__ . '/../admin/templates/layout.php';
|
require __DIR__ . '/../admin/templates/layout.php';
|
||||||
}
|
}
|
||||||
@@ -318,10 +305,7 @@ function adminLog(array $config, string $level, string $message): void
|
|||||||
@mkdir($dir, 0755, true);
|
@mkdir($dir, 0755, true);
|
||||||
}
|
}
|
||||||
$timestamp = date('Y-m-d H:i:s');
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? 'cli';
|
$ip = RequestLogger::getClientIp();
|
||||||
if (($commaPos = strpos($ip, ',')) !== false) {
|
|
||||||
$ip = trim(substr($ip, 0, $commaPos));
|
|
||||||
}
|
|
||||||
@file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND);
|
@file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1489,7 +1473,7 @@ function handleLogs(AdminAuth $auth, array $config): void
|
|||||||
if (isset($_GET['clear'])) {
|
if (isset($_GET['clear'])) {
|
||||||
$target = $activeTab === 'admin' ? $adminLogFile : $requestLogFile;
|
$target = $activeTab === 'admin' ? $adminLogFile : $requestLogFile;
|
||||||
@file_put_contents($target, '');
|
@file_put_contents($target, '');
|
||||||
$message = $activeTab === 'admin' ? 'Activiteitenlog gewist.' : 'Requestlog gewist.';
|
$message = $activeTab === 'admin' ? 'Activiteiten log gewist.' : 'Requestlog gewist.';
|
||||||
header('Location: /admin/logs?tab=' . $activeTab);
|
header('Location: /admin/logs?tab=' . $activeTab);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -103,14 +103,19 @@ $cms = new CodePressCMS($config);
|
|||||||
|
|
||||||
// Log page view (not for media/assets)
|
// Log page view (not for media/assets)
|
||||||
if (!str_starts_with($path, '/-media/') && !str_starts_with($path, '/-assets/')) {
|
if (!str_starts_with($path, '/-media/') && !str_starts_with($path, '/-assets/')) {
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
@session_start();
|
||||||
|
}
|
||||||
|
$loggedInUser = $_SESSION['admin_user'] ?? 'Gast';
|
||||||
|
|
||||||
$requestLogFile = dirname(__DIR__) . '/admin/storage/logs/requests.log';
|
$requestLogFile = dirname(__DIR__) . '/admin/storage/logs/requests.log';
|
||||||
$logger = new RequestLogger($requestLogFile);
|
$logger = new RequestLogger($requestLogFile);
|
||||||
$logger->log(
|
$logger->log(
|
||||||
$_GET['page'] ?? $config['default_page'] ?? 'index',
|
$_GET['page'] ?? $config['default_page'] ?? 'index',
|
||||||
$_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? 'cli',
|
RequestLogger::getClientIp(),
|
||||||
$_SERVER['HTTP_USER_AGENT'] ?? '',
|
$_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||||
$_SERVER['HTTP_REFERER'] ?? '',
|
$_SERVER['HTTP_REFERER'] ?? '',
|
||||||
$_SERVER['HTTP_HOST'] ?? '',
|
$loggedInUser,
|
||||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
|
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user