Security: verwijder hardcoded wachtwoord, voeg random-wachtwoord-generator toe bij eerste installatie

This commit is contained in:
2026-08-27 08:57:05 +00:00
parent 6485f693dc
commit 74612aefbb
55 changed files with 6019 additions and 876 deletions
+57 -11
View File
@@ -1,13 +1,46 @@
<?php
/**
* Analytics - Aggregated stats recorder & statistics manager for CodePress CMS
* Analytics - Aggregated stats recorder & statistics manager for CodePress CMS.
*
* Registreert geaggregeerde bezoekersstatistieken (views, uniques, landen,
* referrers, bots) in een JSON-bestand met file locking en retentie-cleanup.
*
* @since 2.6.4
*/
class Analytics
{
/**
* Pad naar het JSON-statistiekbestand.
*
* @since 2.6.4
* @var string Pad naar stats.json.
*/
private string $statsFile;
/**
* Analytics configuratie-array.
*
* @since 2.6.4
* @var array<string,mixed> Analytics configuratie.
*/
private array $config;
/**
* Initialiseer de Analytics-recorder.
*
* Stelt het pad naar het statistiekbestand in en maakt de opslagmap aan
* indien deze nog niet bestaat.
*
* @since 2.6.4
*
* @param array $analyticsConfig {
* Optioneel. Analytics configuratie.
*
* @type bool $enabled Of analytics actief is. Default false.
* @type int $retention_days Aantal dagen dat statistieken bewaard blijven. Default 400.
* }
*/
public function __construct(array $analyticsConfig = [])
{
$this->config = $analyticsConfig;
@@ -19,14 +52,21 @@ class Analytics
}
/**
* Record a page visit in aggregated stats.json
* Registreer een paginabezoek in de geaggregeerde statistieken.
*
* @param string $page Page key
* @param string $ip Client IP
* @param string $userAgent User Agent string
* @param string $referrer Referrer string
* @param string $country Resolved country code (e.g. NL, BE)
* @param string $status Status string (ok, blocked:ai, blocked:ratelimit, etc.)
* Identificeert bezoeker (bot/mens), anonimiseert het IP-adres per dag met
* een zout, aggregeert totals/countries/pages/referrers/days/uniques, past
* de retentie toe en schrijft atomair (met bestandslock) naar stats.json.
*
* @since 2.6.4
*
* @param string $page Page key van de bezochte pagina.
* @param string $ip Client IP-adres.
* @param string $userAgent User-Agent string van de bezoeker.
* @param string $referrer Referrer URL string.
* @param string|null $country Opgeloste landcode (bijv. NL, BE). Null indien onbekend.
* @param string $status Status string (ok, blocked:ai, blocked:ratelimit, enz.).
* @return void
*/
public function record(string $page, string $ip, string $userAgent, string $referrer, ?string $country, string $status): void
{
@@ -145,10 +185,16 @@ class Analytics
}
/**
* Get aggregated statistics data for a specific period
* Haal geaggregeerde statistieken op voor een specifieke periode.
*
* @param int $days Number of days (e.g. 7, 30, 90, 0 for all)
* @return array Aggregated stats
* Leest stats.json en aggregeert totals, landen, pagina's, referrers en
* een daily chart. Bij $days = 0 wordt alles-tijd geretourneerd, anders
* worden ontbrekende dagen in de range opgevuld voor een vloeiende grafiek.
*
* @since 2.6.4
*
* @param int $days Aantal dagen (bijv. 7, 30, 90), of 0 voor alles-tijd. Default 30.
* @return array<string,mixed> Geaggregeerde statistieken met keys totals, countries, pages, referrers, daily_chart.
*/
public function getStats(int $days = 30): array
{
+41 -12
View File
@@ -1,12 +1,25 @@
<?php
/**
* BotGuard - Bot, AI Crawler, and Scraper detection & protection
* BotGuard - Bot, AI Crawler, and Scraper detection & protection.
*
* Detecteert en blokkeert bots, AI-crawlers en scrapers op basis van de
* User-Agent string, en genereert dynamische robots.txt-inhoud op basis
* van de beveiligingsinstellingen.
*
* @since 2.6.4
*/
class BotGuard
{
/**
* Map of bot signatures by category and pattern
* Geeft de map met bot-handtekeningen per categorie en patroon.
*
* Retourneert een multi-dimensionale array met de categorieën 'ai',
* 'search' en 'scraper', elk met een mapping van patroon naar weergavelabel.
*
* @since 2.6.4
*
* @return array<string,array<string,string>> Bot-handtekeningen per categorie.
*/
public static function getBotSignatures(): array
{
@@ -62,10 +75,16 @@ class BotGuard
}
/**
* Identify a User-Agent string
* Identificeer een User-Agent string.
*
* @param string $ua User-Agent string
* @return array Array with category, pattern, and display label
* Vergelijkt de User-Agent met de bekende bot-handtekeningen (ai, search,
* scraper), daarna met een generieke bot-regex. Bij geen match wordt de
* bezoeker als mens beschouwd.
*
* @since 2.6.4
*
* @param string $ua User-Agent string.
* @return array{category:string,pattern:string,label:string} Array met category, pattern en display label.
*/
public static function identify(string $ua): array
{
@@ -105,11 +124,16 @@ class BotGuard
}
/**
* Determine if a request should be blocked based on security settings
* Bepaal of een request geblokkeerd moet worden op basis van beveiligingsinstellingen.
*
* @param string $ua User-Agent string
* @param array $securitySettings Security configuration array
* @return string|null Reason string if blocked, null if allowed
* Controleert achtereenvolgens: lege User-Agent, custom blocklist, en
* categorie-specifieke blokkades (ai, search, scraper, generic bot).
*
* @since 2.6.4
*
* @param string $ua User-Agent string.
* @param array $securitySettings Beveiligingsconfiguratie-array.
* @return string|null Reden-string indien geblokkeerd (bijv. 'blocked:ai'), null indien toegestaan.
*/
public static function shouldBlock(string $ua, array $securitySettings): ?string
{
@@ -158,10 +182,15 @@ class BotGuard
}
/**
* Generate dynamic robots.txt content based on security settings
* Genereer dynamische robots.txt-inhoud op basis van de beveiligingsinstellingen.
*
* @param array $securitySettings Security configuration array
* @return string Robots.txt content
* Stelt globale regels op voor zoekmachines en voegt, indien AI-bots
* geblokkeerd worden, per AI-crawler een Disallow-blok toe.
*
* @since 2.6.4
*
* @param array $securitySettings Beveiligingsconfiguratie-array.
* @return string Robots.txt-inhoud.
*/
public static function generateRobotsTxt(array $securitySettings): string
{
+140
View File
@@ -1,16 +1,93 @@
<?php
/**
* Cache interface voor key-value opslag met TTL-ondersteuning.
*
* Definieert een algemeen contract voor cache-implementaties binnen CodePress.
* Implementaties hoeven alleen lezen, schrijven, verwijderen en controleren
* van waarden op basis van een sleutel te ondersteunen.
*
* @since 2.6.4
*/
interface CacheInterface {
/**
* Haal een waarde uit de cache op basis van de sleutel.
*
* @since 2.6.4
*
* @param string $key Sleutel van de cache-waarde.
* @return mixed De gecachte waarde, of null wanneer deze ontbreekt of verlopen is.
*/
public function get(string $key);
/**
* Sla een waarde op in de cache met een time-to-live.
*
* @since 2.6.4
*
* @param string $key Sleutel waaronder de waarde wordt opgeslagen.
* @param mixed $value Te cachen waarde.
* @param int $ttl Levensduur in seconden. Default 3600.
* @return bool True bij succes, false bij een schrijffout.
*/
public function set(string $key, $value, int $ttl = 3600): bool;
/**
* Verwijder een waarde uit de cache.
*
* @since 2.6.4
*
* @param string $key Sleutel die verwijderd moet worden.
* @return bool True bij succes of wanneer de sleutel niet bestond.
*/
public function delete(string $key): bool;
/**
* Wis alle waarden uit de cache.
*
* @since 2.6.4
*
* @return bool True bij succes.
*/
public function clear(): bool;
/**
* Controleer of een sleutel bestaat en nog niet verlopen is.
*
* @since 2.6.4
*
* @param string $key Sleutel die gecontroleerd moet worden.
* @return bool True wanneer de sleutel bestaat en geldig is.
*/
public function has(string $key): bool;
}
/**
* Bestandssysteem-gebaseerde cache-implementatie.
*
* Slaat gecachte waarden op als geserialiseerde bestanden in een map. Elke
* cache-sleutel wordt omgezet naar een md5-hash zodat ook lange of vreemde
* sleutels veilig als bestandsnaam gebruikt kunnen worden. Verlopen bestanden
* worden bij lezen of controleren automatisch verwijderd.
*
* @since 2.6.4
*/
class FileCache implements CacheInterface {
/**
* Map waar de cache-bestanden in worden opgeslagen.
*
* @since 2.6.4
* @var string
*/
private string $cacheDir;
/**
* Maak een nieuwe file-cache aan en zorg dat de cachemap bestaat.
*
* @since 2.6.4
*
* @param string $cacheDir Pad naar de cachemap. Default '/tmp/codepress_cache'.
*/
public function __construct(string $cacheDir = '/tmp/codepress_cache') {
$this->cacheDir = $cacheDir;
if (!is_dir($this->cacheDir)) {
@@ -18,6 +95,17 @@ class FileCache implements CacheInterface {
}
}
/**
* Haal een waarde uit de cache.
*
* Wanneer het cache-bestand niet bestaat of verlopen is wordt null
* teruggegeven; in het laatste geval wordt het bestand direct verwijderd.
*
* @since 2.6.4
*
* @param string $key Sleutel van de op te halen waarde.
* @return mixed De gecachte waarde of null.
*/
public function get(string $key) {
$file = $this->getCacheFile($key);
if (!file_exists($file)) {
@@ -33,6 +121,16 @@ class FileCache implements CacheInterface {
return $data['value'];
}
/**
* Sla een waarde op in de cache met de opgegeven TTL.
*
* @since 2.6.4
*
* @param string $key Sleutel waaronder wordt opgeslagen.
* @param mixed $value Te cachen waarde.
* @param int $ttl Levensduur in seconden. Default 3600.
* @return bool True bij succes, false bij een schrijffout.
*/
public function set(string $key, $value, int $ttl = 3600): bool {
$file = $this->getCacheFile($key);
$data = [
@@ -43,6 +141,17 @@ class FileCache implements CacheInterface {
return file_put_contents($file, serialize($data)) !== false;
}
/**
* Verwijder een waarde uit de cache.
*
* Geeft true terug wanneer het bestand niet (meer) bestaat, zodat de
* aanroeper dit niet zelf hoeft te controleren.
*
* @since 2.6.4
*
* @param string $key Sleutel die verwijderd moet worden.
* @return bool True bij succes of wanneer de sleutel niet bestond.
*/
public function delete(string $key): bool {
$file = $this->getCacheFile($key);
if (file_exists($file)) {
@@ -51,6 +160,15 @@ class FileCache implements CacheInterface {
return true;
}
/**
* Wis alle cache-bestanden in de cachemap.
*
* Submappen worden niet verwijderd, alleen losse bestanden.
*
* @since 2.6.4
*
* @return bool True bij succes.
*/
public function clear(): bool {
$files = glob($this->cacheDir . '/*');
foreach ($files as $file) {
@@ -61,6 +179,17 @@ class FileCache implements CacheInterface {
return true;
}
/**
* Controleer of een sleutel bestaat en nog geldig is.
*
* Let op: verlopen bestanden worden hier niet automatisch verwijderd;
* controleer vooraf eventueel via get() indien opruiming gewenst is.
*
* @since 2.6.4
*
* @param string $key Sleutel die gecontroleerd moet worden.
* @return bool True wanneer de sleutel bestaat en de TTL nog niet verlopen is.
*/
public function has(string $key): bool {
$file = $this->getCacheFile($key);
if (!file_exists($file)) {
@@ -71,6 +200,17 @@ class FileCache implements CacheInterface {
return $data['expires'] > time();
}
/**
* Bepaal het pad naar het cache-bestand voor een sleutel.
*
* De sleutel wordt gehashed met md5 zodat deze veilig als bestandsnaam
* gebruikt kan worden, ongeacht de lengte of tekenset van de sleutel.
*
* @since 2.6.4
*
* @param string $key Cache-sleutel.
* @return string Volledig pad naar het cache-bestand.
*/
private function getCacheFile(string $key): string {
return $this->cacheDir . '/' . md5($key) . '.cache';
}
+420 -198
View File
@@ -3,36 +3,86 @@
/**
* CodePressCMS - Lightweight file-based content management system
*
* Features:
* - Markdown, PHP, and HTML content support
* - Dynamic navigation with dropdown menus
* - Search functionality
* - Breadcrumb navigation
* - Auto-linking between pages
* - Bootstrap 5 styling
* - File-based organization
* - SEO friendly URLs
* - Responsive design
*
* @author Edwin Noorlander
* @version 1.0.0
* @license MIT
* Hoofdclass van het file-based CodePress CMS.
*
* Lichtgewicht content management systeem zonder database. Ondersteunt
* Markdown, PHP en HTML content, dynamische navigatie met dropdown-menus,
* zoekfunctionaliteit, breadcrumb-navigatie, auto-linking tussen pagina's,
* Bootstrap 5 styling, file-based organisatie, SEO-vriendelijke URLs en
* responsive design.
*
* @since 2.6.4
*/
class CodePressCMS {
/**
* Configuratie-array met alle site-instellingen.
*
* @since 2.6.4
* @var array
*/
public $config;
/**
* Actieve taalcode van het huidige verzoek (bijv. 'nl' of 'en').
*
* @since 2.6.4
* @var string
*/
public $currentLanguage;
/**
* Resultaten van de laatst uitgevoerde zoekopdracht.
*
* @since 2.6.4
* @var array
*/
public $searchResults = [];
/**
* Opgebouwde menu-structuur vanuit de content-directory.
*
* @since 2.6.4
* @var array
*/
private $menu = [];
/**
* Effectieve standaardpagina die op de taal-root URL wordt geserveerd.
*
* Wordt lazy geinitialiseerd in getEffectiveDefaultPage().
*
* @since 2.6.4
* @var string|null
*/
private ?string $effectiveDefaultPage = null;
/**
* Geladen vertalingen voor de actieve taal.
*
* @since 2.6.4
* @var array
*/
private $translations = [];
/**
* PluginManager-instance die plugins en hooks beheert.
*
* @since 2.6.4
* @var PluginManager
*/
private $pluginManager;
/**
* Constructor - Initialize the CMS with configuration
*
* @param array $config Configuration array containing site settings
* Initialiseer het CMS met de opgegeven configuratie.
*
* Laadt versie-informatie, stelt de actieve taal en vertalingen in,
* initialiseert de PluginManager en de CMSAPI, bouwt het menu op en
* voert een eventuele zoekopdracht uit.
*
* @since 2.6.4
*
* @param array $config Configuration array containing site settings.
* @return void
*/
public function __construct($config) {
$this->config = $config;
@@ -62,12 +112,14 @@ class CodePressCMS {
}
/**
* Build a clean URL for a page
*
* @param string $page Page path (e.g., 'blog/leren/artikel')
* @param string|null $lang Language code
* @param array $params Additional query parameters
* @return string Clean URL
* Bouw een clean URL voor een pagina.
*
* @since 2.6.4
*
* @param string|null $page Page path (e.g., 'blog/leren/artikel').
* @param string|null $lang Language code.
* @param array $params Additional query parameters.
* @return string Clean URL.
*/
public function buildUrl($page = null, $lang = null, $params = []) {
$lang = $lang ?: $this->currentLanguage;
@@ -86,8 +138,16 @@ class CodePressCMS {
}
/**
* Sanitize page parameter to prevent XSS attacks
* Removes any characters that are not alphanumeric, dashes, underscores, or slashes
* Sanitize de pagina-parameter om XSS-aanvallen te voorkomen.
*
* Verwijdert alle tekens die niet alfanumeriek, streepje, underscore of
* slash zijn. De punt blijft behouden zodat content-bestandsextensies
* (md/php/html) in de URL overleven.
*
* @since 2.6.4
*
* @param string $page Page parameter.
* @return string Gesanitizeerde pagina-parameter of 'invalid-page'.
*/
private function sanitizePageParam(string $page): string {
// Remove any characters that could be used for XSS; keep the dot so
@@ -97,9 +157,13 @@ class CodePressCMS {
}
/**
* Resolve the effective default page (handles 'auto' mode)
* Bepaal de effectieve standaardpagina (behandelt 'auto' en 'newest' modus).
*
* @return string Page key that is served on the language root URL
* Het resultaat wordt gecached in $effectiveDefaultPage.
*
* @since 2.6.4
*
* @return string Page key that is served on the language root URL.
*/
public function getEffectiveDefaultPage(): string
{
@@ -115,11 +179,13 @@ class CodePressCMS {
}
/**
* Build a clean admin URL
*
* @param string $route Admin route
* @param array $params Additional query parameters
* @return string Clean admin URL
* Bouw een clean admin URL.
*
* @since 2.6.4
*
* @param string $route Admin route.
* @param array $params Additional query parameters.
* @return string Clean admin URL.
*/
public static function buildAdminUrl($route = '', $params = []) {
$url = $route ? '/admin/' . $route : '/admin';
@@ -130,16 +196,24 @@ class CodePressCMS {
}
/**
* Build URL string for template use (non-static, with current lang context)
* Bouw een URL-string voor gebruik in templates (niet-statisch, met huidige taal-context).
*
* @since 2.6.4
*
* @param string $page Pagina-pad.
* @param array $extraParams Extra query parameters.
* @return string Clean URL.
*/
public function url($page = 'index', $extraParams = []) {
return $this->buildUrl($page, $this->currentLanguage, $extraParams);
}
/**
* Get current language from request or config
*
* @return string Current language code
* Bepaal de actieve taal vanuit het verzoek of de configuratie.
*
* @since 2.6.4
*
* @return string Current language code.
*/
private function getCurrentLanguage() {
$lang = $_GET['lang'] ?? $this->config['language']['default'] ?? 'nl';
@@ -149,11 +223,13 @@ class CodePressCMS {
}
/**
* Get all available languages from the language directory
*
* Each language is a subdirectory under language/ containing a site.php file.
*
* @return array Available languages with their codes and names
* Verzamel alle beschikbare talen vanuit de language-directory.
*
* Elke taal is een subdirectory onder language/ met een site.php-bestand.
*
* @since 2.6.4
*
* @return array Available languages with their codes and names.
*/
public function getAvailableLanguages() {
$langDir = __DIR__ . '/../../../language/';
@@ -187,10 +263,14 @@ class CodePressCMS {
}
/**
* Load site translations for specified language
*
* @param string $lang Language code
* @return array Translations array
* Laad site-vertalingen voor de opgegeven taal.
*
* Valt terug op de standaardtaal en daarna op een lege array.
*
* @since 2.6.4
*
* @param string $lang Language code.
* @return array Translations array.
*/
private function loadTranslations($lang) {
$langDir = __DIR__ . '/../../../language/';
@@ -215,10 +295,14 @@ class CodePressCMS {
}
/**
* Get native language name for language code
*
* @param string $langCode Language code
* @return string Native language name
* Geef de native taalnaam voor een taalcode terug.
*
* Bevat een statische mapping van ISO-codes naar native taalnamen.
*
* @since 2.6.4
*
* @param string $langCode Language code.
* @return string Native language name.
*/
private function getNativeLanguageName($langCode) {
$names = [
@@ -297,18 +381,23 @@ class CodePressCMS {
}
/**
* Get translated text
*
* @param string $key Translation key
* @return string Translated text
* Geef vertaalde tekst voor een sleutel terug.
*
* @since 2.6.4
*
* @param string $key Translation key.
* @return string Translated text, of de sleutel zelf indien geen vertaling gevonden.
*/
public function t($key) {
return $this->translations[$key] ?? $key;
}
/**
* Build menu structure from content directory
*
* Bouw de menu-structuur op vanuit de content-directory.
*
* Roept de plugin-actie 'onMenuBuild' aan na het scannen.
*
* @since 2.6.4
* @return void
*/
private function buildMenu() {
@@ -317,11 +406,13 @@ class CodePressCMS {
}
/**
* Recursively scan directory for content files and folders
*
* @param string $dir Directory path to scan
* @param string $prefix Relative path prefix
* @return array Array of menu items
* Scan recursief een directory voor content-bestanden en mappen.
*
* @since 2.6.4
*
* @param string $dir Directory path to scan.
* @param string $prefix Relative path prefix.
* @return array Array of menu items.
*/
private function scanDirectory($dir, $prefix) {
if (!is_dir($dir)) return [];
@@ -375,9 +466,14 @@ class CodePressCMS {
}
/**
* Perform search across all content files
*
* @param string $query Search query string
* Voer een zoekopdracht uit over alle content-bestanden.
*
* Reset de zoekresultaten, doorzoekt de content-directory en vuurt de
* plugin-actie 'onSearch' af.
*
* @since 2.6.4
*
* @param string $query Search query string.
* @return void
*/
private function performSearch($query) {
@@ -387,11 +483,13 @@ class CodePressCMS {
}
/**
* Recursively search for query in directory files
*
* @param string $dir Directory to search in
* @param string $prefix Relative path prefix
* @param string $query Search query
* Doorzoek recursief de bestanden in een directory naar de query.
*
* @since 2.6.4
*
* @param string $dir Directory to search in.
* @param string $prefix Relative path prefix.
* @param string $query Search query.
* @return void
*/
private function searchInDirectory($dir, $prefix, $query) {
@@ -424,11 +522,13 @@ class CodePressCMS {
}
/**
* Create search snippet with highlighted query
*
* @param string $content Full content to create snippet from
* @param string $query Search query to highlight
* @return string Formatted snippet
* Maak een zoeksnippet met de gevonden query.
*
* @since 2.6.4
*
* @param string $content Full content to create snippet from.
* @param string $query Search query to highlight.
* @return string Formatted snippet.
*/
private function createSnippet($content, $query) {
$content = strip_tags($content);
@@ -441,9 +541,13 @@ class CodePressCMS {
}
/**
* Get current page content based on request
*
* @return array Page data with title and content
* Bepaal de content van de huidige pagina op basis van het verzoek.
*
* Behandelt zoek-, guide-, welcome- en 404-verzoeken, path-traversal-
* validatie, directory-listings en taal-specifieke bestandsversies.
*
* @since 2.6.4
* @return array Page data with title and content.
*/
public function getPage() {
if (isset($_GET['search'])) {
@@ -550,16 +654,19 @@ class CodePressCMS {
}
/**
* Resolve the first content file matching a base path by type priority.
* Bepaal het eerste bestaande content-bestand voor een base-path op type-prioriteit.
*
* A request without explicit extension (e.g. "test") serves the first
* existing file among test.md test.php test.html. When $preferredExt
* is given (the URL carried an extension), that type is tried first so
* /nl/test.php keeps serving test.php even when test.md also exists.
* Een verzoek zonder expliciete extensie (bijv. "test") serveert het
* eerste bestaande bestand uit test.md -> test.php -> test.html. Wanneer
* $preferredExt is opgegeven (de URL bevatte een extensie) wordt dat
* type eerst geprobeerd, zodat /nl/test.php test.php serveert ook als
* test.md bestaat.
*
* @param string $basePath Absolute path without extension
* @param string $preferredExt Optional extension to try first (md|php|html)
* @return array|null ['path' => string, 'content' => array] or null
* @since 2.6.4
*
* @param string $basePath Absolute path without extension.
* @param string|null $preferredExt Optional extension to try first (md|php|html).
* @return array|null ['path' => string, 'content' => array] or null.
*/
private function resolveContentByType(string $basePath, ?string $preferredExt = null): ?array
{
@@ -584,11 +691,13 @@ class CodePressCMS {
}
/**
* Get file information including creation and modification dates
*
* @param string $filePath Path to the file
* @param array $metadata Optional frontmatter metadata
* @return array|null File information or null if file doesn't exist
* Verzamel bestandsinformatie inclusief aanmaak- en wijzigingsdatums.
*
* @since 2.6.4
*
* @param string $filePath Path to the file.
* @param array $metadata Optional frontmatter metadata.
* @return array|null File information or null if file doesn't exist.
*/
private function getFileInfo($filePath, array $metadata = []) {
if (!file_exists($filePath)) {
@@ -624,10 +733,12 @@ class CodePressCMS {
}
/**
* Format file size in human readable format
*
* @param int $bytes File size in bytes
* @return string Formatted file size
* Format een bestandsgrootte in leesbare vorm (B, KB, MB, GB).
*
* @since 2.6.4
*
* @param int $bytes File size in bytes.
* @return string Formatted file size.
*/
private function formatFileSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
@@ -641,9 +752,10 @@ class CodePressCMS {
}
/**
* Get search results page content
*
* @return array Search results page data
* Bouw de content voor de zoekresultatenpagina.
*
* @since 2.6.4
* @return array Search results page data.
*/
private function getSearchResults() {
$query = $_GET['search'];
@@ -670,10 +782,15 @@ class CodePressCMS {
}
/**
* Parse metadata from content
*
* @param string $content Raw content
* @return array Parsed metadata and content without meta block
* Parse YAML-frontmatter metadata uit de content.
*
* Herkent een blok dat begint en eindigt met `---` en levert een array
* met de metadata en de content zonder het meta-blok.
*
* @since 2.6.4
*
* @param string $content Raw content.
* @return array Parsed metadata and content without meta block.
*/
private function parseMetadata($content) {
$metadata = [];
@@ -708,9 +825,13 @@ class CodePressCMS {
}
/**
* Get hosts that count as internal for external-link detection
* Geef de hosts die gelden als intern voor externe-link-detectie.
*
* @return array List of host names
* Bevat de huidige HTTP_HOST (inclusief www-variant) en eventueel de
* author-website host.
*
* @since 2.6.4
* @return array List of host names.
*/
private function getInternalHosts(): array
{
@@ -737,11 +858,14 @@ class CodePressCMS {
}
/**
* Normalize a URL: if no scheme is present, prepend https://.
* Handles hostnames stored without protocol (e.g. "noorlander.info").
* Normaliseer een URL: ontbreekt een scheme, dan wordt https:// voorgevoegd.
*
* @param string $url Raw URL or hostname
* @return string Normalized absolute URL, or '' if empty
* Behandelt hostnames die zonder protocol zijn opgeslagen (bijv. "noorlander.info").
*
* @since 2.6.4
*
* @param string $url Raw URL or hostname.
* @return string Normalized absolute URL, or '' if empty.
*/
private function normalizeUrl(string $url): string
{
@@ -756,11 +880,16 @@ class CodePressCMS {
}
/**
* Parse Markdown content to HTML using League CommonMark
*
* @param string $content Raw Markdown content
* @param string $actualFilePath Path to the source file (used for display name fallback)
* @return array Parsed content with title and body
* Parse Markdown-content naar HTML via League CommonMark.
*
* Trekt eerst frontmatter-metadata uit, bepaalt de titel en configureert
* de CommonMark-omgeving met extensies en custom afbeeldingsgrootte-syntax.
*
* @since 2.6.4
*
* @param string $content Raw Markdown content.
* @param string $actualFilePath Path to the source file (used for display name fallback).
* @return array Parsed content with title and body.
*/
public function parseMarkdown($content, $actualFilePath = '') {
// Parse metadata first
@@ -857,11 +986,16 @@ class CodePressCMS {
}
/**
* Auto-link page titles found in content
*
* @param string $content Content to process for auto-linking
* @param string $excludeTitle Title to exclude from auto-linking (current page title)
* @return string Content with auto-linked page titles
* Auto-link pagina-titels die in de content worden gevonden.
*
* Bestaande `<a>`-tags, `<h1>`-inhoud en Markdown-links worden beschermd
* tegen dubbele linking. De titel van de huidige pagina wordt overgeslagen.
*
* @since 2.6.4
*
* @param string $content Content to process for auto-linking.
* @param string $excludeTitle Title to exclude from auto-linking (current page title).
* @return string Content with auto-linked page titles.
*/
private function autoLinkPageTitles($content, $excludeTitle = '') {
$pages = $this->getAllPageTitles();
@@ -897,9 +1031,11 @@ class CodePressCMS {
}
/**
* Get all content entries (mappen + bestanden) from the content directory.
*
* @return array List of entries: ['path' => ..., 'title' => ..., 'type' => ...]
* Verzamel alle content-entries (mappen + bestanden) uit de content-directory.
*
* @since 2.6.4
*
* @return array List of entries: ['path' => ..., 'title' => ..., 'type' => ...].
* type is 'md'/'php'/'html' voor bestanden, 'folder' voor mappen.
*/
public function getAllPageTitles() {
@@ -909,17 +1045,19 @@ class CodePressCMS {
}
/**
* Recursively scan for content entries in a directory.
*
* Returns a flat list of ['path' => relativePath, 'title' => ..., 'type' => ...].
* Scan recursief een directory voor content-entries.
*
* Levert een platte lijst van ['path' => relativePath, 'title' => ..., 'type' => ...].
* Mappen krijgen type 'folder'; bestanden krijgen hun extensie als type.
* Bestanden met dezelfde naam maar ander type blijven apart (de extensie
* zit in het pad), en mappen krijgen hun eigen entry zodat de boom zichtbaar
* blijft.
*
* @param string $dir Directory to scan
* @param string $prefix Relative path prefix
* @param array &$pages Reference to pages array to populate
*
* @since 2.6.4
*
* @param string $dir Directory to scan.
* @param string $prefix Relative path prefix.
* @param array &$pages Reference to pages array to populate.
* @return void
*/
private function scanForPageTitles($dir, $prefix, &$pages) {
@@ -958,10 +1096,16 @@ class CodePressCMS {
}
/**
* Format display name from filename
*
* @param string $filename Filename without extension
* @return string Formatted display name
* Format een bestands- of mapnaam tot een leesbare weergavenaam.
*
* Verwijdert taal-prefixen (op basis van getAvailableLanguages()) en
* content-extensies, zet streepjes/underscores om in spaties en past
* special cases toe (bijv. 'ict' -> 'ICT').
*
* @since 2.6.4
*
* @param string $filename Filename without extension.
* @return string Formatted display name.
*/
private function formatDisplayName($filename) {
$filename = (string)$filename;
@@ -996,10 +1140,15 @@ class CodePressCMS {
}
/**
* Extract page title from file content
*
* @param string $filePath Path to the file
* @return string|null Extracted title or null if not found
* Extract de paginatitel uit de content van een bestand.
*
* Voor Markdown wordt het eerste H1 gebruikt, voor PHP een $title-
* variabele en voor HTML de <title>- of <h1>-tag.
*
* @since 2.6.4
*
* @param string $filePath Path to the file.
* @return string|null Extracted title or null if not found.
*/
private function extractPageTitle($filePath) {
$content = file_get_contents($filePath);
@@ -1029,10 +1178,16 @@ class CodePressCMS {
}
/**
* Parse PHP file and capture output
*
* @param string $filePath Path to PHP file
* @return array Parsed content with title and body
* Parse een PHP-bestand en vang de output op.
*
* Stelt ContentAPI en frontmatter-metadata beschikbaar aan het
* geinclude bestand. Wanneer het bestand een string returnt wordt
* die als content gebruikt in plaats van de gebufferde output.
*
* @since 2.6.4
*
* @param string $filePath Path to PHP file.
* @return array Parsed content with title and body.
*/
private function parsePHP($filePath) {
// Read file content first to extract metadata
@@ -1076,10 +1231,13 @@ class CodePressCMS {
}
/**
* Parse HTML content and extract title
*
* @param string $content Raw HTML content
* @return array Parsed content with title and body
* Parse HTML-content en extraheer de titel.
*
* @since 2.6.4
*
* @param string $content Raw HTML content.
* @param string $actualFilePath Path to the source file (used for display name fallback).
* @return array Parsed content with title and body.
*/
private function parseHTML($content, $actualFilePath = '') {
// Parse metadata first
@@ -1110,9 +1268,11 @@ class CodePressCMS {
}
/**
* Check if content directory is empty
*
* @return bool True if content directory is empty or doesn't exist
* Controleer of de content-directory leeg is.
*
* @since 2.6.4
*
* @return bool True if content directory is empty or doesn't exist.
*/
public function isContentDirEmpty() {
$contentDir = $this->config['content_dir'];
@@ -1132,12 +1292,13 @@ class CodePressCMS {
}
/**
* Get welcome page content for new installations (empty content directory)
* Geef de content voor de welkomstpagina bij nieuwe installaties.
*
* Shows a clear "new installation" message with next steps when the
* content directory has no content files yet.
* Toont een duidelijke "nieuwe installatie"-boodschap met vervolgstappen
* wanneer de content-directory nog geen content-bestanden bevat.
*
* @return array Welcome page data
* @since 2.6.4
* @return array Welcome page data.
*/
private function getWelcomePage() {
$adminUrl = $this->buildAdminUrl();
@@ -1178,9 +1339,13 @@ class CodePressCMS {
}
/**
* Get guide page content based on user language
*
* @return array Guide page data
* Geef de content van een handleiding-pagina op basis van de gebruikerstaal.
*
* Valt terug op het Engelse equivalent indien de gevraagde taal niet
* beschikbaar is. Bouwt tevens breadcrumbs op voor de handleiding.
*
* @since 2.6.4
* @return array Guide page data.
*/
private function getGuidePage() {
$lang = $this->currentLanguage;
@@ -1247,11 +1412,17 @@ class CodePressCMS {
}
/**
* Generate directory listing page
*
* @param string $pagePath Relative path to directory
* @param string $dirPath Absolute path to directory
* @return array Directory listing page data
* Genereer een directory-listingpagina.
*
* Toont de titel van de map en een lijst met alle mappen en bestanden
* daarin. Leest eventuele layout/plugins-metadata uit een index.md in
* dezelfde map.
*
* @since 2.6.4
*
* @param string $pagePath Relative path to directory.
* @param string $dirPath Absolute path to directory.
* @return array Directory listing page data.
*/
private function getDirectoryListing($pagePath, $dirPath) {
// Get the directory name from the path, not from a potential file
@@ -1351,9 +1522,10 @@ class CodePressCMS {
}
/**
* Get 404 error page content
*
* @return array 404 page data
* Geef de content van de 404-foutpagina.
*
* @since 2.6.4
* @return array 404 page data.
*/
private function getError404() {
$staticFile = __DIR__ . '/../../../admin/static/404.html';
@@ -1371,17 +1543,23 @@ class CodePressCMS {
}
/**
* Get menu structure
*
* @return array Menu structure for navigation
* Geef de opgebouwde menu-structuur terug.
*
* @since 2.6.4
* @return array Menu structure for navigation.
*/
public function getMenu() {
return $this->menu;
}
/**
* Render the complete page with template
*
* Render de volledige pagina met het actieve theme.
*
* Bepaalt pagina-data, vuurt plugin-acties af, stelt template-data samen
* (menu, sidebar, breadcrumbs, SEO, theme-assets) en echoot de
* gerenderde layout via ThemeManager.
*
* @since 2.6.4
* @return void
*/
public function render() {
@@ -1547,11 +1725,16 @@ class CodePressCMS {
}
/**
* Generate breadcrumb navigation HTML
*
* @param bool $hasSidebar Whether sidebar content exists and should show toggle
* @param array|null $guidePage Guide page data (when on a guide page)
* @return string Breadcrumb HTML
* Genereer breadcrumb-navigatie-HTML.
*
* Ondersteunt guide-pagina's, zoekpagina's en reguliere content-pagina's
* met subdirectory's. Toont optioneel een sidebar-toggle-knop.
*
* @since 2.6.4
*
* @param bool $hasSidebar Whether sidebar content exists and should show toggle.
* @param array|null $guidePage Guide page data (when on a guide page).
* @return string Breadcrumb HTML.
*/
public function generateBreadcrumb($hasSidebar = true, $guidePage = null) {
// Sidebar toggle button (shown before home icon in breadcrumb)
@@ -1630,11 +1813,16 @@ class CodePressCMS {
}
/**
* Render menu HTML with dropdown support
*
* @param array $items Menu items to render
* @param int $level Current nesting level
* @return string Rendered menu HTML
* Render menu-HTML met ondersteuning voor dropdowns.
*
* Bestanden op root-niveau worden als tabs getoond; bestanden in mappen
* als dropdown-items. Mappen zonder kinderen worden als uitgevinkt getoond.
*
* @since 2.6.4
*
* @param array $items Menu items to render.
* @param int $level Current nesting level.
* @return string Rendered menu HTML.
*/
private function renderMenu($items, $level = 0) {
$html = '';
@@ -1698,13 +1886,16 @@ class CodePressCMS {
}
/**
* Map a frontmatter layout value to a theme template key.
* Vertaal een frontmatter layout-waarde naar een theme template-sleutel.
*
* Legacy values are translated to the new theme keys. Unknown values
* are passed through so ThemeManager can fall back to default_layout.
* Legacy waarden worden omgezet naar de nieuwe theme-keys. Onbekende
* waarden worden doorgegeven zodat ThemeManager kan terugvallen op
* default_layout.
*
* @param string $layout Layout value from page metadata
* @return string Theme template key
* @since 2.6.4
*
* @param string $layout Layout value from page metadata.
* @return string Theme template key.
*/
private function mapLayoutToThemeKey(string $layout): string {
return match ($layout) {
@@ -1717,12 +1908,14 @@ class CodePressCMS {
}
/**
* Auto-detect the first available content page
* Detecteer automatisch de eerste beschikbare content-pagina.
*
* Scans the content directory for the first .md/.php/.html file
* (preferring non-language-prefixed files) and returns its page key.
* Scant de content-directory naar het eerste .md/.php/.html-bestand
* (met voorkeur voor bestanden zonder taal-prefix) en geeft de page-key terug.
*
* @return string Detected page key, or 'index' as fallback
* @since 2.6.4
*
* @return string Detected page key, or 'index' as fallback.
*/
private function detectDefaultPage(): string
{
@@ -1748,9 +1941,11 @@ class CodePressCMS {
}
/**
* Detect the newest modified page (last modified timestamp)
* Detecteer de meest recent gewijzigde of aangemaakte pagina.
*
* @return string Page key that was most recently modified or created
* @since 2.6.4
*
* @return string Page key that was most recently modified or created.
*/
private function detectNewestPage(): string
{
@@ -1825,9 +2020,10 @@ class CodePressCMS {
}
/**
* Get homepage title
*
* @return string Homepage title
* Geef de titel van de homepagina terug.
*
* @since 2.6.4
* @return string Homepage title.
*/
private function getHomepageTitle() {
// Use a generic "Home" label instead of the page name
@@ -1836,10 +2032,12 @@ class CodePressCMS {
}
/**
* Check if folder contains the currently active page
*
* @param array $children Array of child items
* @return bool True if active page is found in children
* Controleer of een map de actieve pagina bevat.
*
* @since 2.6.4
*
* @param array $children Array of child items.
* @return bool True if active page is found in children.
*/
private function folderContainsActivePage($children) {
foreach ($children as $child) {
@@ -1856,6 +2054,22 @@ class CodePressCMS {
return false;
}
/**
* Verwerk content-HTML vóór renderen.
*
* Herschrijft legacy `-/assets/`-shortcuts en `<img src>`/`<a href>`-
* attributen die wijzen naar bestanden in de content-directory naar het
* `/-media/`-endpoint, zodat deze bestanden (die buiten de webroot
* staan) correct worden geserveerd. Absolute URLs, reeds herschreven
* `/-media/`- en `/-assets/`-URLs, theme/plugin/admin-asset-URLs,
* data:-/mailto:-URLs, fragment-URLs en clean taal-voorziene
* pagina-routes blijven ongemoeid.
*
* @since 2.6.4
*
* @param string $content Content HTML to process.
* @return string Verwerkte content met herschreven media-URLs.
*/
private function processContent(string $content): string
{
// Legacy rewrite for the old -/assets/ shortcut
@@ -1872,12 +2086,20 @@ class CodePressCMS {
// - theme/plugin asset URLs (/themes/..., /plugins/..., /admin/...)
// - data: and mailto: URLs
// - fragment-only URLs (#anchor)
// - clean language-prefixed page routes (/<lang>/<page>) — these are
// valid internal page URLs (used by autoLinkPageTitles) and must not
// be rewritten to the /-media/ endpoint
$langCodes = array_keys($this->getAvailableLanguages());
$langPattern = empty($langCodes) ? '' : '|/(?:' . implode('|', array_map('preg_quote', $langCodes)) . ')/';
$skipPattern = '~^(?:[a-z][a-z0-9+.\-]*:|//|/themes/|/plugins/|/admin/|/-media/|/-assets/|data:|mailto:' . $langPattern . ')~i';
$content = preg_replace_callback(
'~(<(?:img|a)\b[^>]*\b(?:src|href)\s*=\s*")([^"]+)(")~i',
function ($m) {
function ($m) use ($skipPattern) {
$url = $m[2];
// Leave absolute, already-rewritten, asset, data: and anchor URLs alone
if (preg_match('~^(?:[a-z][a-z0-9+.\-]*:|//|/themes/|/plugins/|/admin/|/-media/|/-assets/|data:|mailto:)~i', $url)) {
// Leave absolute, already-rewritten, asset, data:, anchor and
// language-prefixed page-route URLs alone
if (preg_match($skipPattern, $url)) {
return $m[0];
}
// Strip a leading /content/ prefix if present, then prefix with /-media/
+156 -21
View File
@@ -7,13 +7,46 @@
* - ZIP backup of the entire content directory
* - Restore from an uploaded ZIP file
* - Optional git-based versioning (init / commit / log / restore)
*
* @since 2.6.4
*/
class ContentBackup
{
/**
* Pad naar de content-map.
*
* @since 2.6.4
* @var string Pad naar de content-map.
*/
private string $contentDir;
/**
* Pad naar de project-root.
*
* @since 2.6.4
* @var string Pad naar de project-root.
*/
private string $projectRoot;
/**
* Of git beschikbaar is in de content-map.
*
* @since 2.6.4
* @var bool Of git beschikbaar is.
*/
private bool $gitAvailable;
/**
* Initialiseer de ContentBackup-instantie.
*
* Stelt de content-map en project-root in en detecteert of git
* beschikbaar is op het systeem.
*
* @since 2.6.4
*
* @param string $contentDir Pad naar de content-map.
* @param string $projectRoot Optioneel. Pad naar de project-root. Default de map drie niveaus boven deze class.
*/
public function __construct(string $contentDir, string $projectRoot = '')
{
$this->contentDir = rtrim($contentDir, '/');
@@ -22,7 +55,11 @@ class ContentBackup
}
/**
* Check whether git is available and the content dir is inside a git repo.
* Controleer of git beschikbaar is en de content-map in een git-repo zit.
*
* @since 2.6.4
*
* @return bool True indien git beschikbaar is.
*/
public function isGitAvailable(): bool
{
@@ -30,10 +67,15 @@ class ContentBackup
}
/**
* Create a ZIP backup of the content directory.
* Maak een ZIP-backup van de content-map.
*
* @param string $outputPath Where to write the ZIP file
* @return bool True on success
* Controleert of ZipArchive beschikbaar is en de content-map bestaat,
* en voegt recursief alle bestanden en mappen toe aan het ZIP-archief.
*
* @since 2.6.4
*
* @param string $outputPath Pad waar het ZIP-bestand geschreven moet worden.
* @return bool True bij succes, false bij falen.
*/
public function createZipBackup(string $outputPath): bool
{
@@ -54,10 +96,15 @@ class ContentBackup
}
/**
* Restore content from a ZIP file.
* Herstel content vanuit een ZIP-bestand.
*
* @param string $zipPath Path to the uploaded ZIP file
* @return array ['success' => bool, 'message' => string]
* Pakt het ZIP-bestand uit in een tijdelijke map, maakt een backup van de
* huidige content-map, kopieert de nieuwe content en ruimt oude backups op.
*
* @since 2.6.4
*
* @param string $zipPath Pad naar het geüploade ZIP-bestand.
* @return array{success:bool,message:string} Resultaat-array met success en message.
*/
public function restoreFromZip(string $zipPath): array
{
@@ -112,9 +159,14 @@ class ContentBackup
}
/**
* Initialize git in the content directory.
* Initialiseer een git-repository in de content-map.
*
* @return array ['success' => bool, 'message' => string]
* Voert `git init` uit en configureert een default identiteit (user.email
* en user.name) zodat commits werken zonder globale git-config.
*
* @since 2.6.4
*
* @return array{success:bool,message:string} Resultaat-array met success en message.
*/
public function gitInit(): array
{
@@ -135,10 +187,16 @@ class ContentBackup
}
/**
* Commit all changes in the content directory.
* Commit alle wijzigingen in de content-map.
*
* @param string $message Commit message
* @return array ['success' => bool, 'message' => string]
* Voegt alle bestanden toe met `git add -A`, controleert of er wijzigingen
* zijn via `git status --porcelain`, en commit met de opgegeven message.
* Bij een lege message wordt een standaardmessage gegenereerd.
*
* @since 2.6.4
*
* @param string $message Commit message.
* @return array{success:bool,message:string} Resultaat-array met success en message.
*/
public function gitCommit(string $message): array
{
@@ -173,10 +231,15 @@ class ContentBackup
}
/**
* Get the git log for the content directory.
* Haal de git-log op voor de content-map.
*
* @param int $limit Maximum number of entries
* @return array ['success' => bool, 'commits' => array, 'message' => string]
* Leest commits uit met `git log --pretty=format:%H|%h|%ai|%s` en parseert
* deze naar een array met hash, short, date en message per commit.
*
* @since 2.6.4
*
* @param int $limit Maximum aantal entries. Default 20.
* @return array{success:bool,commits:array<int,array{hash:string,short:string,date:string,message:string}>,message:string} Resultaat-array met success, commits en message.
*/
public function gitLog(int $limit = 20): array
{
@@ -221,10 +284,16 @@ class ContentBackup
}
/**
* Restore content to a specific git commit.
* Herstel content naar een specifieke git-commit.
*
* @param string $commitHash Commit hash to restore to
* @return array ['success' => bool, 'message' => string]
* Controleert de commit-hash, saniteert deze en voert
* `git checkout <hash> -- .` uit om de bestanden van die commit
* in de working tree te herstellen.
*
* @since 2.6.4
*
* @param string $commitHash Commit-hash om naar te herstellen.
* @return array{success:bool,message:string} Resultaat-array met success en message.
*/
public function gitRestore(string $commitHash): array
{
@@ -251,7 +320,11 @@ class ContentBackup
}
/**
* Check if the content directory has a git repository.
* Controleer of de content-map een git-repository bevat.
*
* @since 2.6.4
*
* @return bool True indien een .git-map aanwezig is.
*/
public function hasGitRepo(): bool
{
@@ -262,6 +335,15 @@ class ContentBackup
// Private helpers
// -----------------------------------------------------------------------
/**
* Detecteer of git beschikbaar is op het systeem.
*
* Voert `git --version` uit en controleert de exit-code.
*
* @since 2.6.4
*
* @return bool True indien git uitvoerbaar is.
*/
private function detectGit(): bool
{
$result = $this->execGitCapture(['--version']);
@@ -269,9 +351,16 @@ class ContentBackup
}
/**
* Execute a git command and capture output, error, and exit code separately.
* Voer een git-commando uit en capture output, error en exit-code apart.
*
* @return array ['output' => string, 'error' => string, 'exit' => int]
* Gebruikt proc_open met aparte pipes voor stdout, stderr en stdin.
* Alle argumenten worden via escapeshellarg ge-escaped.
*
* @since 2.6.4
*
* @param array<int,string> $args Git-commando-argumenten.
* @param string|null $cwd Optioneel. Werkdirectory voor het git-proces.
* @return array{output:string,error:string,exit:int} Array met output, error en exit-code.
*/
private function execGitCapture(array $args, ?string $cwd = null): array
{
@@ -305,6 +394,19 @@ class ContentBackup
];
}
/**
* Voeg een map recursief toe aan een ZipArchive.
*
* Itereert over alle entries in $dir en voegt mappen (als lege dir) en
* bestanden toe onder de opgegeven $prefix in het ZIP-archief.
*
* @since 2.6.4
*
* @param ZipArchive $zip ZIP-archief om aan toe te voegen.
* @param string $dir Pad naar de bronmap.
* @param string $prefix Prefix (pad binnen het ZIP) voor de entries.
* @return void
*/
private function addDirToZip(ZipArchive $zip, string $dir, string $prefix): void
{
$entries = scandir($dir);
@@ -325,6 +427,18 @@ class ContentBackup
}
}
/**
* Kopieer een map recursief naar een doelmap.
*
* Maakt de doelmap aan indien nodig en kopieert alle bestanden en
* submappen vanuit de bronmap.
*
* @since 2.6.4
*
* @param string $src Pad naar de bronmap.
* @param string $dst Pad naar de doelmap.
* @return void
*/
private function copyDir(string $src, string $dst): void
{
if (!is_dir($src)) {
@@ -352,6 +466,16 @@ class ContentBackup
}
}
/**
* Verwijder een map en alle inhoud recursief.
*
* Verwijdert eerst alle bestanden en submappen, daarna de map zelf.
*
* @since 2.6.4
*
* @param string $dir Pad naar de te verwijderen map.
* @return void
*/
private function removeDir(string $dir): void
{
if (!is_dir($dir)) {
@@ -376,6 +500,17 @@ class ContentBackup
rmdir($dir);
}
/**
* Ruim oude content-backups op, waarbij alleen de laatste bewaard blijft.
*
* Zoekt backups via een glob-patroon op de content-mapnaam met .bak.*
* suffix, sorteert op wijzigingstijd (oudste eerst) en verwijdert alle
* backups behalve de meest recente.
*
* @since 2.6.4
*
* @return void
*/
private function cleanupOldBackups(): void
{
$parentDir = dirname($this->contentDir);
+180 -11
View File
@@ -1,13 +1,40 @@
<?php
/**
* GeoIP - Country lookup provider chain (Local binary, MMDB, and API)
* GeoIP country lookup provider chain.
*
* Ondersteunt drie providers voor het resolueren van een landcode op basis
* van een IP-adres: een lokale binaire database (DB-IP), een MaxMind MMDB
* bestand en een externe API. Providers vallen terug op de lokale database
* wanneer een lookup geen resultaat oplevert.
*
* @since 2.6.4
*/
class GeoIP
{
/**
* De analytics-configuratie uit config.json.
*
* @since 2.6.4
* @var array Bevat o.a. geoip_provider, geoip_mmdb_path en geoip_api_url.
*/
private array $config;
/**
* FileCache instantie voor het cachen van API-lookups.
*
* @since 2.6.4
* @var FileCache|null
*/
private ?FileCache $cache = null;
/**
* Initialiseert de GeoIP-provider met de analytics-configuratie.
*
* @since 2.6.4
*
* @param array $analyticsConfig De "analytics" sectie uit config.json. Default lege array.
*/
public function __construct(array $analyticsConfig = [])
{
$this->config = $analyticsConfig;
@@ -16,10 +43,16 @@ class GeoIP
}
/**
* Resolve country code (2-letter ISO alpha-2, upper-case) from an IP address
* Resolves een 2-letter ISO alpha-2 landcode uit een IP-adres.
*
* @param string $ip IPv4 or IPv6 address
* @return string|null Country code or null if unresolved/private
* De gekozen provider wordt gelezen uit de configuratie. Private en
* gereserveerde ranges worden direct afgewezen. MMDB en API vallen
* terug op de lokale database bij een mislukte lookup.
*
* @since 2.6.4
*
* @param string $ip IPv4- of IPv6-adres.
* @return string|null Landcode in hoofdletters, of null bij onoplosbare/private adressen.
*/
public function lookupCountry(string $ip): ?string
{
@@ -53,7 +86,15 @@ class GeoIP
}
/**
* Normalize a country code; placeholder codes (ZZ/XX) count as unknown
* Normaliseert een landcode en verwerpt placeholder codes.
*
* Geldige codes zijn exact twee letters. De codes ZZ en XX worden
* beschouwd als onbekend en resulteren in null.
*
* @since 2.6.4
*
* @param string|null $code Ruwe landcode uit een provider.
* @return string|null Genormaliseerde landcode of null indien ongeldig/onbekend.
*/
private static function normalizeCode(?string $code): ?string
{
@@ -65,7 +106,16 @@ class GeoIP
}
/**
* Binary search lookup in local DB-IP IPv4/IPv6 binary files
* Binaire zoekopdracht in lokale DB-IP IPv4/IPv6 binaire bestanden.
*
* Leest de bestanden uit `admin/storage/geoip` en voert een binaire
* zoekopdracht uit op basis van het IP-adres. Ondersteunt zowel IPv4
* (10-byte records) als IPv6 (34-byte records).
*
* @since 2.6.4
*
* @param string $ip IPv4- of IPv6-adres.
* @return string|null Landcode in hoofdletters of null indien niet gevonden.
*/
public function lookupLocal(string $ip): ?string
{
@@ -161,7 +211,16 @@ class GeoIP
}
/**
* External API lookup with caching
* Externe API-lookup met caching.
*
* Stuurt een HTTP-verzoek naar de geconfigureerde GeoIP API en cachet
* het resultaat zeven dagen in een FileCache. De API-URL mag een
* optionele API-key bevatten.
*
* @since 2.6.4
*
* @param string $ip IPv4- of IPv6-adres.
* @return string|null Landcode of null indien de lookup faalt.
*/
private function lookupApi(string $ip): ?string
{
@@ -192,7 +251,16 @@ class GeoIP
}
/**
* Pure-PHP MaxMind MMDB reader
* Pure-PHP MaxMind MMDB reader.
*
* Opent een MMDB-bestand en extraheert de landcode via de ingebouwde
* MMDBReader. Fouten worden gesilenced en resulteren in null.
*
* @since 2.6.4
*
* @param string $ip IPv4- of IPv6-adres.
* @param string $filePath Pad naar het MMDB-bestand.
* @return string|null Landcode of null bij een fout.
*/
private function lookupMMDB(string $ip, string $filePath): ?string
{
@@ -206,7 +274,12 @@ class GeoIP
}
/**
* Convert 2-letter ISO country code to regional indicator flag emoji
* Zet een 2-letter ISO landcode om naar een regionale vlag-emoji.
*
* @since 2.6.4
*
* @param string|null $code Landcode of null.
* @return string Vlag-emoji of een wereldbol bij ongeldige invoer.
*/
public static function getCountryFlagEmoji(?string $code): string
{
@@ -222,7 +295,17 @@ class GeoIP
}
/**
* Get country name in Dutch or English
* Geeft de landnaam in Nederlands of Engels.
*
* Bevat een statische mapping van de meest voorkomende landcodes naar
* hun naam in de gevraagde taal. Onbekende codes worden ongewijzigd
* teruggegeven.
*
* @since 2.6.4
*
* @param string|null $code Landcode of null.
* @param string $lang Gewenste taal, 'nl' of 'en'. Default 'nl'.
* @return string Landnaam in de gevraagde taal of de code zelf.
*/
public static function getCountryName(?string $code, string $lang = 'nl'): string
{
@@ -275,14 +358,49 @@ class GeoIP
}
/**
* Built-in pure-PHP MaxMind DB Reader
* Ingebouwde pure-PHP MaxMind DB reader.
*
* Minimale implementatie van de MaxMind DB binary reader, nodig omdat de
* externe GeoIP2 dependency optioneel is. Ondersteunt 28-bit record sizes
* en de veelvoorkomende datatypes (pointer, string, map, uint, bool).
*
* @since 2.6.4
*/
class MMDBReader
{
/**
* Pad naar het MMDB-bestand.
*
* @since 2.6.4
* @var string
*/
private string $file;
/**
* Bestandshandle voor het MMDB-bestand.
*
* @since 2.6.4
* @var resource|null
*/
private $handle;
/**
* Metadata uit de MMDB-header.
*
* @since 2.6.4
* @var array Bevat o.a. node_count, record_size en ip_version.
*/
private array $meta;
/**
* Opent het MMDB-bestand en laadt de metadata.
*
* @since 2.6.4
*
* @param string $file Pad naar het MMDB-bestand.
* @throws \InvalidArgumentException Als het bestand niet bestaat.
* @throws \RuntimeException Als het bestandformaat ongeldig is.
*/
public function __construct(string $file)
{
if (!file_exists($file)) {
@@ -293,6 +411,11 @@ class MMDBReader
$this->loadMetadata();
}
/**
* Sluit de bestandshandle bij het vernietigen van de instantie.
*
* @since 2.6.4
*/
public function __destruct()
{
if ($this->handle) {
@@ -300,6 +423,16 @@ class MMDBReader
}
}
/**
* Laadt de MMDB-metadata vanaf het einde van het bestand.
*
* Zoekt naar de MaxMind marker en decodeert de daaropvolgende data.
*
* @since 2.6.4
*
* @throws \RuntimeException Als de marker niet gevonden wordt.
* @return void
*/
private function loadMetadata(): void
{
$stat = fstat($this->handle);
@@ -319,6 +452,18 @@ class MMDBReader
$this->meta = $this->decodeData($metaOffset)[0];
}
/**
* Voert een lookup uit voor een IP-adres in de MMDB-boom.
*
* Werkt door de binaire boom te doorlopen op basis van de bits van het
* IP-adres. IPv4-adressen in een IPv6-boom worden correct afgehandeld
* via de ipv4_instance_count metadata.
*
* @since 2.6.4
*
* @param string $ip IPv4- of IPv6-adres.
* @return array|null Gedecodeerde datarecord of null indien niet gevonden.
*/
public function get(string $ip): ?array
{
$ipBin = inet_pton($ip);
@@ -355,6 +500,18 @@ class MMDBReader
return null;
}
/**
* Leest een kind-node (left/right) uit de MMDB-boom.
*
* Ondersteunt uitsluitend 28-bit record sizes.
*
* @since 2.6.4
*
* @param int $node Huidig node-index.
* @param int $bit Bitwaarde (0 voor left, 1 voor right).
* @param int $recordSize Record size in bits.
* @return int Doel-node-index of 0 bij niet-ondersteunde record sizes.
*/
private function readNode(int $node, int $bit, int $recordSize): int
{
$bytesPerRecord = $recordSize / 4; // 28-bit -> 3.5 bytes per record
@@ -372,6 +529,18 @@ class MMDBReader
return 0;
}
/**
* Decodeert een MMDB-datarecord vanaf de gegeven offset.
*
* Ondersteunt pointer, string, double, uint, map en bool. De array
* return value bevat de gedecodeerde waarde en de cursorpositie na
* het record.
*
* @since 2.6.4
*
* @param int $offset Start-offset in het bestand.
* @return array Pair [mixed $value, int $newOffset].
*/
private function decodeData(int $offset): array
{
fseek($this->handle, $offset);
+201 -35
View File
@@ -1,34 +1,105 @@
<?php
/**
* LogManager - Dynamic logging for CodePress CMS
* Dynamische logging voor CodePress CMS.
*
* Supports three drivers:
* - syslog: Send log entries to a remote syslog server (UDP) or local syslog.
* - sqlite: Store log entries in a SQLite database (default when no syslog
* server is configured and SQLite is available).
* - file: Fallback to plain text files when SQLite is unavailable.
* Ondersteunt drie drivers:
* - syslog: Stuurt log entries naar een remote syslog-server (UDP) of de
* lokale syslog.
* - sqlite: Slaat log entries op in een SQLite-database (standaard wanneer
* geen syslog-server geconfigureerd is en SQLite beschikbaar is).
* - file: Fallback naar platte-tekstbestanden wanneer SQLite niet
* beschikbaar is.
*
* Which event types are recorded is controlled dynamically via the
* "logging.events" config section (admin, requests, errors, security,
* Welke event-types geregistreerd worden, wordt dynamisch gestuurd via de
* "logging.events" configuratiesectie (admin, requests, errors, security,
* content, system).
*
* @since 2.6.4
*/
class LogManager {
/**
* Event-type voor admin-acties.
*
* @since 2.6.4
* @var string
*/
const EVENT_ADMIN = 'admin';
/**
* Event-type voor request-logging.
*
* @since 2.6.4
* @var string
*/
const EVENT_REQUESTS = 'requests';
/**
* Event-type voor fouten en waarschuwingen.
*
* @since 2.6.4
* @var string
*/
const EVENT_ERRORS = 'errors';
/**
* Event-type voor beveiligingsgebeurtenissen.
*
* @since 2.6.4
* @var string
*/
const EVENT_SECURITY = 'security';
/**
* Event-type voor content-wijzigingen.
*
* @since 2.6.4
* @var string
*/
const EVENT_CONTENT = 'content';
/**
* Event-type voor systeemgebeurtenissen.
*
* @since 2.6.4
* @var string
*/
const EVENT_SYSTEM = 'system';
/**
* De "logging" configuratiesectie, of null indien niet geïnitialiseerd.
*
* @since 2.6.4
* @var array|null
*/
private static $config = null;
/**
* Lazy-initialized PDO-verbinding met de SQLite-database, of null.
*
* @since 2.6.4
* @var \PDO|null
*/
private static $pdo = null;
/**
* Pad naar het SQLite-databasebestand, of null indien niet geïnitialiseerd.
*
* @since 2.6.4
* @var string|null
*/
private static $dbPath = null;
/**
* Initialize the log manager with the logging config section.
* Initialiseert de log-manager met de logging-configuratie.
*
* @param array $loggingConfig The "logging" section from config.json
* Slaat de configuratie op en stelt het pad naar de SQLite-database in
* onder `admin/storage/logs`.
*
* @since 2.6.4
*
* @param array $loggingConfig De "logging" sectie uit config.json.
* @return void
*/
public static function init(array $loggingConfig): void
{
@@ -37,7 +108,11 @@ class LogManager {
}
/**
* Whether logging is enabled at all.
* Geeft aan of logging in het algemeen ingeschakeld is.
*
* @since 2.6.4
*
* @return bool True indien logging ingeschakeld is, anders false.
*/
public static function isEnabled(): bool
{
@@ -45,9 +120,15 @@ class LogManager {
}
/**
* Whether a given event type should be recorded.
* Geeft aan of een bepaald event-type geregistreerd moet worden.
*
* @param string $event One of the EVENT_* constants
* Controleert eerst of logging in het algemeen ingeschakeld is en
* daarna of het specifieke event aan staat in de configuratie.
*
* @since 2.6.4
*
* @param string $event Een van de EVENT_* constanten.
* @return bool True indien het event geregistreerd moet worden.
*/
public static function isEventEnabled(string $event): bool
{
@@ -59,9 +140,15 @@ class LogManager {
}
/**
* Get the local storage driver: 'sqlite' (falling back to 'file' if
* SQLite is unavailable). Syslog is an additional output, not a storage
* driver, so it never replaces local storage.
* Geeft de actieve lokale opslag-driver.
*
* Retourneert 'sqlite' wanneer SQLite beschikbaar is, anders 'file'.
* Syslog is een aanvullende output en vervangt nooit de lokale
* opslag, en komt dus nooit als driver terug.
*
* @since 2.6.4
*
* @return string 'sqlite' of 'file'.
*/
public static function getDriver(): string
{
@@ -75,12 +162,20 @@ class LogManager {
}
/**
* Record a log entry if the event type is enabled.
* Slaat een log-entry op indien het event-type ingeschakeld is.
*
* @param string $event Event type (EVENT_* constant)
* @param string $level Log level (info, warning, error, debug)
* @param string $message Log message
* @param array $context Additional structured context
* De entry wordt altijd lokaal opgeslagen (SQLite of file-fallback) zodat
* de dynamische admin-log altijd entries bevat. Wanneer een syslog-host
* geconfigureerd is, wordt de entry tevens naar de remote syslog-server
* gestuurd. Het IP-adres wordt uit de context of de RequestLogger gehaald.
*
* @since 2.6.4
*
* @param string $event Event-type (een EVENT_* constante).
* @param string $level Loglevel (info, warning, error, debug).
* @param string $message Logbericht.
* @param array $context Aanvullende gestructureerde context. Default lege array.
* @return void
*/
public static function log(string $event, string $level, string $message, array $context = []): void
{
@@ -115,7 +210,15 @@ class LogManager {
}
/**
* Send a log entry to a remote syslog server over UDP.
* Stuurt een log-entry naar een remote syslog-server over UDP.
*
* Bouwt een syslog-bericht op met facility, severity en ident en stuurt
* dit via een UDP-socket. Verbindings- en schrijffouten worden gesilenced.
*
* @since 2.6.4
*
* @param array $entry De te versturen log-entry.
* @return void
*/
private static function writeSyslog(array $entry): void
{
@@ -138,7 +241,16 @@ class LogManager {
}
/**
* Store a log entry in the SQLite database.
* Slaat een log-entry op in de SQLite-database.
*
* De context-array wordt als JSON opgeslagen. Wanneer de PDO-verbinding
* niet beschikbaar is of het schrijven faalt, wordt teruggevallen op
* de file-driver.
*
* @since 2.6.4
*
* @param array $entry De op te slaan log-entry.
* @return void
*/
private static function writeSqlite(array $entry): void
{
@@ -167,7 +279,15 @@ class LogManager {
}
/**
* Append a log entry to a plain text file (fallback driver).
* Voegt een log-entry toe aan een platte-tekstbestand (fallback-driver).
*
* Zorgt dat de doelmap bestaat en schrijft de entry met een vaste
* opmaak. Schrijffouten worden gesilenced.
*
* @since 2.6.4
*
* @param array $entry De toe te voegen log-entry.
* @return void
*/
private static function writeFile(array $entry): void
{
@@ -182,7 +302,15 @@ class LogManager {
}
/**
* Get (and lazily create) the PDO connection to the SQLite database.
* Geeft (en creëert lazy) de PDO-verbinding met de SQLite-database.
*
* Maakt de tabel en indices aan indien deze nog niet bestaan. Bij een
* fout wordt de interne verbinding op null gezet en null teruggegeven,
* zodat de aanroeper kan terugvallen op de file-driver.
*
* @since 2.6.4
*
* @return \PDO|null PDO-verbinding of null indien SQLite niet beschikbaar/faalt.
*/
private static function getPdo(): ?\PDO
{
@@ -222,7 +350,11 @@ class LogManager {
}
/**
* Whether the SQLite PDO driver is available.
* Geeft aan of de SQLite PDO-driver beschikbaar is.
*
* @since 2.6.4
*
* @return bool True indien PDO en de sqlite-driver beschikbaar zijn.
*/
private static function sqliteAvailable(): bool
{
@@ -230,7 +362,16 @@ class LogManager {
}
/**
* Map a facility name to its syslog numeric value.
* Vertaalt een facility-naam naar de bijbehorende numerieke syslog-waarde.
*
* Ondersteunt de standaard facilities (kern, user, mail, daemon, auth,
* syslog, lpr, news, uucp, cron, authpriv, ftp) en local0 t/m local7.
* Onbekende faciliteiten vallen terug op local0 (16).
*
* @since 2.6.4
*
* @param string $facility Naam van de facility. Default 'local0' wordt door aanroeper gezet.
* @return int Numerieke facility-waarde.
*/
private static function syslogFacility(string $facility): int
{
@@ -245,7 +386,15 @@ class LogManager {
}
/**
* Map a log level to its syslog severity value.
* Vertaalt een loglevel naar de bijbehorende syslog-severity.
*
* Ondersteunt debug t/m emergency. Onbekende levels vallen terug op
* info (6). De invoer wordt case-insensitive vergeleken.
*
* @since 2.6.4
*
* @param string $level Loglevel (debug, info, notice, warning, error, ...).
* @return int Numerieke severity-waarde (0 t/m 7).
*/
private static function syslogSeverity(string $level): int
{
@@ -263,13 +412,22 @@ class LogManager {
}
/**
* Query recent log entries from the active store.
* Zoekt recente log-entries op uit de actieve opslag.
*
* @param int $limit Number of entries to return
* @param string|null $event Optional event filter
* @param string|null $level Optional level filter (info, warning, error, ...)
* @param string|null $search Optional text search on the message
* @return array List of log entries (newest first)
* Bij de SQLite-driver wordt een geparametriseerde query uitgevoerd met
* optionele filters op event, level en een zoekterm op het bericht.
* Bij de file-driver wordt het logbestand uitgelezen en met een regex
* geparseerd; de resultaten worden gefilterd en oudste eerst teruggegeven.
* Resultaten worden altijd nieuwste eerst geretourneerd (SQLite via
* ORDER BY id DESC, file via array_reverse).
*
* @since 2.6.4
*
* @param int $limit Aantal terug te geven entries. Default 200.
* @param string|null $event Optioneel event-filter. Default null (geen filter).
* @param string|null $level Optioneel levelfilter (info, warning, error, ...). Default null.
* @param string|null $search Optionele tekstzoekopdracht op het bericht. Default null.
* @return array<int, array<string, mixed>> Lijst met log-entries (nieuwste eerst).
*/
public static function getLogs(int $limit = 200, ?string $event = null, ?string $level = null, ?string $search = null): array
{
@@ -340,7 +498,15 @@ class LogManager {
}
/**
* Clear all stored log entries.
* Wist alle opgeslagen log-entries.
*
* Bij de SQLite-driver worden alle rijen uit de logs-tabel verwijderd.
* Bij de file-driver wordt het logbestand leeggemaakt. Schrijffouten
* worden gesilenced.
*
* @since 2.6.4
*
* @return void
*/
public static function clear(): void
{
+119 -43
View File
@@ -1,27 +1,75 @@
<?php
/**
* Simple Logger Class for CodePress CMS
*
* Provides structured logging with log levels and file output.
*
* @package CodePress
* @version 1.0.0
* Eenvoudige logger voor CodePress CMS.
*
* Voorziet in gestructureerde logging met loglevels en bestandsuitvoer.
* Logregels worden naar een tekstbestand geschreven en, indien aanwezig,
* tevens door de dynamische LogManager gerouteerd.
*
* @since 2.6.4
*/
class Logger {
/**
* Loglevel voor debug-boodschappen.
*
* @since 2.6.4
* @var string
*/
const DEBUG = 'DEBUG';
/**
* Loglevel voor informatieve boodschappen.
*
* @since 2.6.4
* @var string
*/
const INFO = 'INFO';
/**
* Loglevel voor waarschuwingen.
*
* @since 2.6.4
* @var string
*/
const WARNING = 'WARNING';
/**
* Loglevel voor fouten.
*
* @since 2.6.4
* @var string
*/
const ERROR = 'ERROR';
/**
* Pad naar het logbestand, of null indien nog niet geïnitialiseerd.
*
* @since 2.6.4
* @var string|null
*/
private static $logFile = null;
/**
* Of debug-logging actief is.
*
* @since 2.6.4
* @var bool
*/
private static $debugMode = false;
/**
* Initialize logger
*
* @param string $logFile Path to log file
* @param bool $debugMode Enable debug logging
* Initialiseert de logger.
*
* Stelt het pad naar het logbestand in en zorgt dat de bijbehorende
* map bestaat. Indien geen pad wordt opgegeven valt de logger terug
* op een standaardpad onder de core-map.
*
* @since 2.6.4
*
* @param string|null $logFile Pad naar het logbestand. Default null (standaardlocatie).
* @param bool $debugMode Of debug-logging actief is. Default false.
* @return void
*/
public static function init($logFile = null, $debugMode = false) {
if ($logFile === null) {
@@ -39,10 +87,13 @@ class Logger {
}
/**
* Log debug message (only in debug mode)
*
* @param string $message Message to log
* @param array $context Additional context
* Logt een debug-boodschap (enkel in debug-modus).
*
* @since 2.6.4
*
* @param string $message Boodschap die gelogd moet worden.
* @param array $context Aanvullende contextuele data. Default lege array.
* @return void
*/
public static function debug($message, $context = []) {
if (self::$debugMode) {
@@ -51,41 +102,59 @@ class Logger {
}
/**
* Log info message
*
* @param string $message Message to log
* @param array $context Additional context
* Logt een informatieve boodschap.
*
* @since 2.6.4
*
* @param string $message Boodschap die gelogd moet worden.
* @param array $context Aanvullende contextuele data. Default lege array.
* @return void
*/
public static function info($message, $context = []) {
self::write(self::INFO, $message, $context);
}
/**
* Log warning message
*
* @param string $message Message to log
* @param array $context Additional context
* Logt een waarschuwing.
*
* @since 2.6.4
*
* @param string $message Boodschap die gelogd moet worden.
* @param array $context Aanvullende contextuele data. Default lege array.
* @return void
*/
public static function warning($message, $context = []) {
self::write(self::WARNING, $message, $context);
}
/**
* Log error message
*
* @param string $message Message to log
* @param array $context Additional context
* Logt een fout.
*
* @since 2.6.4
*
* @param string $message Boodschap die gelogd moet worden.
* @param array $context Aanvullende contextuele data. Default lege array.
* @return void
*/
public static function error($message, $context = []) {
self::write(self::ERROR, $message, $context);
}
/**
* Write log entry to file
*
* @param string $level Log level
* @param string $message Message to log
* @param array $context Additional context
* Schrijft een logregel naar het bestand en routeert deze door LogManager.
*
* De context-array wordt als JSON aan de logregel toegevoegd. Fouten bij
* het schrijven worden gesilenced (graceful degradation). Wanneer de
* LogManager class beschikbaar is, wordt de entry tevens naar de
* dynamische log gestuurd (errors/warnings naar EVENT_ERRORS, verder
* naar EVENT_SYSTEM).
*
* @since 2.6.4
*
* @param string $level Loglevel (DEBUG, INFO, WARNING, ERROR).
* @param string $message Boodschap die gelogd moet worden.
* @param array $context Aanvullende contextuele data. Default lege array.
* @return void
*/
private static function write($level, $message, $context = []) {
if (self::$logFile === null) {
@@ -109,18 +178,22 @@ class Logger {
}
/**
* Get log file path
*
* @return string Log file path
* Geeft het pad naar het logbestand.
*
* @since 2.6.4
*
* @return string|null Pad naar het logbestand of null indien niet geïnitialiseerd.
*/
public static function getLogFile() {
return self::$logFile;
}
/**
* Clear log file
*
* @return bool Success status
* Verwijdert het logbestand.
*
* @since 2.6.4
*
* @return bool True indien het bestand succesvol verwijderd is, anders false.
*/
public static function clear() {
if (self::$logFile && file_exists(self::$logFile)) {
@@ -130,13 +203,16 @@ class Logger {
}
/**
* Get last N lines from log file
* Geeft de laatste N regels uit het logbestand.
*
* Reads the file backwards in chunks so large log files never have to be
* loaded into memory in their entirety.
* Leest het bestand achterwaarts in chunks zodat grote logbestanden
* nooit volledig in het geheugen geladen hoeven te worden. De regels
* worden inclusief afsluitende newlines teruggegeven, oudste eerst.
*
* @param int $lines Number of lines to retrieve
* @return array Log lines (including trailing newlines, oldest first)
* @since 2.6.4
*
* @param int $lines Aantal terug te halen regels. Default 100.
* @return array<string> Logregels (oudste eerst) of lege array indien niet beschikbaar.
*/
public static function tail($lines = 100) {
if (!self::$logFile || !file_exists(self::$logFile)) {
+70
View File
@@ -1,16 +1,67 @@
<?php
/**
* Limiteert het aantal acties per identifier binnen een tijdvenster.
*
* Houdt per identifier (zoals een IP of gebruikersnaam) bij hoeveel pogingen
* binnen een geconfigureerd tijdvenster zijn gedaan. Oudere pogingen vallen
* buiten het venster en worden genegeerd. De teller wordt opgeslagen in een
* CacheInterface-implementatie, standaard FileCache.
*
* @since 2.6.4
*/
class RateLimiter {
/**
* Maximum aantal toegestane pogingen binnen het tijdvenster.
*
* @since 2.6.4
* @var int
*/
private int $maxAttempts;
/**
* Lengte van het tijdvenster in seconden.
*
* @since 2.6.4
* @var int
*/
private int $timeWindow;
/**
* Cache-implementatie waarin de pogingen worden bijgehouden.
*
* @since 2.6.4
* @var CacheInterface
*/
private CacheInterface $cache;
/**
* Maak een nieuwe RateLimiter aan.
*
* @since 2.6.4
*
* @param int $maxAttempts Maximum aantal pogingen binnen het tijdvenster. Default 10.
* @param int $timeWindow Lengte van het tijdvenster in seconden. Default 60.
* @param CacheInterface|null $cache Optioneel een eigen cache-implementatie; bij null wordt FileCache gebruikt.
*/
public function __construct(int $maxAttempts = 10, int $timeWindow = 60, ?CacheInterface $cache = null) {
$this->maxAttempts = $maxAttempts;
$this->timeWindow = $timeWindow;
$this->cache = $cache ?? new FileCache();
}
/**
* Bepaal of een nieuwe poging voor de identifier is toegestaan.
*
* Verwijder eerst pogingen die buiten het tijdvenster vallen. Wanneer het
* aantal resterende pogingen onder het maximum ligt wordt de huidige
* poging geregistreerd en true teruggegeven; anders false.
*
* @since 2.6.4
*
* @param string $identifier Unieke sleutel voor de te beperken actor (bijv. IP of gebruikersnaam).
* @return bool True wanneer de poging is toegestaan, false wanneer het limiet is bereikt.
*/
public function isAllowed(string $identifier): bool {
$key = 'ratelimit_' . md5($identifier);
$attempts = $this->cache->get($key) ?? [];
@@ -32,6 +83,17 @@ class RateLimiter {
return true;
}
/**
* Geef het aantal nog resterende pogingen voor de identifier.
*
* Telt alleen pogingen die binnen het tijdvenster vallen. Het resultaat
* is nooit negatief.
*
* @since 2.6.4
*
* @param string $identifier Unieke sleutel voor de te beperken actor.
* @return int Aantal resterende pogingen; minimaal 0.
*/
public function getRemainingAttempts(string $identifier): int {
$key = 'ratelimit_' . md5($identifier);
$attempts = $this->cache->get($key) ?? [];
@@ -44,6 +106,14 @@ class RateLimiter {
return max(0, $this->maxAttempts - count($attempts));
}
/**
* Wis alle geregistreerde pogingen voor een identifier.
*
* @since 2.6.4
*
* @param string $identifier Unieke sleutel voor de te beperken actor.
* @return void
*/
public function reset(string $identifier): void {
$key = 'ratelimit_' . md5($identifier);
$this->cache->delete($key);
+115 -6
View File
@@ -1,14 +1,57 @@
<?php
/**
* Logging en analyse van inkomende requests.
*
* Biedt functionaliteit om inkomende requests naar een logbestand te
* schrijven, IP-adressen te anonimiseren, IP's tegen lijsten/CIDR-ranges
* te matchen, het client-IP te bepalen en bezoekerstype (bot/mens/AI)
* te detecteren.
*
* @since 2.6.4
*/
class RequestLogger
{
/**
* Pad naar het logbestand.
*
* @since 2.6.4
* @var string
*/
private string $logFile;
/**
* Maak een nieuwe RequestLogger aan.
*
* @since 2.6.4
*
* @param string $logFile Pad naar het te gebruiken logbestand.
*/
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
/**
* Schrijf een request-regel naar het logbestand.
*
* De gebruikersagent en referrer worden ontdaan van controle-tekens en
* afgekapt tot 500 tekens. De landcode wordt alleen opgenomen wanneer
* deze exact twee tekens lang is. De mapstructuur wordt automatisch
* aangemaakt indien deze nog niet bestaat.
*
* @since 2.6.4
*
* @param string $page Opgevraagde pagina of route.
* @param string $ip Client IP-adres.
* @param string $userAgent HTTP User-Agent header.
* @param string $referrer HTTP Referer header.
* @param string $host Hostnaam van het request.
* @param string $acceptLanguage HTTP Accept-Language header.
* @param string $status Statusaanduiding van het request. Default 'ok'.
* @param string|null $country Tweeletterige landcode of null. Default null.
* @return void
*/
public function log(string $page, string $ip, string $userAgent, string $referrer, string $host, string $acceptLanguage, string $status = 'ok', ?string $country = null): void
{
$dir = dirname($this->logFile);
@@ -25,7 +68,16 @@ class RequestLogger
}
/**
* Mask the last octet (IPv4) or last block (IPv6) of an IP address
* Anonimiseer het laatste deel van een IP-adres.
*
* Voor IPv4 wordt het laatste octet vervangen door 'x'. Voor IPv6 worden
* alleen de eerste vier blokken behouden en de rest vervangen door '::x'.
* Ongeldige adressen worden ongewijzigd teruggegeven.
*
* @since 2.6.4
*
* @param string $ip Het te anonimiseren IP-adres.
* @return string Het geanonimiseerde IP-adres of het origineel bij een ongeldig adres.
*/
public static function anonymizeIp(string $ip): string
{
@@ -45,13 +97,17 @@ class RequestLogger
}
/**
* Check whether an IP matches any entry in a list of IPs/CIDR ranges.
* Controleer of een IP overeenkomt met een entry uit een lijst van IP's of CIDR-ranges.
*
* Supports exact IPv4/IPv6 addresses and CIDR notation (e.g. 192.168.0.0/16).
* Ondersteunt exacte IPv4/IPv6-adressen en CIDR-notatie (bijv. 192.168.0.0/16).
* Bij een prefix-lengte van 0 komt ieder IP van dezelfde adresfamilie overeen.
* Adresfamilies (IPv4 vs IPv6) worden niet onderling gemengd.
*
* @param string $ip The client IP to test
* @param array $list List of IPs and/or CIDR ranges
* @return bool True if the IP matches any entry
* @since 2.6.4
*
* @param string $ip Het te testen client IP-adres.
* @param string[] $list Lijst van IP-adressen en/of CIDR-ranges.
* @return bool True wanneer het IP met een entry overeenkomt.
*/
public static function ipMatchesList(string $ip, array $list): bool
{
@@ -117,6 +173,18 @@ class RequestLogger
return false;
}
/**
* Bepaal het client IP-adres uit de beschikbare request-headers.
*
* Doorloopt in twee passes een vaste set headers (zoals CF-Connecting-IP,
* X-Real-IP, X-Forwarded-For en REMOTE_ADDR). De eerste pass geeft
* voorrang aan geldige publieke IP's en slaat interne/reserved ranges
* over; de tweede pass is een fallback voor lokale ontwikkelomgevingen.
*
* @since 2.6.4
*
* @return string Het gedetecteerde client IP-adres, of '127.0.0.1' als fallback.
*/
public static function getClientIp(): string
{
$headerKeys = [
@@ -165,6 +233,21 @@ class RequestLogger
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
}
/**
* Detecteer het type bezoeker aan de hand van de User-Agent.
*
* Wanneer de BotGuard-class beschikbaar is wordt de UA geïdentificeerd
* en een passend label, badge-kleur en Bootstrap-icon teruggegeven
* (ai, search, scraper, generieke bot, lege UA of mens). Bij een
* ingelogde gebruiker wordt de naam aan het label toegevoegd. Zonder
* BotGuard wordt een algemene bezoeker teruggegeven.
*
* @since 2.6.4
*
* @param string $ua De User-Agent string.
* @param string $user Optionele gebruikersnaam; bij 'Gast' of leeg wordt geen naam toegevoegd. Default ''.
* @return array<string,string> Associatieve array met keys: type, label, badge, icon.
*/
public static function detectVisitorInfo(string $ua, string $user = ''): array
{
if (class_exists('BotGuard')) {
@@ -202,6 +285,18 @@ class RequestLogger
return ['type' => 'unknown', 'label' => 'Bezoeker', 'badge' => 'secondary', 'icon' => 'bi-person'];
}
/**
* Detecteer of het huidige request afkomstig is van een bekende bot.
*
* Leest de User-Agent uit de request-headers en identificeert deze via
* BotGuard wanneer beschikbaar. Alleen de categorieën 'ai', 'search'
* en 'scraper' worden als bot beschouwd en als uppercase string
* teruggegeven; anders null.
*
* @since 2.6.4
*
* @return string|null Boticategorie ('AI', 'SEARCH' of 'SCRAPER') of null bij geen bot.
*/
public static function detectBot(): ?string
{
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
@@ -214,6 +309,20 @@ class RequestLogger
return null;
}
/**
* Lees de laatste regels uit het logbestand en parse deze.
*
* De regels worden geparseerd volgens het vaste logformaat en omgezet
* naar associatieve arrays met tijd, IP, gebruiker, bezoeker-info, taal,
* pagina, UA, referrer, status en landcode. Regels waarvan de host leeg
* is of een IP/cli-waarde bevat worden als gebruiker 'Gast' gemarkeerd.
* Het resultaat is nieuwste-regel-eerst.
*
* @since 2.6.4
*
* @param int $lines Aantal te lezen regels vanaf het einde. Default 100.
* @return array<int,array<string,mixed>> Geparseerde logregels, nieuwste eerst. Lege array bij ontbrekend bestand.
*/
public function getLogs(int $lines = 100): array
{
if (!file_exists($this->logFile)) {
+169 -45
View File
@@ -5,24 +5,62 @@ use Twig\Environment;
use Twig\Loader\FilesystemLoader;
/**
* ThemeManager - Resolves and renders the active theme
* Beheert het actieve thema en de rendering hiervan.
*
* 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 (in assets/css_compiled/)
* - Map a requested layout to a concrete .twig template, falling back
* to the theme's default_layout when the layout is unknown
* 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.4
*/
class ThemeManager {
/**
* Volledige CMS-configuratie.
*
* @since 2.6.4
* @var array Bevat o.a. 'theme_dir' en 'theme'.
*/
private $config;
/**
* Absoluut pad naar de actieve thema-map.
*
* @since 2.6.4
* @var string
*/
private $themeDir;
/**
* Ruwe theme.json configuratie-array.
*
* @since 2.6.4
* @var array
*/
private $themeConfig;
/**
* Twig-omgeving voor het renderen van thema-templates.
*
* @since 2.6.4
* @var Environment
*/
private $twig;
/**
* @param array $config Full CMS config (must contain 'theme_dir' and 'theme')
* 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.4
*
* @param array $config Volledige CMS-configuratie; moet 'theme_dir' en 'theme' bevatten.
*/
public function __construct(array $config) {
$this->config = $config;
@@ -37,55 +75,85 @@ class ThemeManager {
}
/**
* Get the absolute path of the active theme directory
* Geeft het absolute pad van de actieve thema-map.
*
* @since 2.6.4
*
* @return string Absoluut pad naar de thema-map.
*/
public function getThemeDir(): string {
return $this->themeDir;
}
/**
* Get the raw theme.json config array
* Geeft de ruwe theme.json configuratie-array.
*
* @since 2.6.4
*
* @return array Theme.json inhoud, of lege array bij afwezigheid.
*/
public function getThemeConfig(): array {
return $this->themeConfig;
}
/**
* Get the theme title (from theme.json 'title' or 'name')
* Geeft de thema-titel.
*
* Leest eerst 'title' uit theme.json, dan 'name', en als laatste
* fallback de mapnaam van het thema.
*
* @since 2.6.4
*
* @return string Thematische titel.
*/
public function getTitle(): string {
return $this->themeConfig['title'] ?? $this->themeConfig['name'] ?? basename($this->themeDir);
}
/**
* Get the theme config section (default_template, background settings, etc.)
* Geeft de 'config'-sectie van theme.json.
*
* Bevat o.a. default_template en achtergrondinstellingen.
*
* @since 2.6.4
*
* @return array Config-sectie, of lege array bij afwezigheid.
*/
public function getConfig(): array {
return $this->themeConfig['config'] ?? [];
}
/**
* Get the theme template section (layout key => .twig file mapping)
* Geeft de 'template'-sectie van theme.json.
*
* Bevat de mapping van layout-key naar .twig-bestand.
*
* @since 2.6.4
*
* @return array<string,string> Layout-key => .twig-bestand mapping.
*/
public function getTemplates(): array {
return $this->themeConfig['template'] ?? [];
}
/**
* Resolve the .twig template file for a requested layout.
* Bepaalt het .twig-templatebestand voor een aangevraagde layout.
*
* Templates are defined in theme.json under the "template" section:
* Templates worden gedefinieerd in theme.json onder de "template"-sectie:
* { "template": { "full_content": "full_content.twig", ... } }
* The default template is defined in the "config" section:
* De standaardtemplate staat in de "config"-sectie:
* { "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.
* 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.
*
* @param string $layout Requested layout key (e.g. 'left_sidebar')
* @return string Template name usable by the Twig loader
* @since 2.6.4
*
* @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);
@@ -111,16 +179,23 @@ class ThemeManager {
}
/**
* Get the list of available layout keys defined in the "template" section.
* Geeft de lijst met beschikbare layout-keys uit de "template"-sectie.
*
* @return array List of layout keys
* @since 2.6.4
*
* @return array<string> Lijst met layout-keys.
*/
public function getLayouts(): array {
return array_keys($this->getTemplates());
}
/**
* Check whether a template file exists in the theme directory
* Controleert of een templatebestand bestaat in de thema-map.
*
* @since 2.6.4
*
* @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, '/');
@@ -128,11 +203,13 @@ class ThemeManager {
}
/**
* Render a layout template with the given data.
* Rendert een layout-template met de gegeven data.
*
* @param string $layout Requested layout key
* @param array $data Template variables
* @return string Rendered HTML
* @since 2.6.4
*
* @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);
@@ -140,11 +217,17 @@ class ThemeManager {
}
/**
* Compile the theme's SCSS to CSS (cached by source mtime).
* Compiles from assets/scss/theme.scss to assets/css_compiled/theme.css
* Compileert de SCSS van het thema naar CSS (gecacht op bron-mtime).
*
* @param bool $force Force recompilation
* @return string|null Absolute path to the compiled CSS, or null if none
* 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.4
*
* @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';
@@ -193,8 +276,15 @@ class ThemeManager {
}
/**
* Get the public URL for the theme CSS.
* Uses assets/css_compiled/theme.css (compiled from SCSS by scssphp).
* 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.4
*
* @return string|null Publieke CSS-URL, of null indien niet beschikbaar.
*/
public function getCssUrl(): ?string {
$compiledCss = $this->themeDir . '/assets/css_compiled/theme.css';
@@ -212,7 +302,11 @@ class ThemeManager {
}
/**
* Get the public URL for the theme JS, or null if unavailable.
* Geeft de publieke URL voor de thema-JS, of null indien niet beschikbaar.
*
* @since 2.6.4
*
* @return string|null Publieke JS-URL, of null indien niet beschikbaar.
*/
public function getJsUrl(): ?string {
$jsFile = $this->themeDir . '/assets/js/theme.js';
@@ -223,7 +317,12 @@ class ThemeManager {
}
/**
* Get public URL for a theme asset.
* Geeft de publieke URL voor een thema-asset.
*
* @since 2.6.4
*
* @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);
@@ -231,14 +330,22 @@ class ThemeManager {
}
/**
* Check if theme has SCSS source file.
* Controleert of het thema een SCSS-bronbestand heeft.
*
* @since 2.6.4
*
* @return bool True als assets/scss/theme.scss bestaat.
*/
public function hasScss(): bool {
return is_file($this->themeDir . '/assets/scss/theme.scss');
}
/**
* Check if compiled CSS is newer than SCSS source.
* Controleert of de gecompileerde CSS nieuwer is dan de SCSS-bron.
*
* @since 2.6.4
*
* @return bool True als de CSS-_mtime >= SCSS-mtime, anders false.
*/
public function isScssCompiled(): bool {
$scssFile = $this->themeDir . '/assets/scss/theme.scss';
@@ -252,15 +359,24 @@ class ThemeManager {
}
/**
* Check if theme has manual CSS file.
* Controleert of het thema een handmatig CSS-bestand heeft.
*
* @since 2.6.4
*
* @return bool True als assets/css/theme.css bestaat.
*/
public function hasManualCss(): bool {
return is_file($this->themeDir . '/assets/css/theme.css');
}
/**
* Get list of CSS files in theme assets/css/ directory.
* Excludes css_compiled directory.
* Geeft een lijst met CSS-bestanden in de assets/css/-map van het thema.
*
* De css_compiled-map wordt uitgesloten.
*
* @since 2.6.4
*
* @return array<string> Lijst met publieke CSS-URLs.
*/
public function getCssFiles(): array {
$cssDir = $this->themeDir . '/assets/css';
@@ -281,7 +397,11 @@ class ThemeManager {
}
/**
* Get list of JS files in theme assets/js/ directory.
* Geeft een lijst met JS-bestanden in de assets/js/-map van het thema.
*
* @since 2.6.4
*
* @return array<string> Lijst met publieke JS-URLs.
*/
public function getJsFiles(): array {
$jsDir = $this->themeDir . '/assets/js';
@@ -302,7 +422,11 @@ class ThemeManager {
}
/**
* Get URL for favicon if it exists.
* Geeft de URL voor de favicon indien deze bestaat.
*
* @since 2.6.4
*
* @return string|null Publieke favicon-URL, of null indien niet beschikbaar.
*/
public function getFaviconUrl(): ?string {
$faviconFile = $this->themeDir . '/assets/img/favicon.svg';
+40
View File
@@ -1,9 +1,30 @@
<?php
/**
* Configuratie-loader voor CodePress CMS.
*
* Laadt config.json (of maakt deze aan op basis van config.json.example of
* een ingebouwde standaardconfiguratie), voegt ontbrekende secties
* (security, analytics, logging) samen met defaults, converteert relatieve
* content_dir-paden naar absoluut, laadt de actieve theme-configuratie en
* returnt de uiteindelijke configuratie-array. Bij falen volgt een
* minimale fallback-configuratie.
*
* @since 2.6.4
* @package CodePress
*/
// Simple configuration loader
$configJsonPath = __DIR__ . '/../../config.json';
$configExamplePath = __DIR__ . '/../../config.json.example';
/**
* config.json automatisch aanmaken als deze nog niet bestaat.
*
* Kopieert config.json.example indien aanwezig; anders wordt een ingebouwde
* standaardconfiguratie weggeschreven.
*
* @since 2.6.4
*/
// Auto-create config.json if it does not exist
if (!file_exists($configJsonPath)) {
if (file_exists($configExamplePath)) {
@@ -83,6 +104,15 @@ if (!file_exists($configJsonPath)) {
}
}
/**
* config.json inladen en ontbrekende secties aanvullen met defaults.
*
* Voegt default-waarden samen voor security, analytics en logging,
* garandeert de excluded_ips-standaardlijst en converteert relatieve
* content_dir-paden naar absoluut.
*
* @since 2.6.4
*/
if (file_exists($configJsonPath)) {
$jsonContent = file_get_contents($configJsonPath);
$config = json_decode($jsonContent, true);
@@ -150,6 +180,11 @@ if (file_exists($configJsonPath)) {
$config['content_dir'] = $projectRoot . $config['content_dir'];
}
/**
* Actieve theme-configuratie laden vanuit theme.json.
*
* @since 2.6.4
*/
// Load active theme
$activeTheme = $config['active_theme'] ?? 'default';
$themeDir = __DIR__ . '/../../themes/' . $activeTheme;
@@ -166,6 +201,11 @@ if (file_exists($configJsonPath)) {
}
}
/**
* Minimale fallback-configuratie wanneer config.json niet geladen kon worden.
*
* @since 2.6.4
*/
// Fallback to minimal config
return [
'site_title' => 'CodePress',
+60 -20
View File
@@ -1,37 +1,52 @@
<?php
/**
* CodePress CMS Core Loader
*
* This file serves as the central entry point for the CodePress CMS core system.
* It loads all essential classes and configuration needed for the CMS to function.
*
* Architecture:
* - config.php: Configuration loader that merges default settings with config.json
* - ThemeManager.php: Twig-based theme rendering + SCSS compilation
* - CodePressCMS.php: Main CMS class handling content, navigation, search, and rendering
*
* Usage:
* This file is included by public/index.php, which then:
* 1. Loads configuration from config.php
* 2. Creates a new CodePressCMS instance with the config
* 3. Calls render() to output the complete page
*
* The separation allows for:
* - Clean public entry point (public/index.php)
* - Reusable core components
* - Proper class organization with PHPDoc documentation
* CodePress CMS Core Loader.
*
* Centraal bootstrap-bestand voor de CodePress CMS-core. Laadt alle
* essentiële classes en configuratie die nodig zijn voor het CMS:
* configuratie, Composer-autoloader, core utility classes, logger,
* plugin-systeem, ContentAPI en de hoofdclass CodePressCMS.
*
* Architectuur:
* - config.php: configuratie-loader die defaults samenvoegt met config.json.
* - ThemeManager.php: Twig-based theme rendering + SCSS-compilatie.
* - CodePressCMS.php: hoofdclass voor content, navigation, search en rendering.
*
* Gebruik:
* Dit bestand wordt geïnclude door public/index.php, dat vervolgens:
* 1. Configuratie laadt vanuit config.php.
* 2. Een CodePressCMS-instantie aanmaakt met de config.
* 3. render() aanroept om de pagina uit te voeren.
*
* @since 2.6.4
* @package CodePress
*/
/**
* Configuratie-systeem laden: defaults en config.json-merge.
*
* @since 2.6.4
*/
// Load configuration system - handles default settings and config.json merging
require_once 'config.php';
/**
* Composer-autoloader laden indien aanwezig.
*
* @since 2.6.4
*/
// Load Composer autoloader
$autoloader = dirname(__DIR__, 2) . '/vendor/autoload.php';
if (file_exists($autoloader)) {
require_once $autoloader;
}
/**
* Core utility classes laden.
*
* @since 2.6.4
*/
// Load core utility classes
require_once 'class/Cache.php';
require_once 'class/RateLimiter.php';
@@ -41,21 +56,46 @@ require_once 'class/GeoIP.php';
require_once 'class/Analytics.php';
require_once 'class/ThemeManager.php';
/**
* Logger-klassen laden voor gestructureerde logging met log levels.
*
* @since 2.6.4
*/
// Load Logger class - structured logging with log levels
require_once 'class/Logger.php';
require_once 'class/LogManager.php';
/**
* Plugin-systeem laden (interfaces, API's en PluginManager).
*
* @since 2.6.4
*/
// Load Plugin system
require_once 'plugin/PluginAPIInterface.php';
require_once 'plugin/CMSAPI.php';
require_once 'plugin/AdminPluginAPI.php';
require_once 'plugin/PluginManager.php';
/**
* ContentAPI laden: CMS-data-toegang voor PHP content-bestanden.
*
* @since 2.6.4
*/
// Load ContentAPI class - provides CMS data access for PHP content files
require_once 'class/ContentAPI.php';
/**
* Hoofdclass laden: content parsing, navigation, search en page rendering.
*
* @since 2.6.4
*/
// Load main CMS class - handles content parsing, navigation, search, and page rendering
require_once 'class/CodePressCMS.php';
/**
* Logger initialiseren (debug-mode kan via config worden ingeschakeld).
*
* @since 2.6.4
*/
// Initialize logger (debug mode can be enabled in config)
Logger::init();
+109 -24
View File
@@ -1,16 +1,54 @@
<?php
/**
* Lightweight API wrapper for plugins in the admin context.
* Provides read-only access to the site config (no CMS instance needed).
* Lichtgewicht API-wrapper voor plugins in de admin-context.
*
* Biedt alleen-lezen toegang tot de site-config (zonder CMS-instance nodig).
*
* @since 2.6.4
*/
class AdminPluginAPI implements PluginAPIInterface
{
/**
* De site-configuratie als associatieve array.
*
* @since 2.6.4
* @var array<string,mixed>
*/
private array $config;
/**
* Het absolute pad naar de project-root.
*
* @since 2.6.4
* @var string
*/
private string $projectRoot;
/**
* De actieve admin-taalcode (bijv. 'nl', 'en').
*
* @since 2.6.4
* @var string
*/
private string $adminLanguage;
/**
* De admin PluginManager-instantie, optioneel via setPluginManager geïnjecteerd.
*
* @since 2.6.4
* @var PluginManager|null
*/
private ?PluginManager $pluginManager = null;
/**
* Construeer een AdminPluginAPI-instantie met de opgegeven site-config.
*
* @since 2.6.4
*
* @param array<string,mixed> $siteConfig De site-configuratie.
* @param string $projectRoot Optioneel absoluut pad naar de project-root.
*/
public function __construct(array $siteConfig, string $projectRoot = '')
{
$this->config = $siteConfig;
@@ -20,8 +58,13 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Inject the admin PluginManager so plugins can resolve their own
* translations through the same fallback chain.
* Injecteer de admin PluginManager zodat plugins hun eigen vertalingen
* via dezelfde fallback-keten kunnen resolven.
*
* @since 2.6.4
*
* @param PluginManager $pm De admin PluginManager-instantie.
* @return void
*/
public function setPluginManager(PluginManager $pm): void
{
@@ -29,7 +72,13 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Get configuration value using dot notation.
* Haal een configuratiewaarde op via dot-notatie.
*
* @since 2.6.4
*
* @param string $key Configuratie-sleutel in dot-notatie (bijv. 'language.default').
* @param mixed $default Standaardwaarde indien de sleutel niet bestaat.
* @return mixed De gevonden waarde of $default indien niet gevonden.
*/
public function getConfig(string $key, $default = null)
{
@@ -47,7 +96,11 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Get the project root directory.
* Haal de project-rootdirectory op.
*
* @since 2.6.4
*
* @return string Absoluut pad naar de project-root.
*/
public function getProjectRoot(): string
{
@@ -55,15 +108,30 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Get the content directory path.
* Haal het pad naar de contentdirectory op als absoluut pad.
*
* Relatieve paden uit config.json (bijv. 'content') worden afgezet tegen
* de project-root, conform het gedrag van cms/core/config.php.
*
* @since 2.6.4
*
* @return string Absoluut pad naar de contentdirectory.
*/
public function getContentDir(): string
{
return $this->config['content_dir'] ?? ($this->projectRoot . '/content');
$dir = $this->config['content_dir'] ?? ($this->projectRoot . '/content');
if ($dir !== '' && $dir[0] !== DIRECTORY_SEPARATOR && !preg_match('#^[A-Za-z]:[/\\\\]#', $dir)) {
$dir = $this->projectRoot . '/' . $dir;
}
return rtrim($dir, '/');
}
/**
* Get the plugins directory path.
* Haal het pad naar de pluginsdirectory op.
*
* @since 2.6.4
*
* @return string Absoluut pad naar de pluginsdirectory.
*/
public function getPluginsDir(): string
{
@@ -71,7 +139,11 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Get the list of enabled plugins.
* Haal de lijst met ingeschakelde plugins op.
*
* @since 2.6.4
*
* @return array<int,string> Lijst met ingeschakelde plugin-namen.
*/
public function getEnabledPlugins(): array
{
@@ -79,7 +151,11 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Get the version info from version.php.
* Haal de versie-informatie uit version.php op.
*
* @since 2.6.4
*
* @return array<string,mixed> Versie-informatie met minimaal de sleutel 'version'.
*/
public function getVersionInfo(): array
{
@@ -94,8 +170,13 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Get the active admin language code (e.g. 'nl', 'en').
* System plugins should use this to resolve their translations.
* Haal de actieve admin-taalcode op (bijv. 'nl', 'en').
*
* System-plugins gebruiken deze om hun vertalingen te resolven.
*
* @since 2.6.4
*
* @return string De actieve admin-taalcode.
*/
public function getAdminLanguage(): string
{
@@ -103,14 +184,16 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Get the translations for a plugin in the admin context.
* Haal de vertalingen voor een plugin op in de admin-context.
*
* Fallback chain (handled by PluginManager):
* requested language -> plugin default_language -> empty array.
* Fallback-keten (afgehandeld door PluginManager):
* aangevraagde taal -> default_language van de plugin -> lege array.
*
* @param string $pluginName Plugin directory name
* @param string|null $lang Override language; defaults to the active admin language
* @return array Translations [key => value]
* @since 2.6.4
*
* @param string $pluginName Plugin-directorynaam.
* @param string|null $lang Override-taal; standaard de actieve admin-taal.
* @return array<string,string> Vertalingen als [key => value].
*/
public function getPluginTranslations(string $pluginName, ?string $lang = null): array
{
@@ -122,12 +205,14 @@ class AdminPluginAPI implements PluginAPIInterface
}
/**
* Translate a single key for a plugin in the admin context.
* Vertaal een enkele sleutel voor een plugin in de admin-context.
*
* @param string $key Translation key
* @param string $pluginName Plugin directory name
* @param string|null $lang Override language; defaults to the active admin language
* @return string Translated string, or $key if not found
* @since 2.6.4
*
* @param string $key Vertaalsleutel.
* @param string $pluginName Plugin-directorynaam.
* @param string|null $lang Override-taal; standaard de actieve admin-taal.
* @return string Vertaalde string, of $key indien niet gevonden.
*/
public function t(string $key, string $pluginName, ?string $lang = null): string
{
+225 -38
View File
@@ -1,18 +1,51 @@
<?php
/**
* Front-end plugin-API voor content-plugins.
*
* Biedt toegang tot de CodePressCMS-instance, pagina-informatie, menu, zoekresultaten,
* vertalingen en bestandsoperaties binnen de front-end context.
*
* @since 2.6.4
*/
class CMSAPI implements PluginAPIInterface
{
/**
* De CodePressCMS-hoofdinstantie.
*
* @since 2.6.4
* @var CodePressCMS
*/
private CodePressCMS $cms;
/**
* De front-end PluginManager-instantie, optioneel via setPluginManager geïnjecteerd.
*
* @since 2.6.4
* @var PluginManager|null
*/
private ?PluginManager $pluginManager = null;
/**
* Construeer een CMSAPI-instantie met de opgegeven CodePressCMS.
*
* @since 2.6.4
*
* @param CodePressCMS $cms De CodePressCMS-hoofdinstantie.
*/
public function __construct(CodePressCMS $cms)
{
$this->cms = $cms;
}
/**
* Inject the front-end PluginManager so content plugins can resolve
* their own translations through the same fallback chain.
* Injecteer de front-end PluginManager zodat content-plugins hun eigen
* vertalingen via dezelfde fallback-keten kunnen resolven.
*
* @since 2.6.4
*
* @param PluginManager $pm De front-end PluginManager-instantie.
* @return void
*/
public function setPluginManager(PluginManager $pm): void
{
@@ -20,7 +53,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get current page information
* Haal de informatie van de huidige pagina op.
*
* @since 2.6.4
*
* @return array<string,mixed> Pagina-informatie van de huidige pagina.
*/
public function getCurrentPage(): array
{
@@ -28,7 +65,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get current page title
* Haal de titel van de huidige pagina op.
*
* @since 2.6.4
*
* @return string De titel van de huidige pagina, of lege string.
*/
public function getCurrentPageTitle(): string
{
@@ -37,7 +78,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get current page content
* Haal de inhoud van de huidige pagina op.
*
* @since 2.6.4
*
* @return string De HTML-inhoud van de huidige pagina, of lege string.
*/
public function getCurrentPageContent(): string
{
@@ -46,7 +91,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get current page URL
* Haal de URL van de huidige pagina op.
*
* @since 2.6.4
*
* @return string De URL van de huidige pagina inclusief query-parameters.
*/
public function getCurrentPageUrl(): string
{
@@ -56,7 +105,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get menu structure
* Haal de menustructuur op.
*
* @since 2.6.4
*
* @return array<int,array<string,mixed>> De menustructuur.
*/
public function getMenu(): array
{
@@ -64,7 +117,13 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get configuration value
* Haal een configuratiewaarde op via dot-notatie.
*
* @since 2.6.4
*
* @param string $key Configuratie-sleutel in dot-notatie (bijv. 'language.default').
* @param mixed $default Standaardwaarde indien de sleutel niet bestaat.
* @return mixed De gevonden waarde of $default indien niet gevonden.
*/
public function getConfig(string $key, $default = null)
{
@@ -82,7 +141,70 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get translation
* Haal de project-rootdirectory op (absoluut, genormaliseerd).
*
* @since 2.6.4
*
* @return string Absoluut pad naar de project-root.
*/
public function getProjectRoot(): string
{
return dirname(__DIR__, 3);
}
/**
* Haal het pad naar de contentdirectory op (absoluut, genormaliseerd).
*
* De CMS-config lost relatieve content_dir-waarden al op
* (zie cms/core/config.php), dus deze wordt as-is teruggegeven.
*
* @since 2.6.4
*
* @return string Absoluut pad naar de contentdirectory.
*/
public function getContentDir(): string
{
return rtrim($this->cms->config['content_dir'] ?? ($this->getProjectRoot() . '/content'), '/');
}
/**
* Haal het pad naar de pluginsdirectory op (absoluut, genormaliseerd).
*
* @since 2.6.4
*
* @return string Absoluut pad naar de pluginsdirectory.
*/
public function getPluginsDir(): string
{
return $this->getProjectRoot() . '/plugins';
}
/**
* Haal de versie-informatie uit version.php op.
*
* @since 2.6.4
*
* @return array<string,mixed> Versie-informatie met minimaal de sleutel 'version'.
*/
public function getVersionInfo(): array
{
$versionFile = $this->getProjectRoot() . '/version.php';
if (file_exists($versionFile)) {
$data = include $versionFile;
if (is_array($data)) {
return $data;
}
}
return ['version' => '0.0.0'];
}
/**
* Haal een vertaling op voor de opgegeven sleutel.
*
* @since 2.6.4
*
* @param string $key Vertaalsleutel.
* @return string De vertaalde string, of $key indien niet gevonden.
*/
public function translate(string $key): string
{
@@ -90,7 +212,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get current language
* Haal de huidige content-taal op.
*
* @since 2.6.4
*
* @return string De huidige taalcode (bijv. 'nl', 'en').
*/
public function getCurrentLanguage(): string
{
@@ -98,7 +224,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Check if user is on homepage
* Controleer of de gebruiker zich op de homepage bevindt.
*
* @since 2.6.4
*
* @return bool True indien de huidige pagina de default_page is.
*/
public function isHomepage(): bool
{
@@ -108,7 +238,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get file info for current page
* Haal de bestandsinformatie van de huidige pagina op.
*
* @since 2.6.4
*
* @return array<string,mixed>|null Bestandsinformatie of null indien niet aanwezig.
*/
public function getCurrentPageFileInfo(): ?array
{
@@ -117,7 +251,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get breadcrumb data
* Haal de breadcrumb-HTML op.
*
* @since 2.6.4
*
* @return string De gegenereerde breadcrumb-HTML.
*/
public function getBreadcrumb(): string
{
@@ -125,7 +263,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Check if content directory has content
* Controleer of de contentdirectory inhoud bevat.
*
* @since 2.6.4
*
* @return bool True indien de contentdirectory niet leeg is.
*/
public function hasContent(): bool
{
@@ -133,7 +275,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get search results if searching
* Haal de zoekresultaten op indien een zoekopdracht actief is.
*
* @since 2.6.4
*
* @return array<int,array<string,mixed>> De zoekresultaten, of een lege array.
*/
public function getSearchResults(): array
{
@@ -144,7 +290,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Check if currently searching
* Controleer of momenteel een zoekopdracht wordt uitgevoerd.
*
* @since 2.6.4
*
* @return bool True indien de search-query-parameter aanwezig is.
*/
public function isSearching(): bool
{
@@ -152,7 +302,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get available languages
* Haal de beschikbare talen op.
*
* @since 2.6.4
*
* @return array<int,string> Lijst met beschikbare taalcodes.
*/
public function getAvailableLanguages(): array
{
@@ -160,7 +314,13 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Create URL for page
* Maak een URL voor een pagina.
*
* @since 2.6.4
*
* @param string $page Pagina-identifier.
* @param string|null $lang Taalcode; standaard de huidige content-taal.
* @return string De gegenereerde pagina-URL met query-parameters.
*/
public function createUrl(string $page, ?string $lang = null): string
{
@@ -169,7 +329,15 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Execute PHP file and capture output
* Voer een PHP-bestand uit en vang de output op.
*
* Het bestand moet binnen de CMS-directory liggen om willekeurige
* file-inclusion te voorkomen.
*
* @since 2.6.4
*
* @param string $filePath Absoluut pad naar het PHP-bestand.
* @return string De opgevangen output, of lege string indien ongeldig.
*/
public function executePhpFile(string $filePath): string
{
@@ -190,7 +358,12 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get content from PHP/HTML/Markdown file
* Haal de inhoud op uit een PHP-, HTML- of Markdown-bestand.
*
* @since 2.6.4
*
* @param string $filePath Pad naar het bestand.
* @return string De bestandsinhoud (gerenderd indien PHP/Markdown), of lege string.
*/
public function getFileContent(string $filePath): string
{
@@ -215,7 +388,12 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Check if file exists in content directory
* Controleer of een bestand in de contentdirectory bestaat.
*
* @since 2.6.4
*
* @param string $filename Bestandsnaam relatief ten opzichte van de contentdirectory.
* @return bool True indien het bestand bestaat.
*/
public function contentFileExists(string $filename): bool
{
@@ -224,9 +402,11 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get all content entries (mappen + bestanden) as a list of entry arrays.
* Haal alle content-items (mappen + bestanden) op als lijst met entry-arrays.
*
* @return array List of ['path' => ..., 'title' => ..., 'type' => ...]
* @since 2.6.4
*
* @return array<int,array<string,mixed>> Lijst met entries ['path' => ..., 'title' => ..., 'type' => ...].
*/
public function getAllPages(): array
{
@@ -234,10 +414,13 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get the author metadata for the current page.
* Returns author_name, author_email and created from the page frontmatter.
* Haal de auteursmetadata van de huidige pagina op.
*
* @return array Author metadata with keys: author_name, author_email, created
* Geeft author_name, author_email en created terug uit de pagina-frontmatter.
*
* @since 2.6.4
*
* @return array<string,string> Auteursmetadata met sleutels: author_name, author_email, created.
*/
public function getPageAuthor(): array
{
@@ -251,15 +434,17 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Get the translations for a plugin in the front-end context.
* Haal de vertalingen voor een plugin op in de front-end context.
*
* Content plugins follow the current content language. Fallback chain
* (handled by PluginManager): requested language -> plugin
* default_language -> empty array.
* Content-plugins volgen de huidige content-taal. Fallback-keten
* (afgehandeld door PluginManager): aangevraagde taal ->
* default_language van de plugin -> lege array.
*
* @param string $pluginName Plugin directory name
* @param string|null $lang Override language; defaults to the current content language
* @return array Translations [key => value]
* @since 2.6.4
*
* @param string $pluginName Plugin-directorynaam.
* @param string|null $lang Override-taal; standaard de huidige content-taal.
* @return array<string,string> Vertalingen als [key => value].
*/
public function getPluginTranslations(string $pluginName, ?string $lang = null): array
{
@@ -271,12 +456,14 @@ class CMSAPI implements PluginAPIInterface
}
/**
* Translate a single key for a plugin in the front-end context.
* Vertaal een enkele sleutel voor een plugin in de front-end context.
*
* @param string $key Translation key
* @param string $pluginName Plugin directory name
* @param string|null $lang Override language; defaults to the current content language
* @return string Translated string, or $key if not found
* @since 2.6.4
*
* @param string $key Vertaalsleutel.
* @param string $pluginName Plugin-directorynaam.
* @param string|null $lang Override-taal; standaard de huidige content-taal.
* @return string Vertaalde string, of $key indien niet gevonden.
*/
public function t(string $key, string $pluginName, ?string $lang = null): string
{
+70 -14
View File
@@ -1,33 +1,89 @@
<?php
/**
* Interface for plugin API objects.
* Both CMSAPI (front-end) and AdminPluginAPI (admin) implement this.
* Interface voor plugin-API-objecten.
*
* Zowel CMSAPI (front-end) als AdminPluginAPI (admin) implementeren deze interface.
*
* @since 2.6.4
*/
interface PluginAPIInterface
{
/**
* Haal een configuratiewaarde op via dot-notatie.
*
* @since 2.6.4
*
* @param string $key Configuratie-sleutel in dot-notatie (bijv. 'language.default').
* @param mixed $default Standaardwaarde indien de sleutel niet bestaat.
* @return mixed De gevonden waarde of $default indien niet gevonden.
*/
public function getConfig(string $key, $default = null);
/**
* Get the translations for a plugin in the current context.
* Haal de project-rootdirectory op (absoluut, genormaliseerd).
*
* System plugins resolve against the admin language; content plugins
* against the current content language. The implementation handles the
* fallback to the plugin's default_language.
* @since 2.6.4
*
* @param string $pluginName Plugin directory name
* @param string|null $lang Override language (optional)
* @return array Translations [key => value]
* @return string Absoluut pad naar de project-root.
*/
public function getProjectRoot(): string;
/**
* Haal het pad naar de contentdirectory op (absoluut, genormaliseerd).
*
* Bij een relatieve configuratiewaarde wordt deze afgezet tegen de project-root.
*
* @since 2.6.4
*
* @return string Absoluut pad naar de contentdirectory.
*/
public function getContentDir(): string;
/**
* Haal het pad naar de pluginsdirectory op (absoluut, genormaliseerd).
*
* @since 2.6.4
*
* @return string Absoluut pad naar de pluginsdirectory.
*/
public function getPluginsDir(): string;
/**
* Haal de versie-informatie uit version.php op als associatieve array.
*
* Bevat altijd minimaal de sleutel 'version'.
*
* @since 2.6.4
*
* @return array<string,mixed> Versie-informatie met minimaal de sleutel 'version'.
*/
public function getVersionInfo(): array;
/**
* Haal de vertalingen voor een plugin op in de huidige context.
*
* System-plugins resolven tegen de admin-taal; content-plugins tegen de
* huidige content-taal. De implementatie regelt de fallback naar de
* default_language van de plugin.
*
* @since 2.6.4
*
* @param string $pluginName Plugin-directorynaam.
* @param string|null $lang Optionele override-taal.
* @return array<string,string> Vertalingen als [key => value].
*/
public function getPluginTranslations(string $pluginName, ?string $lang = null): array;
/**
* Translate a single key for a plugin in the current context.
* Vertaal een enkele sleutel voor een plugin in de huidige context.
*
* @param string $key Translation key
* @param string $pluginName Plugin directory name
* @param string|null $lang Override language (optional)
* @return string Translated string, or $key if not found
* @since 2.6.4
*
* @param string $key Vertaalsleutel.
* @param string $pluginName Plugin-directorynaam.
* @param string|null $lang Optionele override-taal.
* @return string Vertaalde string, of $key indien niet gevonden.
*/
public function t(string $key, string $pluginName, ?string $lang = null): string;
}
+242 -37
View File
@@ -1,15 +1,82 @@
<?php
/**
* Beheert het laden en hooks-systeem van plugins.
*
* Laadt ingeschakelde plugins uit de pluginsdirectory, registreert automatisch
* action- en filter-hooks, en biedt methoden voor plugin-configuratie,
* vertalingen, admin-routes en sidebar-content.
*
* @since 2.6.4
*/
class PluginManager
{
/**
* Geladen plugin-instanties, geïndexeerd op plugin-naam.
*
* @since 2.6.4
* @var array<string,object>
*/
private array $plugins = [];
/**
* Het absolute pad naar de pluginsdirectory.
*
* @since 2.6.4
* @var string
*/
private string $pluginsPath;
/**
* De geïnjecteerde plugin-API-instantie (CMSAPI of AdminPluginAPI).
*
* @since 2.6.4
* @var PluginAPIInterface|null
*/
private $api = null;
/**
* Lijst met ingeschakelde plugin-namen.
*
* @since 2.6.4
* @var array<int,string>
*/
private array $enabledPlugins = [];
/**
* Geregistreerde action-hooks, gegroepeerd per hook-naam en prioriteit.
*
* @since 2.6.4
* @var array<string,array<int,array<int,callable>>>
*/
private array $actions = [];
/**
* Geregistreerde filter-hooks, gegroepeerd per hook-naam en prioriteit.
*
* @since 2.6.4
* @var array<string,array<int,array<int,callable>>>
*/
private array $filters = [];
/**
* De standaardtaal van de site, gebruikt als fallback voor plugins
* zonder eigen default_language in plugin.json.
*
* @since 2.6.4
* @var string
*/
private string $siteDefaultLanguage = 'nl';
/**
* Construeer een PluginManager en laadt direct de ingeschakelde plugins.
*
* @since 2.6.4
*
* @param string $pluginsPath Absoluut pad naar de pluginsdirectory.
* @param array<int,string> $enabledPlugins Lijst met ingeschakelde plugin-namen.
* @param string $siteDefaultLanguage Standaardtaal van de site.
*/
public function __construct(string $pluginsPath, array $enabledPlugins = [], string $siteDefaultLanguage = 'nl')
{
$this->pluginsPath = $pluginsPath;
@@ -19,14 +86,31 @@ class PluginManager
}
/**
* Set the CMS site default language. Used as a fallback when a plugin
* does not declare its own `default_language` in plugin.json.
* Stel de standaardtaal van de site in.
*
* Wordt gebruikt als fallback wanneer een plugin geen eigen `default_language`
* declareert in plugin.json.
*
* @since 2.6.4
*
* @param string $lang Taalcode (bijv. 'nl', 'en').
* @return void
*/
public function setSiteDefaultLanguage(string $lang): void
{
$this->siteDefaultLanguage = $lang !== '' ? $lang : 'nl';
}
/**
* Stel de plugin-API in en geef deze door aan alle geladen plugins.
*
* Plugins die een setAPI()-methode implementeren, ontvangen de API-instantie.
*
* @since 2.6.4
*
* @param PluginAPIInterface $api De plugin-API-instantie.
* @return void
*/
public function setAPI($api): void
{
$this->api = $api;
@@ -38,6 +122,16 @@ class PluginManager
}
}
/**
* Laad alle ingeschakelde plugins uit de pluginsdirectory.
*
* Per plugin wordt het hoofdbestand (<naam>.php) geïncludeerd, de plugin-class
* geïnstantieerd, en action- en filter-hooks automatisch geregistreerd.
*
* @since 2.6.4
*
* @return void
*/
private function loadPlugins(): void
{
if (!is_dir($this->pluginsPath)) {
@@ -82,16 +176,45 @@ class PluginManager
}
}
/**
* Registreer een action-callback voor een hook.
*
* @since 2.6.4
*
* @param string $hook Naam van de action-hook.
* @param callable $callback De callback die bij de hook wordt uitgevoerd.
* @param int $priority Uitvoer-volgorde; lager wordt eerder uitgevoerd.
* @return void
*/
public function addAction(string $hook, callable $callback, int $priority = 10): void
{
$this->actions[$hook][$priority][] = $callback;
}
/**
* Registreer een filter-callback voor een hook.
*
* @since 2.6.4
*
* @param string $hook Naam van de filter-hook.
* @param callable $callback De callback die de waarde filtert.
* @param int $priority Uitvoer-volgorde; lager wordt eerder uitgevoerd.
* @return void
*/
public function addFilter(string $hook, callable $callback, int $priority = 10): void
{
$this->filters[$hook][$priority][] = $callback;
}
/**
* Voer alle geregistreerde callbacks voor een action-hook uit.
*
* @since 2.6.4
*
* @param string $hook Naam van de action-hook.
* @param mixed ...$args Argumenten die aan elke callback worden doorgegeven.
* @return void
*/
public function doAction(string $hook, ...$args): void
{
if (!isset($this->actions[$hook])) return;
@@ -103,6 +226,16 @@ class PluginManager
}
}
/**
* Pas alle geregistreerde filter-callbacks toe op een waarde.
*
* @since 2.6.4
*
* @param string $hook Naam van de filter-hook.
* @param mixed $value De initiële waarde die wordt gefilterd.
* @param mixed ...$args Extra argumenten voor elke filter-callback.
* @return mixed De gefilterde waarde.
*/
public function applyFilters(string $hook, $value, ...$args)
{
if (!isset($this->filters[$hook])) return $value;
@@ -115,32 +248,66 @@ class PluginManager
return $value;
}
/**
* Haal een specifieke plugin-instantie op.
*
* @since 2.6.4
*
* @param string $name Plugin-naam (directorynaam).
* @return object|null De plugin-instantie of null indien niet geladen.
*/
public function getPlugin(string $name): ?object
{
return $this->plugins[$name] ?? null;
}
/**
* Haal alle geladen plugin-instanties op.
*
* @since 2.6.4
*
* @return array<string,object> Plugin-instanties geïndexeerd op naam.
*/
public function getAllPlugins(): array
{
return $this->plugins;
}
/**
* Haal de lijst met ingeschakelde plugin-namen op.
*
* @since 2.6.4
*
* @return array<int,string> Lijst met ingeschakelde plugin-namen.
*/
public function getEnabledPlugins(): array
{
return $this->enabledPlugins;
}
/**
* Controleer of een plugin is ingeschakeld.
*
* @since 2.6.4
*
* @param string $pluginName Plugin-naam (directorynaam).
* @return bool True indien de plugin is ingeschakeld.
*/
public function isEnabled(string $pluginName): bool
{
return in_array($pluginName, $this->enabledPlugins, true);
}
/**
* Get the runtime config for a plugin: defaults from plugin.json `settings`
* merged with overrides from the plugin's config.json.
* Haal de runtime-configuratie van een plugin op.
*
* @param string $pluginName Plugin name (directory name)
* @return array Resolved config: [key => value, ...]
* Standaardwaarden uit plugin.json `settings` worden samengevoegd met
* overrides uit het config.json van de plugin.
*
* @since 2.6.4
*
* @param string $pluginName Plugin-naam (directorynaam).
* @return array<string,mixed> Opgeloste configuratie als [key => value].
*/
public function getPluginConfig(string $pluginName): array
{
@@ -167,15 +334,17 @@ class PluginManager
}
/**
* Get the default (fallback) language declared by a plugin.
* Haal de standaard (fallback) taal van een plugin op.
*
* Resolved from (in order):
* Resolved in volgorde van:
* 1. plugin.json `default_language`
* 2. CMS site config `language.default`
* 2. CMS site-config `language.default`
* 3. 'nl'
*
* @param string $pluginName Plugin directory name
* @return string Language code (e.g. 'nl', 'en')
* @since 2.6.4
*
* @param string $pluginName Plugin-directorynaam.
* @return string Taalcode (bijv. 'nl', 'en').
*/
public function getPluginDefaultLanguage(string $pluginName): string
{
@@ -190,15 +359,17 @@ class PluginManager
}
/**
* Load the translations for a plugin for the requested language.
* Laad de vertalingen voor een plugin voor de aangevraagde taal.
*
* Fallback chain: requested language -> plugin default language -> empty array.
* Fallback-keten: aangevraagde taal -> standaardtaal van de plugin -> lege array.
*
* @param string $pluginName Plugin directory name
* @param string $lang Requested language code
* @param string $context Translation context: 'admin' or 'site' (front-end).
* Defaults to 'admin' for system plugins, 'site' for content plugins.
* @return array Translations [key => value]
* @since 2.6.4
*
* @param string $pluginName Plugin-directorynaam.
* @param string $lang Aangevraagde taalcode.
* @param string $context Vertaalcontext: 'admin' of 'site' (front-end).
* Standaard 'admin' voor system-plugins, 'site' voor content-plugins.
* @return array<string,string> Vertalingen als [key => value].
*/
public function getPluginTranslations(string $pluginName, string $lang, string $context = 'admin'): array
{
@@ -232,14 +403,16 @@ class PluginManager
}
/**
* Collect translations for every loaded plugin for the requested language.
* Verzamel vertalingen voor alle geladen plugins voor de aangevraagde taal.
*
* Returns an associative array keyed by plugin name:
* Geeft een associatieve array terug, geïndexeerd op plugin-naam:
* ['Statistics' => [...], 'Logs' => [...], ...]
*
* @param string $lang Requested language code
* @param string $context 'admin' or 'site'
* @return array Plugin translations keyed by plugin name
* @since 2.6.4
*
* @param string $lang Aangevraagde taalcode.
* @param string $context 'admin' of 'site'.
* @return array<string,array<string,string>> Plugin-vertalingen geïndexeerd op plugin-naam.
*/
public function getAllPluginTranslations(string $lang, string $context = 'admin'): array
{
@@ -252,6 +425,16 @@ class PluginManager
return $all;
}
/**
* Controleer of een plugin viewable is.
*
* Een plugin is viewable tenzij zijn configuratie 'viewable' expliciet op false zet.
*
* @since 2.6.4
*
* @param object $plugin De plugin-instantie.
* @return bool True indien de plugin viewable is.
*/
public function isPluginViewable(object $plugin): bool
{
if (method_exists($plugin, 'getConfig')) {
@@ -261,6 +444,17 @@ class PluginManager
return true;
}
/**
* Haal de samengevoegde sidebar-content van alle viewable plugins op.
*
* Elke plugin met een getSidebarContent()-methode levert HTML die wordt
* ingepakt in een Bootstrap-card met de plugin-titel.
*
* @since 2.6.4
*
* @param array<int,string>|null $allowedPlugins Optionele whitelist van plugin-namen.
* @return string De samengevoegde sidebar-HTML.
*/
public function getSidebarContent(?array $allowedPlugins = null): string
{
$sidebarContent = '';
@@ -300,9 +494,11 @@ class PluginManager
}
/**
* Get CSS URLs from all enabled plugins that provide getCssUrl().
* Haal CSS-URL's op van alle ingeschakelde plugins die getCssUrl() implementeren.
*
* @return array List of CSS URLs
* @since 2.6.4
*
* @return array<int,string> Lijst met CSS-URL's.
*/
public function getPluginCssUrls(): array
{
@@ -319,10 +515,13 @@ class PluginManager
}
/**
* Collect admin menu items from all enabled plugins that implement getAdminMenu().
* Each plugin returns [['route' => 'plugin-name/action', 'label' => 'Label', 'icon' => 'bi-icon']].
* Verzamel admin-menu-items van alle plugins die getAdminMenu() implementeren.
*
* @return array Admin menu items from all plugins
* Elke plugin levert [['route' => 'plugin-name/action', 'label' => 'Label', 'icon' => 'bi-icon']].
*
* @since 2.6.4
*
* @return array<int,array<string,mixed>> Admin-menu-items van alle plugins.
*/
public function getAdminMenuItems(): array
{
@@ -341,12 +540,16 @@ class PluginManager
}
/**
* Dispatch an admin route to a plugin that handles it.
* Plugins implement handleAdminRoute(string $action): ?string (returns rendered HTML or null).
* Dispatch een admin-route naar de plugin die deze afhandelt.
*
* @param string $pluginName Plugin name
* @param string $action Action/sub-route within the plugin
* @return string|null Rendered HTML, or null if plugin doesn't handle it
* Plugins implementeren handleAdminRoute(string $action): ?string
* (geeft gerenderde HTML of null terug).
*
* @since 2.6.4
*
* @param string $pluginName Plugin-naam.
* @param string $action Actie/sub-route binnen de plugin.
* @return string|null Gerenderde HTML, of null indien de plugin deze niet afhandelt.
*/
public function dispatchAdminRoute(string $pluginName, string $action): ?string
{
@@ -361,10 +564,12 @@ class PluginManager
}
/**
* Find which plugin handles a given admin route.
* Bepaal welke plugin een opgegeven admin-route afhandelt.
*
* @param string $route The admin route (e.g. 'statistics' or 'statistics/details')
* @return array|null ['plugin' => name, 'action' => action, 'permission' => required permission] or null
* @since 2.6.4
*
* @param string $route De admin-route (bijv. 'statistics' of 'statistics/details').
* @return array<string,mixed>|null ['plugin' => naam, 'action' => actie, 'permission' => vereiste permissie] of null.
*/
public function resolveAdminRoute(string $route): ?array
{
@@ -382,4 +587,4 @@ class PluginManager
}
return null;
}
}
}