v2.6.0: Content backup/git versioning, plugin type system, docs update
New features: - ContentBackup class with ZIP backup/restore and git versioning - Admin backup & restore page (content-backup.twig) with git init/commit/log/restore - Plugin type system: system (blue) vs content (green) with visual badges - PluginAPIInterface + AdminPluginAPI for plugin architecture - Essential plugin flag (cannot edit/deactivate/delete) Improvements: - Consolidated enabled_plugins config (removed plugins.enabled) - Removed Analytics/Logging toggles from admin config page - Fixed Dashboard plugin Twig comments rendered as text - Updated 20 guide files (NL+EN): configuratie, plugins, plugin-development, core-classes, theme-json, layouts, scss-styling, admin-beheerder, nieuw-thema, architectuur - Improved accessibility test script (grep -E, min/max checks) Cleanup: - Removed unused classes: ARIAComponents, AccessibilityManager, ContentSecurityPolicy, etc. - Removed vendor packages: mustache/mustache, php-mqtt/client - Removed old templates: logs.twig, statistics.twig (now plugins) - Moved language files to language/ directory Tests: - Pentest: 30/30 passed, 0 vulnerabilities - WCAG 2.1 AA: 25/25 passed, 100% compliance
This commit is contained in:
+423
-112
@@ -27,6 +27,11 @@ 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
|
||||
$siteConfigForLogging = file_exists($appConfig['config_json'])
|
||||
@@ -62,6 +67,49 @@ $twig->addFunction(new \Twig\TwigFunction('role_label', function($role) {
|
||||
return AdminAuth::getRoleLabel($role);
|
||||
}));
|
||||
|
||||
// Load admin interface translations
|
||||
// admin_language is resolved from: config admin_language -> language.default -> 'nl'
|
||||
function loadAdminTranslations(?array $siteConfig = null): array {
|
||||
$adminLang = $siteConfig['admin_language'] ?? ($siteConfig['language']['default'] ?? 'nl');
|
||||
$langDir = __DIR__ . '/../language/';
|
||||
$file = $langDir . $adminLang . '/admin.php';
|
||||
if (file_exists($file)) {
|
||||
$t = include $file;
|
||||
if (is_array($t)) {
|
||||
return $t;
|
||||
}
|
||||
}
|
||||
// Fallback to Dutch admin translations
|
||||
$fallback = $langDir . 'nl/admin.php';
|
||||
if (file_exists($fallback)) {
|
||||
$t = include $fallback;
|
||||
if (is_array($t)) {
|
||||
return $t;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
$siteConfigForI18n = file_exists($appConfig['config_json'])
|
||||
? (json_decode(file_get_contents($appConfig['config_json']), true) ?? [])
|
||||
: [];
|
||||
$adminTranslations = loadAdminTranslations($siteConfigForI18n);
|
||||
$adminLangCode = $siteConfigForI18n['admin_language'] ?? ($siteConfigForI18n['language']['default'] ?? 'nl');
|
||||
|
||||
$twig->addGlobal('ta', $adminTranslations);
|
||||
$twig->addGlobal('admin_lang', $adminLangCode);
|
||||
$twig->addFunction(new \Twig\TwigFunction('ta', function($key) use ($adminTranslations) {
|
||||
return $adminTranslations[$key] ?? $key;
|
||||
}));
|
||||
|
||||
// Initialize admin PluginManager for plugin-provided admin pages and menu items
|
||||
$enabledPluginsList = $siteConfigForI18n['enabled_plugins'] ?? [];
|
||||
$adminPluginManager = new PluginManager(__DIR__ . '/../plugins', $enabledPluginsList);
|
||||
$adminPluginAPI = new AdminPluginAPI($siteConfigForI18n);
|
||||
$adminPluginManager->setAPI($adminPluginAPI);
|
||||
$adminPluginMenuItems = $adminPluginManager->getAdminMenuItems();
|
||||
$twig->addGlobal('plugin_admin_menu', $adminPluginMenuItems);
|
||||
|
||||
// Routing
|
||||
$route = $_GET['route'] ?? '';
|
||||
|
||||
@@ -139,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;
|
||||
@@ -178,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;
|
||||
@@ -190,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;
|
||||
@@ -234,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;
|
||||
@@ -273,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,
|
||||
@@ -281,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,
|
||||
@@ -316,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' => '',
|
||||
@@ -833,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 = '';
|
||||
|
||||
@@ -843,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,
|
||||
@@ -913,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->getFullStats();
|
||||
$route = 'statistics';
|
||||
|
||||
echo $twig->render('pages/statistics.twig', [
|
||||
'user' => $user,
|
||||
'route' => $route,
|
||||
'csrf_token' => $csrf,
|
||||
'stats' => $stats,
|
||||
'sidebar_color' => getSidebarColor($config),
|
||||
'needs_editor' => false,
|
||||
'message' => '',
|
||||
'message_type' => 'info',
|
||||
]);
|
||||
}
|
||||
|
||||
function handleTheme($auth, $config, $twig, $user, $csrf, $siteConfig): void
|
||||
{
|
||||
$themesDir = __DIR__ . '/../themes';
|
||||
@@ -1042,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) {
|
||||
@@ -1053,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;
|
||||
}
|
||||
@@ -1246,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]);
|
||||
@@ -1256,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');
|
||||
@@ -1327,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') {
|
||||
@@ -1461,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
|
||||
{
|
||||
@@ -1596,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);
|
||||
}
|
||||
|
||||
@@ -1659,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)) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user