v2.6.0: Content backup/git versioning, plugin type system, docs update

New features:
- ContentBackup class with ZIP backup/restore and git versioning
- Admin backup & restore page (content-backup.twig) with git init/commit/log/restore
- Plugin type system: system (blue) vs content (green) with visual badges
- PluginAPIInterface + AdminPluginAPI for plugin architecture
- Essential plugin flag (cannot edit/deactivate/delete)

Improvements:
- Consolidated enabled_plugins config (removed plugins.enabled)
- Removed Analytics/Logging toggles from admin config page
- Fixed Dashboard plugin Twig comments rendered as text
- Updated 20 guide files (NL+EN): configuratie, plugins, plugin-development,
  core-classes, theme-json, layouts, scss-styling, admin-beheerder, nieuw-thema, architectuur
- Improved accessibility test script (grep -E, min/max checks)

Cleanup:
- Removed unused classes: ARIAComponents, AccessibilityManager, ContentSecurityPolicy, etc.
- Removed vendor packages: mustache/mustache, php-mqtt/client
- Removed old templates: logs.twig, statistics.twig (now plugins)
- Moved language files to language/ directory

Tests:
- Pentest: 30/30 passed, 0 vulnerabilities
- WCAG 2.1 AA: 25/25 passed, 100% compliance
This commit is contained in:
2026-08-15 19:21:04 +02:00
parent cd498c8c3a
commit 1492dcf71f
207 changed files with 3742 additions and 22688 deletions
+234
View File
@@ -0,0 +1,234 @@
<?php
class Dashboard
{
private ?PluginAPIInterface $api = null;
public function setAPI(PluginAPIInterface $api): void
{
$this->api = $api;
}
public function getConfig(): array
{
return [
'title' => 'Dashboard',
'viewable' => false,
'type' => 'system',
];
}
public function getAdminMenu(): array
{
return [
[
'plugin' => 'Dashboard',
'route' => 'dashboard',
'label' => 'Dashboard',
'icon' => 'bi-speedometer2',
'section' => 'general',
],
];
}
public function handleAdminRoute(string $action): ?string
{
$siteConfig = $this->getSiteConfig();
$contentDir = $this->api ? $this->api->getContentDir() : '';
$pluginsDir = $this->api ? $this->api->getPluginsDir() : '';
$enabledPlugins = $this->api ? $this->api->getEnabledPlugins() : [];
$versionInfo = $this->api ? $this->api->getVersionInfo() : ['version' => '0.0.0'];
$stats = [
'pages' => $this->countFiles($contentDir, ['md', 'php', 'html']),
'directories' => $this->countDirs($contentDir),
'content_size' => $this->formatSize($this->dirSize($contentDir)),
'config_exists' => !empty($siteConfig),
'php_version' => PHP_VERSION,
'cms_version' => $versionInfo['version'] ?? '0.0.0',
'os' => PHP_OS_FAMILY . ' (' . php_uname('r') . ')',
];
// Build plugin overview
$pluginOverview = [];
if (is_dir($pluginsDir)) {
foreach (glob($pluginsDir . '/*', GLOB_ONLYDIR) as $pluginDir) {
$pluginName = basename($pluginDir);
$pluginType = $this->getPluginType($pluginDir, $pluginName);
$pluginOverview[$pluginName] = [
'enabled' => in_array($pluginName, $enabledPlugins, true),
'type' => $pluginType,
];
}
}
ksort($pluginOverview);
$siteTitle = $siteConfig['site_title'] ?? 'CodePress';
$defaultLang = $siteConfig['language']['default'] ?? 'nl';
ob_start();
?>
<h2 class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h2>
<div class="row g-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-info-circle"></i> Site informatie</div>
<div class="card-body">
<table class="table table-sm mb-0">
<tr><td class="text-muted">Site titel</td><td><?= htmlspecialchars($siteTitle) ?></td></tr>
<tr><td class="text-muted">Standaard taal</td><td><?= htmlspecialchars($defaultLang) ?></td></tr>
<tr><td class="text-muted">CodePress versie</td><td><?= htmlspecialchars($stats['cms_version']) ?></td></tr>
<tr><td class="text-muted">PHP versie</td><td><?= htmlspecialchars($stats['php_version']) ?></td></tr>
<tr><td class="text-muted">Besturingssysteem</td><td><?= htmlspecialchars($stats['os']) ?></td></tr>
<tr><td class="text-muted">Config geladen</td><td><?php if ($stats['config_exists']): ?><span class="badge bg-success">Ja</span><?php else: ?><span class="badge bg-danger">Nee</span><?php endif; ?></td></tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header"><i class="bi bi-folder2-open"></i> Content informatie</div>
<div class="card-body">
<table class="table table-sm mb-0">
<tr><td class="text-muted">Pagina's</td><td><span class="badge bg-primary"><?= $stats['pages'] ?></span></td></tr>
<tr><td class="text-muted">Mappen</td><td><span class="badge bg-warning text-dark"><?= $stats['directories'] ?></span></td></tr>
<tr><td class="text-muted">Content grootte</td><td><?= htmlspecialchars($stats['content_size']) ?></td></tr>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-plug"></i> Plugins</span>
<a href="/admin/plugins" class="btn btn-sm btn-outline-secondary">Beheren</a>
</div>
<div class="card-body">
<table class="table table-sm mb-0">
<?php foreach ($pluginOverview as $pluginName => $info): ?>
<tr>
<td>
<i class="bi bi-plug-fill"></i> <?= htmlspecialchars($pluginName) ?>
<?php if ($info['type'] === 'system'): ?>
<span class="badge bg-secondary fs-7">systeem</span>
<?php else: ?>
<span class="badge bg-info fs-7">content</span>
<?php endif; ?>
</td>
<td>
<?php if ($info['enabled']): ?>
<span class="badge bg-success">Actief</span>
<?php else: ?>
<span class="badge bg-secondary">Inactief</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($pluginOverview)): ?>
<tr><td colspan="2" class="text-muted text-center">Geen plugins gevonden.</td></tr>
<?php endif; ?>
</table>
</div>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
private function getSiteConfig(): array
{
if ($this->api === null) {
return [];
}
// AdminPluginAPI holds the full site config
if ($this->api instanceof AdminPluginAPI) {
return $this->api->getConfig('', []) ?? [];
}
return [];
}
private function getPluginType(string $pluginDir, string $pluginName): string
{
$pluginFile = $pluginDir . '/' . $pluginName . '.php';
if (!file_exists($pluginFile)) {
return 'content';
}
// Read the file to find 'type' in the config array
$source = file_get_contents($pluginFile);
if (preg_match("/'type'\s*=>\s*'([^']+)'/", $source, $m)) {
return $m[1];
}
return 'content';
}
private function countFiles(string $dir, array $extensions = []): int
{
if (!is_dir($dir)) return 0;
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if (!$file->isFile()) continue;
// Skip hidden/dash-prefixed
$relative = substr($file->getRealPath(), strlen(realpath($dir)) + 1);
$skip = false;
foreach (explode('/', str_replace('\\', '/', $relative)) as $seg) {
if ($seg !== '' && ($seg[0] === '.' || $seg[0] === '-')) { $skip = true; break; }
}
if ($skip) continue;
$ext = strtolower($file->getExtension());
if (empty($extensions) || in_array($ext, $extensions, true)) {
$count++;
}
}
return $count;
}
private function countDirs(string $dir): int
{
if (!is_dir($dir)) return 0;
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir()) {
$name = $file->getFilename();
if ($name[0] === '.' || $name[0] === '-') continue;
$count++;
}
}
return $count;
}
private function dirSize(string $dir): int
{
if (!is_dir($dir)) return 0;
$size = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$size += $file->getSize();
}
}
return $size;
}
private function formatSize(int $bytes): string
{
if ($bytes <= 0) return '0 B';
$units = ['B', 'KB', 'MB', 'GB'];
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
}