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.'];
|
||||
}
|
||||
|
||||
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 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">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -8,7 +17,7 @@
|
||||
<link rel="stylesheet" href="/assets/css/bootstrap-icons.css">
|
||||
<style>
|
||||
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: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; }
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> Opslaan
|
||||
</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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -62,6 +62,26 @@
|
||||
</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="card shadow-sm">
|
||||
<div class="card-header"><i class="bi bi-lightning"></i> Snelle acties</div>
|
||||
|
||||
@@ -83,11 +83,18 @@
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong><i class="bi bi-plug"></i> <?= htmlspecialchars($plugin['name']) ?></strong>
|
||||
<?php if ($plugin['viewable']): ?>
|
||||
<span class="badge bg-success">Zichtbaar</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">Systeem</span>
|
||||
<?php endif; ?>
|
||||
<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']): ?>
|
||||
<span class="badge bg-info">Zichtbaar</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">Systeem</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm mb-0">
|
||||
@@ -119,8 +126,14 @@
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="/admin/plugins-toggle?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-warning' : 'btn-outline-success' ?>" title="<?= $plugin['viewable'] ? 'Verbergen' : 'Tonen' ?>">
|
||||
<i class="bi <?= $plugin['viewable'] ? 'bi-eye-slash' : 'bi-eye' ?>"></i> <?= $plugin['viewable'] ? 'Verberg' : 'Toon' ?>
|
||||
<button type="submit" class="btn btn-sm <?= $plugin['enabled'] ? 'btn-outline-warning' : 'btn-outline-success' ?>" title="<?= $plugin['enabled'] ? 'Uitschakelen' : 'Activeren' ?>">
|
||||
<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>
|
||||
</form>
|
||||
<?php if ($plugin['has_config']): ?>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row g-4">
|
||||
<!-- Users list -->
|
||||
<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="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
@@ -12,7 +12,7 @@
|
||||
<th>Gebruikersnaam</th>
|
||||
<th>Rol</th>
|
||||
<th>Aangemaakt</th>
|
||||
<th style="width: 160px;">Acties</th>
|
||||
<th style="width: 200px;">Acties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -28,19 +28,23 @@
|
||||
<td><span class="badge bg-primary"><?= htmlspecialchars($u['role']) ?></span></td>
|
||||
<td class="text-muted"><?= htmlspecialchars($u['created']) ?></td>
|
||||
<td>
|
||||
<!-- Change password -->
|
||||
<form method="POST" action="/admin/users" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
<input type="hidden" name="pw_username" value="<?= htmlspecialchars($u['username']) ?>">
|
||||
<div class="input-group input-group-sm d-inline-flex" style="width: auto;">
|
||||
<input type="password" name="new_password" placeholder="Nieuw ww" class="form-control form-control-sm" style="width: 100px;" required minlength="8">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning" title="Wachtwoord wijzigen">
|
||||
<i class="bi bi-key"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<?php if ($u['username'] !== $user['username']): ?>
|
||||
<?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">
|
||||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||||
<input type="hidden" name="action" value="change_password">
|
||||
<input type="hidden" name="pw_username" value="<?= htmlspecialchars($u['username']) ?>">
|
||||
<div class="input-group input-group-sm d-inline-flex" style="width: auto;">
|
||||
<input type="password" name="new_password" placeholder="Nieuw ww" class="form-control form-control-sm" style="width: 100px;" required minlength="8">
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning" title="Wachtwoord wijzigen">
|
||||
<i class="bi bi-key"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<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="action" value="delete">
|
||||
@@ -57,6 +61,41 @@
|
||||
</table>
|
||||
</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>
|
||||
|
||||
<!-- Add user form -->
|
||||
|
||||
@@ -46,7 +46,8 @@ class CodePressCMS {
|
||||
$this->translations = $this->loadTranslations($this->currentLanguage);
|
||||
|
||||
// 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);
|
||||
$this->pluginManager->setAPI($api);
|
||||
|
||||
@@ -207,6 +208,7 @@ class CodePressCMS {
|
||||
*/
|
||||
private function buildMenu() {
|
||||
$this->menu = $this->scanDirectory($this->config['content_dir'], '');
|
||||
$this->pluginManager->doAction('onMenuBuild', $this->menu);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,6 +276,7 @@ class CodePressCMS {
|
||||
private function performSearch($query) {
|
||||
$this->searchResults = [];
|
||||
$this->searchInDirectory($this->config['content_dir'], '', $query);
|
||||
$this->pluginManager->doAction('onSearch', $query, $this->searchResults);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -437,7 +440,9 @@ class CodePressCMS {
|
||||
}
|
||||
|
||||
$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']);
|
||||
|
||||
return [
|
||||
@@ -615,32 +620,32 @@ class CodePressCMS {
|
||||
* @return string Content with auto-linked page titles
|
||||
*/
|
||||
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
|
||||
$pages = $this->getAllPageTitles();
|
||||
|
||||
foreach ($pages as $pagePath => $pageTitle) {
|
||||
// Create a pattern that matches the exact page title (case-insensitive)
|
||||
// Use word boundaries and avoid H1 tags to prevent linking inside headings
|
||||
$pattern = '/\b' . preg_quote($pageTitle, '/') . '\b(?!(?=<\/h1>))/i';
|
||||
if (strtolower($pageTitle) === strtolower($excludeTitle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -702,39 +707,29 @@ class CodePressCMS {
|
||||
* @return string Formatted display name
|
||||
*/
|
||||
private function formatDisplayName($filename) {
|
||||
// Preserve leading dash before processing
|
||||
$hasLeadingDash = $filename[0] === '-';
|
||||
if ($hasLeadingDash) {
|
||||
$filename = substr($filename, 1);
|
||||
}
|
||||
|
||||
// Remove language prefixes dynamically based on available languages
|
||||
$availableLangs = array_keys($this->getAvailableLanguages());
|
||||
$langPattern = '/^(' . implode('|', $availableLangs) . ')\.(.+)$/';
|
||||
if (preg_match($langPattern, $filename, $matches)) {
|
||||
$filename = $matches[2];
|
||||
}
|
||||
|
||||
// Remove file extensions (.md, .php, .html) from display names
|
||||
$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 = trim($name);
|
||||
$name = ucwords(strtolower($name));
|
||||
|
||||
// Post-process special cases in compound names
|
||||
foreach ($specialCases as $lower => $correct) {
|
||||
$name = str_ireplace(ucfirst($lower), $correct, $name);
|
||||
$specialCases = ['phpinfo' => 'phpinfo', 'ict' => 'ICT'];
|
||||
$lower = strtolower($name);
|
||||
if (isset($specialCases[$lower])) {
|
||||
$name = $specialCases[$lower];
|
||||
} else {
|
||||
$name = str_ireplace(array_keys($specialCases), array_values($specialCases), $name);
|
||||
}
|
||||
|
||||
return ($hasLeadingDash ? '- ' : '') . $name;
|
||||
@@ -869,57 +864,24 @@ class CodePressCMS {
|
||||
*
|
||||
* @return array Guide page data
|
||||
*/
|
||||
private function getGuidePage() {
|
||||
private function getGuidePage() {
|
||||
$lang = $this->currentLanguage;
|
||||
$guideFile = __DIR__ . '/../../../guide/' . $lang . '.codepress.md';
|
||||
|
||||
if (!file_exists($guideFile)) {
|
||||
$guideFile = __DIR__ . '/../../../guide/en.codepress.md'; // Fallback to English
|
||||
}
|
||||
}
|
||||
|
||||
$content = file_get_contents($guideFile);
|
||||
|
||||
// Parse metadata first
|
||||
$parsed = $this->parseMetadata($content);
|
||||
$metadata = $parsed['metadata'];
|
||||
$contentWithoutMeta = $parsed['content'];
|
||||
// Reuse parseMarkdown to avoid duplicating CommonMark setup
|
||||
$result = $this->parseMarkdown($content, $guideFile);
|
||||
|
||||
// Configure CommonMark environment (autoloader already loaded in bootstrap)
|
||||
$config = [
|
||||
'html_input' => 'strip',
|
||||
'allow_unsafe_links' => false,
|
||||
'max_nesting_level' => 100,
|
||||
];
|
||||
// Override title for guide
|
||||
$result['title'] = $this->t('manual') . ' - CodePress CMS';
|
||||
$result['layout'] = $result['metadata']['layout'] ?? 'content';
|
||||
|
||||
// Create environment with extensions
|
||||
$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'
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1046,6 +1008,9 @@ private function getGuidePage() {
|
||||
*/
|
||||
public function render() {
|
||||
$page = $this->getPage();
|
||||
$this->pluginManager->doAction('onPageLoad', $page);
|
||||
$this->pluginManager->doAction('onBeforeRender');
|
||||
|
||||
$menu = $this->getMenu();
|
||||
$breadcrumb = $this->generateBreadcrumb();
|
||||
|
||||
@@ -1065,9 +1030,9 @@ private function getGuidePage() {
|
||||
// Prepare template data
|
||||
$templateData = [
|
||||
'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,
|
||||
'layout' => $layout,
|
||||
'page_metadata' => $page['metadata'] ?? [],
|
||||
@@ -1101,8 +1066,11 @@ private function getGuidePage() {
|
||||
// Language
|
||||
'current_lang' => $this->currentLanguage,
|
||||
'current_lang_upper' => strtoupper($this->currentLanguage),
|
||||
'current_page' => $_GET['page'] ?? $this->config['default_page'],
|
||||
'available_langs' => array_map(function($lang) {
|
||||
$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;
|
||||
}, $this->getAvailableLanguages()),
|
||||
// Translations
|
||||
@@ -1180,6 +1148,8 @@ private function getGuidePage() {
|
||||
$renderedLayout = SimpleTemplate::render($finalTemplate, $templateData);
|
||||
|
||||
echo $renderedLayout;
|
||||
|
||||
$this->pluginManager->doAction('onAfterRender', $renderedLayout);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,10 +5,14 @@ class PluginManager
|
||||
private array $plugins = [];
|
||||
private string $pluginsPath;
|
||||
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->enabledPlugins = $enabledPlugins;
|
||||
$this->loadPlugins();
|
||||
}
|
||||
|
||||
@@ -33,6 +37,11 @@ class PluginManager
|
||||
|
||||
foreach ($pluginDirs as $pluginDir) {
|
||||
$pluginName = basename($pluginDir);
|
||||
|
||||
if (!in_array($pluginName, $this->enabledPlugins, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pluginFile = $pluginDir . '/' . $pluginName . '.php';
|
||||
|
||||
if (file_exists($pluginFile)) {
|
||||
@@ -45,11 +54,60 @@ class PluginManager
|
||||
if ($this->api && method_exists($this->plugins[$pluginName], 'setAPI')) {
|
||||
$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
|
||||
{
|
||||
return $this->plugins[$name] ?? null;
|
||||
@@ -60,6 +118,16 @@ class PluginManager
|
||||
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
|
||||
{
|
||||
if (method_exists($plugin, 'getConfig')) {
|
||||
@@ -78,7 +146,6 @@ class PluginManager
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter by allowed plugins for this page
|
||||
if ($allowedPlugins !== null && !in_array($pluginName, $allowedPlugins, true)) {
|
||||
continue;
|
||||
}
|
||||
@@ -107,4 +174,4 @@ class PluginManager
|
||||
|
||||
return $sidebarContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<ul class="dropdown-menu dropdown-menu-end" role="menu">
|
||||
{{#available_langs}}
|
||||
<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}}
|
||||
</a>
|
||||
</li>
|
||||
@@ -47,7 +47,7 @@
|
||||
<ul class="dropdown-menu dropdown-menu-end" role="menu">
|
||||
{{#available_langs}}
|
||||
<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}}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
"website": "https:\/\/noorlander.info"
|
||||
},
|
||||
"show_version": false,
|
||||
"enabled_plugins": [
|
||||
"MQTTTracker",
|
||||
"HTMLBlock",
|
||||
"test"
|
||||
],
|
||||
"features": {
|
||||
"auto_link_pages": true,
|
||||
"search_enabled": true,
|
||||
|
||||
@@ -32,11 +32,11 @@ class MQTTTracker
|
||||
$this->config = [
|
||||
'enabled' => true,
|
||||
'viewable' => false,
|
||||
'broker_host' => 'localhost',
|
||||
'broker_port' => 1883,
|
||||
'broker_host' => getenv('MQTT_BROKER_HOST') ?: 'localhost',
|
||||
'broker_port' => getenv('MQTT_BROKER_PORT') ?: 1883,
|
||||
'client_id' => 'codepress_cms',
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'username' => getenv('MQTT_USERNAME') ?: '',
|
||||
'password' => getenv('MQTT_PASSWORD') ?: '',
|
||||
'topic_prefix' => 'codepress',
|
||||
'track_visitors' => true,
|
||||
'track_pages' => true,
|
||||
@@ -48,7 +48,13 @@ class MQTTTracker
|
||||
|
||||
if (file_exists($configFile)) {
|
||||
$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);
|
||||
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