- border-radius: 0 on all dropdown-menu and dropdown-item - padding: 0 and margin: 0 on dropdown-menu - margin-left: 0 on submenu (no gap between parent and child) - margin-top: -1px on submenu (seamless border overlap) - CSS hover opens submenu on desktop, click on mobile - white-space: nowrap on dropdown items - Fix statistics getFullStats -> getStats - Fix Twig ?? operator -> default filter on dashboard/statistics - Asset server (public/asset.php) for production static file serving - .htaccess rewrite rules for themes/admin/plugins assets
1719 lines
58 KiB
PHP
1719 lines
58 KiB
PHP
<?php
|
|
|
|
/**
|
|
* CodePress Admin Console - Entry Point
|
|
* Access via: //admin/login|dashboard|content|config|plugins|users|logout
|
|
*/
|
|
|
|
// Security headers
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
header_remove('X-Powered-By');
|
|
|
|
// Load Composer autoloader for CommonMark and Twig
|
|
$autoloader = __DIR__ . '/../vendor/autoload.php';
|
|
if (file_exists($autoloader)) {
|
|
require_once $autoloader;
|
|
}
|
|
|
|
// Load admin components
|
|
$appConfig = require __DIR__ . '/../admin/config/app.php';
|
|
require_once __DIR__ . '/../admin/src/AdminAuth.php';
|
|
require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
|
|
require_once __DIR__ . '/../cms/core/class/BotGuard.php';
|
|
require_once __DIR__ . '/../cms/core/class/Cache.php';
|
|
require_once __DIR__ . '/../cms/core/class/GeoIP.php';
|
|
require_once __DIR__ . '/../cms/core/class/Analytics.php';
|
|
require_once __DIR__ . '/../cms/core/class/LogManager.php';
|
|
|
|
// Initialize dynamic logging from site config
|
|
$siteConfigForLogging = file_exists($appConfig['config_json'])
|
|
? (json_decode(file_get_contents($appConfig['config_json']), true) ?? [])
|
|
: [];
|
|
LogManager::init($siteConfigForLogging['logging'] ?? []);
|
|
|
|
$auth = new AdminAuth($appConfig);
|
|
|
|
// Initialize Twig (admin theme: admin/theme/default/views)
|
|
$adminTheme = 'default';
|
|
$twigLoader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/../admin/theme/' . $adminTheme . '/views');
|
|
$twig = new \Twig\Environment($twigLoader, [
|
|
'cache' => __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->getStats();
|
|
$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', "<?php\n// Plugin: " . ucfirst($pluginName) . "\n\necho 'Hello from " . $pluginName . "';\n");
|
|
|
|
adminLog($config, 'info', $user['username'] . ' creëerde plugin ' . $pluginName);
|
|
header('Location: /admin/plugins-edit?plugin=' . urlencode($pluginName));
|
|
exit;
|
|
} else {
|
|
$message = 'Plugin bestaat al.';
|
|
$messageType = 'danger';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$route = 'plugins-new';
|
|
|
|
echo $twig->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 = '<p>Handleiding niet gevonden.</p>';
|
|
} 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);
|
|
}
|
|
|