Fix plugin security, hooks system, and admin features
- Add plugin allowlist (enabled_plugins in config.json) - Add enable/disable toggle in admin (separate from visibility) - Add plugin hooks system (actions + filters with auto-registration) - Fix autoLinkPageTitles nested <a> tag vulnerability - Move MQTT credentials to environment variables - Preserve current page in language switcher - Fix ctime/birthtime for file creation date - Deduplicate getGuidePage() CommonMark setup - Simplify formatDisplayName() logic - Add admin activity log to dashboard - Add own password change with current password verification - Apply theme header_color to admin sidebar - Add content preview button in editor
This commit is contained in:
+140
-1
@@ -105,6 +105,10 @@ switch ($route) {
|
||||
handlePluginToggle($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'plugins-toggle-visibility':
|
||||
handlePluginToggleVisibility($auth, $appConfig);
|
||||
break;
|
||||
|
||||
case 'plugins-delete':
|
||||
handlePluginDelete($auth, $appConfig);
|
||||
break;
|
||||
@@ -167,7 +171,7 @@ function handleDashboard(AdminAuth $auth, array $config): void
|
||||
$stats = [
|
||||
'pages' => countFiles($contentDir, ['md', 'php', 'html']),
|
||||
'directories' => countDirs($contentDir),
|
||||
'plugins' => countDirs($pluginsDir),
|
||||
'plugins' => countEnabledPlugins($pluginsDir, $configJson),
|
||||
'config_exists' => file_exists($configJson),
|
||||
'content_size' => formatSize(dirSize($contentDir)),
|
||||
'php_version' => PHP_VERSION,
|
||||
@@ -176,6 +180,25 @@ function handleDashboard(AdminAuth $auth, array $config): void
|
||||
// Load site config
|
||||
$siteConfig = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
@@ -249,6 +272,18 @@ function handleContent(AdminAuth $auth, array $config): void
|
||||
require __DIR__ . '/../admin/templates/layout.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 = $_SERVER['REMOTE_ADDR'] ?? 'cli';
|
||||
@file_put_contents($logFile, "[{$timestamp}] [{$level}] [{$ip}] {$message}\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
function handleContentEdit(AdminAuth $auth, array $config): void
|
||||
{
|
||||
$user = $auth->getCurrentUser();
|
||||
@@ -278,6 +313,7 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
||||
} else {
|
||||
// Handle rename
|
||||
$newFilename = trim($_POST['filename'] ?? '');
|
||||
$wasRenamed = false;
|
||||
if (!empty($newFilename)) {
|
||||
$newFilename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newFilename);
|
||||
$newFilename .= '.' . $fileExt;
|
||||
@@ -288,6 +324,8 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
||||
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, './');
|
||||
@@ -307,6 +345,9 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
||||
: '';
|
||||
$content = updateContentFrontmatter($content, 'plugins', $plugins);
|
||||
file_put_contents($filePath, $content);
|
||||
if (!$wasRenamed) {
|
||||
adminLog($config, 'info', $user['username'] . ' sloeg ' . basename($filePath) . ' op');
|
||||
}
|
||||
}
|
||||
|
||||
$message = 'Bestand opgeslagen.';
|
||||
@@ -346,6 +387,10 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
||||
? array_map('trim', explode(',', $currentPlugins))
|
||||
: [];
|
||||
|
||||
// Get current language for preview links
|
||||
$siteConfig = file_exists($config['config_json']) ? json_decode(file_get_contents($config['config_json']), true) : [];
|
||||
$currentLang = $siteConfig['language']['default'] ?? 'nl';
|
||||
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
|
||||
@@ -405,6 +450,7 @@ function handleContentNew(AdminAuth $auth, array $config): void
|
||||
}
|
||||
|
||||
file_put_contents($filePath, $content);
|
||||
adminLog($config, 'info', $user['username'] . ' maakte ' . $filename . ' aan in ' . $dir);
|
||||
header('Location: /admin/content?dir=' . urlencode($dir));
|
||||
exit;
|
||||
}
|
||||
@@ -449,6 +495,8 @@ function handleContentDelete(AdminAuth $auth, array $config): void
|
||||
&& strpos($realPath, $realContentDir) === 0
|
||||
) {
|
||||
if (is_file($filePath)) {
|
||||
$user = $auth->getCurrentUser();
|
||||
adminLog($config, 'info', $user['username'] . ' verwijderde ' . basename($filePath));
|
||||
unlink($filePath);
|
||||
}
|
||||
}
|
||||
@@ -622,6 +670,7 @@ function handleConfig(AdminAuth $auth, array $config): void
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
file_put_contents($configJson, json_encode($parsed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
adminLog($config, 'info', $user['username'] . ' wijzigde site configuratie');
|
||||
$message = 'Configuratie opgeslagen.';
|
||||
$messageType = 'success';
|
||||
$jsonContent = null; // reload from file
|
||||
@@ -791,6 +840,15 @@ function handlePlugins(AdminAuth $auth, array $config): void
|
||||
$user = $auth->getCurrentUser();
|
||||
$csrf = $auth->getCsrfToken();
|
||||
$pluginsDir = $config['plugins_dir'];
|
||||
|
||||
// Load enabled_plugins from config.json
|
||||
$siteConfig = [];
|
||||
if (file_exists($config['config_json'])) {
|
||||
$siteConfig = json_decode(file_get_contents($config['config_json']), true) ?? [];
|
||||
}
|
||||
$enabledPlugins = $siteConfig['enabled_plugins'] ?? [];
|
||||
$regenerateConfig = false;
|
||||
|
||||
$plugins = [];
|
||||
|
||||
if (is_dir($pluginsDir)) {
|
||||
@@ -804,9 +862,19 @@ function handlePlugins(AdminAuth $auth, array $config): void
|
||||
$hasMainFile = file_exists($pluginPath . '/' . $item . '.php');
|
||||
$hasReadme = file_exists($pluginPath . '/README.md');
|
||||
|
||||
$isEnabled = in_array($item, $enabledPlugins, true);
|
||||
|
||||
// Auto-register plugin in enabled_plugins if it has a main file and is not in the list
|
||||
if ($hasMainFile && !$isEnabled) {
|
||||
$enabledPlugins[] = $item;
|
||||
$regenerateConfig = true;
|
||||
$isEnabled = true;
|
||||
}
|
||||
|
||||
$plugins[] = [
|
||||
'name' => $item,
|
||||
'path' => $pluginPath,
|
||||
'enabled' => $isEnabled,
|
||||
'viewable' => $pluginConfig['viewable'] ?? true,
|
||||
'config' => $pluginConfig,
|
||||
'has_config' => $hasConfig,
|
||||
@@ -816,6 +884,12 @@ function handlePlugins(AdminAuth $auth, array $config): void
|
||||
}
|
||||
}
|
||||
|
||||
// Save updated enabled_plugins back to config.json
|
||||
if ($regenerateConfig) {
|
||||
$siteConfig['enabled_plugins'] = $enabledPlugins;
|
||||
file_put_contents($config['config_json'], json_encode($siteConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
$route = 'plugins';
|
||||
require __DIR__ . '/../admin/templates/layout.php';
|
||||
}
|
||||
@@ -981,6 +1055,45 @@ function handlePluginToggle(AdminAuth $auth, array $config): void
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||
$siteConfig = [];
|
||||
$configJson = $config['config_json'];
|
||||
if (file_exists($configJson)) {
|
||||
$siteConfig = json_decode(file_get_contents($configJson), true) ?? [];
|
||||
}
|
||||
$enabledPlugins = $siteConfig['enabled_plugins'] ?? [];
|
||||
|
||||
if (in_array($pluginName, $enabledPlugins, true)) {
|
||||
$enabledPlugins = array_values(array_filter($enabledPlugins, fn($p) => $p !== $pluginName));
|
||||
} else {
|
||||
$enabledPlugins[] = $pluginName;
|
||||
}
|
||||
|
||||
$siteConfig['enabled_plugins'] = $enabledPlugins;
|
||||
file_put_contents($configJson, json_encode($siteConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' schakelde plugin ' . $pluginName . ' ' . (in_array($pluginName, $enabledPlugins, true) ? 'uit' : 'in'));
|
||||
}
|
||||
|
||||
header('Location: /admin/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
function handlePluginToggleVisibility(AdminAuth $auth, array $config): void
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /admin/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pluginsDir = $config['plugins_dir'];
|
||||
$pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '', $_GET['plugin'] ?? '');
|
||||
$pluginPath = $pluginsDir . '/' . $pluginName;
|
||||
|
||||
if (empty($pluginName) || !is_dir($pluginPath)) {
|
||||
header('Location: /admin/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||
$configFile = $pluginPath . '/config.json';
|
||||
if (file_exists($configFile)) {
|
||||
@@ -990,6 +1103,7 @@ function handlePluginToggle(AdminAuth $auth, array $config): void
|
||||
} else {
|
||||
file_put_contents($configFile, json_encode(['viewable' => false], JSON_PRETTY_PRINT));
|
||||
}
|
||||
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' wijzigde zichtbaarheid van plugin ' . $pluginName);
|
||||
}
|
||||
|
||||
header('Location: /admin/plugins');
|
||||
@@ -1019,6 +1133,7 @@ function handlePluginDelete(AdminAuth $auth, array $config): void
|
||||
$file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
|
||||
}
|
||||
rmdir($pluginPath);
|
||||
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' verwijderde plugin ' . $pluginName);
|
||||
}
|
||||
|
||||
header('Location: /admin/plugins');
|
||||
@@ -1193,6 +1308,22 @@ function handleUsers(AdminAuth $auth, array $config): void
|
||||
);
|
||||
$message = $result['message'];
|
||||
$messageType = $result['success'] ? 'success' : 'danger';
|
||||
} elseif ($action === 'change_own_password') {
|
||||
$currentPassword = $_POST['current_password'] ?? '';
|
||||
$newPassword = $_POST['new_password'] ?? '';
|
||||
$confirmPassword = $_POST['confirm_password'] ?? '';
|
||||
if ($newPassword !== $confirmPassword) {
|
||||
$message = 'De nieuwe wachtwoorden komen niet overeen.';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
$result = $auth->changeOwnPassword(
|
||||
$user['username'],
|
||||
$currentPassword,
|
||||
$newPassword
|
||||
);
|
||||
$message = $result['message'];
|
||||
$messageType = $result['success'] ? 'success' : 'danger';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1266,6 +1397,14 @@ function countDirs(string $dir): int
|
||||
return $count;
|
||||
}
|
||||
|
||||
function countEnabledPlugins(string $pluginsDir, string $configJson): int
|
||||
{
|
||||
if (!is_dir($pluginsDir)) return 0;
|
||||
$siteConfig = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
||||
$enabled = $siteConfig['enabled_plugins'] ?? [];
|
||||
return count(array_filter($enabled, fn($p) => is_dir($pluginsDir . '/' . $p)));
|
||||
}
|
||||
|
||||
function dirSize(string $dir): int
|
||||
{
|
||||
$size = 0;
|
||||
|
||||
Reference in New Issue
Block a user