__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); })); // Load admin interface translations // admin_language is resolved from: config admin_language -> language.default -> 'nl' function loadAdminTranslations(?array $siteConfig = null): array { $adminLang = $siteConfig['admin_language'] ?? ($siteConfig['language']['default'] ?? 'nl'); $langDir = __DIR__ . '/../language/'; $file = $langDir . $adminLang . '/admin.php'; if (file_exists($file)) { $t = include $file; if (is_array($t)) { return $t; } } // Fallback to Dutch admin translations $fallback = $langDir . 'nl/admin.php'; if (file_exists($fallback)) { $t = include $fallback; if (is_array($t)) { return $t; } } return []; } $siteConfigForI18n = file_exists($appConfig['config_json']) ? (json_decode(file_get_contents($appConfig['config_json']), true) ?? []) : []; $adminTranslations = loadAdminTranslations($siteConfigForI18n); $adminLangCode = $siteConfigForI18n['admin_language'] ?? ($siteConfigForI18n['language']['default'] ?? 'nl'); $twig->addGlobal('ta', $adminTranslations); $twig->addGlobal('admin_lang', $adminLangCode); $twig->addFunction(new \Twig\TwigFunction('ta', function($key) use ($adminTranslations) { return $adminTranslations[$key] ?? $key; })); // Initialize admin PluginManager for plugin-provided admin pages and menu items $enabledPluginsList = $siteConfigForI18n['enabled_plugins'] ?? []; $adminPluginManager = new PluginManager(__DIR__ . '/../plugins', $enabledPluginsList); $adminPluginAPI = new AdminPluginAPI($siteConfigForI18n); $adminPluginManager->setAPI($adminPluginAPI); $adminPluginMenuItems = $adminPluginManager->getAdminMenuItems(); $twig->addGlobal('plugin_admin_menu', $adminPluginMenuItems); // 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 // Check if a plugin handles this route (e.g. 'statistics', 'logs') $pluginRouteMatch = $adminPluginManager->resolveAdminRoute($route); if ($pluginRouteMatch !== null) { // Permission check: plugins require 'plugins' permission for now if (!$auth->hasPermission('plugins')) { http_response_code(403); echo $twig->render('pages/error.twig', [ 'user' => $user, 'route' => '', 'csrf_token' => $csrf, 'error_code' => 403, 'error_title' => $adminTranslations['no_permission_title'] ?? 'Geen toegang', 'error_message' => $adminTranslations['no_permission'] ?? 'Geen toegang.', 'sidebar_color' => getSidebarColor($appConfig), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', ]); exit; } $pluginHtml = $adminPluginManager->dispatchAdminRoute($pluginRouteMatch['plugin'], $pluginRouteMatch['action']); if ($pluginHtml !== null) { // Wrap plugin output in admin layout echo $twig->render('pages/plugin-page.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'sidebar_color' => getSidebarColor($appConfig), 'needs_editor' => false, 'message' => '', 'message_type' => 'info', 'plugin_content' => $pluginHtml, ]); exit; } // If plugin returned null, fall through to 404 } switch ($route) { case 'logout': $auth->logout(); header('Location: /admin/login'); exit; 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-backup': handleContentBackup($auth, $appConfig, $twig, $user, $csrf); break; case 'content-restore': handleContentRestore($auth, $appConfig); break; case 'content-git-init': handleContentGitInit($auth, $appConfig); break; case 'content-git-commit': handleContentGitCommit($auth, $appConfig); break; case 'content-git-restore': handleContentGitRestore($auth, $appConfig); 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 '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 '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), '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') . ')', ]; // Build plugin overview (name => enabled status) $pluginOverview = []; $enabledPlugins = $siteConfig['enabled_plugins'] ?? []; if (is_dir($pluginsDir)) { foreach (glob($pluginsDir . '/*', GLOB_ONLYDIR) as $pluginDir) { $pluginName = basename($pluginDir); $pluginOverview[$pluginName] = [ 'enabled' => in_array($pluginName, $enabledPlugins, true), ]; } } ksort($pluginOverview); echo $twig->render('pages/dashboard.twig', [ 'user' => $user, 'route' => 'dashboard', 'csrf_token' => $csrf, 'stats' => $stats, 'site_config' => $siteConfig, 'plugin_overview' => $pluginOverview, '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 handleContentBackup($auth, $config, $twig, $user, $csrf): void { $contentDir = $config['content_dir']; $projectRoot = $config['codepress_root']; $backup = new ContentBackup($contentDir, $projectRoot); $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } elseif (isset($_POST['action'])) { $action = $_POST['action']; if ($action === 'download_zip') { $backupDir = $projectRoot . '/var/tmp'; if (!is_dir($backupDir)) { @mkdir($backupDir, 0755, true); } $backupFile = $backupDir . '/content-backup-' . date('YmdHis') . '.zip'; if ($backup->createZipBackup($backupFile)) { adminLog($config, 'info', $user['username'] . ' maakte een content ZIP backup aan'); header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="' . basename($backupFile) . '"'); header('Content-Length: ' . filesize($backupFile)); readfile($backupFile); unlink($backupFile); exit; } else { $message = 'Kon geen ZIP backup maken.'; $messageType = 'danger'; } } } } // Get git info $gitAvailable = $backup->isGitAvailable(); $hasGitRepo = $backup->hasGitRepo(); $gitCommits = []; if ($hasGitRepo) { $logResult = $backup->gitLog(20); $gitCommits = $logResult['commits'] ?? []; } $route = 'content-backup'; echo $twig->render('pages/content-backup.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'git_available' => $gitAvailable, 'has_git_repo' => $hasGitRepo, 'git_commits' => $gitCommits, 'sidebar_color' => getSidebarColor($config), 'needs_editor' => false, 'message' => $message, 'message_type' => $messageType, ]); } function handleContentRestore($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/content-backup'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/content-backup?error=csrf'); exit; } if (empty($_FILES['zipfile']['tmp_name'])) { header('Location: /admin/content-backup?error=nofile'); exit; } $contentDir = $config['content_dir']; $projectRoot = $config['codepress_root']; $backup = new ContentBackup($contentDir, $projectRoot); $result = $backup->restoreFromZip($_FILES['zipfile']['tmp_name']); if ($result['success']) { adminLog($config, 'info', $user['username'] . ' herstelde content uit ZIP backup'); header('Location: /admin/content-backup?restored=1'); } else { adminLog($config, 'warning', $user['username'] . ' - content restore mislukt: ' . $result['message']); header('Location: /admin/content-backup?error=' . urlencode($result['message'])); } exit; } function handleContentGitInit($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/content-backup'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/content-backup?error=csrf'); exit; } $backup = new ContentBackup($config['content_dir'], $config['codepress_root']); $result = $backup->gitInit(); if ($result['success']) { adminLog($config, 'info', $user['username'] . ' initialiseerde git in content/'); } header('Location: /admin/content-backup?git=' . urlencode($result['message'])); exit; } function handleContentGitCommit($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/content-backup'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/content-backup?error=csrf'); exit; } $message = trim($_POST['commit_message'] ?? ''); $backup = new ContentBackup($config['content_dir'], $config['codepress_root']); $result = $backup->gitCommit($message); if ($result['success']) { adminLog($config, 'info', $user['username'] . ' committe content: ' . $message); } header('Location: /admin/content-backup?git=' . urlencode($result['message'])); exit; } function handleContentGitRestore($auth, $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: /admin/content-backup'); exit; } $user = $auth->getCurrentUser(); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: /admin/content-backup?error=csrf'); exit; } $commitHash = $_POST['commit'] ?? ''; $backup = new ContentBackup($config['content_dir'], $config['codepress_root']); $result = $backup->gitRestore($commitHash); if ($result['success']) { adminLog($config, 'info', $user['username'] . ' herstelde content naar git commit ' . $commitHash); } header('Location: /admin/content-backup?git=' . urlencode($result['message'])); exit; } { $configFile = $config['config_json']; $message = ''; $messageType = ''; $ta = loadAdminTranslations(file_exists($configFile) ? (json_decode(file_get_contents($configFile), true) ?? []) : []); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = $ta['invalid_csrf'] ?? 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $newConfig = json_decode(file_get_contents($configFile), true) ?? []; $newConfig['site_title'] = $_POST['site_title'] ?? ''; $defaultPage = $_POST['default_page'] ?? 'auto'; if ($defaultPage === 'specific') { $defaultPage = $_POST['default_page_specific'] ?? 'auto'; } $newConfig['default_page'] = $defaultPage; $newConfig['language']['default'] = $_POST['content_language'] ?? 'nl'; $newConfig['admin_language'] = $_POST['admin_language'] ?? 'nl'; backupContentFile($configFile); file_put_contents($configFile, json_encode($newConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); adminLog($config, 'info', $user['username'] . ' bewerkte configuratie'); // Redirect after successful save (PRG pattern) so the new admin // language is applied immediately without a manual reload. header('Location: /admin/config?saved=1'); exit; } } // Show success message after redirect if (isset($_GET['saved'])) { $message = $ta['saved'] ?? 'Configuratie opgeslagen.'; $messageType = 'success'; } $currentConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : []; $route = 'config'; $contentPages = collectContentPages($config['content_dir']); $currentDefaultPage = $currentConfig['default_page'] ?? 'auto'; echo $twig->render('pages/config.twig', [ 'user' => $user, 'route' => $route, 'csrf_token' => $csrf, 'config' => $currentConfig, 'content_pages' => $contentPages, 'current_default_page' => $currentDefaultPage, '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 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['enabled_plugins'] ?? []; $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), 'type' => 'content', ]; if (file_exists($pluginJson)) { $data = json_decode(file_get_contents($pluginJson), true); $pluginData = array_merge($pluginData, $data); } $pluginFile = $pluginDir . '/' . $pluginName . '.php'; if (file_exists($pluginFile) && !isset($pluginData['type'])) { $source = file_get_contents($pluginFile); if (preg_match("/'type'\s*=>\s*'([^']+)'/", $source, $m)) { $pluginData['type'] = $m[1]; } } $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['enabled_plugins'] ?? []; 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['enabled_plugins'] = 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'; $newEmail = trim($_POST['new_email'] ?? ''); $newAuthorName = trim($_POST['new_author_name'] ?? ''); $newAuthorEmail = trim($_POST['new_author_email'] ?? ''); $result = $auth->addUser($newUsername, $newPassword, $newRole, $newEmail, $newAuthorName, $newAuthorEmail); $message = $result['message']; $messageType = $result['success'] ? 'success' : 'danger'; } elseif ($action === 'profile') { $profileUser = $_POST['profile_username'] ?? ''; $profileEmail = trim($_POST['profile_email'] ?? ''); $profileAuthorName = trim($_POST['profile_author_name'] ?? ''); $profileAuthorEmail = trim($_POST['profile_author_email'] ?? ''); $result = $auth->updateUserProfile($profileUser, $profileEmail, $profileAuthorName, $profileAuthorEmail); $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 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['enabled_plugins'] ?? []; 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; } /** * Recursively collect all content pages (.md/.php/.html) as pageKey => label pairs. * Language-prefixed files (nl./en.) are stripped of their prefix for the key. * Directories are included if they contain content (represented by their path). * * @param string $contentDir Absolute path to the content directory * @return array