CMS 2.0 - Theme engine, logging, admin improvements
Major changes: - New ThemeManager with Twig templating and SCSS compilation - Dynamic themes system (themes/default, themes/demo) - LogManager with SQLite storage and syslog forwarding - RequestLogger with static helper methods - Admin UI overhaul (Bootstrap 5, dark mode) - Admin config page with logging and theme settings - Admin logs page with filters and search - Removed legacy Mustache templates - Removed test plugin and theme - Composer dependencies: Twig, scssphp, CommonMark, MaxMind GeoIP
This commit is contained in:
+230
-150
@@ -26,6 +26,13 @@ require_once __DIR__ . '/../cms/core/class/BotGuard.php';
|
||||
require_once __DIR__ . '/../cms/core/class/Cache.php';
|
||||
require_once __DIR__ . '/../cms/core/class/GeoIP.php';
|
||||
require_once __DIR__ . '/../cms/core/class/Analytics.php';
|
||||
require_once __DIR__ . '/../cms/core/class/LogManager.php';
|
||||
|
||||
// Initialize dynamic logging from site config
|
||||
$siteConfigForLogging = file_exists($appConfig['config_json'])
|
||||
? (json_decode(file_get_contents($appConfig['config_json']), true) ?? [])
|
||||
: [];
|
||||
LogManager::init($siteConfigForLogging['logging'] ?? []);
|
||||
|
||||
$auth = new AdminAuth($appConfig);
|
||||
|
||||
@@ -108,6 +115,14 @@ switch ($route) {
|
||||
handleTheme($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'theme-new':
|
||||
handleThemeNew($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'theme-delete':
|
||||
handleThemeDelete($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'plugins':
|
||||
handlePlugins($auth, $appConfig);
|
||||
break;
|
||||
@@ -328,6 +343,11 @@ function adminLog(array $config, string $level, string $message): void
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$ip = RequestLogger::getClientIp();
|
||||
@file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND);
|
||||
|
||||
// Also record through the dynamic log manager (admin event)
|
||||
if (class_exists('LogManager')) {
|
||||
LogManager::log(LogManager::EVENT_ADMIN, $level, $message, ['ip' => $ip]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleContentEdit(AdminAuth $auth, array $config): void
|
||||
@@ -410,6 +430,18 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
||||
$currentLayout = 'sidebar-content';
|
||||
}
|
||||
|
||||
// Load available layouts from the active theme's theme.json
|
||||
$themeLayouts = [];
|
||||
$configJson = $config['config_json'];
|
||||
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
||||
if (!is_array($configData)) $configData = [];
|
||||
$activeThemeName = $configData['active_theme'] ?? 'default';
|
||||
$activeThemeFile = $config['codepress_root'] . '/themes/' . $activeThemeName . '/theme.json';
|
||||
if (file_exists($activeThemeFile)) {
|
||||
$themeJson = json_decode(file_get_contents($activeThemeFile), true) ?? [];
|
||||
$themeLayouts = $themeJson['template'] ?? [];
|
||||
}
|
||||
|
||||
$fileName = basename($filePath);
|
||||
$route = 'content-edit';
|
||||
|
||||
@@ -522,6 +554,18 @@ function handleContentNew(AdminAuth $auth, array $config): void
|
||||
}
|
||||
}
|
||||
|
||||
// Load available layouts from the active theme's .twig files
|
||||
$themeLayouts = [];
|
||||
$configJson = $config['config_json'];
|
||||
$configData = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
||||
if (!is_array($configData)) $configData = [];
|
||||
$activeThemeName = $configData['active_theme'] ?? 'default';
|
||||
$activeThemeFile = $config['codepress_root'] . '/themes/' . $activeThemeName . '/theme.json';
|
||||
if (file_exists($activeThemeFile)) {
|
||||
$themeJson = json_decode(file_get_contents($activeThemeFile), true) ?? [];
|
||||
$themeLayouts = $themeJson['template'] ?? [];
|
||||
}
|
||||
|
||||
$route = 'content-new';
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
@@ -748,7 +792,7 @@ function handleConfig(AdminAuth $auth, array $config): void
|
||||
$configData['seo']['description'] = trim($_POST['seo_description'] ?? '');
|
||||
$configData['seo']['keywords'] = trim($_POST['seo_keywords'] ?? '');
|
||||
$configData['author']['name'] = trim($_POST['author_name'] ?? '');
|
||||
$configData['author']['website'] = trim($_POST['author_website'] ?? '');
|
||||
$configData['author']['website'] = stripUrlScheme(trim($_POST['author_website'] ?? ''));
|
||||
$configData['show_version'] = !empty($_POST['show_version']);
|
||||
$configData['features']['auto_link_pages'] = !empty($_POST['feature_auto_link']);
|
||||
$configData['features']['search_enabled'] = !empty($_POST['feature_search']);
|
||||
@@ -767,6 +811,20 @@ function handleConfig(AdminAuth $auth, array $config): void
|
||||
};
|
||||
$configData['analytics']['excluded_ips'] = $parseLines($_POST['excluded_ips'] ?? '');
|
||||
|
||||
// Logging settings
|
||||
$configData['logging']['enabled'] = !empty($_POST['logging_enabled']);
|
||||
$configData['logging']['driver'] = ($_POST['logging_driver'] ?? 'sqlite') === 'syslog' ? 'syslog' : 'sqlite';
|
||||
$configData['logging']['syslog_host'] = trim($_POST['syslog_host'] ?? '');
|
||||
$configData['logging']['syslog_port'] = max(1, min(65535, (int)($_POST['syslog_port'] ?? 514)));
|
||||
$configData['logging']['syslog_facility'] = preg_replace('/[^a-z0-9]/', '', $_POST['syslog_facility'] ?? 'local0');
|
||||
$configData['logging']['syslog_ident'] = trim($_POST['syslog_ident'] ?? 'codepress');
|
||||
$selectedEvents = $_POST['logging_events'] ?? [];
|
||||
$allEvents = ['admin', 'requests', 'errors', 'security', 'content', 'system'];
|
||||
$configData['logging']['events'] = [];
|
||||
foreach ($allEvents as $ev) {
|
||||
$configData['logging']['events'][$ev] = in_array($ev, $selectedEvents, true);
|
||||
}
|
||||
|
||||
file_put_contents($configJson, json_encode($configData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
adminLog($config, 'info', $user['username'] . ' wijzigde site configuratie');
|
||||
$message = 'Configuratie opgeslagen.';
|
||||
@@ -993,115 +1051,19 @@ function handleTheme(AdminAuth $auth, array $config): void
|
||||
$csrf = $auth->getCsrfToken();
|
||||
$configJson = $config['config_json'];
|
||||
$themesDir = $config['codepress_root'] . '/themes';
|
||||
$publicThemes = $config['codepress_root'] . '/public/themes';
|
||||
$message = '';
|
||||
$messageType = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||
$message = 'Ongeldige CSRF token.';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'save') {
|
||||
$themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['theme'] ?? '');
|
||||
$themeFile = $themesDir . '/' . $themeName . '/theme.json';
|
||||
if (file_exists($themeFile)) {
|
||||
$themeData = json_decode(file_get_contents($themeFile), true) ?? [];
|
||||
foreach (['header_color', 'header_font_color', 'navigation_color', 'navigation_font_color', 'sidebar_background', 'sidebar_border'] as $field) {
|
||||
$value = trim($_POST[$field] ?? '');
|
||||
if (preg_match('/^#[0-9a-fA-F]{6}$/', $value)) {
|
||||
$themeData[$field] = $value;
|
||||
}
|
||||
}
|
||||
$themeData['name'] = trim($_POST['theme_name'] ?? $themeData['name'] ?? $themeName);
|
||||
$themeData['header_height'] = preg_match('/^\d+$/', $_POST['header_height'] ?? '') ? trim($_POST['header_height']) : ($themeData['header_height'] ?? '56');
|
||||
$themeData['nav_height'] = preg_match('/^\d+$/', $_POST['nav_height'] ?? '') ? trim($_POST['nav_height']) : ($themeData['nav_height'] ?? '42');
|
||||
$themeData['background_image_opacity'] = preg_match('/^\d+$/', $_POST['background_image_opacity'] ?? '') ? max(0, min(100, intval($_POST['background_image_opacity']))) : ($themeData['background_image_opacity'] ?? '100');
|
||||
|
||||
// Handle background image upload
|
||||
if (!empty($_FILES['bg_image']['name']) && $_FILES['bg_image']['error'] === UPLOAD_ERR_OK) {
|
||||
$ext = strtolower(pathinfo($_FILES['bg_image']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'])) {
|
||||
$imgName = $themeName . '_bg.' . $ext;
|
||||
if (!is_dir($publicThemes)) mkdir($publicThemes, 0755, true);
|
||||
move_uploaded_file($_FILES['bg_image']['tmp_name'], $publicThemes . '/' . $imgName);
|
||||
$themeData['background_image'] = $imgName;
|
||||
}
|
||||
}
|
||||
// Handle background image URL
|
||||
$bgUrl = trim($_POST['background_image_url'] ?? '');
|
||||
if (!empty($bgUrl)) {
|
||||
$themeData['background_image'] = $bgUrl;
|
||||
}
|
||||
// Handle background image removal
|
||||
if (!empty($_POST['bg_image_remove'])) {
|
||||
if ($themeData['background_image'] ?? '') {
|
||||
$oldFile = $publicThemes . '/' . $themeData['background_image'];
|
||||
if (file_exists($oldFile)) unlink($oldFile);
|
||||
}
|
||||
$themeData['background_image'] = '';
|
||||
}
|
||||
|
||||
file_put_contents($themeFile, json_encode($themeData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
$message = 'Thema opgeslagen.';
|
||||
$messageType = 'success';
|
||||
}
|
||||
} elseif ($action === 'activate') {
|
||||
$themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['theme'] ?? '');
|
||||
if (file_exists($themesDir . '/' . $themeName . '/theme.json')) {
|
||||
$cfg = json_decode(file_get_contents($configJson), true) ?? [];
|
||||
$cfg['active_theme'] = $themeName;
|
||||
file_put_contents($configJson, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
$message = 'Thema geactiveerd.';
|
||||
$messageType = 'success';
|
||||
}
|
||||
} elseif ($action === 'create') {
|
||||
$newName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['new_name'] ?? '');
|
||||
if (empty($newName)) {
|
||||
$message = 'Geef een naam voor het nieuwe thema.';
|
||||
$messageType = 'danger';
|
||||
} elseif (file_exists($themesDir . '/' . $newName . '/theme.json')) {
|
||||
$message = 'Thema bestaat al.';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
$defaults = [
|
||||
'name' => $newName,
|
||||
'header_color' => '#0a369d',
|
||||
'header_font_color' => '#ffffff',
|
||||
'header_height' => '56',
|
||||
'navigation_color' => '#2754b4',
|
||||
'navigation_font_color' => '#ffffff',
|
||||
'nav_height' => '42',
|
||||
'sidebar_background' => '#f8f9fa',
|
||||
'sidebar_border' => '#dee2e6',
|
||||
'background_image' => '',
|
||||
'background_image_opacity' => '100',
|
||||
];
|
||||
$newThemeDir = $themesDir . '/' . $newName;
|
||||
mkdir($newThemeDir, 0755, true);
|
||||
file_put_contents($newThemeDir . '/theme.json', json_encode($defaults, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
$message = 'Thema aangemaakt.';
|
||||
$messageType = 'success';
|
||||
}
|
||||
} elseif ($action === 'delete') {
|
||||
$themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['theme'] ?? '');
|
||||
$themeDir = $themesDir . '/' . $themeName;
|
||||
if (is_dir($themeDir) && $themeName !== 'default') {
|
||||
$themeFile = $themeDir . '/theme.json';
|
||||
if (file_exists($themeFile)) unlink($themeFile);
|
||||
// Remove theme directory if empty
|
||||
$remaining = array_diff(scandir($themeDir), ['.', '..']);
|
||||
if (empty($remaining)) rmdir($themeDir);
|
||||
$cfg = json_decode(file_get_contents($configJson), true) ?? [];
|
||||
if (($cfg['active_theme'] ?? '') === $themeName) {
|
||||
$cfg['active_theme'] = 'default';
|
||||
file_put_contents($configJson, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
$message = 'Thema verwijderd.';
|
||||
$messageType = 'success';
|
||||
}
|
||||
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||
$themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['theme'] ?? '');
|
||||
if ($themeName !== '' && file_exists($themesDir . '/' . $themeName . '/theme.json')) {
|
||||
$cfg = json_decode(file_get_contents($configJson), true) ?? [];
|
||||
$cfg['active_theme'] = $themeName;
|
||||
file_put_contents($configJson, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
adminLog($config, 'info', $user['username'] . ' activeerde thema ' . $themeName);
|
||||
$message = 'Thema geactiveerd.';
|
||||
$messageType = 'success';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1123,18 +1085,135 @@ function handleTheme(AdminAuth $auth, array $config): void
|
||||
}
|
||||
}
|
||||
|
||||
// Load theme being edited
|
||||
$editThemeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['edit'] ?? '');
|
||||
$editTheme = null;
|
||||
if ($editThemeName && isset($themes[$editThemeName])) {
|
||||
$editTheme = $themes[$editThemeName];
|
||||
$editTheme['name'] = $editThemeName;
|
||||
}
|
||||
|
||||
$route = 'theme';
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
function handleThemeNew(AdminAuth $auth, array $config): void
|
||||
{
|
||||
$user = $auth->getCurrentUser();
|
||||
$csrf = $auth->getCsrfToken();
|
||||
$themesDir = $config['codepress_root'] . '/themes';
|
||||
$message = '';
|
||||
$messageType = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||
$message = 'Ongeldige CSRF token.';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
$newName = trim($_POST['theme_name'] ?? '');
|
||||
|
||||
if (empty($newName)) {
|
||||
$message = 'Thema naam is verplicht.';
|
||||
$messageType = 'danger';
|
||||
} elseif (!preg_match('/^[a-zA-Z][a-zA-Z0-9_-]*$/', $newName)) {
|
||||
$message = 'Ongeldige thema naam. Gebruik alleen letters, cijfers, streepjes en underscores. Begin met een letter.';
|
||||
$messageType = 'danger';
|
||||
} elseif (file_exists($themesDir . '/' . $newName . '/theme.json')) {
|
||||
$message = 'Thema met deze naam bestaat al.';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
$newThemeDir = $themesDir . '/' . $newName;
|
||||
mkdir($newThemeDir, 0755, true);
|
||||
|
||||
$defaults = [
|
||||
'title' => $newName,
|
||||
'config' => [
|
||||
'default_template' => 'full_content',
|
||||
],
|
||||
'template' => [
|
||||
'full_content' => 'full_content.twig',
|
||||
'left_sidebar' => 'left_sidebar.twig',
|
||||
'right_sidebar' => 'right_sidebar.twig',
|
||||
'custom1' => 'custom1.twig',
|
||||
],
|
||||
];
|
||||
file_put_contents($newThemeDir . '/theme.json', json_encode($defaults, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// Scaffold a full theme folder from the default theme
|
||||
$defaultThemeDir = $themesDir . '/default';
|
||||
if (is_dir($defaultThemeDir)) {
|
||||
$copyDirs = ['partials', 'css', 'js'];
|
||||
foreach ($copyDirs as $sub) {
|
||||
$src = $defaultThemeDir . '/' . $sub;
|
||||
if (is_dir($src)) {
|
||||
$dst = $newThemeDir . '/' . $sub;
|
||||
mkdir($dst, 0755, true);
|
||||
foreach (scandir($src) as $f) {
|
||||
if ($f[0] === '.') continue;
|
||||
if (is_file($src . '/' . $f)) {
|
||||
copy($src . '/' . $f, $dst . '/' . $f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (['base.twig', 'full_content.twig', 'left_sidebar.twig', 'right_sidebar.twig', 'custom1.twig'] as $tpl) {
|
||||
if (is_file($defaultThemeDir . '/' . $tpl)) {
|
||||
copy($defaultThemeDir . '/' . $tpl, $newThemeDir . '/' . $tpl);
|
||||
}
|
||||
}
|
||||
// Copy the default theme's preview image if present
|
||||
if (is_file($defaultThemeDir . '/theme.png')) {
|
||||
copy($defaultThemeDir . '/theme.png', $newThemeDir . '/theme.png');
|
||||
}
|
||||
}
|
||||
|
||||
adminLog($config, 'info', $user['username'] . ' maakte thema ' . $newName . ' aan');
|
||||
header('Location: /admin/theme');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$route = 'theme-new';
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
function handleThemeDelete(AdminAuth $auth, array $config): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /admin/theme');
|
||||
exit;
|
||||
}
|
||||
|
||||
$configJson = $config['config_json'];
|
||||
$themesDir = $config['codepress_root'] . '/themes';
|
||||
$themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['theme'] ?? '');
|
||||
$themeDir = $themesDir . '/' . $themeName;
|
||||
|
||||
if ($themeName === '' || $themeName === 'default' || !is_dir($themeDir)) {
|
||||
header('Location: /admin/theme');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||
// Recursively remove the entire theme directory
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($themeDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) {
|
||||
@rmdir($file->getRealPath());
|
||||
} else {
|
||||
@unlink($file->getRealPath());
|
||||
}
|
||||
}
|
||||
@rmdir($themeDir);
|
||||
|
||||
$cfg = json_decode(file_get_contents($configJson), true) ?? [];
|
||||
if (($cfg['active_theme'] ?? '') === $themeName) {
|
||||
$cfg['active_theme'] = 'default';
|
||||
file_put_contents($configJson, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' verwijderde thema ' . $themeName);
|
||||
}
|
||||
|
||||
header('Location: /admin/theme');
|
||||
exit;
|
||||
}
|
||||
|
||||
function handlePlugins(AdminAuth $auth, array $config): void
|
||||
{
|
||||
$user = $auth->getCurrentUser();
|
||||
@@ -1707,55 +1786,39 @@ 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'];
|
||||
// Filters
|
||||
$filterEvent = $_GET['event'] ?? '';
|
||||
$filterLevel = $_GET['level'] ?? '';
|
||||
$filterSearch = trim($_GET['search'] ?? '');
|
||||
$limit = max(10, min(1000, (int)($_GET['limit'] ?? 200)));
|
||||
|
||||
// Clear
|
||||
if (isset($_GET['clear'])) {
|
||||
$target = $activeTab === 'admin' ? $adminLogFile : $requestLogFile;
|
||||
@file_put_contents($target, '');
|
||||
$message = $activeTab === 'admin' ? 'Activiteiten log gewist.' : 'Requestlog gewist.';
|
||||
header('Location: /admin/logs?tab=' . $activeTab);
|
||||
LogManager::clear();
|
||||
$message = 'Log gewist.';
|
||||
header('Location: /admin/logs');
|
||||
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);
|
||||
$entries = LogManager::getLogs(1000, $filterEvent ?: null, $filterLevel ?: null, $filterSearch ?: null);
|
||||
$out = '';
|
||||
foreach ($entries as $e) {
|
||||
$out .= '[' . $e['time'] . '] [' . $e['event'] . '] [' . $e['level'] . '] [' . $e['ip'] . '] ' . $e['message'] . "\n";
|
||||
}
|
||||
header('Content-Type: text/plain');
|
||||
header('Content-Disposition: attachment; filename="codepress.log"');
|
||||
echo $out;
|
||||
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 log entries with filters
|
||||
$logEntries = LogManager::getLogs($limit, $filterEvent ?: null, $filterLevel ?: null, $filterSearch ?: null);
|
||||
|
||||
// Read request log
|
||||
$requestLogger = new RequestLogger($requestLogFile);
|
||||
$requestLogs = $requestLogger->getLogs(200);
|
||||
// Available event types for the filter dropdown
|
||||
$eventTypes = ['admin', 'requests', 'errors', 'security', 'content', 'system'];
|
||||
$levelTypes = ['info', 'warning', 'error', 'debug'];
|
||||
|
||||
$route = 'logs';
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
@@ -1826,6 +1889,22 @@ function handleUpdate(AdminAuth $auth, array $config): void
|
||||
|
||||
// --- Frontmatter helpers ---
|
||||
|
||||
/**
|
||||
* Remove the URL scheme (http://, https://) from a website value so it is
|
||||
* stored as a bare hostname. The engine adds the scheme back when rendering.
|
||||
*
|
||||
* @param string $url Raw URL value
|
||||
* @return string Hostname without scheme
|
||||
*/
|
||||
function stripUrlScheme(string $url): string
|
||||
{
|
||||
$url = trim($url);
|
||||
if (preg_match('#^[a-zA-Z][a-zA-Z0-9+.\-]*://#', $url)) {
|
||||
$url = preg_replace('#^[a-zA-Z][a-zA-Z0-9+.\-]*://#', '', $url);
|
||||
}
|
||||
return rtrim($url, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep a timestamped copy of a content file before it is overwritten.
|
||||
*
|
||||
@@ -1840,6 +1919,7 @@ function backupContentFile(string $filePath, int $keep = 5): void
|
||||
|
||||
$contentRoot = realpath(dirname(__DIR__) . '/content');
|
||||
$realFile = realpath($filePath);
|
||||
|
||||
if (!$contentRoot || !$realFile || strpos($realFile, $contentRoot) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user