Plugin internationalisatie: plugins hebben eigen language/ mappen. Systeem plugins volgen admin taal (admin.php), content plugins volgen content taal (site.php). Fallback chain: geselecteerd -> plugin default_language -> CMS default. PluginManager/AdminPluginAPI/CMSAPI uitgebreid met getPluginTranslations()/t(). plugin.json settings ondersteunen label_key/help_key/option_label_key. Plugin uniformiteit: alle 6 plugins hebben uniforme structuur (README.md, assets/.gitkeep, language/nl|en/). plugins/README.md herschreven. guide plugin-development.md (NL+EN) volledig herschreven. Plugin editor vernieuwd: geneste bestandsbrowser zijbalk, nieuw bestand aanmaken, uploaden naar assets/, verwijderen en verplaatsen. Nieuwe routes: plugins-file-upload, plugins-file-delete, plugins-file-move. Path-traversal bescherming + protected plugins geblokkeerd. Media invoegen in editor: nieuw /admin/media-list JSON endpoint + herbruikbare _media-modal.twig include. Plugin-context scant assets/ map. editor-toolbar.js modeMap uitgebreid voor css/scss/js/json. Plugin overzicht knoppen: alleen iconen met title/aria-label. Tests: pentest 30/30, WCAG 2.1 AA 25/25.
242 lines
10 KiB
PHP
242 lines
10 KiB
PHP
<?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',
|
|
'label_key' => 'menu_label',
|
|
'icon' => 'bi-speedometer2',
|
|
'section' => 'general',
|
|
'permission' => 'dashboard',
|
|
],
|
|
];
|
|
}
|
|
|
|
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'];
|
|
|
|
// Plugin translations (admin context). Falls back to plugin default_language.
|
|
$t = $this->api ? $this->api->getPluginTranslations('Dashboard') : [];
|
|
$tr = function (string $key) use ($t): string {
|
|
return $t[$key] ?? $key;
|
|
};
|
|
|
|
$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> <?= htmlspecialchars($tr('page_title')) ?></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> <?= htmlspecialchars($tr('site_info')) ?></div>
|
|
<div class="card-body">
|
|
<table class="table table-sm mb-0">
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('site_title')) ?></td><td><?= htmlspecialchars($siteTitle) ?></td></tr>
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('default_lang')) ?></td><td><?= htmlspecialchars($defaultLang) ?></td></tr>
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('cms_version')) ?></td><td><?= htmlspecialchars($stats['cms_version']) ?></td></tr>
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('php_version')) ?></td><td><?= htmlspecialchars($stats['php_version']) ?></td></tr>
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('os')) ?></td><td><?= htmlspecialchars($stats['os']) ?></td></tr>
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('config_loaded')) ?></td><td><?php if ($stats['config_exists']): ?><span class="badge bg-success"><?= htmlspecialchars($tr('yes')) ?></span><?php else: ?><span class="badge bg-danger"><?= htmlspecialchars($tr('no')) ?></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> <?= htmlspecialchars($tr('content_info')) ?></div>
|
|
<div class="card-body">
|
|
<table class="table table-sm mb-0">
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('pages')) ?></td><td><span class="badge bg-primary"><?= $stats['pages'] ?></span></td></tr>
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('folders')) ?></td><td><span class="badge bg-warning text-dark"><?= $stats['directories'] ?></span></td></tr>
|
|
<tr><td class="text-muted"><?= htmlspecialchars($tr('content_size')) ?></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> <?= htmlspecialchars($tr('plugins')) ?></span>
|
|
<a href="/admin/plugins" class="btn btn-sm btn-outline-secondary"><?= htmlspecialchars($tr('manage')) ?></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"><?= htmlspecialchars($tr('plugin_type_system')) ?></span>
|
|
<?php else: ?>
|
|
<span class="badge bg-info fs-7"><?= htmlspecialchars($tr('plugin_type_content')) ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<?php if ($info['enabled']): ?>
|
|
<span class="badge bg-success"><?= htmlspecialchars($tr('active')) ?></span>
|
|
<?php else: ?>
|
|
<span class="badge bg-secondary"><?= htmlspecialchars($tr('inactive')) ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($pluginOverview)): ?>
|
|
<tr><td colspan="2" class="text-muted text-center"><?= htmlspecialchars($tr('no_plugins')) ?></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];
|
|
}
|
|
}
|