__DIR__ . '/../var/cache/twig', 'auto_reload' => true, 'debug' => true, ]); // Add Twig functions $twig->addFunction(new \Twig\TwigFunction('get_country_flag', function($country) { return GeoIP::getCountryFlagEmoji($country); })); $twig->addFunction(new \Twig\TwigFunction('get_country_name', function($country) { return GeoIP::getCountryName($country); })); // Add Twig global for user role and permission checks $twig->addGlobal('user_role', $auth->getCurrentRole()); $twig->addFunction(new \Twig\TwigFunction('has_permission', function($route) use ($auth) { return $auth->hasPermission($route); })); $twig->addFunction(new \Twig\TwigFunction('role_label', function($role) { return AdminAuth::getRoleLabel($role); })); // Routing $route = $_GET['route'] ?? ''; // Helper to get sidebar color function getSidebarColor($config) { $siteConfig = file_exists($config['config_json']) ? json_decode(file_get_contents($config['config_json']), true) : []; $activeTheme = $siteConfig['active_theme'] ?? 'default'; $themeFile = __DIR__ . "/../themes/{$activeTheme}/theme.json"; if (file_exists($themeFile)) { $theme = json_decode(file_get_contents($themeFile), true); return $theme['header_color'] ?? '#0a369d'; } return '#0a369d'; } // Essential plugins that cannot be disabled or deleted function getProtectedPlugins(): array { return ['Navigation']; } function isProtectedPlugin(string $pluginName): bool { return in_array($pluginName, getProtectedPlugins(), true); } // Public routes (no auth required) if ($route === 'login') { $error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $result = $auth->login($_POST['username'] ?? '', $_POST['password'] ?? '', $_POST['csrf_token'] ?? ''); if ($result['success']) { header('Location: /admin/dashboard'); exit; } $error = $result['message']; } echo $twig->render('login.twig', [ 'error' => $error, 'csrf_token' => $auth->getCsrfToken(), ]); exit; } // All other routes require authentication if (!$auth->isAuthenticated()) { header('Location: /admin/login'); exit; } $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $userRole = $auth->getCurrentRole(); $siteConfig = file_exists($appConfig['config_json']) ? json_decode(file_get_contents($appConfig['config_json']), true) : []; // Check route permission (except dashboard which everyone can access) if ($route !== '' && $route !== 'dashboard' && !$auth->hasPermission($route)) { http_response_code(403); echo $twig->render('pages/error.twig', [ 'user' => $user, 'route' => '', 'csrf_token' => $csrf, 'error_code' => 403, 'error_title' => 'Geen toegang', 'error_message' => 'Je hebt geen rechten om deze pagina te bekijken.', 'sidebar_color' => getSidebarColor($appConfig), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); exit; } // Authenticated routes switch ($route) { case 'logout': $auth->logout(); header('Location: /admin/login'); exit; case 'dashboard': case '': handleDashboard($auth, $appConfig, $twig, $user, $csrf, $siteConfig); break; case 'content': handleContent($auth, $appConfig, $twig, $user, $csrf, $siteConfig); break; case 'content-edit': handleContentEdit($auth, $appConfig, $twig, $user, $csrf, $siteConfig); break; case 'content-new': handleContentNew($auth, $appConfig, $twig, $user, $csrf, $siteConfig); break; case 'content-delete': handleContentDelete($auth, $appConfig); break; case 'content-dir-create': handleContentDirCreate($auth, $appConfig); break; case 'content-dir-rename': handleContentDirRename($auth, $appConfig, $twig, $user, $csrf); break; case 'content-move': handleContentMove($auth, $appConfig, $twig, $user, $csrf); break; case 'content-dir-delete': handleContentDirDelete($auth, $appConfig); break; case 'config': handleConfig($auth, $appConfig, $twig, $user, $csrf); break; case 'security': handleSecurity($auth, $appConfig, $twig, $user, $csrf); break; case 'statistics': handleStatistics($auth, $appConfig, $twig, $user, $csrf, $siteConfig); break; case 'theme': handleTheme($auth, $appConfig, $twig, $user, $csrf, $siteConfig); break; case 'theme-new': handleThemeNew($auth, $appConfig, $twig, $user, $csrf); break; case 'plugins': handlePlugins($auth, $appConfig, $twig, $user, $csrf); break; case 'plugins-new': handlePluginsNew($auth, $appConfig, $twig, $user, $csrf); break; case 'plugins-edit': handlePluginsEdit($auth, $appConfig, $twig, $user, $csrf); break; case 'plugins-config': handlePluginsConfig($auth, $appConfig, $twig, $user, $csrf); break; case 'plugins-toggle': handlePluginsToggle($auth, $appConfig); break; case 'plugins-delete': handlePluginsDelete($auth, $appConfig); break; case 'users': handleUsers($auth, $appConfig, $twig, $user, $csrf); break; case 'guide': handleGuide($auth, $appConfig, $twig, $user, $csrf); break; case 'logs': handleLogs($auth, $appConfig, $twig, $user, $csrf); break; case 'update': handleUpdate($auth, $appConfig, $twig, $user, $csrf); break; case 'media': handleMedia($auth, $appConfig, $twig, $user, $csrf); break; default: handleDashboard($auth, $appConfig, $twig, $user, $csrf, $siteConfig); } // ============================================================================ // HANDLER FUNCTIONS // ============================================================================ function handleDashboard($auth, $config, $twig, $user, $csrf, $siteConfig): void { $contentDir = $config['content_dir']; $pluginsDir = $config['plugins_dir']; $configJson = $config['config_json']; $versionFile = __DIR__ . '/../version.php'; $versionInfo = []; if (file_exists($versionFile)) { $verData = include $versionFile; $versionInfo = is_array($verData) ? $verData : []; } if (empty($versionInfo['version'])) { $versionInfo['version'] = '0.0.0'; } $stats = [ 'pages' => countFiles($contentDir, ['md', 'php', 'html']), 'directories' => countDirs($contentDir), 'plugins' => countEnabledPlugins($pluginsDir, $configJson), 'config_exists' => file_exists($configJson), 'content_size' => formatSize(dirSize($contentDir)), 'php_version' => PHP_VERSION, 'cms_version' => $versionInfo['version'], 'os' => PHP_OS_FAMILY . ' (' . php_uname('r') . ')', ]; // Load recent activity log $logFile = $config['log_file']; $recentLogs = []; if (file_exists($logFile)) { $lines = file($logFile); $lines = array_slice($lines, -20); foreach ($lines as $line) { if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] (.+)$/', trim($line), $m)) { $recentLogs[] = [ 'time' => $m[1], 'level' => strtolower($m[2]), 'ip' => $m[3], 'message' => $m[4], ]; } } $recentLogs = array_reverse($recentLogs); } // Load recent request log $requestLogFile = $config['request_log']; $requestLogger = new RequestLogger($requestLogFile); $recentRequests = $requestLogger->getLogs(20); // Analytics summary (last 30 days) $siteAnalytics = is_array($siteConfig['analytics'] ?? null) ? $siteConfig['analytics'] : []; $analytics = new Analytics($siteAnalytics); $analyticsSummary = $analytics->getStats(30); echo $twig->render('pages/dashboard.twig', [ 'user' => $user, 'route' => 'dashboard', 'csrf_token' => $csrf, 'stats' => $stats, 'site_config' => $siteConfig, 'recent_logs' => $recentLogs, 'recent_requests' => $recentRequests, 'analytics_summary' => $analyticsSummary, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); } function handleContent($auth, $config, $twig, $user, $csrf, $siteConfig): void { $contentDir = $config['content_dir']; $subdir = $_GET['dir'] ?? ''; // Prevent path traversal $subdir = str_replace(['../', '..\\'], '', $subdir); $subdir = trim($subdir, '/'); if ($subdir === '.' || $subdir === '') { $subdir = ''; } $fullPath = rtrim($contentDir, '/') . '/' . $subdir; if (!is_dir($fullPath)) { $fullPath = $contentDir; $subdir = ''; } $message = ''; $messageType = ''; // Handle file upload if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file']['name'][0])) { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $allowedExt = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf', 'zip', 'mp4', 'webm', 'ogg', 'mp3', 'wav', 'doc', 'docx', 'xls', 'xlsx']; $uploaded = 0; $errors = []; foreach ($_FILES['file']['name'] as $i => $name) { if ($_FILES['file']['error'][$i] !== UPLOAD_ERR_OK) continue; $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); if (!in_array($ext, $allowedExt)) { $errors[] = htmlspecialchars($name) . ' (niet toegestaan type)'; continue; } $filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $name); $dest = rtrim($fullPath, '/') . '/' . $filename; $n = 1; while (file_exists($dest)) { $p = pathinfo($filename); $dest = rtrim($fullPath, '/') . '/' . $p['filename'] . '_' . $n . '.' . ($p['extension'] ?? $ext); $n++; } if (move_uploaded_file($_FILES['file']['tmp_name'][$i], $dest)) { $uploaded++; } else { $errors[] = htmlspecialchars($name); } } if ($uploaded > 0) { $message = $uploaded . ' bestand(en) geüpload.'; $messageType = 'success'; } if (!empty($errors)) { $message .= ' Fouten: ' . implode(', ', $errors); $messageType = $messageType ?: 'danger'; } } } $items = scanContentDir($fullPath, $subdir); echo $twig->render('pages/content.twig', [ 'user' => $user, 'route' => 'content', 'csrf_token' => $csrf, 'subdir' => $subdir, 'items' => $items, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): void { $contentDir = $config['content_dir']; $file = $_GET['file'] ?? ''; $file = str_replace(['../', '..\\'], '', $file); $filePath = rtrim($contentDir, '/') . '/' . $file; $message = ''; $messageType = ''; // Validate path $realPath = realpath($filePath); $realContentDir = realpath($contentDir); if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0) { header('Location: /admin/content'); exit; } $fileExt = pathinfo($filePath, PATHINFO_EXTENSION); $isEditable = in_array($fileExt, ['md', 'php', 'html']); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { // Handle rename $newFilename = trim($_POST['filename'] ?? ''); $wasRenamed = false; if (!empty($newFilename)) { $newFilename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newFilename); $newFilename .= '.' . $fileExt; $parentDir = dirname($filePath); $newFilePath = $parentDir . '/' . $newFilename; $realParentDir = realpath($parentDir); if ($realParentDir && strpos($realParentDir, $realContentDir) === 0) { if ($newFilePath !== $filePath && !file_exists($newFilePath)) { rename($filePath, $newFilePath); $wasRenamed = true; adminLog($config, 'info', $user['username'] . ' hernoemde ' . basename($filePath) . ' naar ' . $newFilename); $filePath = $newFilePath; $newFile = dirname($file) . '/' . $newFilename; $file = ltrim($newFile, './'); } } } // Save content only for editable files if ($isEditable) { $content = $_POST['content'] ?? ''; $layout = $_POST['layout'] ?? ''; if ($layout) { $content = updateContentFrontmatter($content, 'layout', $layout); } $plugins = isset($_POST['plugins']) && is_array($_POST['plugins']) ? implode(', ', array_map('trim', $_POST['plugins'])) : ''; $content = updateContentFrontmatter($content, 'plugins', $plugins); backupContentFile($filePath); file_put_contents($filePath, $content); if (!$wasRenamed) { adminLog($config, 'info', $user['username'] . ' bewerkte ' . basename($filePath)); } $message = 'Bestand opgeslagen.'; $messageType = 'success'; } } } $fileName = basename($filePath); $fileContent = $isEditable ? file_get_contents($filePath) : ''; $currentLayout = extractFrontmatterValue($fileContent, 'layout'); $currentPlugins = extractFrontmatterValue($fileContent, 'plugins'); $selectedPlugins = array_map('trim', explode(',', $currentPlugins)); if ($selectedPlugins === ['']) $selectedPlugins = []; // Get available layouts from active theme's theme.json $themeLayouts = []; $themeDefaultLayout = 'full_content'; $activeThemeName = $siteConfig['active_theme'] ?? 'default'; $themeDir = __DIR__ . "/../themes/{$activeThemeName}"; $themeJsonFile = $themeDir . '/theme.json'; if (file_exists($themeJsonFile)) { $themeJson = json_decode(file_get_contents($themeJsonFile), true); $themeDefaultLayout = $themeJson['config']['default_template'] ?? 'full_content'; if (isset($themeJson['template']) && is_array($themeJson['template'])) { foreach ($themeJson['template'] as $key => $twigFile) { // Skip internal layouts that users shouldn't select if (in_array($key, ['guide'], true)) continue; $themeLayouts[$key] = $twigFile; } } } else { // Fallback: scan theme directory for .twig files if (is_dir($themeDir)) { foreach (glob($themeDir . '/*.twig') as $layoutFile) { $layoutName = basename($layoutFile, '.twig'); if (!in_array($layoutName, ['base', 'guide'], true)) { $themeLayouts[$layoutName] = $layoutName . '.twig'; } } } } // Get available plugins $pluginsDir = $config['plugins_dir']; $availablePlugins = []; if (is_dir($pluginsDir)) { foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginDir) { $pluginName = basename($pluginDir); if ($pluginName !== '.' && $pluginName !== '..' && file_exists($pluginDir . '/plugin.json')) { $availablePlugins[] = $pluginName; } } } $currentLang = extractLanguagePrefix($file); $route = 'content-edit'; $fileDir = trim(dirname($file), '.\\/'); echo $twig->render('pages/content-edit.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'file' => $file, 'fileDir' => $fileDir, 'fileName' => $fileName, 'fileExt' => $fileExt, 'fileContent' => $fileContent, 'isEditable' => $isEditable, 'currentLayout' => $currentLayout ?: $themeDefaultLayout, 'themeLayouts' => $themeLayouts, 'themeDefaultLayout' => $themeDefaultLayout, 'activeThemeName' => $activeThemeName, 'availablePlugins' => $availablePlugins, 'selectedPlugins' => $selectedPlugins, 'currentLang' => $currentLang, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => $isEditable, 'message' => $message, 'message_type' => $messageType, ]); } function handleContentNew($auth, $config, $twig, $user, $csrf, $siteConfig): void { $contentDir = $config['content_dir']; $dir = $_GET['dir'] ?? ''; $dir = str_replace(['../', '..\\'], '', $dir); $fullPath = rtrim($contentDir, '/') . '/' . $dir; $message = ''; $messageType = ''; // Get available layouts from active theme's theme.json $themeLayouts = []; $themeDefaultLayout = 'full_content'; $activeThemeName = $siteConfig['active_theme'] ?? 'default'; $themeDir = __DIR__ . "/../themes/{$activeThemeName}"; $themeJsonFile = $themeDir . '/theme.json'; if (file_exists($themeJsonFile)) { $themeJson = json_decode(file_get_contents($themeJsonFile), true); $themeDefaultLayout = $themeJson['config']['default_template'] ?? 'full_content'; if (isset($themeJson['template']) && is_array($themeJson['template'])) { foreach ($themeJson['template'] as $key => $twigFile) { if (in_array($key, ['guide'], true)) continue; $themeLayouts[$key] = $twigFile; } } } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $filename = trim($_POST['filename'] ?? ''); $ext = $_POST['extension'] ?? 'md'; $layout = $_POST['layout'] ?? $themeDefaultLayout; if (!empty($filename)) { $filename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $filename); $filename .= '.' . $ext; $dest = rtrim($fullPath, '/') . '/' . $filename; if (!file_exists($dest)) { $content = "---\nlayout: " . $layout . "\n---\n\n# Nieuwe pagina\n\n"; file_put_contents($dest, $content); adminLog($config, 'info', $user['username'] . ' creëerde ' . $filename); header('Location: /admin/content-edit?file=' . urlencode($dir . '/' . $filename)); exit; } else { $message = 'Bestand bestaat al.'; $messageType = 'danger'; } } } } $route = 'content-new'; $availableExtensions = ['md' => 'Markdown', 'php' => 'PHP', 'html' => 'HTML']; echo $twig->render('pages/content-new.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'dir' => $dir, 'availableExtensions' => $availableExtensions, 'themeLayouts' => $themeLayouts, 'themeDefaultLayout' => $themeDefaultLayout, 'activeThemeName' => $activeThemeName, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleContentDelete($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/content'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/content'); exit; } $contentDir = $config['content_dir']; $file = $_POST['file'] ?? $_GET['file'] ?? ''; $file = str_replace(['../', '..\\'], '', $file); $filePath = rtrim($contentDir, '/') . '/' . $file; if (file_exists($filePath) && is_file($filePath)) { unlink($filePath); adminLog($config, 'info', $user['username'] . ' verwijderde ' . $file); } header('Location: /admin/content?dir=' . urlencode(dirname($file))); exit; } function handleContentDirCreate($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/content'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/content'); exit; } $contentDir = $config['content_dir']; $subdir = $_GET['dir'] ?? ''; $subdir = str_replace(['../', '..\\'], '', $subdir); $dirname = trim($_POST['dirname'] ?? ''); if (!empty($dirname)) { $dirname = preg_replace('/[^a-zA-Z0-9._-]/', '-', $dirname); $newPath = rtrim($contentDir, '/') . '/' . ($subdir ? $subdir . '/' : '') . $dirname; if (!file_exists($newPath)) { @mkdir($newPath, 0755, true); adminLog($config, 'info', $user['username'] . ' creëerde map ' . $dirname); } } header('Location: /admin/content?dir=' . urlencode($subdir)); exit; } function handleContentDirRename($auth, $config, $twig, $user, $csrf): void { $contentDir = $config['content_dir']; $dir = $_GET['dir'] ?? ''; $dir = str_replace(['../', '..\\'], '', $dir); $fullPath = rtrim($contentDir, '/') . '/' . $dir; $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $newName = trim($_POST['newname'] ?? ''); if (!empty($newName)) { $newName = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newName); $parentDir = dirname($fullPath); $newPath = $parentDir . '/' . $newName; if (!file_exists($newPath) && $newPath !== $fullPath) { rename($fullPath, $newPath); adminLog($config, 'info', $user['username'] . ' hernoemde map ' . basename($dir) . ' naar ' . $newName); header('Location: /admin/content?dir=' . urlencode(dirname($dir) . '/' . $newName)); exit; } else { $message = 'Map bestaat al of ongeldige naam.'; $messageType = 'danger'; } } } } $route = 'content-dir-rename'; echo $twig->render('pages/content-dir-form.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'dir' => $dir, 'fullPath' => $fullPath, 'currentName' => basename($dir), 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleContentDirDelete($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/content'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/content'); exit; } $contentDir = $config['content_dir']; $dir = $_GET['dir'] ?? $_POST['dir'] ?? ''; $dir = str_replace(['../', '..\\'], '', $dir); $fullPath = rtrim($contentDir, '/') . '/' . $dir; if (is_dir($fullPath)) { $files = scandir($fullPath); $files = array_diff($files, ['.', '..']); if (empty($files)) { rmdir($fullPath); adminLog($config, 'info', $user['username'] . ' verwijderde map ' . $dir); } } header('Location: /admin/content?dir=' . urlencode(dirname($dir))); exit; } function handleContentMove($auth, $config, $twig, $user, $csrf): void { $contentDir = $config['content_dir']; $item = $_GET['item'] ?? ''; $item = str_replace(['../', '..\\'], '', $item); $fullPath = rtrim($contentDir, '/') . '/' . $item; $message = ''; $messageType = ''; // Get all directories for destination selection $directories = []; $realContentDir = realpath($contentDir); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($contentDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isDir()) { $path = str_replace($realContentDir, '', realpath($file->getPathname())); $path = trim($path, '/\\'); if ($path && $path !== $item && !str_starts_with($path, '-') && !str_starts_with($path, '.')) { $directories[] = $path; } } } sort($directories); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $dest = trim($_POST['destination'] ?? ''); if (!empty($dest)) { $dest = str_replace(['../', '..\\'], '', $dest); $destPath = rtrim($contentDir, '/') . '/' . $dest; if (is_dir($destPath)) { $newPath = $destPath . '/' . basename($item); if (!file_exists($newPath)) { rename($fullPath, $newPath); adminLog($config, 'info', $user['username'] . ' verplaatste ' . $item . ' naar ' . $dest); header('Location: /admin/content?dir=' . urlencode($dest)); exit; } else { $message = 'Bestand of map bestaat al op de bestemming.'; $messageType = 'danger'; } } } } } $route = 'content-move'; $isDir = is_dir($fullPath); $itemDir = trim(dirname($item), '.\\/'); echo $twig->render('pages/content-move-form.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'item' => $item, 'itemDir' => $itemDir, 'fullPath' => $fullPath, 'itemName' => basename($item), 'isDir' => $isDir, 'directories' => $directories, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleConfig($auth, $config, $twig, $user, $csrf): void { $configFile = $config['config_json']; $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $newConfig = json_decode(file_get_contents($configFile), true) ?? []; $newConfig['site_title'] = $_POST['site_title'] ?? ''; $newConfig['language']['default'] = $_POST['language_default'] ?? 'nl'; $newConfig['author']['name'] = $_POST['author_name'] ?? ''; $newConfig['author']['email'] = $_POST['author_email'] ?? ''; $newConfig['analytics']['enabled'] = isset($_POST['analytics_enabled']); $newConfig['logging']['enabled'] = isset($_POST['logging_enabled']); backupContentFile($configFile); file_put_contents($configFile, json_encode($newConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); adminLog($config, 'info', $user['username'] . ' bewerkte configuratie'); $message = 'Configuratie opgeslagen.'; $messageType = 'success'; } } $currentConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : []; $route = 'config'; echo $twig->render('pages/config.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'config' => $currentConfig, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleSecurity($auth, $config, $twig, $user, $csrf): void { $adminConfigFile = $config['admin_config']; $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $adminConfig = json_decode(file_get_contents($adminConfigFile), true) ?? []; $adminConfig['security']['force_ssl'] = isset($_POST['force_ssl']); $adminConfig['security']['session_timeout'] = (int)($_POST['session_timeout'] ?? 3600); $adminConfig['security']['max_login_attempts'] = (int)($_POST['max_login_attempts'] ?? 5); file_put_contents($adminConfigFile, json_encode($adminConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); adminLog($config, 'info', $user['username'] . ' bewerkte beveiligingsinstellingen'); $message = 'Beveiligingsinstellingen opgeslagen.'; $messageType = 'success'; } } $adminConfig = file_exists($adminConfigFile) ? json_decode(file_get_contents($adminConfigFile), true) : []; $route = 'security'; echo $twig->render('pages/security.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'config' => $adminConfig, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleStatistics($auth, $config, $twig, $user, $csrf, $siteConfig): void { $analytics = new Analytics($siteConfig['analytics'] ?? []); $stats = $analytics->getFullStats(); $route = 'statistics'; echo $twig->render('pages/statistics.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'stats' => $stats, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); } function handleTheme($auth, $config, $twig, $user, $csrf, $siteConfig): void { $themesDir = __DIR__ . '/../themes'; $themes = []; foreach (glob($themesDir . '/*', GLOB_ONLYDIR) as $themeDir) { $themeName = basename($themeDir); $themeJson = $themeDir . '/theme.json'; $themeData = [ 'name' => $themeName, 'title' => $themeName, 'active' => ($siteConfig['active_theme'] ?? 'default') === $themeName, ]; if (file_exists($themeJson)) { $data = json_decode(file_get_contents($themeJson), true); $themeData = array_merge($themeData, $data); } $themes[] = $themeData; } $message = ''; $messageType = ''; // Handle theme compilation if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['compile_scss'])) { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $themeToCompile = $_POST['theme'] ?? 'default'; // SCSS compile logic here $message = 'SCSS gecompileerd voor ' . $themeToCompile; $messageType = 'success'; adminLog($config, 'info', $user['username'] . ' compileerde SCSS voor ' . $themeToCompile); } } $route = 'theme'; echo $twig->render('pages/theme.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'themes' => $themes, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleThemeNew($auth, $config, $twig, $user, $csrf): void { $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $themeName = trim($_POST['name'] ?? ''); if (!empty($themeName)) { $themeName = preg_replace('/[^a-zA-Z0-9_-]/', '-', $themeName); $themesDir = __DIR__ . '/../themes'; $newThemeDir = $themesDir . '/' . $themeName; if (!file_exists($newThemeDir)) { @mkdir($newThemeDir, 0755, true); @mkdir($newThemeDir . '/partials', 0755, true); @mkdir($newThemeDir . '/css', 0755, true); @mkdir($newThemeDir . '/js', 0755, true); $themeJson = [ 'title' => ucfirst($themeName), 'default_layout' => 'full_content', 'header_color' => '#0a369d', ]; file_put_contents($newThemeDir . '/theme.json', json_encode($themeJson, JSON_PRETTY_PRINT)); adminLog($config, 'info', $user['username'] . ' creëerde thema ' . $themeName); header('Location: /admin/theme'); exit; } else { $message = 'Thema bestaat al.'; $messageType = 'danger'; } } } } $route = 'theme-new'; echo $twig->render('pages/theme-new.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handlePlugins($auth, $config, $twig, $user, $csrf): void { $pluginsDir = $config['plugins_dir']; $configFile = $config['config_json']; $siteConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : []; $enabledPlugins = $siteConfig['plugins']['enabled'] ?? []; $plugins = []; foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginDir) { $pluginName = basename($pluginDir); $pluginJson = $pluginDir . '/plugin.json'; $pluginData = [ 'name' => $pluginName, 'enabled' => in_array($pluginName, $enabledPlugins), 'protected' => isProtectedPlugin($pluginName), ]; if (file_exists($pluginJson)) { $data = json_decode(file_get_contents($pluginJson), true); $pluginData = array_merge($pluginData, $data); } $plugins[] = $pluginData; } $route = 'plugins'; echo $twig->render('pages/plugins.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'plugins' => $plugins, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); } function handlePluginsNew($auth, $config, $twig, $user, $csrf): void { $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $pluginName = trim($_POST['name'] ?? ''); if (!empty($pluginName)) { $pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '-', $pluginName); $pluginsDir = $config['plugins_dir']; $newPluginDir = $pluginsDir . $pluginName; if (!file_exists($newPluginDir)) { @mkdir($newPluginDir, 0755, true); $pluginJson = [ 'name' => ucfirst($pluginName), 'version' => '1.0.0', 'author' => $user['username'], ]; file_put_contents($newPluginDir . '/plugin.json', json_encode($pluginJson, JSON_PRETTY_PRINT)); file_put_contents($newPluginDir . '/' . $pluginName . '.php', "render('pages/plugins-new.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handlePluginsEdit($auth, $config, $twig, $user, $csrf): void { $pluginsDir = $config['plugins_dir']; $plugin = $_GET['plugin'] ?? ''; $plugin = preg_replace('/[^a-zA-Z0-9_-]/', '', $plugin); // Block editing protected plugins if (isProtectedPlugin($plugin)) { adminLog($config, 'warning', $user['username'] . ' probeerde essentiële plugin ' . $plugin . ' te bewerken'); header('Location: /admin/plugins'); exit; } $pluginDir = $pluginsDir . $plugin; $pluginFile = $pluginDir . '/' . $plugin . '.php'; $message = ''; $messageType = ''; if (!file_exists($pluginFile)) { header('Location: /admin/plugins'); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $content = $_POST['content'] ?? ''; file_put_contents($pluginFile, $content); adminLog($config, 'info', $user['username'] . ' bewerkte plugin ' . $plugin); $message = 'Plugin opgeslagen.'; $messageType = 'success'; } } $pluginContent = file_get_contents($pluginFile); $route = 'plugins-edit'; echo $twig->render('pages/plugins-edit.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'pluginName' => $plugin, 'pluginContent' => $pluginContent, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => true, 'message' => $message, 'message_type' => $messageType, ]); } function handlePluginsConfig($auth, $config, $twig, $user, $csrf): void { $pluginsDir = $config['plugins_dir']; $plugin = $_GET['plugin'] ?? ''; $plugin = preg_replace('/[^a-zA-Z0-9_-]/', '', $plugin); $pluginDir = $pluginsDir . $plugin; $configFile = $pluginDir . '/config.json'; $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $newConfig = $_POST['config'] ?? ''; file_put_contents($configFile, $newConfig); adminLog($config, 'info', $user['username'] . ' bewerkte plugin config ' . $plugin); $message = 'Plugin configuratie opgeslagen.'; $messageType = 'success'; } } $pluginConfig = file_exists($configFile) ? file_get_contents($configFile) : '{}'; $route = 'plugins-config'; echo $twig->render('pages/plugin-config.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'pluginName' => $plugin, 'pluginConfig' => $pluginConfig, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handlePluginsToggle($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/plugins'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/plugins'); exit; } $plugin = $_POST['plugin'] ?? ''; $plugin = preg_replace('/[^a-zA-Z0-9_-]/', '', $plugin); // Block toggling protected plugins if (isProtectedPlugin($plugin)) { adminLog($config, 'warning', $user['username'] . ' probeerde essentiële plugin ' . $plugin . ' te deactiveren'); header('Location: /admin/plugins'); exit; } $configFile = $config['config_json']; $siteConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : []; $enabledPlugins = $siteConfig['plugins']['enabled'] ?? []; if (in_array($plugin, $enabledPlugins)) { $enabledPlugins = array_diff($enabledPlugins, [$plugin]); adminLog($config, 'info', $user['username'] . ' deactiveerde plugin ' . $plugin); } else { $enabledPlugins[] = $plugin; adminLog($config, 'info', $user['username'] . ' activeerde plugin ' . $plugin); } $siteConfig['plugins']['enabled'] = array_values($enabledPlugins); file_put_contents($configFile, json_encode($siteConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); header('Location: /admin/plugins'); exit; } function handlePluginsDelete($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/plugins'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/plugins'); exit; } $plugin = $_POST['plugin'] ?? ''; $plugin = preg_replace('/[^a-zA-Z0-9_-]/', '', $plugin); // Block deleting protected plugins if (isProtectedPlugin($plugin)) { adminLog($config, 'warning', $user['username'] . ' probeerde essentiële plugin ' . $plugin . ' te verwijderen'); header('Location: /admin/plugins'); exit; } $pluginsDir = $config['plugins_dir']; $pluginDir = $pluginsDir . $plugin; if (is_dir($pluginDir)) { // Remove plugin directory recursively $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($pluginDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { if ($file->isDir()) { rmdir($file->getPathname()); } else { unlink($file->getPathname()); } } rmdir($pluginDir); adminLog($config, 'info', $user['username'] . ' verwijderde plugin ' . $plugin); } header('Location: /admin/plugins'); exit; } function handleUsers($auth, $config, $twig, $user, $csrf): void { $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 === 'add') { $newUsername = trim($_POST['new_username'] ?? ''); $newPassword = $_POST['new_password'] ?? ''; $newRole = $_POST['new_role'] ?? 'content-manager'; $result = $auth->addUser($newUsername, $newPassword, $newRole); $message = $result['message']; $messageType = $result['success'] ? 'success' : 'danger'; } elseif ($action === 'delete') { $deleteUser = $_POST['delete_username'] ?? ''; if ($deleteUser !== $user['username']) { $result = $auth->deleteUser($deleteUser); $message = $result['message']; $messageType = $result['success'] ? 'success' : 'danger'; } else { $message = 'Je kunt jezelf niet verwijderen.'; $messageType = 'danger'; } } elseif ($action === 'change_role') { $roleUser = $_POST['role_username'] ?? ''; $newRole = $_POST['new_role'] ?? ''; $result = $auth->changeRole($roleUser, $newRole); $message = $result['message']; $messageType = $result['success'] ? 'success' : 'danger'; } } } $route = 'users'; $users = $auth->getUsers(); $roles = AdminAuth::getRoles(); echo $twig->render('pages/users.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'users' => $users, 'roles' => $roles, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleGuide($auth, $config, $twig, $user, $csrf): void { $lang = $_GET['lang'] ?? 'nl'; $page = $_GET['page'] ?? ''; $rootDir = realpath(__DIR__ . '/..'); // Get guide file if ($page) { $guideFile = $rootDir . '/guide/' . $lang . '/' . $page . '.md'; } else { $guideFile = $rootDir . '/guide/' . $lang . '/index.md'; } // Fallback to English if (!file_exists($guideFile) && $lang !== 'en') { $guideFile = $rootDir . '/guide/en/' . ($page ? $page . '.md' : 'index.md'); } // Load content if (!file_exists($guideFile)) { $content = '

Handleiding niet gevonden.

'; } else { $content = file_get_contents($guideFile); // Parse Markdown with Table support if (class_exists('League\CommonMark\Environment\Environment')) { $environment = new \League\CommonMark\Environment\Environment([ 'html_input' => 'strip', 'heading_permalink' => [ 'symbol' => '', 'aria_hidden' => true, 'html_class' => 'heading-permalink', 'id_prefix' => '', 'fragment_prefix' => '', 'apply_id_to_heading' => true, 'insert' => 'after', 'min_heading_level' => 1, 'max_heading_level' => 6, 'title' => 'Permalink', ], ]); $environment->addExtension(new \League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension()); $environment->addExtension(new \League\CommonMark\Extension\Table\TableExtension()); $environment->addExtension(new \League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension()); $converter = new \League\CommonMark\MarkdownConverter($environment); $content = $converter->convert($content)->getContent(); } else { $content = nl2br(htmlspecialchars($content)); } } // Build guide navigation sidebar using Navigation plugin $guideNav = ''; $navPluginFile = $rootDir . '/plugins/Navigation/Navigation.php'; if (file_exists($navPluginFile)) { require_once $navPluginFile; if (class_exists('Navigation')) { $navPlugin = new Navigation(); $guideNav = $navPlugin->getSidebarContent(); } } // Build breadcrumbs $breadcrumbs = []; if ($page) { $parts = explode('/', $page); $path = ''; foreach ($parts as $part) { $path .= ($path ? '/' : '') . $part; $breadcrumbs[] = [ 'title' => str_replace('-', ' ', ucfirst($part)), 'url' => $path, ]; } } echo $twig->render('pages/guide.twig', [ 'user' => $user, 'route' => 'guide', 'csrf_token' => $csrf, 'lang' => $lang, 'page' => $page, 'guide_breadcrumbs' => $breadcrumbs, 'guide_lang' => $lang, 'guide_page' => $page, 'content' => $content, 'guide_nav' => $guideNav, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); } function handleLogs($auth, $config, $twig, $user, $csrf): void { $tab = $_GET['tab'] ?? 'admin'; $logFile = $tab === 'requests' ? $config['request_log'] : $config['log_file']; $logs = []; if (file_exists($logFile)) { $lines = file($logFile); $lines = array_slice($lines, -100); // Last 100 lines foreach ($lines as $line) { if (preg_match('/^\[([^\]]+)\] \[([^\]]+)\] \[([^\]]+)\] (.+)$/', trim($line), $m)) { $logs[] = [ 'time' => $m[1], 'level' => strtolower($m[2]), 'ip' => $m[3], 'message' => $m[4], ]; } } $logs = array_reverse($logs); } $route = 'logs'; echo $twig->render('pages/logs.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'tab' => $tab, 'logs' => $logs, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); } function handleUpdate($auth, $config, $twig, $user, $csrf): void { $versionFile = __DIR__ . '/../version.php'; $versionInfo = file_exists($versionFile) ? include $versionFile : []; $versionInfo['version'] = $versionInfo['version'] ?? '0.0.0'; $isGitWritable = is_writable(__DIR__ . '/../.git'); $route = 'update'; echo $twig->render('pages/update.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'version' => $versionInfo['version'], 'isGitWritable' => $isGitWritable, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); } function handleMedia($auth, $config, $twig, $user, $csrf): void { $contentDir = $config['content_dir']; $subdir = $_GET['dir'] ?? ''; $subdir = str_replace(['../', '..\\'], '', $subdir); $fullPath = rtrim($contentDir, '/') . '/' . $subdir; $items = scanContentDir($fullPath, $subdir); $route = 'media'; echo $twig->render('pages/media.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'subdir' => $subdir, 'items' => $items, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); } // ============================================================================ // HELPER FUNCTIONS (from original admin.php) // ============================================================================ function adminLog(array $config, string $level, string $message): void { $logFile = $config['log_file']; $dir = dirname($logFile); if (!is_dir($dir)) { @mkdir($dir, 0755, true); } $timestamp = date('Y-m-d H:i:s'); $ip = RequestLogger::getClientIp(); @file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND); if (class_exists('LogManager')) { LogManager::log(LogManager::EVENT_ADMIN, $level, $message, ['ip' => $ip]); } } function countFiles(string $dir, array $extensions = []): int { $count = 0; if (!is_dir($dir)) return 0; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); foreach ($iterator as $file) { if ($file->isFile()) { if (empty($extensions) || in_array($file->getExtension(), $extensions)) { $count++; } } } return $count; } function countDirs(string $dir): int { $count = 0; if (!is_dir($dir)) return 0; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); foreach ($iterator as $file) { if ($file->isDir() && !str_starts_with($file->getFilename(), '.')) { $count++; } } return $count; } function countEnabledPlugins(string $pluginsDir, string $configJson): int { $config = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : []; $enabled = $config['plugins']['enabled'] ?? []; return count($enabled); } function dirSize(string $dir): int { $size = 0; if (!is_dir($dir)) return 0; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); foreach ($iterator as $file) { if ($file->isFile()) { $size += $file->getSize(); } } return $size; } function formatSize(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $i = 0; while ($bytes >= 1024 && $i < count($units) - 1) { $bytes /= 1024; $i++; } return round($bytes, 2) . ' ' . $units[$i]; } function scanContentDir(string $fullPath, string $subdir): array { $items = []; if (!is_dir($fullPath)) return $items; $files = scandir($fullPath); foreach ($files as $file) { if ($file === '.' || $file === '..' || str_starts_with($file, '.')) { continue; } $path = $fullPath . '/' . $file; $isDir = is_dir($path); $items[] = [ 'name' => $file, 'path' => $subdir ? $subdir . '/' . $file : $file, 'is_dir' => $isDir, 'extension' => $isDir ? '' : strtolower(pathinfo($file, PATHINFO_EXTENSION)), 'size' => $isDir ? '-' : formatSize(filesize($path)), 'modified' => date('Y-m-d H:i', filemtime($path)), ]; } // Sort: directories first, then files, alphabetically usort($items, function($a, $b) { if ($a['is_dir'] && !$b['is_dir']) return -1; if (!$a['is_dir'] && $b['is_dir']) return 1; return strcasecmp($a['name'], $b['name']); }); return $items; } function updateContentFrontmatter(string $content, string $key, string $value): string { if (preg_match('/^---\s*\n(.+?)\n---\s*\n(.*)$/s', $content, $m)) { $frontmatter = $m[1]; $body = $m[2]; $lines = explode("\n", $frontmatter); $found = false; foreach ($lines as $i => $line) { if (str_starts_with($line, $key . ':')) { $lines[$i] = $key . ': ' . $value; $found = true; break; } } if (!$found) { $lines[] = $key . ': ' . $value; } return "---\n" . implode("\n", $lines) . "\n---\n" . $body; } return "---\n" . $key . ': ' . $value . "\n---\n\n" . $content; } function extractFrontmatterValue(string $content, string $key): string { if (preg_match('/^---\s*\n(.+?)\n---/s', $content, $m)) { $frontmatter = $m[1]; if (preg_match('/^' . preg_quote($key, '/') . ': (.+?)$/m', $frontmatter, $mm)) { return trim($mm[1]); } } return ''; } function extractLanguagePrefix(string $file): string { $parts = explode('/', $file); $filename = end($parts); if (preg_match('/^(nl|en|de|fr|es)\./', $filename, $m)) { return $m[1]; } return 'nl'; } function backupContentFile(string $filePath): void { $backupDir = dirname($filePath) . '/.bak'; if (!is_dir($backupDir)) { @mkdir($backupDir, 0755, true); } $backupFile = $backupDir . '/' . basename($filePath) . '.' . date('YmdHis'); @copy($filePath, $backupFile); }