CMS 2.0 - Theme engine, logging, admin improvements

Major changes:
- New ThemeManager with Twig templating and SCSS compilation
- Dynamic themes system (themes/default, themes/demo)
- LogManager with SQLite storage and syslog forwarding
- RequestLogger with static helper methods
- Admin UI overhaul (Bootstrap 5, dark mode)
- Admin config page with logging and theme settings
- Admin logs page with filters and search
- Removed legacy Mustache templates
- Removed test plugin and theme
- Composer dependencies: Twig, scssphp, CommonMark, MaxMind GeoIP
This commit is contained in:
2026-08-08 18:02:14 +02:00
parent cc0e4c19c8
commit a1e5baacac
833 changed files with 108974 additions and 1386 deletions
+203
View File
@@ -0,0 +1,203 @@
<?php
use ScssPhp\ScssPhp\Compiler;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* ThemeManager - Resolves and renders the active theme
*
* Responsibilities:
* - Resolve the active theme directory from config
* - Load theme.json (title, default_layout, template mapping, colors)
* - Build a Twig environment rooted at the theme directory
* - Compile theme SCSS to CSS at runtime (cached by mtime)
* - Map a requested layout to a concrete .twig template, falling back
* to the theme's default_layout when the layout is unknown
*/
class ThemeManager {
private $config;
private $themeDir;
private $themeConfig;
private $twig;
private $compiledCssDir;
/**
* @param array $config Full CMS config (must contain 'theme_dir' and 'theme')
*/
public function __construct(array $config) {
$this->config = $config;
$this->themeDir = $config['theme_dir'] ?? (__DIR__ . '/../../../themes/' . ($config['active_theme'] ?? 'default'));
$this->themeConfig = $config['theme'] ?? [];
$this->compiledCssDir = __DIR__ . '/../../../public/themes';
$loader = new FilesystemLoader($this->themeDir);
$this->twig = new Environment($loader, [
'cache' => false,
'autoescape' => false,
]);
}
/**
* Get the absolute path of the active theme directory
*/
public function getThemeDir(): string {
return $this->themeDir;
}
/**
* Get the raw theme.json config array
*/
public function getThemeConfig(): array {
return $this->themeConfig;
}
/**
* Get the theme title (from theme.json 'title' or 'name')
*/
public function getTitle(): string {
return $this->themeConfig['title'] ?? $this->themeConfig['name'] ?? basename($this->themeDir);
}
/**
* Get the theme config section (default_template, background settings, etc.)
*/
public function getConfig(): array {
return $this->themeConfig['config'] ?? [];
}
/**
* Get the theme template section (layout key => .twig file mapping)
*/
public function getTemplates(): array {
return $this->themeConfig['template'] ?? [];
}
/**
* Resolve the .twig template file for a requested layout.
*
* Templates are defined in theme.json under the "template" section:
* { "template": { "full_content": "full_content.twig", ... } }
* The default template is defined in the "config" section:
* { "config": { "default_template": "full_content", ... } }
*
* Priority:
* 1. If the layout is a known key in the "template" section, use its mapped file.
* 2. Otherwise fall back to config.default_template.
* 3. Final safety net: full_content.twig.
*
* @param string $layout Requested layout key (e.g. 'left_sidebar')
* @return string Template name usable by the 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';
}
/**
* Get the list of available layout keys defined in the "template" section.
*
* @return array List of layout keys
*/
public function getLayouts(): array {
return array_keys($this->getTemplates());
}
/**
* Check whether a template file exists in the theme directory
*/
private function templateExists(string $file): bool {
$path = $this->themeDir . '/' . ltrim($file, '/');
return is_file($path);
}
/**
* Render a layout template with the given data.
*
* @param string $layout Requested layout key
* @param array $data Template variables
* @return string Rendered HTML
*/
public function render(string $layout, array $data): string {
$template = $this->getTemplateForLayout($layout);
return $this->twig->render($template, $data);
}
/**
* Compile the theme's SCSS to CSS (cached by source mtime).
*
* @return string|null Absolute path to the compiled CSS, or null if none
*/
public function compileCss(): ?string {
$scssFile = $this->themeDir . '/css/theme.scss';
if (!is_file($scssFile)) {
return null;
}
$themeName = basename($this->themeDir);
$outDir = $this->compiledCssDir . '/' . $themeName;
$outFile = $outDir . '/theme.css';
$cacheFile = $outDir . '/.mtime';
$mtime = filemtime($scssFile);
if (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 . '/css');
$css = $compiler->compileString(file_get_contents($scssFile))->getCss();
file_put_contents($outFile, $css);
file_put_contents($cacheFile, (string)$mtime);
return $outFile;
} catch (\Throwable $e) {
error_log('ThemeManager SCSS compile error: ' . $e->getMessage());
return null;
}
}
/**
* Get the public URL for the compiled theme CSS, or null if unavailable.
*/
public function getCssUrl(): ?string {
$compiled = $this->compileCss();
if ($compiled === null) {
return null;
}
return '/themes/' . basename($this->themeDir) . '/theme.css';
}
/**
* Get the public URL for the theme JS, or null if unavailable.
*/
public function getJsUrl(): ?string {
$jsFile = $this->themeDir . '/js/theme.js';
if (!is_file($jsFile)) {
return null;
}
return '/themes/' . basename($this->themeDir) . '/js/theme.js';
}
}