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';