Merge development v2.6.0 into main

Resolved conflicts by taking development (v2.6.0) version for all files.
Removed statistics.twig (replaced by Statistics plugin).
This commit is contained in:
2026-08-15 19:32:47 +02:00
207 changed files with 3736 additions and 23879 deletions
+423 -171
View File
@@ -27,7 +27,10 @@ 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';
require_once __DIR__ . '/../cms/core/class/ContentBackup.php';
require_once __DIR__ . '/../cms/core/plugin/PluginAPIInterface.php';
require_once __DIR__ . '/../cms/core/plugin/CMSAPI.php';
require_once __DIR__ . '/../cms/core/plugin/AdminPluginAPI.php';
require_once __DIR__ . '/../cms/core/plugin/PluginManager.php';
// Initialize dynamic logging from site config
@@ -64,14 +67,48 @@ $twig->addFunction(new \Twig\TwigFunction('role_label', function($role) {
return AdminAuth::getRoleLabel($role);
}));
// Initialize PluginManager for admin (system plugins provide admin menu items)
$siteConfigForPlugins = file_exists($appConfig['config_json'])
// 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) ?? [])
: [];
$enabledPluginsList = $siteConfigForPlugins['plugins']['enabled'] ?? $siteConfigForPlugins['enabled_plugins'] ?? [];
$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);
$pluginAdminMenuItems = $adminPluginManager->getAdminMenuItems();
$twig->addGlobal('plugin_admin_menu', $pluginAdminMenuItems);
$adminPluginAPI = new AdminPluginAPI($siteConfigForI18n);
$adminPluginManager->setAPI($adminPluginAPI);
$adminPluginMenuItems = $adminPluginManager->getAdminMenuItems();
$twig->addGlobal('plugin_admin_menu', $adminPluginMenuItems);
// Routing
$route = $_GET['route'] ?? '';
@@ -150,17 +187,51 @@ if ($route !== '' && $route !== 'dashboard' && !$auth->hasPermission($route)) {
}
// 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 'dashboard':
case '':
handleDashboard($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
case 'content':
handleContent($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
break;
@@ -189,6 +260,26 @@ switch ($route) {
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;
@@ -201,10 +292,6 @@ switch ($route) {
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;
@@ -245,10 +332,6 @@ switch ($route) {
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;
@@ -258,22 +341,7 @@ switch ($route) {
break;
default:
// Check if a system plugin handles this route
$pluginContent = $adminPluginManager->handleAdminRoute($route);
if ($pluginContent !== null) {
echo $twig->render('pages/plugin-admin.twig', [
'user' => $user,
'route' => $route,
'csrf_token' => $csrf,
'sidebar_color' => getSidebarColor($appConfig),
'needs_editor' => false,
'message' => '',
'message_type' => 'info',
'plugin_content' => $pluginContent,
]);
} else {
handleDashboard($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
}
handleDashboard($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
}
// ============================================================================
@@ -299,7 +367,6 @@ function handleDashboard($auth, $config, $twig, $user, $csrf, $siteConfig): void
$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,
@@ -307,34 +374,18 @@ function handleDashboard($auth, $config, $twig, $user, $csrf, $siteConfig): void
'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],
];
}
// 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),
];
}
$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);
ksort($pluginOverview);
echo $twig->render('pages/dashboard.twig', [
'user' => $user,
@@ -342,9 +393,7 @@ function handleDashboard($auth, $config, $twig, $user, $csrf, $siteConfig): void
'csrf_token' => $csrf,
'stats' => $stats,
'site_config' => $siteConfig,
'recent_logs' => $recentLogs,
'recent_requests' => $recentRequests,
'analytics_summary' => $analyticsSummary,
'plugin_overview' => $pluginOverview,
'sidebar_color' => getSidebarColor($config),
'needs_editor' => false,
'message' => '',
@@ -534,38 +583,18 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
}
}
// Get available content plugins (type=content in plugin.json, or no type = content)
// Get available plugins
$pluginsDir = $config['plugins_dir'];
$availablePlugins = [];
$sidebarLayouts = [];
if (is_dir($pluginsDir)) {
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginDir) {
$pluginName = basename($pluginDir);
if ($pluginName !== '.' && $pluginName !== '..') {
$hasJson = file_exists($pluginDir . '/plugin.json');
$hasPhp = file_exists($pluginDir . '/' . $pluginName . '.php');
if ($hasJson || $hasPhp) {
// Check plugin type — only content plugins show in sidebar
$pluginType = 'content';
if ($hasJson) {
$pj = json_decode(file_get_contents($pluginDir . '/plugin.json'), true);
$pluginType = $pj['type'] ?? 'content';
}
if ($pluginType === 'content') {
$availablePlugins[] = $pluginName;
}
}
if ($pluginName !== '.' && $pluginName !== '..' && file_exists($pluginDir . '/plugin.json')) {
$availablePlugins[] = $pluginName;
}
}
}
// Determine which layouts have a sidebar (based on theme twig files)
foreach ($themeLayouts as $key => $twigFile) {
if (strpos($key, 'sidebar') !== false) {
$sidebarLayouts[] = $key;
}
}
$currentLang = extractLanguagePrefix($file);
$route = 'content-edit';
$fileDir = trim(dirname($file), '.\\/');
@@ -583,7 +612,6 @@ function handleContentEdit($auth, $config, $twig, $user, $csrf, $siteConfig): vo
'currentLayout' => $currentLayout ?: $themeDefaultLayout,
'themeLayouts' => $themeLayouts,
'themeDefaultLayout' => $themeDefaultLayout,
'sidebarLayouts' => $sidebarLayouts,
'activeThemeName' => $activeThemeName,
'availablePlugins' => $availablePlugins,
'selectedPlugins' => $selectedPlugins,
@@ -880,9 +908,12 @@ function handleContentMove($auth, $config, $twig, $user, $csrf): void
}
function handleConfig($auth, $config, $twig, $user, $csrf): void
function handleContentBackup($auth, $config, $twig, $user, $csrf): void
{
$configFile = $config['config_json'];
$contentDir = $config['content_dir'];
$projectRoot = $config['codepress_root'];
$backup = new ContentBackup($contentDir, $projectRoot);
$message = '';
$messageType = '';
@@ -890,31 +921,222 @@ function handleConfig($auth, $config, $twig, $user, $csrf): void
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'] ?? '';
$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']);
$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');
$message = 'Configuratie opgeslagen.';
$messageType = 'success';
// 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,
@@ -960,24 +1182,6 @@ function handleSecurity($auth, $config, $twig, $user, $csrf): void
]);
}
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';
@@ -1089,7 +1293,7 @@ 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'] ?? [];
$enabledPlugins = $siteConfig['enabled_plugins'] ?? [];
$plugins = [];
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginDir) {
@@ -1100,12 +1304,21 @@ function handlePlugins($auth, $config, $twig, $user, $csrf): void
'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;
}
@@ -1135,10 +1348,6 @@ function handlePluginsNew($auth, $config, $twig, $user, $csrf): void
$messageType = 'danger';
} else {
$pluginName = trim($_POST['name'] ?? '');
$pluginType = $_POST['type'] ?? 'content';
if (!in_array($pluginType, ['content', 'system'], true)) {
$pluginType = 'content';
}
if (!empty($pluginName)) {
$pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '-', $pluginName);
$pluginsDir = $config['plugins_dir'];
@@ -1151,19 +1360,11 @@ function handlePluginsNew($auth, $config, $twig, $user, $csrf): void
'name' => ucfirst($pluginName),
'version' => '1.0.0',
'author' => $user['username'],
'type' => $pluginType,
];
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");
// Generate plugin template based on type
if ($pluginType === 'system') {
$template = "<?php\n\nclass $pluginName\n{\n private ?CMSAPI \$api = null;\n\n public function setAPI(CMSAPI \$api): void\n {\n \$this->api = \$api;\n }\n\n public function getConfig(): array\n {\n return ['title' => ucfirst('$pluginName'), 'type' => 'system'];\n }\n\n // Register admin menu items\n public function getAdminMenu(): array\n {\n return [\n ['label' => ucfirst('$pluginName'), 'route' => '" . strtolower($pluginName) . "', 'icon' => 'bi-gear'],\n ];\n }\n\n // Register admin routes this plugin handles\n public function getAdminRoutes(): array\n {\n return ['" . strtolower($pluginName) . "'];\n }\n\n // Handle admin route\n public function handleAdminRoute(string \$route): void\n {\n // Render your admin page here\n echo '<h1>" . ucfirst($pluginName) . "</h1><p>Systeem plugin admin pagina.</p>';\n }\n}\n";
} else {
$template = "<?php\n\nclass $pluginName\n{\n private ?CMSAPI \$api = null;\n\n public function setAPI(CMSAPI \$api): void\n {\n \$this->api = \$api;\n }\n\n public function getConfig(): array\n {\n return ['title' => ucfirst('$pluginName'), 'type' => 'content'];\n }\n\n public function getSidebarContent(): string\n {\n return '<p>" . ucfirst($pluginName) . " sidebar content.</p>';\n }\n}\n";
}
file_put_contents($newPluginDir . '/' . $pluginName . '.php', $template);
adminLog($config, 'info', $user['username'] . ' creëerde ' . $pluginType . ' plugin ' . $pluginName);
adminLog($config, 'info', $user['username'] . ' creëerde plugin ' . $pluginName);
header('Location: /admin/plugins-edit?plugin=' . urlencode($pluginName));
exit;
} else {
@@ -1305,7 +1506,7 @@ function handlePluginsToggle($auth, $config): void
$configFile = $config['config_json'];
$siteConfig = file_exists($configFile) ? json_decode(file_get_contents($configFile), true) : [];
$enabledPlugins = $siteConfig['plugins']['enabled'] ?? [];
$enabledPlugins = $siteConfig['enabled_plugins'] ?? [];
if (in_array($plugin, $enabledPlugins)) {
$enabledPlugins = array_diff($enabledPlugins, [$plugin]);
@@ -1315,7 +1516,7 @@ function handlePluginsToggle($auth, $config): void
adminLog($config, 'info', $user['username'] . ' activeerde plugin ' . $plugin);
}
$siteConfig['plugins']['enabled'] = array_values($enabledPlugins);
$siteConfig['enabled_plugins'] = array_values($enabledPlugins);
file_put_contents($configFile, json_encode($siteConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
header('Location: /admin/plugins');
@@ -1386,8 +1587,20 @@ function handleUsers($auth, $config, $twig, $user, $csrf): void
$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);
$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') {
@@ -1520,42 +1733,6 @@ function handleGuide($auth, $config, $twig, $user, $csrf): void
'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
{
@@ -1655,7 +1832,7 @@ function countDirs(string $dir): int
function countEnabledPlugins(string $pluginsDir, string $configJson): int
{
$config = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
$enabled = $config['plugins']['enabled'] ?? [];
$enabled = $config['enabled_plugins'] ?? [];
return count($enabled);
}
@@ -1718,6 +1895,81 @@ function scanContentDir(string $fullPath, string $subdir): array
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<string,string> Sorted list of [pageKey => displayLabel]
*/
function collectContentPages(string $contentDir): array
{
$pages = [];
$realBase = realpath($contentDir);
if (!$realBase || !is_dir($realBase)) {
return $pages;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($realBase, RecursiveDirectoryIterator::SKIP_DOTS)
);
$langRegex = '/^(nl|en|de|fr)\./';
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) {
continue;
}
$ext = strtolower($fileInfo->getExtension());
if (!in_array($ext, ['md', 'php', 'html'], true)) {
continue;
}
$relative = substr($fileInfo->getRealPath(), strlen($realBase) + 1);
$relative = str_replace('\\', '/', $relative);
// Skip hidden / dash-prefixed segments (private assets etc.)
$skip = false;
foreach (explode('/', $relative) as $segment) {
if ($segment !== '' && ($segment[0] === '.' || $segment[0] === '-')) {
$skip = true;
break;
}
}
if ($skip) {
continue;
}
// Strip extension
$key = preg_replace('/\.(md|php|html)$/i', '', $relative);
// Strip language prefix from the filename component
$dirPart = dirname($key);
$dirPart = ($dirPart === '.' || $dirPart === '') ? '' : $dirPart . '/';
$filePart = basename($key);
if (preg_match($langRegex, $filePart, $m)) {
$filePart = substr($filePart, strlen($m[1]) + 1);
}
$key = $dirPart . $filePart;
// folder/index -> folder
if (str_ends_with($key, '/index')) {
$key = substr($key, 0, -6);
}
if ($key === '') {
$key = 'index';
}
$label = ucfirst(str_replace(['-', '/'], [' ', ' / '], $key));
$pages[$key] = $label;
}
ksort($pages);
return $pages;
}
function updateContentFrontmatter(string $content, string $key, string $value): string
{
if (preg_match('/^---\s*\n(.+?)\n---\s*\n(.*)$/s', $content, $m)) {
-20
View File
@@ -1,20 +0,0 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
$rootDir = dirname(__DIR__);
$lang = 'nl';
$pagePath = '';
$guideFile = $rootDir . '/guide/' . $lang . '/index.md';
echo "File: $guideFile\n";
echo "Exists: " . (file_exists($guideFile) ? 'YES' : 'NO') . "\n\n";
if (file_exists($guideFile)) {
$content = file_get_contents($guideFile);
$environment = new \League\CommonMark\Environment\Environment(['html_input' => 'strip']);
$environment->addExtension(new \League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension());
$environment->addExtension(new \League\CommonMark\Extension\Table\TableExtension());
$converter = new \League\CommonMark\MarkdownConverter($environment);
echo $converter->convert($content)->getContent();
}