isAuthenticated()) { header('Location: admin.php?route=login'); exit; } // Authenticated routes switch ($route) { case 'logout': $auth->logout(); header('Location: admin.php?route=login'); exit; case 'dashboard': case '': handleDashboard($auth, $appConfig); break; case 'content': handleContent($auth, $appConfig); break; case 'content-edit': handleContentEdit($auth, $appConfig); break; case 'content-new': handleContentNew($auth, $appConfig); break; case 'content-delete': handleContentDelete($auth, $appConfig); break; case 'content-dir-create': handleContentDirCreate($auth, $appConfig); break; case 'content-dir-rename': handleContentDirRename($auth, $appConfig); break; case 'content-move': handleContentMove($auth, $appConfig); break; case 'content-dir-delete': handleContentDirDelete($auth, $appConfig); break; case 'config': handleConfig($auth, $appConfig); break; case 'theme': handleTheme($auth, $appConfig); break; case 'plugins': handlePlugins($auth, $appConfig); break; case 'plugins-config': handlePluginConfig($auth, $appConfig); break; case 'plugins-edit': handlePluginEdit($auth, $appConfig); break; case 'plugins-new': handlePluginNew($auth, $appConfig); break; case 'plugins-toggle': handlePluginToggle($auth, $appConfig); break; case 'plugins-delete': handlePluginDelete($auth, $appConfig); break; case 'media': handleMedia($auth, $appConfig); break; case 'media-list': handleMediaList($auth, $appConfig); break; case 'users': handleUsers($auth, $appConfig); break; default: header('Location: admin.php?route=dashboard'); exit; } // --- Route Handlers --- function handleLogin(AdminAuth $auth): void { $error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; $result = $auth->login($username, $password); if ($result['success']) { header('Location: admin.php?route=dashboard'); exit; } $error = $result['message']; } require __DIR__ . '/../admin/templates/login.php'; } function handleDashboard(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); // Gather stats $contentDir = $config['content_dir']; $pluginsDir = $config['plugins_dir']; $configJson = $config['config_json']; $stats = [ 'pages' => countFiles($contentDir, ['md', 'php', 'html']), 'directories' => countDirs($contentDir), 'plugins' => countDirs($pluginsDir), 'config_exists' => file_exists($configJson), 'content_size' => formatSize(dirSize($contentDir)), 'php_version' => PHP_VERSION, ]; // Load site config $siteConfig = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : []; require __DIR__ . '/../admin/templates/layout.php'; } function handleContent(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $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); $route = 'content'; require __DIR__ . '/../admin/templates/layout.php'; } function handleContentEdit(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $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.php?route=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'] ?? ''); 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); $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 && in_array($layout, ['sidebar-content', 'content', 'sidebar', 'content-sidebar', 'content-sidebar-reverse'])) { $content = updateContentFrontmatter($content, 'layout', $layout); } $plugins = isset($_POST['plugins']) && is_array($_POST['plugins']) ? implode(', ', array_map('trim', $_POST['plugins'])) : ''; $content = updateContentFrontmatter($content, 'plugins', $plugins); file_put_contents($filePath, $content); } $message = 'Bestand opgeslagen.'; $messageType = 'success'; } } if ($isEditable) { $fileContent = file_get_contents($filePath); $currentLayout = parseFrontmatterField($fileContent, 'layout', 'sidebar-content'); } else { $fileContent = ''; $currentLayout = 'sidebar-content'; } $fileName = basename($filePath); $route = 'content-edit'; // Get available plugins for per-page visibility $pluginsDir = $config['plugins_dir']; $availablePlugins = []; if (is_dir($pluginsDir)) { $items = scandir($pluginsDir); sort($items); foreach ($items as $item) { if ($item[0] === '.') continue; $pluginPath = $pluginsDir . '/' . $item; if (!is_dir($pluginPath) || !file_exists($pluginPath . '/' . $item . '.php')) continue; $hasConfig = file_exists($pluginPath . '/config.json'); $pluginConfig = $hasConfig ? json_decode(file_get_contents($pluginPath . '/config.json'), true) : []; if (!($pluginConfig['viewable'] ?? true)) continue; $availablePlugins[] = $item; } } $currentPlugins = parseFrontmatterField($fileContent ?? '', 'plugins', ''); $selectedPlugins = !empty($currentPlugins) ? array_map('trim', explode(',', $currentPlugins)) : []; require __DIR__ . '/../admin/templates/layout.php'; } function handleContentNew(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $contentDir = $config['content_dir']; $dir = $_GET['dir'] ?? ''; $dir = str_replace(['../', '..\\'], '', $dir); $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $filename = trim($_POST['filename'] ?? ''); $content = $_POST['content'] ?? ''; $type = $_POST['type'] ?? 'md'; if (empty($filename)) { $message = 'Bestandsnaam is verplicht.'; $messageType = 'danger'; } else { // Sanitize filename $filename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $filename); if (!preg_match('/\.(md|php|html)$/', $filename)) { $filename .= '.' . $type; } $targetDir = rtrim($contentDir, '/') . '/' . $dir; $filePath = $targetDir . '/' . $filename; if (file_exists($filePath)) { $message = 'Bestand bestaat al.'; $messageType = 'danger'; } else { if (!is_dir($targetDir)) { mkdir($targetDir, 0755, true); } // Build frontmatter $frontmatterLines = []; $layout = $_POST['layout'] ?? ''; if ($layout && $layout !== 'sidebar-content' && in_array($layout, ['sidebar-content', 'content', 'sidebar', 'content-sidebar', 'content-sidebar-reverse'])) { $frontmatterLines[] = 'layout: ' . $layout; } $plugins = isset($_POST['plugins']) && is_array($_POST['plugins']) ? implode(', ', array_map('trim', $_POST['plugins'])) : ''; if (!empty($plugins)) { $frontmatterLines[] = 'plugins: ' . $plugins; } if (!empty($frontmatterLines)) { $content = "---\n" . implode("\n", $frontmatterLines) . "\n---\n" . $content; } file_put_contents($filePath, $content); header('Location: admin.php?route=content&dir=' . urlencode($dir)); exit; } } } } // Get available plugins for per-page visibility $pluginsDir = $config['plugins_dir']; $availablePlugins = []; if (is_dir($pluginsDir)) { $items = scandir($pluginsDir); sort($items); foreach ($items as $item) { if ($item[0] === '.') continue; $pluginPath = $pluginsDir . '/' . $item; if (!is_dir($pluginPath) || !file_exists($pluginPath . '/' . $item . '.php')) continue; $hasConfig = file_exists($pluginPath . '/config.json'); $pluginConfig = $hasConfig ? json_decode(file_get_contents($pluginPath . '/config.json'), true) : []; if (!($pluginConfig['viewable'] ?? true)) continue; $availablePlugins[] = $item; } } $route = 'content-new'; require __DIR__ . '/../admin/templates/layout.php'; } function handleContentDelete(AdminAuth $auth, array $config): void { $contentDir = $config['content_dir']; $file = $_GET['file'] ?? ''; $file = str_replace(['../', '..\\'], '', $file); $filePath = rtrim($contentDir, '/') . '/' . $file; $realPath = realpath($filePath); $realContentDir = realpath($contentDir); if ($_SERVER['REQUEST_METHOD'] === 'POST' && $auth->verifyCsrf($_POST['csrf_token'] ?? '') && $realPath && $realContentDir && strpos($realPath, $realContentDir) === 0 ) { if (is_file($filePath)) { unlink($filePath); } } $dir = dirname($file); header('Location: admin.php?route=content&dir=' . urlencode($dir === '.' ? '' : $dir)); exit; } function handleContentDirCreate(AdminAuth $auth, array $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: admin.php?route=content'); exit; } $contentDir = $config['content_dir']; $subdir = $_GET['dir'] ?? ''; $subdir = str_replace(['../', '..\\'], '', $subdir); if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { header('Location: admin.php?route=content&dir=' . urlencode($subdir)); exit; } $dirname = trim($_POST['dirname'] ?? ''); if (!empty($dirname)) { $dirname = preg_replace('/[^a-zA-Z0-9._-]/', '-', $dirname); $targetDir = rtrim($contentDir, '/') . '/' . ($subdir ? $subdir . '/' . $dirname : $dirname); if (!file_exists($targetDir)) { mkdir($targetDir, 0755, true); } } header('Location: admin.php?route=content&dir=' . urlencode($subdir)); exit; } function handleContentDirRename(AdminAuth $auth, array $config): void { $contentDir = $config['content_dir']; $dir = $_GET['dir'] ?? ''; $dir = str_replace(['../', '..\\'], '', $dir); $fullPath = rtrim($contentDir, '/') . '/' . $dir; $realPath = realpath($fullPath); $realContentDir = realpath($contentDir); if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0 || !is_dir($fullPath)) { header('Location: admin.php?route=content'); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $parentDirPath = dirname($fullPath); if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $newName = trim($_POST['new_name'] ?? ''); if (!empty($newName)) { $newName = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newName); $newPath = $parentDirPath . '/' . $newName; if (!file_exists($newPath)) { rename($fullPath, $newPath); } } } $parentRelative = dirname($dir); header('Location: admin.php?route=content&dir=' . urlencode($parentRelative === '.' ? '' : $parentRelative)); exit; } $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $route = 'content-dir-rename'; require __DIR__ . '/../admin/templates/layout.php'; } function handleContentMove(AdminAuth $auth, array $config): void { $contentDir = $config['content_dir']; $item = $_GET['item'] ?? ''; $item = str_replace(['../', '..\\'], '', $item); $item = ltrim($item, './'); $fullPath = rtrim($contentDir, '/') . '/' . $item; $realPath = realpath($fullPath); $realContentDir = realpath($contentDir); if (!$realPath || !$realContentDir || strpos($realPath, $realContentDir) !== 0) { header('Location: admin.php?route=content'); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $targetDir = trim($_POST['target_dir'] ?? ''); $targetDir = str_replace(['../', '..\\'], '', $targetDir); $targetFullPath = rtrim($contentDir, '/') . '/' . $targetDir; $realTarget = realpath($targetFullPath); if ($realTarget && strpos($realTarget, $realContentDir) === 0 && is_dir($realTarget)) { $destPath = $realTarget . '/' . basename($fullPath); if (!file_exists($destPath)) { rename($fullPath, $destPath); } } } $parentRelative = is_file($fullPath) ? dirname($item) : dirname($item); header('Location: admin.php?route=content&dir=' . urlencode($parentRelative === '.' ? '' : $parentRelative)); exit; } $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); // Get all available content subdirectories for the move dropdown $dirs = getAllContentDirs($contentDir, $realContentDir); $route = 'content-move'; require __DIR__ . '/../admin/templates/layout.php'; } function handleContentDirDelete(AdminAuth $auth, array $config): void { $contentDir = $config['content_dir']; $dir = $_GET['dir'] ?? ''; $dir = str_replace(['../', '..\\'], '', $dir); $fullPath = rtrim($contentDir, '/') . '/' . $dir; $realPath = realpath($fullPath); $realContentDir = realpath($contentDir); if ($_SERVER['REQUEST_METHOD'] === 'POST' && $auth->verifyCsrf($_POST['csrf_token'] ?? '') && $realPath && $realContentDir && strpos($realPath, $realContentDir) === 0 && is_dir($fullPath) ) { // Only delete empty directories $items = array_diff(scandir($fullPath), ['.', '..']); if (empty($items)) { rmdir($fullPath); } } $parentDir = dirname($dir); header('Location: admin.php?route=content&dir=' . urlencode($parentDir === '.' ? '' : $parentDir)); exit; } function handleConfig(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $configJson = $config['config_json']; $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $jsonContent = $_POST['config_content'] ?? ''; $parsed = json_decode($jsonContent, true); if ($parsed === null && json_last_error() !== JSON_ERROR_NONE) { $message = 'Ongeldige JSON: ' . json_last_error_msg(); $messageType = 'danger'; } else { file_put_contents($configJson, json_encode($parsed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $message = 'Configuratie opgeslagen.'; $messageType = 'success'; $jsonContent = null; // reload from file } } } $siteConfig = file_exists($configJson) ? file_get_contents($configJson) : '{}'; if (isset($jsonContent)) { $siteConfig = $jsonContent; } $route = 'config'; require __DIR__ . '/../admin/templates/layout.php'; } function handleTheme(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $configJson = $config['config_json']; $themesDir = $config['codepress_root'] . '/themes'; $publicThemes = $config['codepress_root'] . '/public/themes'; $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $action = $_POST['action'] ?? ''; if ($action === 'save') { $themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['theme'] ?? ''); $themeFile = $themesDir . '/' . $themeName . '/theme.json'; if (file_exists($themeFile)) { $themeData = json_decode(file_get_contents($themeFile), true) ?? []; foreach (['header_color', 'header_font_color', 'navigation_color', 'navigation_font_color', 'sidebar_background', 'sidebar_border'] as $field) { $value = trim($_POST[$field] ?? ''); if (preg_match('/^#[0-9a-fA-F]{6}$/', $value)) { $themeData[$field] = $value; } } $themeData['name'] = trim($_POST['theme_name'] ?? $themeData['name'] ?? $themeName); $themeData['header_height'] = preg_match('/^\d+$/', $_POST['header_height'] ?? '') ? trim($_POST['header_height']) : ($themeData['header_height'] ?? '56'); $themeData['nav_height'] = preg_match('/^\d+$/', $_POST['nav_height'] ?? '') ? trim($_POST['nav_height']) : ($themeData['nav_height'] ?? '42'); $themeData['background_image_opacity'] = preg_match('/^\d+$/', $_POST['background_image_opacity'] ?? '') ? max(0, min(100, intval($_POST['background_image_opacity']))) : ($themeData['background_image_opacity'] ?? '100'); // Handle background image upload if (!empty($_FILES['bg_image']['name']) && $_FILES['bg_image']['error'] === UPLOAD_ERR_OK) { $ext = strtolower(pathinfo($_FILES['bg_image']['name'], PATHINFO_EXTENSION)); if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'])) { $imgName = $themeName . '_bg.' . $ext; if (!is_dir($publicThemes)) mkdir($publicThemes, 0755, true); move_uploaded_file($_FILES['bg_image']['tmp_name'], $publicThemes . '/' . $imgName); $themeData['background_image'] = $imgName; } } // Handle background image URL $bgUrl = trim($_POST['background_image_url'] ?? ''); if (!empty($bgUrl)) { $themeData['background_image'] = $bgUrl; } // Handle background image removal if (!empty($_POST['bg_image_remove'])) { if ($themeData['background_image'] ?? '') { $oldFile = $publicThemes . '/' . $themeData['background_image']; if (file_exists($oldFile)) unlink($oldFile); } $themeData['background_image'] = ''; } file_put_contents($themeFile, json_encode($themeData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $message = 'Thema opgeslagen.'; $messageType = 'success'; } } elseif ($action === 'activate') { $themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['theme'] ?? ''); if (file_exists($themesDir . '/' . $themeName . '/theme.json')) { $cfg = json_decode(file_get_contents($configJson), true) ?? []; $cfg['active_theme'] = $themeName; file_put_contents($configJson, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $message = 'Thema geactiveerd.'; $messageType = 'success'; } } elseif ($action === 'create') { $newName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['new_name'] ?? ''); if (empty($newName)) { $message = 'Geef een naam voor het nieuwe thema.'; $messageType = 'danger'; } elseif (file_exists($themesDir . '/' . $newName . '/theme.json')) { $message = 'Thema bestaat al.'; $messageType = 'danger'; } else { $defaults = [ 'name' => $newName, 'header_color' => '#0a369d', 'header_font_color' => '#ffffff', 'header_height' => '56', 'navigation_color' => '#2754b4', 'navigation_font_color' => '#ffffff', 'nav_height' => '42', 'sidebar_background' => '#f8f9fa', 'sidebar_border' => '#dee2e6', 'background_image' => '', 'background_image_opacity' => '100', ]; $newThemeDir = $themesDir . '/' . $newName; mkdir($newThemeDir, 0755, true); file_put_contents($newThemeDir . '/theme.json', json_encode($defaults, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $message = 'Thema aangemaakt.'; $messageType = 'success'; } } elseif ($action === 'delete') { $themeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_POST['theme'] ?? ''); $themeDir = $themesDir . '/' . $themeName; if (is_dir($themeDir) && $themeName !== 'default') { $themeFile = $themeDir . '/theme.json'; if (file_exists($themeFile)) unlink($themeFile); // Remove theme directory if empty $remaining = array_diff(scandir($themeDir), ['.', '..']); if (empty($remaining)) rmdir($themeDir); $cfg = json_decode(file_get_contents($configJson), true) ?? []; if (($cfg['active_theme'] ?? '') === $themeName) { $cfg['active_theme'] = 'default'; file_put_contents($configJson, json_encode($cfg, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } $message = 'Thema verwijderd.'; $messageType = 'success'; } } } } // Load active theme name $configData = json_decode(file_get_contents($configJson), true) ?? []; $activeTheme = $configData['active_theme'] ?? 'default'; // Load all themes $themes = []; if (is_dir($themesDir)) { foreach (scandir($themesDir) as $item) { if ($item[0] === '.') continue; $themeJson = $themesDir . '/' . $item . '/theme.json'; if (is_dir($themesDir . '/' . $item) && file_exists($themeJson)) { $themeData = json_decode(file_get_contents($themeJson), true) ?? []; $themes[$item] = $themeData; } } } // Load theme being edited $editThemeName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['edit'] ?? ''); $editTheme = null; if ($editThemeName && isset($themes[$editThemeName])) { $editTheme = $themes[$editThemeName]; $editTheme['name'] = $editThemeName; } $route = 'theme'; require __DIR__ . '/../admin/templates/layout.php'; } function handlePlugins(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $pluginsDir = $config['plugins_dir']; $plugins = []; if (is_dir($pluginsDir)) { foreach (scandir($pluginsDir) as $item) { if ($item[0] === '.') continue; $pluginPath = $pluginsDir . '/' . $item; if (!is_dir($pluginPath)) continue; $hasConfig = file_exists($pluginPath . '/config.json'); $pluginConfig = $hasConfig ? json_decode(file_get_contents($pluginPath . '/config.json'), true) : []; $hasMainFile = file_exists($pluginPath . '/' . $item . '.php'); $hasReadme = file_exists($pluginPath . '/README.md'); $plugins[] = [ 'name' => $item, 'path' => $pluginPath, 'viewable' => $pluginConfig['viewable'] ?? true, 'config' => $pluginConfig, 'has_config' => $hasConfig, 'has_main' => $hasMainFile, 'has_readme' => $hasReadme, ]; } } $route = 'plugins'; require __DIR__ . '/../admin/templates/layout.php'; } function handlePluginConfig(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $pluginsDir = $config['plugins_dir']; $pluginName = $_GET['plugin'] ?? ''; $pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $pluginName); $pluginPath = $pluginsDir . '/' . $pluginName; $configFile = $pluginPath . '/config.json'; $message = ''; $messageType = ''; if (empty($pluginName) || !is_dir($pluginPath)) { header('Location: admin.php?route=plugins'); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $postConfig = $_POST['config'] ?? []; $currentConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : []; $newConfig = array_replace_recursive($currentConfig, $postConfig); // Convert checkbox values: unchecked checkboxes are not sent foreach (array_keys($currentConfig) as $key) { if (is_bool($currentConfig[$key])) { $newConfig[$key] = isset($postConfig[$key]) && $postConfig[$key] === '1'; } } file_put_contents($configFile, json_encode($newConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); $message = 'Configuratie opgeslagen voor plugin: ' . htmlspecialchars($pluginName); $messageType = 'success'; } } $pluginConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : []; $route = 'plugins-config'; require __DIR__ . '/../admin/templates/layout.php'; } function handlePluginEdit(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $pluginsDir = $config['plugins_dir']; $pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['plugin'] ?? ''); $pluginPath = $pluginsDir . '/' . $pluginName; $pluginFile = $pluginPath . '/' . $pluginName . '.php'; $message = ''; $messageType = ''; if (empty($pluginName) || !is_dir($pluginPath) || !file_exists($pluginFile)) { header('Location: admin.php?route=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); $message = 'Plugin opgeslagen.'; $messageType = 'success'; // Clear opcache if available if (function_exists('opcache_invalidate')) { opcache_invalidate($pluginFile, true); } } } $fileContent = file_get_contents($pluginFile); $route = 'plugins-edit'; require __DIR__ . '/../admin/templates/layout.php'; } function handlePluginNew(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $pluginsDir = $config['plugins_dir']; $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { $pluginName = trim($_POST['plugin_name'] ?? ''); $pluginDesc = trim($_POST['plugin_desc'] ?? ''); if (empty($pluginName)) { $message = 'Plugin naam is verplicht.'; $messageType = 'danger'; } elseif (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $pluginName)) { $message = 'Ongeldige plugin naam. Gebruik alleen letters, cijfers en underscores. Begin met een letter.'; $messageType = 'danger'; } else { $pluginPath = $pluginsDir . '/' . $pluginName; if (is_dir($pluginPath)) { $message = 'Plugin met deze naam bestaat al.'; $messageType = 'danger'; } else { mkdir($pluginPath, 0755, true); $pluginFile = $pluginPath . '/' . $pluginName . '.php'; $boilerplate = "config = [\n 'viewable' => true,\n ];\n }\n\n"; $boilerplate .= " public function setAPI(CMSAPI \$api): void\n {\n \$this->api = \$api;\n }\n\n"; $boilerplate .= " public function getSidebarContent(): string\n {\n return '';\n }\n\n"; $boilerplate .= " public function getConfig(): array\n {\n return \$this->config;\n }\n\n"; $boilerplate .= " public function setConfig(array \$config): void\n {\n \$this->config = array_merge(\$this->config, \$config);\n }\n}"; file_put_contents($pluginFile, $boilerplate); $configFile = $pluginPath . '/config.json'; file_put_contents($configFile, json_encode(['viewable' => true], JSON_PRETTY_PRINT)); if ($pluginDesc) { $readme = "# $pluginName\n\n$pluginDesc\n"; file_put_contents($pluginPath . '/README.md', $readme); } header('Location: admin.php?route=plugins'); exit; } } } } $route = 'plugins-new'; require __DIR__ . '/../admin/templates/layout.php'; } function handlePluginToggle(AdminAuth $auth, array $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: admin.php?route=plugins'); exit; } $pluginsDir = $config['plugins_dir']; $pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['plugin'] ?? ''); $pluginPath = $pluginsDir . '/' . $pluginName; if (empty($pluginName) || !is_dir($pluginPath)) { header('Location: admin.php?route=plugins'); exit; } if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $configFile = $pluginPath . '/config.json'; if (file_exists($configFile)) { $pluginConfig = json_decode(file_get_contents($configFile), true); $pluginConfig['viewable'] = !($pluginConfig['viewable'] ?? true); file_put_contents($configFile, json_encode($pluginConfig, JSON_PRETTY_PRINT)); } else { file_put_contents($configFile, json_encode(['viewable' => false], JSON_PRETTY_PRINT)); } } header('Location: admin.php?route=plugins'); exit; } function handlePluginDelete(AdminAuth $auth, array $config): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Location: admin.php?route=plugins'); exit; } $pluginsDir = $config['plugins_dir']; $pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['plugin'] ?? ''); $pluginPath = $pluginsDir . '/' . $pluginName; if (empty($pluginName) || !is_dir($pluginPath)) { header('Location: admin.php?route=plugins'); exit; } if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $di = new RecursiveDirectoryIterator($pluginPath, RecursiveDirectoryIterator::SKIP_DOTS); $files = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); foreach ($files as $file) { $file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath()); } rmdir($pluginPath); } header('Location: admin.php?route=plugins'); exit; } function handleMedia(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $assetsDir = $config['assets_dir']; $message = ''; $messageType = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!$auth->verifyCsrf($_POST['csrf_token'] ?? '')) { $message = 'Ongeldige CSRF token.'; $messageType = 'danger'; } else { if (!empty($_FILES['file']['name'][0]) && is_array($_FILES['file']['name'])) { if (!is_dir($assetsDir)) { mkdir($assetsDir, 0755, true); } $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 = $assetsDir . '/' . $filename; $n = 1; while (file_exists($dest)) { $p = pathinfo($filename); $dest = $assetsDir . '/' . $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'; } } if (!empty($_POST['delete'])) { $file = basename($_POST['delete']); $path = $assetsDir . '/' . $file; if (file_exists($path)) { unlink($path); $message = 'Bestand verwijderd.'; $messageType = 'success'; } } } } // List files $files = []; if (is_dir($assetsDir)) { foreach (scandir($assetsDir) as $item) { if ($item[0] === '.' || is_dir($assetsDir . '/' . $item)) continue; $ext = strtolower(pathinfo($item, PATHINFO_EXTENSION)); $isImage = in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']); $files[] = [ 'name' => $item, 'url' => '/-assets/' . $item, 'size' => filesize($assetsDir . '/' . $item), 'is_image' => $isImage, 'ext' => $ext, 'modified' => date('Y-m-d H:i', filemtime($assetsDir . '/' . $item)), ]; } } // Sort newest first usort($files, fn($a, $b) => strcmp($b['modified'], $a['modified'])); $route = 'media'; require __DIR__ . '/../admin/templates/layout.php'; } function handleMediaList(AdminAuth $auth, array $config): void { $contentDir = realpath(__DIR__ . '/../content'); $files = []; if ($contentDir === false || !is_dir($contentDir)) { header('Content-Type: application/json'); echo json_encode([]); exit; } $mediaExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'mp3', 'wav', 'ogg', 'mp4', 'webm']; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($contentDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $path => $info) { if ($info->isDir()) continue; $rel = substr($path, strlen($contentDir) + 1); if (strpos($rel, '.') === 0) continue; $ext = strtolower($info->getExtension()); if (!in_array($ext, $mediaExts)) continue; $isImage = in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']); $isAudio = in_array($ext, ['mp3', 'wav', 'ogg']); $isVideo = in_array($ext, ['mp4', 'webm']); $files[] = [ 'name' => $info->getFilename(), 'url' => '/-media/' . $rel, 'ext' => $ext, 'is_image' => $isImage, 'is_audio' => $isAudio, 'is_video' => $isVideo, ]; } usort($files, fn($a, $b) => strcasecmp($a['name'], $b['name'])); header('Content-Type: application/json'); echo json_encode($files); exit; } function handleUsers(AdminAuth $auth, array $config): void { $user = $auth->getCurrentUser(); $csrf = $auth->getCsrfToken(); $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') { $result = $auth->addUser( trim($_POST['username'] ?? ''), $_POST['password'] ?? '', $_POST['role'] ?? 'admin' ); $message = $result['message']; $messageType = $result['success'] ? 'success' : 'danger'; } elseif ($action === 'delete') { $result = $auth->deleteUser($_POST['delete_username'] ?? ''); $message = $result['message']; $messageType = $result['success'] ? 'success' : 'danger'; } elseif ($action === 'change_password') { $result = $auth->changePassword( $_POST['pw_username'] ?? '', $_POST['new_password'] ?? '' ); $message = $result['message']; $messageType = $result['success'] ? 'success' : 'danger'; } } } $users = $auth->getUsers(); $route = 'users'; require __DIR__ . '/../admin/templates/layout.php'; } // --- Frontmatter helpers --- function parseFrontmatterField(string $content, string $key, string $default = ''): string { if (preg_match('/^---\s*\n(.*?)\n---\s*\n/s', $content, $matches)) { $metaContent = $matches[1]; foreach (explode("\n", $metaContent) as $line) { if (strpos($line, $key . ':') === 0) { [, $value] = explode(':', $line, 2); return trim($value, " \t'\""); } } } return $default; } function updateContentFrontmatter(string $content, string $key, string $value): string { if (preg_match('/^(---\s*\n)(.*?)(\n---\s*\n.*)$/s', $content, $matches)) { $metaContent = $matches[2]; $body = $matches[3]; $found = false; $lines = explode("\n", $metaContent); foreach ($lines as $i => $line) { if (strpos($line, $key . ':') === 0) { $lines[$i] = $key . ': ' . $value; $found = true; break; } } if (!$found) { $lines[] = $key . ': ' . $value; } return $matches[1] . implode("\n", $lines) . $body; } else { return "---\n" . $key . ': ' . $value . "\n---\n" . $content; } } // --- Helper functions --- function countFiles(string $dir, array $extensions): int { $count = 0; if (!is_dir($dir)) return 0; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); foreach ($iterator as $file) { if ($file->isFile() && in_array($file->getExtension(), $extensions)) { $count++; } } return $count; } function countDirs(string $dir): int { if (!is_dir($dir)) return 0; $count = 0; foreach (scandir($dir) as $item) { if ($item[0] !== '.' && is_dir($dir . '/' . $item)) $count++; } return $count; } function dirSize(string $dir): int { $size = 0; if (!is_dir($dir)) return 0; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); foreach ($iterator as $file) { if ($file->isFile()) $size += $file->getSize(); } return $size; } function formatSize(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB']; $i = 0; while ($bytes >= 1024 && $i < count($units) - 1) { $bytes /= 1024; $i++; } return round($bytes, 1) . ' ' . $units[$i]; } function scanContentDir(string $fullPath, string $subdir): array { $items = []; if (!is_dir($fullPath)) return $items; foreach (scandir($fullPath) as $item) { if ($item[0] === '.') continue; // Skip assets directory in content listing if ($item === 'assets' && is_dir($itemPath)) continue; $itemPath = $fullPath . '/' . $item; $relativePath = $subdir ? $subdir . '/' . $item : $item; $items[] = [ 'name' => $item, 'path' => $relativePath, 'is_dir' => is_dir($itemPath), 'size' => is_file($itemPath) ? formatSize(filesize($itemPath)) : '', 'modified' => date('d-m-Y H:i', filemtime($itemPath)), 'extension' => is_file($itemPath) ? pathinfo($item, PATHINFO_EXTENSION) : '', ]; } // Sort: directories first, then files alphabetically usort($items, function ($a, $b) { if ($a['is_dir'] !== $b['is_dir']) return $b['is_dir'] - $a['is_dir']; return strcasecmp($a['name'], $b['name']); }); return $items; } function getAllContentDirs(string $contentDir, string $realContentDir): array { $dirs = []; if (!is_dir($contentDir)) return $dirs; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($contentDir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $path => $fileInfo) { if (!$fileInfo->isDir()) continue; $realPath = $fileInfo->getRealPath(); if (strpos($realPath, $realContentDir) !== 0) continue; $relative = substr($realPath, strlen($realContentDir) + 1); if ($relative === false || $relative === '') continue; // Skip hidden directories (starting with -) if (strpos(basename($relative), '-') === 0) continue; $dirs[] = $relative; } sort($dirs); return $dirs; }