- Bug: dashboard toonde 0 content (AdminPluginAPI::getContentDir() gaf relatief pad terug zonder normalisatie) - Dynamische pad-resolutie: PluginAPIInterface uitgebreid met getProjectRoot/getContentDir/getPluginsDir/getVersionInfo; CMSAPI en AdminPluginAPI implementeren deze universeel - public/index.php media-serving gebruikt $config['content_dir'] i.p.v. hardcoded /content - Navigation en Logs plugins halen paden via de API i.p.v. hardcoded dirname(__DIR__) - WordPress-stijl docblocks toegevoegd voor alle classes, methods, properties en functies (~450 docblocks, @since 2.6.5) - Security: hardcoded plaintext-wachtwoord 'admin' verwijderd uit AdminAuth.php; bij eerste installatie wordt een cryptografisch veilig wachtwoord gegenereerd (random_bytes, 16 tekens) en eenmalig op het inlogscherm getoond - Security: git-geschiedenis schoongemaakt (admin.json, admin.json.example, admin-console/config/admin.json verwijderd uit alle commits; filter-branch over alle branches + tags, gc --prune --aggressive) - README.md, README.en.md, AGENTS.md bijgewerkt - Test-scripts bijgewerkt naar clean-URL structuur + actuele ARIA-waarden - Versie verhoogd naar 2.6.5 - Tests: pentest 29/29, WCAG 25/25, functioneel 16/16, enhanced 25/25
439 lines
13 KiB
PHP
439 lines
13 KiB
PHP
<?php
|
|
|
|
use ScssPhp\ScssPhp\Compiler;
|
|
use Twig\Environment;
|
|
use Twig\Loader\FilesystemLoader;
|
|
|
|
/**
|
|
* Beheert het actieve thema en de rendering hiervan.
|
|
*
|
|
* Verantwoordelijkheden:
|
|
* - Bepaalt de actieve thema-map aan de hand van de configuratie.
|
|
* - Laadt theme.json (titel, default_layout, template-mapping, kleuren).
|
|
* - Bouwt een Twig-omgeving geworteld in de thema-map.
|
|
* - Compileert thema-SCSS naar CSS (in assets/css_compiled/).
|
|
* - Mapt een aangevraagde layout naar een concreet .twig-template, met
|
|
* fallback naar de default_layout van het thema wanneer de layout
|
|
* onbekend is.
|
|
*
|
|
* @since 2.6.5
|
|
*/
|
|
class ThemeManager {
|
|
/**
|
|
* Volledige CMS-configuratie.
|
|
*
|
|
* @since 2.6.5
|
|
* @var array Bevat o.a. 'theme_dir' en 'theme'.
|
|
*/
|
|
private $config;
|
|
|
|
/**
|
|
* Absoluut pad naar de actieve thema-map.
|
|
*
|
|
* @since 2.6.5
|
|
* @var string
|
|
*/
|
|
private $themeDir;
|
|
|
|
/**
|
|
* Ruwe theme.json configuratie-array.
|
|
*
|
|
* @since 2.6.5
|
|
* @var array
|
|
*/
|
|
private $themeConfig;
|
|
|
|
/**
|
|
* Twig-omgeving voor het renderen van thema-templates.
|
|
*
|
|
* @since 2.6.5
|
|
* @var Environment
|
|
*/
|
|
private $twig;
|
|
|
|
/**
|
|
* Constructor: initialiseert de thema-manager met de CMS-configuratie.
|
|
*
|
|
* Stelt de thema-map, theme.json-configuratie en een Twig-omgeving in.
|
|
* De Twig-cache staat uit en auto-escape is uitgeschakeld (de thema's
|
|
* beheren eigen escaping waar nodig).
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @param array $config Volledige CMS-configuratie; moet 'theme_dir' en 'theme' bevatten.
|
|
*/
|
|
public function __construct(array $config) {
|
|
$this->config = $config;
|
|
$this->themeDir = $config['theme_dir'] ?? (__DIR__ . '/../../../themes/' . ($config['active_theme'] ?? 'default'));
|
|
$this->themeConfig = $config['theme'] ?? [];
|
|
|
|
$loader = new FilesystemLoader($this->themeDir);
|
|
$this->twig = new Environment($loader, [
|
|
'cache' => false,
|
|
'autoescape' => false,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Geeft het absolute pad van de actieve thema-map.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return string Absoluut pad naar de thema-map.
|
|
*/
|
|
public function getThemeDir(): string {
|
|
return $this->themeDir;
|
|
}
|
|
|
|
/**
|
|
* Geeft de ruwe theme.json configuratie-array.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return array Theme.json inhoud, of lege array bij afwezigheid.
|
|
*/
|
|
public function getThemeConfig(): array {
|
|
return $this->themeConfig;
|
|
}
|
|
|
|
/**
|
|
* Geeft de thema-titel.
|
|
*
|
|
* Leest eerst 'title' uit theme.json, dan 'name', en als laatste
|
|
* fallback de mapnaam van het thema.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return string Thematische titel.
|
|
*/
|
|
public function getTitle(): string {
|
|
return $this->themeConfig['title'] ?? $this->themeConfig['name'] ?? basename($this->themeDir);
|
|
}
|
|
|
|
/**
|
|
* Geeft de 'config'-sectie van theme.json.
|
|
*
|
|
* Bevat o.a. default_template en achtergrondinstellingen.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return array Config-sectie, of lege array bij afwezigheid.
|
|
*/
|
|
public function getConfig(): array {
|
|
return $this->themeConfig['config'] ?? [];
|
|
}
|
|
|
|
/**
|
|
* Geeft de 'template'-sectie van theme.json.
|
|
*
|
|
* Bevat de mapping van layout-key naar .twig-bestand.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return array<string,string> Layout-key => .twig-bestand mapping.
|
|
*/
|
|
public function getTemplates(): array {
|
|
return $this->themeConfig['template'] ?? [];
|
|
}
|
|
|
|
/**
|
|
* Bepaalt het .twig-templatebestand voor een aangevraagde layout.
|
|
*
|
|
* Templates worden gedefinieerd in theme.json onder de "template"-sectie:
|
|
* { "template": { "full_content": "full_content.twig", ... } }
|
|
* De standaardtemplate staat in de "config"-sectie:
|
|
* { "config": { "default_template": "full_content", ... } }
|
|
*
|
|
* Prioriteit:
|
|
* 1. Als de layout een bekende key is in de "template"-sectie, gebruik
|
|
* het bijbehorende bestand.
|
|
* 2. Anders fallback naar config.default_template.
|
|
* 3. Laatste veiligheidsnet: full_content.twig.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @param string $layout Aangevraagde layout-key (bijv. 'left_sidebar').
|
|
* @return string Templacenaam bruikbaar voor de Twig-loader.
|
|
*/
|
|
public function getTemplateForLayout(string $layout): string {
|
|
$layout = trim($layout);
|
|
$templates = $this->getTemplates();
|
|
|
|
if ($layout !== '' && isset($templates[$layout])) {
|
|
$file = $templates[$layout];
|
|
if ($this->templateExists($file)) {
|
|
return $file;
|
|
}
|
|
}
|
|
|
|
$config = $this->getConfig();
|
|
$default = $config['default_template'] ?? 'full_content';
|
|
if (isset($templates[$default])) {
|
|
$file = $templates[$default];
|
|
if ($this->templateExists($file)) {
|
|
return $file;
|
|
}
|
|
}
|
|
|
|
return 'full_content.twig';
|
|
}
|
|
|
|
/**
|
|
* Geeft de lijst met beschikbare layout-keys uit de "template"-sectie.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return array<string> Lijst met layout-keys.
|
|
*/
|
|
public function getLayouts(): array {
|
|
return array_keys($this->getTemplates());
|
|
}
|
|
|
|
/**
|
|
* Controleert of een templatebestand bestaat in de thema-map.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @param string $file Bestandsnaam van het template (relatief t.o.v. thema-map).
|
|
* @return bool True als het bestand bestaat.
|
|
*/
|
|
private function templateExists(string $file): bool {
|
|
$path = $this->themeDir . '/' . ltrim($file, '/');
|
|
return is_file($path);
|
|
}
|
|
|
|
/**
|
|
* Rendert een layout-template met de gegeven data.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @param string $layout Aangevraagde layout-key.
|
|
* @param array $data Template-variabelen.
|
|
* @return string Gerenderde HTML.
|
|
*/
|
|
public function render(string $layout, array $data): string {
|
|
$template = $this->getTemplateForLayout($layout);
|
|
return $this->twig->render($template, $data);
|
|
}
|
|
|
|
/**
|
|
* Compileert de SCSS van het thema naar CSS (gecacht op bron-mtime).
|
|
*
|
|
* Compileert vanuit assets/scss/theme.scss naar assets/css_compiled/theme.css.
|
|
* De gecompileerde CSS en een .mtime-cachebestand worden op alleen-lezen
|
|
* (0444) gezet om handmatige aanpassingen te voorkomen. Bij een fout
|
|
* wordt deze gelogd en null teruggegeven.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @param bool $force Forceer hercompilatie ongeacht de cache. Default false.
|
|
* @return string|null Absoluut pad naar de gecompileerde CSS, of null bij fout/afwezigheid.
|
|
*/
|
|
public function compileCss(bool $force = false): ?string {
|
|
$scssFile = $this->themeDir . '/assets/scss/theme.scss';
|
|
if (!is_file($scssFile)) {
|
|
return null;
|
|
}
|
|
|
|
$outDir = $this->themeDir . '/assets/css_compiled';
|
|
$outFile = $outDir . '/theme.css';
|
|
$cacheFile = $outDir . '/.mtime';
|
|
|
|
$mtime = filemtime($scssFile);
|
|
if (!$force && is_file($outFile) && is_file($cacheFile) && (int)file_get_contents($cacheFile) === $mtime) {
|
|
return $outFile;
|
|
}
|
|
|
|
if (!is_dir($outDir)) {
|
|
mkdir($outDir, 0755, true);
|
|
}
|
|
|
|
try {
|
|
$compiler = new Compiler();
|
|
$compiler->setImportPaths($this->themeDir . '/assets/scss');
|
|
$css = $compiler->compileString(file_get_contents($scssFile))->getCss();
|
|
|
|
// Remove read-only before writing (file may be locked from previous compile)
|
|
if (is_file($outFile)) {
|
|
@chmod($outFile, 0644);
|
|
}
|
|
if (is_file($cacheFile)) {
|
|
@chmod($cacheFile, 0644);
|
|
}
|
|
|
|
file_put_contents($outFile, $css);
|
|
file_put_contents($cacheFile, (string)$mtime);
|
|
|
|
// Set read-only to prevent manual edits
|
|
@chmod($outFile, 0444);
|
|
@chmod($cacheFile, 0444);
|
|
|
|
return $outFile;
|
|
} catch (\Throwable $e) {
|
|
error_log('ThemeManager SCSS compile error: ' . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Geeft de publieke URL voor de thema-CSS.
|
|
*
|
|
* Gebruikt assets/css_compiled/theme.css (gecompileerd vanuit SCSS door
|
|
* scssphp). Indien de gecompileerde CSS nog niet bestaat, wordt deze
|
|
* alsnog gecompileerd.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return string|null Publieke CSS-URL, of null indien niet beschikbaar.
|
|
*/
|
|
public function getCssUrl(): ?string {
|
|
$compiledCss = $this->themeDir . '/assets/css_compiled/theme.css';
|
|
|
|
if (is_file($compiledCss)) {
|
|
return $this->getThemeAssetUrl('css_compiled/theme.css');
|
|
}
|
|
|
|
$compiled = $this->compileCss();
|
|
if ($compiled !== null) {
|
|
return $this->getThemeAssetUrl('css_compiled/theme.css');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Geeft de publieke URL voor de thema-JS, of null indien niet beschikbaar.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return string|null Publieke JS-URL, of null indien niet beschikbaar.
|
|
*/
|
|
public function getJsUrl(): ?string {
|
|
$jsFile = $this->themeDir . '/assets/js/theme.js';
|
|
if (!is_file($jsFile)) {
|
|
return null;
|
|
}
|
|
return $this->getThemeAssetUrl('js/theme.js');
|
|
}
|
|
|
|
/**
|
|
* Geeft de publieke URL voor een thema-asset.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @param string $assetPath Pad naar de asset binnen de assets/-map van het thema.
|
|
* @return string Publieke URL in de vorm '/themes/<naam>/assets/<path>'.
|
|
*/
|
|
private function getThemeAssetUrl(string $assetPath): string {
|
|
$themeName = basename($this->themeDir);
|
|
return '/themes/' . $themeName . '/assets/' . $assetPath;
|
|
}
|
|
|
|
/**
|
|
* Controleert of het thema een SCSS-bronbestand heeft.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return bool True als assets/scss/theme.scss bestaat.
|
|
*/
|
|
public function hasScss(): bool {
|
|
return is_file($this->themeDir . '/assets/scss/theme.scss');
|
|
}
|
|
|
|
/**
|
|
* Controleert of de gecompileerde CSS nieuwer is dan de SCSS-bron.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return bool True als de CSS-_mtime >= SCSS-mtime, anders false.
|
|
*/
|
|
public function isScssCompiled(): bool {
|
|
$scssFile = $this->themeDir . '/assets/scss/theme.scss';
|
|
$cssFile = $this->themeDir . '/assets/css_compiled/theme.css';
|
|
|
|
if (!is_file($scssFile) || !is_file($cssFile)) {
|
|
return false;
|
|
}
|
|
|
|
return filemtime($cssFile) >= filemtime($scssFile);
|
|
}
|
|
|
|
/**
|
|
* Controleert of het thema een handmatig CSS-bestand heeft.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return bool True als assets/css/theme.css bestaat.
|
|
*/
|
|
public function hasManualCss(): bool {
|
|
return is_file($this->themeDir . '/assets/css/theme.css');
|
|
}
|
|
|
|
/**
|
|
* Geeft een lijst met CSS-bestanden in de assets/css/-map van het thema.
|
|
*
|
|
* De css_compiled-map wordt uitgesloten.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return array<string> Lijst met publieke CSS-URLs.
|
|
*/
|
|
public function getCssFiles(): array {
|
|
$cssDir = $this->themeDir . '/assets/css';
|
|
$files = [];
|
|
|
|
if (!is_dir($cssDir)) {
|
|
return $files;
|
|
}
|
|
|
|
foreach (scandir($cssDir) as $file) {
|
|
if ($file[0] === '.') continue;
|
|
if (pathinfo($file, PATHINFO_EXTENSION) === 'css') {
|
|
$files[] = $this->getThemeAssetUrl('css/' . $file);
|
|
}
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* Geeft een lijst met JS-bestanden in de assets/js/-map van het thema.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return array<string> Lijst met publieke JS-URLs.
|
|
*/
|
|
public function getJsFiles(): array {
|
|
$jsDir = $this->themeDir . '/assets/js';
|
|
$files = [];
|
|
|
|
if (!is_dir($jsDir)) {
|
|
return $files;
|
|
}
|
|
|
|
foreach (scandir($jsDir) as $file) {
|
|
if ($file[0] === '.') continue;
|
|
if (pathinfo($file, PATHINFO_EXTENSION) === 'js') {
|
|
$files[] = $this->getThemeAssetUrl('js/' . $file);
|
|
}
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* Geeft de URL voor de favicon indien deze bestaat.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return string|null Publieke favicon-URL, of null indien niet beschikbaar.
|
|
*/
|
|
public function getFaviconUrl(): ?string {
|
|
$faviconFile = $this->themeDir . '/assets/img/favicon.svg';
|
|
if (!is_file($faviconFile)) {
|
|
return null;
|
|
}
|
|
return $this->getThemeAssetUrl('img/favicon.svg');
|
|
}
|
|
}
|