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:
@@ -211,6 +211,29 @@ class AdminAuth
|
|||||||
return ['success' => false, 'message' => 'Gebruiker niet gevonden.'];
|
return ['success' => false, 'message' => 'Gebruiker niet gevonden.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function changeOwnPassword(string $username, string $currentPassword, string $newPassword): array
|
||||||
|
{
|
||||||
|
$user = $this->findUser($username);
|
||||||
|
if (!$user) {
|
||||||
|
return ['success' => false, 'message' => 'Gebruiker niet gevonden.'];
|
||||||
|
}
|
||||||
|
if (!password_verify($currentPassword, $user['password_hash'])) {
|
||||||
|
return ['success' => false, 'message' => 'Huidig wachtwoord is onjuist.'];
|
||||||
|
}
|
||||||
|
if (strlen($newPassword) < 8) {
|
||||||
|
return ['success' => false, 'message' => 'Nieuw wachtwoord moet minimaal 8 tekens zijn.'];
|
||||||
|
}
|
||||||
|
foreach ($this->adminConfig['users'] as &$u) {
|
||||||
|
if ($u['username'] === $username) {
|
||||||
|
$u['password_hash'] = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||||
|
$this->saveAdminConfig();
|
||||||
|
$this->log('info', "Eigen wachtwoord gewijzigd: {$username}");
|
||||||
|
return ['success' => true, 'message' => 'Wachtwoord gewijzigd.'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['success' => false, 'message' => 'Fout bij wijzigen wachtwoord.'];
|
||||||
|
}
|
||||||
|
|
||||||
// --- Private helpers ---
|
// --- Private helpers ---
|
||||||
|
|
||||||
private function findUser(string $username): ?array
|
private function findUser(string $username): ?array
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<?php
|
||||||
|
// Load theme sidebar color from site config
|
||||||
|
$layoutConfigFile = __DIR__ . '/../../config.json';
|
||||||
|
$layoutSiteConfig = file_exists($layoutConfigFile) ? json_decode(file_get_contents($layoutConfigFile), true) : [];
|
||||||
|
$layoutActiveTheme = $layoutSiteConfig['active_theme'] ?? 'default';
|
||||||
|
$layoutThemeDir = dirname(__DIR__, 1) . '/../themes/' . $layoutActiveTheme;
|
||||||
|
$layoutThemeFile = $layoutThemeDir . '/theme.json';
|
||||||
|
$layoutThemeConfig = file_exists($layoutThemeFile) ? json_decode(file_get_contents($layoutThemeFile), true) : [];
|
||||||
|
$layoutSidebarColor = $layoutThemeConfig['header_color'] ?? '#0a369d';
|
||||||
|
?><!DOCTYPE html>
|
||||||
<html lang="nl">
|
<html lang="nl">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
@@ -8,7 +17,7 @@
|
|||||||
<link rel="stylesheet" href="/assets/css/bootstrap-icons.css">
|
<link rel="stylesheet" href="/assets/css/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
body { background-color: #f5f6fa; min-height: 100vh; }
|
body { background-color: #f5f6fa; min-height: 100vh; }
|
||||||
.admin-sidebar { background-color: #0a369d; min-height: 100vh; width: 240px; position: fixed; top: 0; left: 0; z-index: 100; }
|
.admin-sidebar { background-color: <?= htmlspecialchars($layoutSidebarColor) ?>; min-height: 100vh; width: 240px; position: fixed; top: 0; left: 0; z-index: 100; }
|
||||||
.admin-sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 0.75rem 1.25rem; border-radius: 0; }
|
.admin-sidebar .nav-link { color: rgba(255,255,255,0.8); padding: 0.75rem 1.25rem; border-radius: 0; }
|
||||||
.admin-sidebar .nav-link:hover { color: #fff; background-color: rgba(255,255,255,0.1); }
|
.admin-sidebar .nav-link:hover { color: #fff; background-color: rgba(255,255,255,0.1); }
|
||||||
.admin-sidebar .nav-link.active { color: #fff; background-color: rgba(255,255,255,0.2); border-left: 3px solid #fff; }
|
.admin-sidebar .nav-link.active { color: #fff; background-color: rgba(255,255,255,0.2); border-left: 3px solid #fff; }
|
||||||
|
|||||||
@@ -48,6 +48,11 @@
|
|||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="bi bi-check-lg"></i> Opslaan
|
<i class="bi bi-check-lg"></i> Opslaan
|
||||||
</button>
|
</button>
|
||||||
|
<?php if ($isEditable): ?>
|
||||||
|
<a href="/<?= $currentLang ?>/<?= htmlspecialchars(pathinfo($file, PATHINFO_DIRNAME) . '/' . pathinfo($file, PATHINFO_FILENAME)) ?>" target="_blank" class="btn btn-outline-info" title="Open in nieuw tabblad">
|
||||||
|
<i class="bi bi-eye"></i> Preview
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
<a href="/admin/content?dir=<?= urlencode(dirname($file)) ?>" class="btn btn-outline-secondary" id="back-btn">Terug</a>
|
<a href="/admin/content?dir=<?= urlencode(dirname($file)) ?>" class="btn btn-outline-secondary" id="back-btn">Terug</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -62,6 +62,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header"><i class="bi bi-activity"></i> Recente activiteit</div>
|
||||||
|
<div class="card-body" style="max-height: 300px; overflow-y: auto;">
|
||||||
|
<?php if (empty($recentLogs)): ?>
|
||||||
|
<p class="text-muted mb-0">Geen activiteit geregistreerd.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<?php foreach ($recentLogs as $log): ?>
|
||||||
|
<li class="mb-2 pb-2 border-bottom small">
|
||||||
|
<span class="text-muted"><?= htmlspecialchars($log['time']) ?></span>
|
||||||
|
<span class="badge bg-<?= $log['level'] === 'warning' ? 'warning text-dark' : ($log['level'] === 'error' ? 'danger' : 'info') ?> me-1"><?= htmlspecialchars($log['level']) ?></span>
|
||||||
|
<?= htmlspecialchars($log['message']) ?>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header"><i class="bi bi-lightning"></i> Snelle acties</div>
|
<div class="card-header"><i class="bi bi-lightning"></i> Snelle acties</div>
|
||||||
|
|||||||
@@ -83,12 +83,19 @@
|
|||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<strong><i class="bi bi-plug"></i> <?= htmlspecialchars($plugin['name']) ?></strong>
|
<strong><i class="bi bi-plug"></i> <?= htmlspecialchars($plugin['name']) ?></strong>
|
||||||
|
<div>
|
||||||
|
<?php if ($plugin['enabled']): ?>
|
||||||
|
<span class="badge bg-success me-1">Actief</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge bg-secondary me-1">Uitgeschakeld</span>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($plugin['viewable']): ?>
|
<?php if ($plugin['viewable']): ?>
|
||||||
<span class="badge bg-success">Zichtbaar</span>
|
<span class="badge bg-info">Zichtbaar</span>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="badge bg-secondary">Systeem</span>
|
<span class="badge bg-secondary">Systeem</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<table class="table table-sm mb-0">
|
<table class="table table-sm mb-0">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -119,8 +126,14 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<form method="POST" action="/admin/plugins-toggle?plugin=<?= urlencode($plugin['name']) ?>" class="d-inline">
|
<form method="POST" action="/admin/plugins-toggle?plugin=<?= urlencode($plugin['name']) ?>" class="d-inline">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
<button type="submit" class="btn btn-sm <?= $plugin['viewable'] ? 'btn-outline-warning' : 'btn-outline-success' ?>" title="<?= $plugin['viewable'] ? 'Verbergen' : 'Tonen' ?>">
|
<button type="submit" class="btn btn-sm <?= $plugin['enabled'] ? 'btn-outline-warning' : 'btn-outline-success' ?>" title="<?= $plugin['enabled'] ? 'Uitschakelen' : 'Activeren' ?>">
|
||||||
<i class="bi <?= $plugin['viewable'] ? 'bi-eye-slash' : 'bi-eye' ?>"></i> <?= $plugin['viewable'] ? 'Verberg' : 'Toon' ?>
|
<i class="bi <?= $plugin['enabled'] ? 'bi-pause-circle' : 'bi-play-circle' ?>"></i> <?= $plugin['enabled'] ? 'Uitschakelen' : 'Activeren' ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/admin/plugins-toggle-visibility?plugin=<?= urlencode($plugin['name']) ?>" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<button type="submit" class="btn btn-sm <?= $plugin['viewable'] ? 'btn-outline-secondary' : 'btn-outline-info' ?>" title="<?= $plugin['viewable'] ? 'Verbergen' : 'Tonen' ?>">
|
||||||
|
<i class="bi <?= $plugin['viewable'] ? 'bi-eye-slash' : 'bi-eye' ?>"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<?php if ($plugin['has_config']): ?>
|
<?php if ($plugin['has_config']): ?>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Users list -->
|
<!-- Users list -->
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-header"><i class="bi bi-list"></i> Huidige gebruikers</div>
|
<div class="card-header"><i class="bi bi-list"></i> Huidige gebruikers</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<th>Gebruikersnaam</th>
|
<th>Gebruikersnaam</th>
|
||||||
<th>Rol</th>
|
<th>Rol</th>
|
||||||
<th>Aangemaakt</th>
|
<th>Aangemaakt</th>
|
||||||
<th style="width: 160px;">Acties</th>
|
<th style="width: 200px;">Acties</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -28,7 +28,12 @@
|
|||||||
<td><span class="badge bg-primary"><?= htmlspecialchars($u['role']) ?></span></td>
|
<td><span class="badge bg-primary"><?= htmlspecialchars($u['role']) ?></span></td>
|
||||||
<td class="text-muted"><?= htmlspecialchars($u['created']) ?></td>
|
<td class="text-muted"><?= htmlspecialchars($u['created']) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<!-- Change password -->
|
<?php if ($u['username'] === $user['username']): ?>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-warning" data-bs-toggle="modal" data-bs-target="#changeOwnPasswordModal" title="Eigen wachtwoord wijzigen">
|
||||||
|
<i class="bi bi-key"></i> Wachtwoord
|
||||||
|
</button>
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- Change password (admin for other users) -->
|
||||||
<form method="POST" action="/admin/users" class="d-inline">
|
<form method="POST" action="/admin/users" class="d-inline">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
<input type="hidden" name="action" value="change_password">
|
<input type="hidden" name="action" value="change_password">
|
||||||
@@ -40,7 +45,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<?php if ($u['username'] !== $user['username']): ?>
|
|
||||||
<form method="POST" action="/admin/users" class="d-inline ms-1" onsubmit="return confirm('Weet je zeker dat je deze gebruiker wilt verwijderen?')">
|
<form method="POST" action="/admin/users" class="d-inline ms-1" onsubmit="return confirm('Weet je zeker dat je deze gebruiker wilt verwijderen?')">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
<input type="hidden" name="action" value="delete">
|
<input type="hidden" name="action" value="delete">
|
||||||
@@ -57,6 +61,41 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Change own password modal -->
|
||||||
|
<div class="modal fade" id="changeOwnPasswordModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="POST" action="/admin/users">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||||
|
<input type="hidden" name="action" value="change_own_password">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="bi bi-key"></i> Eigen wachtwoord wijzigen</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="current_password" class="form-label">Huidig wachtwoord</label>
|
||||||
|
<input type="password" class="form-control" id="current_password" name="current_password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_password" class="form-label">Nieuw wachtwoord</label>
|
||||||
|
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="8">
|
||||||
|
<small class="form-text text-muted">Minimaal 8 tekens.</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">Bevestig nieuw wachtwoord</label>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required minlength="8">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuleren</button>
|
||||||
|
<button type="submit" class="btn btn-warning"><i class="bi bi-check-lg"></i> Wachtwoord wijzigen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add user form -->
|
<!-- Add user form -->
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ class CodePressCMS {
|
|||||||
$this->translations = $this->loadTranslations($this->currentLanguage);
|
$this->translations = $this->loadTranslations($this->currentLanguage);
|
||||||
|
|
||||||
// Initialize plugin manager (files already loaded in cms/core/index.php)
|
// Initialize plugin manager (files already loaded in cms/core/index.php)
|
||||||
$this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins');
|
$enabledPlugins = $this->config['enabled_plugins'] ?? [];
|
||||||
|
$this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins', $enabledPlugins);
|
||||||
$api = new CMSAPI($this);
|
$api = new CMSAPI($this);
|
||||||
$this->pluginManager->setAPI($api);
|
$this->pluginManager->setAPI($api);
|
||||||
|
|
||||||
@@ -207,6 +208,7 @@ class CodePressCMS {
|
|||||||
*/
|
*/
|
||||||
private function buildMenu() {
|
private function buildMenu() {
|
||||||
$this->menu = $this->scanDirectory($this->config['content_dir'], '');
|
$this->menu = $this->scanDirectory($this->config['content_dir'], '');
|
||||||
|
$this->pluginManager->doAction('onMenuBuild', $this->menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -274,6 +276,7 @@ class CodePressCMS {
|
|||||||
private function performSearch($query) {
|
private function performSearch($query) {
|
||||||
$this->searchResults = [];
|
$this->searchResults = [];
|
||||||
$this->searchInDirectory($this->config['content_dir'], '', $query);
|
$this->searchInDirectory($this->config['content_dir'], '', $query);
|
||||||
|
$this->pluginManager->doAction('onSearch', $query, $this->searchResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -437,7 +440,9 @@ class CodePressCMS {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$stats = stat($filePath);
|
$stats = stat($filePath);
|
||||||
$created = date('d-m-Y H:i', $stats['ctime']);
|
// Use birthtime if available (macOS/BSD), fall back to mtime on Linux where ctime is inode change time
|
||||||
|
$createdTimestamp = $stats['birthtime'] ?? $stats['mtime'];
|
||||||
|
$created = date('d-m-Y H:i', $createdTimestamp);
|
||||||
$modified = date('d-m-Y H:i', $stats['mtime']);
|
$modified = date('d-m-Y H:i', $stats['mtime']);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -615,32 +620,32 @@ class CodePressCMS {
|
|||||||
* @return string Content with auto-linked page titles
|
* @return string Content with auto-linked page titles
|
||||||
*/
|
*/
|
||||||
private function autoLinkPageTitles($content, $excludeTitle = '') {
|
private function autoLinkPageTitles($content, $excludeTitle = '') {
|
||||||
|
// Protect existing <a> tags and <h1> content with placeholders
|
||||||
|
$placeholders = [];
|
||||||
|
$content = preg_replace_callback('/<a\b[^>]*>.*?<\/a>|<h1\b[^>]*>.*?<\/h1>|\[([^\]]*)\]\(([^)]*)\)|^#{1,6}\s.*$/m', function($m) use (&$placeholders) {
|
||||||
|
$key = '@@LINK_' . count($placeholders) . '@@';
|
||||||
|
$placeholders[$key] = $m[0];
|
||||||
|
return $key;
|
||||||
|
}, $content);
|
||||||
|
|
||||||
// Get all available pages with their titles
|
// Get all available pages with their titles
|
||||||
$pages = $this->getAllPageTitles();
|
$pages = $this->getAllPageTitles();
|
||||||
|
|
||||||
foreach ($pages as $pagePath => $pageTitle) {
|
foreach ($pages as $pagePath => $pageTitle) {
|
||||||
// Create a pattern that matches the exact page title (case-insensitive)
|
if (strtolower($pageTitle) === strtolower($excludeTitle)) {
|
||||||
// Use word boundaries and avoid H1 tags to prevent linking inside headings
|
continue;
|
||||||
$pattern = '/\b' . preg_quote($pageTitle, '/') . '\b(?!(?=<\/h1>))/i';
|
|
||||||
|
|
||||||
// Replace with link, but avoid linking inside existing links, headings, or markdown
|
|
||||||
$replacement = function($matches) use ($pageTitle, $pagePath, $excludeTitle) {
|
|
||||||
$text = $matches[0];
|
|
||||||
|
|
||||||
// Check if we're inside an existing link or markdown syntax, or if it's the current page title
|
|
||||||
if (preg_match('/\[.*?\]\(.*?\)/', $text) ||
|
|
||||||
preg_match('/\[.*?\]:/', $text) ||
|
|
||||||
preg_match('/<a[^>]*>/', $text) ||
|
|
||||||
preg_match('/href=/', $text) ||
|
|
||||||
preg_match('/<h1>/', $text) ||
|
|
||||||
strtolower($text) === strtolower($excludeTitle)) {
|
|
||||||
return $text; // Don't link existing links, current page title, or H1 headings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<a href="' . $this->buildUrl($pagePath) . '" class="auto-link" title="' . $this->t('go_to') . ' ' . htmlspecialchars($pageTitle) . '">' . $text . '</a>';
|
$pattern = '/\b' . preg_quote($pageTitle, '/') . '\b/i';
|
||||||
};
|
|
||||||
|
|
||||||
$content = preg_replace_callback($pattern, $replacement, $content);
|
$content = preg_replace_callback($pattern, function($matches) use ($pageTitle, $pagePath) {
|
||||||
|
return '<a href="' . $this->buildUrl($pagePath) . '" class="auto-link" title="' . $this->t('go_to') . ' ' . htmlspecialchars($pageTitle) . '">' . $matches[0] . '</a>';
|
||||||
|
}, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore protected placeholders
|
||||||
|
foreach ($placeholders as $key => $value) {
|
||||||
|
$content = str_replace($key, $value, $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $content;
|
return $content;
|
||||||
@@ -702,39 +707,29 @@ class CodePressCMS {
|
|||||||
* @return string Formatted display name
|
* @return string Formatted display name
|
||||||
*/
|
*/
|
||||||
private function formatDisplayName($filename) {
|
private function formatDisplayName($filename) {
|
||||||
// Preserve leading dash before processing
|
|
||||||
$hasLeadingDash = $filename[0] === '-';
|
$hasLeadingDash = $filename[0] === '-';
|
||||||
if ($hasLeadingDash) {
|
if ($hasLeadingDash) {
|
||||||
$filename = substr($filename, 1);
|
$filename = substr($filename, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove language prefixes dynamically based on available languages
|
|
||||||
$availableLangs = array_keys($this->getAvailableLanguages());
|
$availableLangs = array_keys($this->getAvailableLanguages());
|
||||||
$langPattern = '/^(' . implode('|', $availableLangs) . ')\.(.+)$/';
|
$langPattern = '/^(' . implode('|', $availableLangs) . ')\.(.+)$/';
|
||||||
if (preg_match($langPattern, $filename, $matches)) {
|
if (preg_match($langPattern, $filename, $matches)) {
|
||||||
$filename = $matches[2];
|
$filename = $matches[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove file extensions (.md, .php, .html) from display names
|
|
||||||
$filename = preg_replace('/\.(md|php|html)$/', '', $filename);
|
$filename = preg_replace('/\.(md|php|html)$/', '', $filename);
|
||||||
|
|
||||||
// Handle special cases (case-sensitive display names)
|
|
||||||
$specialCases = [
|
|
||||||
'phpinfo' => 'phpinfo',
|
|
||||||
'ict' => 'ICT',
|
|
||||||
];
|
|
||||||
if (isset($specialCases[strtolower($filename)])) {
|
|
||||||
return ($hasLeadingDash ? '- ' : '') . $specialCases[strtolower($filename)];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace hyphens and underscores with spaces, then title case
|
|
||||||
$name = str_replace(['-', '_'], ' ', $filename);
|
$name = str_replace(['-', '_'], ' ', $filename);
|
||||||
$name = trim($name);
|
$name = trim($name);
|
||||||
$name = ucwords(strtolower($name));
|
$name = ucwords(strtolower($name));
|
||||||
|
|
||||||
// Post-process special cases in compound names
|
$specialCases = ['phpinfo' => 'phpinfo', 'ict' => 'ICT'];
|
||||||
foreach ($specialCases as $lower => $correct) {
|
$lower = strtolower($name);
|
||||||
$name = str_ireplace(ucfirst($lower), $correct, $name);
|
if (isset($specialCases[$lower])) {
|
||||||
|
$name = $specialCases[$lower];
|
||||||
|
} else {
|
||||||
|
$name = str_ireplace(array_keys($specialCases), array_values($specialCases), $name);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ($hasLeadingDash ? '- ' : '') . $name;
|
return ($hasLeadingDash ? '- ' : '') . $name;
|
||||||
@@ -869,7 +864,7 @@ class CodePressCMS {
|
|||||||
*
|
*
|
||||||
* @return array Guide page data
|
* @return array Guide page data
|
||||||
*/
|
*/
|
||||||
private function getGuidePage() {
|
private function getGuidePage() {
|
||||||
$lang = $this->currentLanguage;
|
$lang = $this->currentLanguage;
|
||||||
$guideFile = __DIR__ . '/../../../guide/' . $lang . '.codepress.md';
|
$guideFile = __DIR__ . '/../../../guide/' . $lang . '.codepress.md';
|
||||||
|
|
||||||
@@ -879,47 +874,14 @@ private function getGuidePage() {
|
|||||||
|
|
||||||
$content = file_get_contents($guideFile);
|
$content = file_get_contents($guideFile);
|
||||||
|
|
||||||
// Parse metadata first
|
// Reuse parseMarkdown to avoid duplicating CommonMark setup
|
||||||
$parsed = $this->parseMetadata($content);
|
$result = $this->parseMarkdown($content, $guideFile);
|
||||||
$metadata = $parsed['metadata'];
|
|
||||||
$contentWithoutMeta = $parsed['content'];
|
|
||||||
|
|
||||||
// Configure CommonMark environment (autoloader already loaded in bootstrap)
|
// Override title for guide
|
||||||
$config = [
|
$result['title'] = $this->t('manual') . ' - CodePress CMS';
|
||||||
'html_input' => 'strip',
|
$result['layout'] = $result['metadata']['layout'] ?? 'content';
|
||||||
'allow_unsafe_links' => false,
|
|
||||||
'max_nesting_level' => 100,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Create environment with extensions
|
return $result;
|
||||||
$environment = new \League\CommonMark\Environment\Environment($config);
|
|
||||||
$environment->addExtension(new \League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension());
|
|
||||||
$environment->addExtension(new \League\CommonMark\Extension\Autolink\AutolinkExtension());
|
|
||||||
$environment->addExtension(new \League\CommonMark\Extension\Strikethrough\StrikethroughExtension());
|
|
||||||
$environment->addExtension(new \League\CommonMark\Extension\Table\TableExtension());
|
|
||||||
$environment->addExtension(new \League\CommonMark\Extension\TaskList\TaskListExtension());
|
|
||||||
|
|
||||||
// Create converter
|
|
||||||
$converter = new \League\CommonMark\MarkdownConverter($environment);
|
|
||||||
|
|
||||||
// Convert to HTML
|
|
||||||
$body = $converter->convert($contentWithoutMeta)->getContent();
|
|
||||||
|
|
||||||
// Extract title from metadata or first H1
|
|
||||||
$title = $metadata['title'] ?? '';
|
|
||||||
if (empty($title) && preg_match('/^#\s+(.+)$/m', $contentWithoutMeta, $matches)) {
|
|
||||||
$title = trim($matches[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set special title for guide
|
|
||||||
$title = $this->t('manual') . ' - CodePress CMS';
|
|
||||||
|
|
||||||
return [
|
|
||||||
'title' => $title,
|
|
||||||
'content' => $body,
|
|
||||||
'metadata' => $metadata,
|
|
||||||
'layout' => $metadata['layout'] ?? 'content'
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1046,6 +1008,9 @@ private function getGuidePage() {
|
|||||||
*/
|
*/
|
||||||
public function render() {
|
public function render() {
|
||||||
$page = $this->getPage();
|
$page = $this->getPage();
|
||||||
|
$this->pluginManager->doAction('onPageLoad', $page);
|
||||||
|
$this->pluginManager->doAction('onBeforeRender');
|
||||||
|
|
||||||
$menu = $this->getMenu();
|
$menu = $this->getMenu();
|
||||||
$breadcrumb = $this->generateBreadcrumb();
|
$breadcrumb = $this->generateBreadcrumb();
|
||||||
|
|
||||||
@@ -1065,9 +1030,9 @@ private function getGuidePage() {
|
|||||||
// Prepare template data
|
// Prepare template data
|
||||||
$templateData = [
|
$templateData = [
|
||||||
'site_title' => $this->config['site_title'],
|
'site_title' => $this->config['site_title'],
|
||||||
'page_title' => htmlspecialchars($page['title']),
|
'page_title' => htmlspecialchars($this->pluginManager->applyFilters('onTitleFilter', $page['title'])),
|
||||||
|
|
||||||
'content' => $this->processContent($page['content']),
|
'content' => $this->pluginManager->applyFilters('onContentFilter', $this->processContent($page['content'])),
|
||||||
'sidebar_content' => $sidebarContent,
|
'sidebar_content' => $sidebarContent,
|
||||||
'layout' => $layout,
|
'layout' => $layout,
|
||||||
'page_metadata' => $page['metadata'] ?? [],
|
'page_metadata' => $page['metadata'] ?? [],
|
||||||
@@ -1101,8 +1066,11 @@ private function getGuidePage() {
|
|||||||
// Language
|
// Language
|
||||||
'current_lang' => $this->currentLanguage,
|
'current_lang' => $this->currentLanguage,
|
||||||
'current_lang_upper' => strtoupper($this->currentLanguage),
|
'current_lang_upper' => strtoupper($this->currentLanguage),
|
||||||
|
'current_page' => $_GET['page'] ?? $this->config['default_page'],
|
||||||
'available_langs' => array_map(function($lang) {
|
'available_langs' => array_map(function($lang) {
|
||||||
$lang['is_current'] = $lang['code'] === $this->currentLanguage;
|
$lang['is_current'] = $lang['code'] === $this->currentLanguage;
|
||||||
|
$page = $_GET['page'] ?? $this->config['default_page'];
|
||||||
|
$lang['url'] = '/' . $lang['code'] . ($page !== $this->config['default_page'] ? '/' . $page : '');
|
||||||
return $lang;
|
return $lang;
|
||||||
}, $this->getAvailableLanguages()),
|
}, $this->getAvailableLanguages()),
|
||||||
// Translations
|
// Translations
|
||||||
@@ -1180,6 +1148,8 @@ private function getGuidePage() {
|
|||||||
$renderedLayout = SimpleTemplate::render($finalTemplate, $templateData);
|
$renderedLayout = SimpleTemplate::render($finalTemplate, $templateData);
|
||||||
|
|
||||||
echo $renderedLayout;
|
echo $renderedLayout;
|
||||||
|
|
||||||
|
$this->pluginManager->doAction('onAfterRender', $renderedLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ class PluginManager
|
|||||||
private array $plugins = [];
|
private array $plugins = [];
|
||||||
private string $pluginsPath;
|
private string $pluginsPath;
|
||||||
private ?CMSAPI $api = null;
|
private ?CMSAPI $api = null;
|
||||||
|
private array $enabledPlugins = [];
|
||||||
|
private array $actions = [];
|
||||||
|
private array $filters = [];
|
||||||
|
|
||||||
public function __construct(string $pluginsPath)
|
public function __construct(string $pluginsPath, array $enabledPlugins = [])
|
||||||
{
|
{
|
||||||
$this->pluginsPath = $pluginsPath;
|
$this->pluginsPath = $pluginsPath;
|
||||||
|
$this->enabledPlugins = $enabledPlugins;
|
||||||
$this->loadPlugins();
|
$this->loadPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +37,11 @@ class PluginManager
|
|||||||
|
|
||||||
foreach ($pluginDirs as $pluginDir) {
|
foreach ($pluginDirs as $pluginDir) {
|
||||||
$pluginName = basename($pluginDir);
|
$pluginName = basename($pluginDir);
|
||||||
|
|
||||||
|
if (!in_array($pluginName, $this->enabledPlugins, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$pluginFile = $pluginDir . '/' . $pluginName . '.php';
|
$pluginFile = $pluginDir . '/' . $pluginName . '.php';
|
||||||
|
|
||||||
if (file_exists($pluginFile)) {
|
if (file_exists($pluginFile)) {
|
||||||
@@ -45,10 +54,59 @@ class PluginManager
|
|||||||
if ($this->api && method_exists($this->plugins[$pluginName], 'setAPI')) {
|
if ($this->api && method_exists($this->plugins[$pluginName], 'setAPI')) {
|
||||||
$this->plugins[$pluginName]->setAPI($this->api);
|
$this->plugins[$pluginName]->setAPI($this->api);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-register hooks from plugin methods
|
||||||
|
$hookMethods = ['onPageLoad', 'onBeforeRender', 'onAfterRender', 'onSearch', 'onMenuBuild'];
|
||||||
|
foreach ($hookMethods as $hook) {
|
||||||
|
if (method_exists($this->plugins[$pluginName], $hook)) {
|
||||||
|
$this->addAction($hook, [$this->plugins[$pluginName], $hook]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register filter methods
|
||||||
|
$filterMethods = ['onContentFilter', 'onTitleFilter', 'onMenuFilter'];
|
||||||
|
foreach ($filterMethods as $filter) {
|
||||||
|
if (method_exists($this->plugins[$pluginName], $filter)) {
|
||||||
|
$this->addFilter($filter, [$this->plugins[$pluginName], $filter]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addAction(string $hook, callable $callback, int $priority = 10): void
|
||||||
|
{
|
||||||
|
$this->actions[$hook][$priority][] = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addFilter(string $hook, callable $callback, int $priority = 10): void
|
||||||
|
{
|
||||||
|
$this->filters[$hook][$priority][] = $callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function doAction(string $hook, ...$args): void
|
||||||
|
{
|
||||||
|
if (!isset($this->actions[$hook])) return;
|
||||||
|
ksort($this->actions[$hook]);
|
||||||
|
foreach ($this->actions[$hook] as $callbacks) {
|
||||||
|
foreach ($callbacks as $callback) {
|
||||||
|
$callback(...$args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyFilters(string $hook, $value, ...$args)
|
||||||
|
{
|
||||||
|
if (!isset($this->filters[$hook])) return $value;
|
||||||
|
ksort($this->filters[$hook]);
|
||||||
|
foreach ($this->filters[$hook] as $callbacks) {
|
||||||
|
foreach ($callbacks as $callback) {
|
||||||
|
$value = $callback($value, ...$args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
public function getPlugin(string $name): ?object
|
public function getPlugin(string $name): ?object
|
||||||
{
|
{
|
||||||
@@ -60,6 +118,16 @@ class PluginManager
|
|||||||
return $this->plugins;
|
return $this->plugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEnabledPlugins(): array
|
||||||
|
{
|
||||||
|
return $this->enabledPlugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEnabled(string $pluginName): bool
|
||||||
|
{
|
||||||
|
return in_array($pluginName, $this->enabledPlugins, true);
|
||||||
|
}
|
||||||
|
|
||||||
public function isPluginViewable(object $plugin): bool
|
public function isPluginViewable(object $plugin): bool
|
||||||
{
|
{
|
||||||
if (method_exists($plugin, 'getConfig')) {
|
if (method_exists($plugin, 'getConfig')) {
|
||||||
@@ -78,7 +146,6 @@ class PluginManager
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by allowed plugins for this page
|
|
||||||
if ($allowedPlugins !== null && !in_array($pluginName, $allowedPlugins, true)) {
|
if ($allowedPlugins !== null && !in_array($pluginName, $allowedPlugins, true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<ul class="dropdown-menu dropdown-menu-end" role="menu">
|
<ul class="dropdown-menu dropdown-menu-end" role="menu">
|
||||||
{{#available_langs}}
|
{{#available_langs}}
|
||||||
<li role="none">
|
<li role="none">
|
||||||
<a class="dropdown-item {{#is_current}}active{{/is_current}}" href="/{{code}}" role="menuitem" {{#is_current}}aria-current="true"{{/is_current}} lang="{{code}}">
|
<a class="dropdown-item {{#is_current}}active{{/is_current}}" href="{{url}}" role="menuitem" {{#is_current}}aria-current="true"{{/is_current}} lang="{{code}}">
|
||||||
{{native_name}}
|
{{native_name}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<ul class="dropdown-menu dropdown-menu-end" role="menu">
|
<ul class="dropdown-menu dropdown-menu-end" role="menu">
|
||||||
{{#available_langs}}
|
{{#available_langs}}
|
||||||
<li role="none">
|
<li role="none">
|
||||||
<a class="dropdown-item {{#is_current}}active{{/is_current}}" href="/{{code}}" role="menuitem" {{#is_current}}aria-current="true"{{/is_current}} lang="{{code}}">
|
<a class="dropdown-item {{#is_current}}active{{/is_current}}" href="{{url}}" role="menuitem" {{#is_current}}aria-current="true"{{/is_current}} lang="{{code}}">
|
||||||
{{native_name}}
|
{{native_name}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -20,6 +20,11 @@
|
|||||||
"website": "https:\/\/noorlander.info"
|
"website": "https:\/\/noorlander.info"
|
||||||
},
|
},
|
||||||
"show_version": false,
|
"show_version": false,
|
||||||
|
"enabled_plugins": [
|
||||||
|
"MQTTTracker",
|
||||||
|
"HTMLBlock",
|
||||||
|
"test"
|
||||||
|
],
|
||||||
"features": {
|
"features": {
|
||||||
"auto_link_pages": true,
|
"auto_link_pages": true,
|
||||||
"search_enabled": true,
|
"search_enabled": true,
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ class MQTTTracker
|
|||||||
$this->config = [
|
$this->config = [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'viewable' => false,
|
'viewable' => false,
|
||||||
'broker_host' => 'localhost',
|
'broker_host' => getenv('MQTT_BROKER_HOST') ?: 'localhost',
|
||||||
'broker_port' => 1883,
|
'broker_port' => getenv('MQTT_BROKER_PORT') ?: 1883,
|
||||||
'client_id' => 'codepress_cms',
|
'client_id' => 'codepress_cms',
|
||||||
'username' => '',
|
'username' => getenv('MQTT_USERNAME') ?: '',
|
||||||
'password' => '',
|
'password' => getenv('MQTT_PASSWORD') ?: '',
|
||||||
'topic_prefix' => 'codepress',
|
'topic_prefix' => 'codepress',
|
||||||
'track_visitors' => true,
|
'track_visitors' => true,
|
||||||
'track_pages' => true,
|
'track_pages' => true,
|
||||||
@@ -48,7 +48,13 @@ class MQTTTracker
|
|||||||
|
|
||||||
if (file_exists($configFile)) {
|
if (file_exists($configFile)) {
|
||||||
$jsonConfig = json_decode(file_get_contents($configFile), true);
|
$jsonConfig = json_decode(file_get_contents($configFile), true);
|
||||||
$this->config = array_merge($this->config, $jsonConfig);
|
// Only merge non-sensitive keys from config file
|
||||||
|
$sensitiveKeys = ['password', 'username'];
|
||||||
|
foreach ($jsonConfig as $key => $value) {
|
||||||
|
if (!in_array($key, $sensitiveKeys, true)) {
|
||||||
|
$this->config[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+140
-1
@@ -105,6 +105,10 @@ switch ($route) {
|
|||||||
handlePluginToggle($auth, $appConfig);
|
handlePluginToggle($auth, $appConfig);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'plugins-toggle-visibility':
|
||||||
|
handlePluginToggleVisibility($auth, $appConfig);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'plugins-delete':
|
case 'plugins-delete':
|
||||||
handlePluginDelete($auth, $appConfig);
|
handlePluginDelete($auth, $appConfig);
|
||||||
break;
|
break;
|
||||||
@@ -167,7 +171,7 @@ function handleDashboard(AdminAuth $auth, array $config): void
|
|||||||
$stats = [
|
$stats = [
|
||||||
'pages' => countFiles($contentDir, ['md', 'php', 'html']),
|
'pages' => countFiles($contentDir, ['md', 'php', 'html']),
|
||||||
'directories' => countDirs($contentDir),
|
'directories' => countDirs($contentDir),
|
||||||
'plugins' => countDirs($pluginsDir),
|
'plugins' => countEnabledPlugins($pluginsDir, $configJson),
|
||||||
'config_exists' => file_exists($configJson),
|
'config_exists' => file_exists($configJson),
|
||||||
'content_size' => formatSize(dirSize($contentDir)),
|
'content_size' => formatSize(dirSize($contentDir)),
|
||||||
'php_version' => PHP_VERSION,
|
'php_version' => PHP_VERSION,
|
||||||
@@ -176,6 +180,25 @@ function handleDashboard(AdminAuth $auth, array $config): void
|
|||||||
// Load site config
|
// Load site config
|
||||||
$siteConfig = file_exists($configJson) ? json_decode(file_get_contents($configJson), true) : [];
|
$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';
|
require __DIR__ . '/../admin/templates/layout.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,6 +272,18 @@ function handleContent(AdminAuth $auth, array $config): void
|
|||||||
require __DIR__ . '/../admin/templates/layout.php';
|
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
|
function handleContentEdit(AdminAuth $auth, array $config): void
|
||||||
{
|
{
|
||||||
$user = $auth->getCurrentUser();
|
$user = $auth->getCurrentUser();
|
||||||
@@ -278,6 +313,7 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
|||||||
} else {
|
} else {
|
||||||
// Handle rename
|
// Handle rename
|
||||||
$newFilename = trim($_POST['filename'] ?? '');
|
$newFilename = trim($_POST['filename'] ?? '');
|
||||||
|
$wasRenamed = false;
|
||||||
if (!empty($newFilename)) {
|
if (!empty($newFilename)) {
|
||||||
$newFilename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newFilename);
|
$newFilename = preg_replace('/[^a-zA-Z0-9._-]/', '-', $newFilename);
|
||||||
$newFilename .= '.' . $fileExt;
|
$newFilename .= '.' . $fileExt;
|
||||||
@@ -288,6 +324,8 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
|||||||
if ($realParentDir && strpos($realParentDir, $realContentDir) === 0) {
|
if ($realParentDir && strpos($realParentDir, $realContentDir) === 0) {
|
||||||
if ($newFilePath !== $filePath && !file_exists($newFilePath)) {
|
if ($newFilePath !== $filePath && !file_exists($newFilePath)) {
|
||||||
rename($filePath, $newFilePath);
|
rename($filePath, $newFilePath);
|
||||||
|
$wasRenamed = true;
|
||||||
|
adminLog($config, 'info', $user['username'] . ' hernoemde ' . basename($filePath) . ' naar ' . $newFilename);
|
||||||
$filePath = $newFilePath;
|
$filePath = $newFilePath;
|
||||||
$newFile = dirname($file) . '/' . $newFilename;
|
$newFile = dirname($file) . '/' . $newFilename;
|
||||||
$file = ltrim($newFile, './');
|
$file = ltrim($newFile, './');
|
||||||
@@ -307,6 +345,9 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
|||||||
: '';
|
: '';
|
||||||
$content = updateContentFrontmatter($content, 'plugins', $plugins);
|
$content = updateContentFrontmatter($content, 'plugins', $plugins);
|
||||||
file_put_contents($filePath, $content);
|
file_put_contents($filePath, $content);
|
||||||
|
if (!$wasRenamed) {
|
||||||
|
adminLog($config, 'info', $user['username'] . ' sloeg ' . basename($filePath) . ' op');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$message = 'Bestand opgeslagen.';
|
$message = 'Bestand opgeslagen.';
|
||||||
@@ -346,6 +387,10 @@ function handleContentEdit(AdminAuth $auth, array $config): void
|
|||||||
? array_map('trim', explode(',', $currentPlugins))
|
? 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';
|
require __DIR__ . '/../admin/templates/layout.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +450,7 @@ function handleContentNew(AdminAuth $auth, array $config): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
file_put_contents($filePath, $content);
|
file_put_contents($filePath, $content);
|
||||||
|
adminLog($config, 'info', $user['username'] . ' maakte ' . $filename . ' aan in ' . $dir);
|
||||||
header('Location: /admin/content?dir=' . urlencode($dir));
|
header('Location: /admin/content?dir=' . urlencode($dir));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -449,6 +495,8 @@ function handleContentDelete(AdminAuth $auth, array $config): void
|
|||||||
&& strpos($realPath, $realContentDir) === 0
|
&& strpos($realPath, $realContentDir) === 0
|
||||||
) {
|
) {
|
||||||
if (is_file($filePath)) {
|
if (is_file($filePath)) {
|
||||||
|
$user = $auth->getCurrentUser();
|
||||||
|
adminLog($config, 'info', $user['username'] . ' verwijderde ' . basename($filePath));
|
||||||
unlink($filePath);
|
unlink($filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -622,6 +670,7 @@ function handleConfig(AdminAuth $auth, array $config): void
|
|||||||
$messageType = 'danger';
|
$messageType = 'danger';
|
||||||
} else {
|
} else {
|
||||||
file_put_contents($configJson, json_encode($parsed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
file_put_contents($configJson, json_encode($parsed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||||
|
adminLog($config, 'info', $user['username'] . ' wijzigde site configuratie');
|
||||||
$message = 'Configuratie opgeslagen.';
|
$message = 'Configuratie opgeslagen.';
|
||||||
$messageType = 'success';
|
$messageType = 'success';
|
||||||
$jsonContent = null; // reload from file
|
$jsonContent = null; // reload from file
|
||||||
@@ -791,6 +840,15 @@ function handlePlugins(AdminAuth $auth, array $config): void
|
|||||||
$user = $auth->getCurrentUser();
|
$user = $auth->getCurrentUser();
|
||||||
$csrf = $auth->getCsrfToken();
|
$csrf = $auth->getCsrfToken();
|
||||||
$pluginsDir = $config['plugins_dir'];
|
$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 = [];
|
$plugins = [];
|
||||||
|
|
||||||
if (is_dir($pluginsDir)) {
|
if (is_dir($pluginsDir)) {
|
||||||
@@ -804,9 +862,19 @@ function handlePlugins(AdminAuth $auth, array $config): void
|
|||||||
$hasMainFile = file_exists($pluginPath . '/' . $item . '.php');
|
$hasMainFile = file_exists($pluginPath . '/' . $item . '.php');
|
||||||
$hasReadme = file_exists($pluginPath . '/README.md');
|
$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[] = [
|
$plugins[] = [
|
||||||
'name' => $item,
|
'name' => $item,
|
||||||
'path' => $pluginPath,
|
'path' => $pluginPath,
|
||||||
|
'enabled' => $isEnabled,
|
||||||
'viewable' => $pluginConfig['viewable'] ?? true,
|
'viewable' => $pluginConfig['viewable'] ?? true,
|
||||||
'config' => $pluginConfig,
|
'config' => $pluginConfig,
|
||||||
'has_config' => $hasConfig,
|
'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';
|
$route = 'plugins';
|
||||||
require __DIR__ . '/../admin/templates/layout.php';
|
require __DIR__ . '/../admin/templates/layout.php';
|
||||||
}
|
}
|
||||||
@@ -981,6 +1055,45 @@ function handlePluginToggle(AdminAuth $auth, array $config): void
|
|||||||
exit;
|
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'] ?? '')) {
|
if ($auth->verifyCsrf($_POST['csrf_token'] ?? '')) {
|
||||||
$configFile = $pluginPath . '/config.json';
|
$configFile = $pluginPath . '/config.json';
|
||||||
if (file_exists($configFile)) {
|
if (file_exists($configFile)) {
|
||||||
@@ -990,6 +1103,7 @@ function handlePluginToggle(AdminAuth $auth, array $config): void
|
|||||||
} else {
|
} else {
|
||||||
file_put_contents($configFile, json_encode(['viewable' => false], JSON_PRETTY_PRINT));
|
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');
|
header('Location: /admin/plugins');
|
||||||
@@ -1019,6 +1133,7 @@ function handlePluginDelete(AdminAuth $auth, array $config): void
|
|||||||
$file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
|
$file->isDir() ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
|
||||||
}
|
}
|
||||||
rmdir($pluginPath);
|
rmdir($pluginPath);
|
||||||
|
adminLog($config, 'info', ($_SESSION['admin_user'] ?? 'unknown') . ' verwijderde plugin ' . $pluginName);
|
||||||
}
|
}
|
||||||
|
|
||||||
header('Location: /admin/plugins');
|
header('Location: /admin/plugins');
|
||||||
@@ -1193,6 +1308,22 @@ function handleUsers(AdminAuth $auth, array $config): void
|
|||||||
);
|
);
|
||||||
$message = $result['message'];
|
$message = $result['message'];
|
||||||
$messageType = $result['success'] ? 'success' : 'danger';
|
$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;
|
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
|
function dirSize(string $dir): int
|
||||||
{
|
{
|
||||||
$size = 0;
|
$size = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user