@@ -16,7 +16,7 @@
= count($adminLogs) ?> regels
@@ -69,7 +69,7 @@
Tijd |
IP |
Pagina |
- Domein |
+ Type / Gebruiker |
Taal |
User agent |
Referrer |
@@ -81,7 +81,11 @@
= htmlspecialchars($log['time']) ?> |
= htmlspecialchars($log['ip']) ?> |
= htmlspecialchars($log['page']) ?> |
- = htmlspecialchars($log['host']) ?> |
+
+
+ = $log['visitor_info']['label'] ?>
+
+ |
= htmlspecialchars($log['lang']) ?> |
= htmlspecialchars(substr($log['ua'], 0, 60)) ?>= strlen($log['ua']) > 60 ? '…' : '' ?> |
= htmlspecialchars(substr($log['referrer'], 0, 40)) ?>= strlen($log['referrer']) > 40 ? '…' : '' ?> |
diff --git a/cms/core/class/CodePressCMS.php b/cms/core/class/CodePressCMS.php
index b8fb745..6a7eb4a 100644
--- a/cms/core/class/CodePressCMS.php
+++ b/cms/core/class/CodePressCMS.php
@@ -90,7 +90,11 @@ class CodePressCMS {
{
if ($this->effectiveDefaultPage === null) {
$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;
}
@@ -438,7 +442,7 @@ class CodePressCMS {
}
if (isset($result) && $actualFilePath) {
- $result['file_info'] = $this->getFileInfo($actualFilePath);
+ $result['file_info'] = $this->getFileInfo($actualFilePath, $result['metadata'] ?? []);
return $result;
}
@@ -449,19 +453,31 @@ class CodePressCMS {
* Get file information including creation and modification dates
*
* @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
*/
- private function getFileInfo($filePath) {
+ private function getFileInfo($filePath, array $metadata = []) {
if (!file_exists($filePath)) {
return null;
}
$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']);
+ // 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 [
'created' => $created,
'modified' => $modified,
@@ -1366,6 +1382,83 @@ class CodePressCMS {
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
*
diff --git a/cms/core/class/RequestLogger.php b/cms/core/class/RequestLogger.php
index 139f639..ab01913 100644
--- a/cms/core/class/RequestLogger.php
+++ b/cms/core/class/RequestLogger.php
@@ -23,6 +23,114 @@ class RequestLogger
@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
{
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
@@ -69,10 +177,17 @@ class RequestLogger
foreach ($content as $line) {
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[] = [
'time' => $m[1],
'ip' => $m[2],
- 'host' => $m[3],
+ 'user' => $user,
+ 'visitor_info' => $visitorInfo,
'lang' => $m[4],
'page' => $m[5],
'ua' => $m[6],
diff --git a/config.json b/config.json
index f49c861..64617bb 100644
--- a/config.json
+++ b/config.json
@@ -3,7 +3,7 @@
"content_dir": "content",
"templates_dir": "cms\/templates",
"active_theme": "default",
- "default_page": "auto",
+ "default_page": "newest",
"language": {
"default": "nl",
"available": [
diff --git a/guide/nl.codepress.md b/guide/nl.codepress.md
index 4b06dc2..d16357b 100644
--- a/guide/nl.codepress.md
+++ b/guide/nl.codepress.md
@@ -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/`
- **Gebruikersbeheer** - Gebruikers toevoegen, verwijderen, wachtwoorden wijzigen
- **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
- Session-based authenticatie met bcrypt hashing
- 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:
-- **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.
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.
diff --git a/public/admin.php b/public/admin.php
index 3d3b914..e405139 100644
--- a/public/admin.php
+++ b/public/admin.php
@@ -21,6 +21,7 @@ if (file_exists($autoloader)) {
// Load admin components
$appConfig = require __DIR__ . '/../admin/config/app.php';
require_once __DIR__ . '/../admin/src/AdminAuth.php';
+require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
$auth = new AdminAuth($appConfig);
@@ -220,22 +221,8 @@ function handleDashboard(AdminAuth $auth, array $config): void
// Load recent request log
$requestLogFile = $config['request_log'];
- $recentRequests = [];
- if (file_exists($requestLogFile)) {
- $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);
- }
+ $requestLogger = new RequestLogger($requestLogFile);
+ $recentRequests = $requestLogger->getLogs(20);
require __DIR__ . '/../admin/templates/layout.php';
}
@@ -318,10 +305,7 @@ function adminLog(array $config, string $level, string $message): void
@mkdir($dir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
- $ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? 'cli';
- if (($commaPos = strpos($ip, ',')) !== false) {
- $ip = trim(substr($ip, 0, $commaPos));
- }
+ $ip = RequestLogger::getClientIp();
@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'])) {
$target = $activeTab === 'admin' ? $adminLogFile : $requestLogFile;
@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);
exit;
}
diff --git a/public/index.php b/public/index.php
index f4209ca..c5f7dc2 100644
--- a/public/index.php
+++ b/public/index.php
@@ -103,14 +103,19 @@ $cms = new CodePressCMS($config);
// Log page view (not for media/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';
$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',
+ RequestLogger::getClientIp(),
$_SERVER['HTTP_USER_AGENT'] ?? '',
$_SERVER['HTTP_REFERER'] ?? '',
- $_SERVER['HTTP_HOST'] ?? '',
+ $loggedInUser,
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
);
}