Security: verwijder hardcoded wachtwoord, voeg random-wachtwoord-generator toe bij eerste installatie
This commit is contained in:
+1
-2
@@ -55,10 +55,9 @@ composer install
|
||||
|
||||
```bash
|
||||
cp config.json.example config.json
|
||||
cp admin/config/admin.json.example admin/config/admin.json
|
||||
```
|
||||
|
||||
Edit `config.json` with your site title, language and plugins. Change the admin password in `admin/config/admin.json` (default `admin`/`admin`).
|
||||
Edit `config.json` with your site title, language and plugins. On the first visit to `/admin`, `admin/config/admin.json` is created automatically with a random password that is shown on the login screen. Save this password safely and change it immediately after login.
|
||||
|
||||
### Step 3a — Apache 2.4+
|
||||
|
||||
|
||||
@@ -55,10 +55,9 @@ composer install
|
||||
|
||||
```bash
|
||||
cp config.json.example config.json
|
||||
cp admin/config/admin.json.example admin/config/admin.json
|
||||
```
|
||||
|
||||
Pas `config.json` aan met je site titel, taal en plugins. Wijzig het admin wachtwoord in `admin/config/admin.json` (standaard `admin`/`admin`).
|
||||
Pas `config.json` aan met je site titel, taal en plugins. Bij de eerste keer dat je `/admin` opent wordt `admin/config/admin.json` automatisch aangemaakt met een willekeurig wachtwoord dat op het inlogscherm wordt getoond. Sla dit wachtwoord veilig op en wijzig het direct na login.
|
||||
|
||||
### Stap 3a — Apache 2.4+
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# TODO
|
||||
|
||||
## bug
|
||||
- [x] Admin /content
|
||||
- [x] onderwater heet de is bij Admin de content, theme en plugin de pluginFileTree overal de zelfde naam. Dit is niet logies en consistent. Als de zelfde twig gebruikt word hou dan de naamgeving algemeen.
|
||||
- [x] Bij Admin/content "Content bestanden" werkt de > bij het openen van de mappen niet. Wees consistenter en compacter. Hergebuik code en maak het universeler. Deze kan gelijk zijn aan de theme en plugin .
|
||||
- [x] bij media invoeren zie ik nu card's voor de images of media. Dit moet anders. Gebruik weer een filetree
|
||||
- [x] Bij het editen van de content zie ik Zichtbare plugin's. Hier staan nu ook system plugin's in. Maak hier een pulldown multiselect waar ik alleen de active content plugin's ziet
|
||||
- [x] Ook zie ik hier Auteur naam en auteur email, maar deze zijn niet ingevult. Deze zijn bekend omdat ik ingelocht bent.
|
||||
- [x] De auteur_name en auteur_email moeten hier weg
|
||||
- [x] Ook wil ik dit zie bij elke aanpassing van een pagina --- created: xx-xx-xx xx:xx:xx edited: xx-xx-xx xx:xx:xx ---
|
||||
- [x] De git init is hier niet handig. Haal dit weg. Dit wordt later een systeem plugin
|
||||
- [x] Houw de code consistend en compact. How in de gaten dat de functionaliteit niet omzeep geholpen wordt.
|
||||
- [x] GET http://development.codepress.noorlander.info/content/_images/D_D.jpg → 403 Forbidden — opgelost: /admin/media-list content-scope gebruikt nu /-media/ prefix i.p.v. /content/
|
||||
|
||||
## Voltooid ✅
|
||||
|
||||
- [x] diep link werken niet goed. Pagina titels worden wel herkend maar de gegenereerde link werken niet. — processContent() herschreef geldige /<lang>/<page> URLs naar /-media/ (404). Skip-regex uitgebreid met dynamische taal-prefixen via getAvailableLanguages().
|
||||
- [x] De tree van de admin/content admin/theme en admin/plugin
|
||||
- [x] Mappen en bestanden moeten kunnen worden versleept kunnen worden om te verplaatsen. — Drag-and-drop via tree-interactions.js + uniform AJAX move endpoint (/admin/tree-move) voor alle drie de scopes.
|
||||
- [x] Ik moet in de tree ook een bestand/map kunnen selecteren
|
||||
- [x] om te kunnen editen — klik op bestand opent editor, klik op map opent map-detail paneel
|
||||
- [x] verwijderen — prullenbak-knop per bestand in tree + verwijder-knop per map in map-detail paneel
|
||||
- [x] op deze plaats een nieuwe map/bestand te kunnen maken — per-map actie-knoppen in tree (nieuw bestand, nieuwe map) + knoppen in map-detail paneel
|
||||
- [x] verplaatsen — drag-and-drop + potlood-knop per bestand (verplaats-formulier)
|
||||
- [x] Alle tree's moeten op de zelfde manier kunnen werken — _file-tree.twig is uniform: alle drie de trees hebben treeDirActions, treeDirSelectRoute, drag-drop, per-node actie-knoppen, map-detail paneel. Nieuwe routes: theme-dir-create-in/rename-in/delete-in, plugins-dir-create-in/rename-in/delete-in, tree-move.
|
||||
|
||||
## Nice to have
|
||||
- [ ] Multidomein implementeren (elk domein = eigen thema + content, één admin + site name)
|
||||
@@ -37,6 +37,10 @@
|
||||
- [ ] AGENTS.md + config.json.example bijwerken
|
||||
- [ ] Verificatie: php -l, curl met Host-header, domein-switch in admin testen
|
||||
|
||||
## v2.6.4 (2026-08-26) ✅
|
||||
- [x] Bug: diep link werken niet goed — processContent() herschreef geldige /<lang>/<page> URLs naar /-media/ (404); skip-regex dynamisch uitgebreid met getAvailableLanguages()
|
||||
- [x] Bug: uniforme tree met drag-and-drop voor admin/content, admin/theme, admin/plugin — tree-interactions.js (drag-drop + per-node actie-knoppen), uniform AJAX move endpoint (/admin/tree-move), map-detail paneel voor alle drie de scopes, theme/plugin dir create/rename/delete routes, handleidingen bijgewerkt
|
||||
|
||||
## Voltooid ✅
|
||||
|
||||
- [x] Admin code en niet gebruikte mappen/bestanden opschonen
|
||||
|
||||
+421
-20
@@ -1,18 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AdminAuth - File-based authentication for CodePress Admin
|
||||
* File-based authenticatie en autorisatie voor het CodePress admin paneel.
|
||||
*
|
||||
* Beheert gebruikerssessies, rollen met permissies, CSRF-tokens, brute-force
|
||||
* lockout en gebruikersbeheer. Alle data wordt opgeslagen in JSON-bestanden
|
||||
* (geen database).
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
class AdminAuth
|
||||
{
|
||||
/**
|
||||
* Applicatie-configuratie uit config.json.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var array Applicatie-configuratie.
|
||||
*/
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
* Admin-configuratie geladen uit admin.json (gebruikers en security-instellingen).
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var array Admin-configuratie met keys 'users' en 'security'.
|
||||
*/
|
||||
private array $adminConfig;
|
||||
|
||||
/**
|
||||
* Pad naar het JSON-bestand met mislukte inlogpogingen voor brute-force lockout.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var string Absoluut pad naar login_attempts.json.
|
||||
*/
|
||||
private string $lockFile;
|
||||
|
||||
/**
|
||||
* Role definitions with permissions.
|
||||
* Each role maps to a list of allowed route prefixes.
|
||||
* 'admin' has wildcard '*' access.
|
||||
* Tijdelijk gegenereerd wachtwoord bij eerste installatie.
|
||||
*
|
||||
* Wordt gevuld wanneer loadAdminConfig() een nieuwe admin.json aanmaakt
|
||||
* met een cryptografisch veilig willekeurig wachtwoord. De login-pagina
|
||||
* toont dit wachtwoord eenmalig via getGeneratedPassword().
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var string Leeg tenzij net aangemaakt bij eerste installatie.
|
||||
*/
|
||||
private string $generatedPassword = '';
|
||||
|
||||
/**
|
||||
* Permissies per rol.
|
||||
*
|
||||
* Elke rol mapt naar een lijst van toegestane route-prefixen. De rol
|
||||
* 'admin' heeft wildcard '*' toegang tot alle routes.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var array<string, string[]> Map van rol-sleutel naar lijst met toegestane routes.
|
||||
*/
|
||||
public const ROLE_PERMISSIONS = [
|
||||
'admin' => ['*'],
|
||||
@@ -22,7 +64,10 @@ class AdminAuth
|
||||
];
|
||||
|
||||
/**
|
||||
* Human-readable role labels.
|
||||
* Leesbare rol-labels voor weergave in de admin-interface.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var array<string, string> Map van rol-sleutel naar label.
|
||||
*/
|
||||
public const ROLE_LABELS = [
|
||||
'admin' => 'Admin',
|
||||
@@ -31,6 +76,16 @@ class AdminAuth
|
||||
'site-admin' => 'Site Admin',
|
||||
];
|
||||
|
||||
/**
|
||||
* Constructor: initialiseer authenticatie met de applicatie-configuratie.
|
||||
*
|
||||
* Laadt de admin-configuratie, bepaalt het pad naar het lockout-bestand en
|
||||
* start de sessie met de juiste security-instellingen.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param array $appConfig Applicatie-configuratie; vereist keys 'log_file' en 'admin_config'.
|
||||
*/
|
||||
public function __construct(array $appConfig)
|
||||
{
|
||||
$this->config = $appConfig;
|
||||
@@ -39,6 +94,17 @@ class AdminAuth
|
||||
$this->startSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Laadt de admin-configuratie uit admin.json.
|
||||
*
|
||||
* Als het bestand nog niet bestaat wordt het aangemaakt vanuit
|
||||
* admin.json.example, of indien die ook ontbreekt met een standaard admin-
|
||||
* gebruiker. Geeft een lege structuur terug als de JSON ongeldig is.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return array Admin-configuratie met keys 'users' en 'security'.
|
||||
*/
|
||||
private function loadAdminConfig(): array
|
||||
{
|
||||
$path = $this->config['admin_config'];
|
||||
@@ -48,11 +114,19 @@ class AdminAuth
|
||||
if (file_exists($examplePath)) {
|
||||
@copy($examplePath, $path);
|
||||
} else {
|
||||
// Genereer een cryptografisch veilig wachtwoord bij eerste
|
||||
// installatie. Het wachtwoord wordt nooit in de broncode
|
||||
// opgeslagen; de hash wordt weggeschreven naar admin.json
|
||||
// en het plaintext wachtwoord éénmalig getoond op het
|
||||
// inlogscherm via getGeneratedPassword().
|
||||
$plainPassword = $this->generateRandomPassword();
|
||||
$this->generatedPassword = $plainPassword;
|
||||
|
||||
$defaultAdminConfig = [
|
||||
'users' => [
|
||||
[
|
||||
'username' => 'admin',
|
||||
'password_hash' => password_hash('admin', PASSWORD_BCRYPT),
|
||||
'password_hash' => password_hash($plainPassword, PASSWORD_BCRYPT),
|
||||
'role' => 'admin',
|
||||
'created' => date('Y-m-d'),
|
||||
]
|
||||
@@ -74,6 +148,55 @@ class AdminAuth
|
||||
return is_array($data) ? $data : ['users' => [], 'security' => []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Genereer een cryptografisch veilig willekeurig wachtwoord.
|
||||
*
|
||||
* Gebruik random_bytes() (CSPRNG) met een mix van alfanumerieke tekens
|
||||
* en een vaste lengte, zodat het wachtwoord voldoende entropie heeft
|
||||
* en veilig te typen is.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return string Willekeurig wachtwoord van 16 alfanumerieke tekens.
|
||||
*/
|
||||
private function generateRandomPassword(int $length = 16): string
|
||||
{
|
||||
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$max = strlen($alphabet) - 1;
|
||||
$password = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$password .= $alphabet[random_int(0, $max)];
|
||||
}
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geef het bij eerste installatie gegenereerde wachtwoord terug.
|
||||
*
|
||||
* De login-pagina gebruikt dit om het wachtwoord eenmalig te tonen
|
||||
* zodra admin.json is aangemaakt. Na de eerste succesvolle login
|
||||
* wordt het veld geleegd. Geeft een lege string terug wanneer er
|
||||
* geen nieuw wachtwoord is gegenereerd in deze request.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return string Plaintext wachtwoord of lege string.
|
||||
*/
|
||||
public function getGeneratedPassword(): string
|
||||
{
|
||||
return $this->generatedPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slaat de huidige admin-configuratie op naar admin.json.
|
||||
*
|
||||
* Schrijft de interne admin-configuratie weg als pretty-printed JSON met
|
||||
* behoud van unicode-tekens.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function saveAdminConfig(): void
|
||||
{
|
||||
file_put_contents(
|
||||
@@ -82,6 +205,17 @@ class AdminAuth
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start de sessie met veilige cookie-parameters en handhaaft de session timeout.
|
||||
*
|
||||
* Stelt cookie-flags in (HttpOnly, SameSite=Strict, Secure bij HTTPS), start
|
||||
* de sessie en beëindigt deze bij inactiviteit langer dan de geconfigureerde
|
||||
* timeout. Vernieuwt de activiteit-timestamp bij geauthenticeerde gebruikers.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function startSession(): void
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
@@ -113,6 +247,20 @@ class AdminAuth
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifieert inloggegevens en start een admin-sessie bij succes.
|
||||
*
|
||||
* Controleert eerst of het account niet is vergrendeld door brute-force
|
||||
* protection. Bij een fout wachtwoord wordt een mislukte poging geregistreerd
|
||||
* en gelogd. Bij succes worden mislukte pogingen gewist en de sessie
|
||||
* gevuld met gebruiker, rol en CSRF-token.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @param string $password Wachtwoord (plaintext).
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function login(string $username, string $password): array
|
||||
{
|
||||
// Check brute-force lockout
|
||||
@@ -145,6 +293,16 @@ class AdminAuth
|
||||
return ['success' => true, 'message' => 'Ingelogd.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Logt de huidige gebruiker uit en vernietigt de sessie.
|
||||
*
|
||||
* Maakt de sessie-array leeg, verwijdert het sessie-cookie en vernietigt de
|
||||
* sessie. De uitlogactie wordt gelogd.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
$user = $_SESSION['admin_user'] ?? 'unknown';
|
||||
@@ -160,11 +318,28 @@ class AdminAuth
|
||||
$this->log('info', "Uitgelogd: {$user}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Controleert of de huidige bezoeker is ingelogd.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return bool True indien een admin-gebruiker in de sessie staat.
|
||||
*/
|
||||
public function isAuthenticated(): bool
|
||||
{
|
||||
return isset($_SESSION['admin_user']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de gegevens van de ingelogde gebruiker, verrijkt met profielvelden.
|
||||
*
|
||||
* Levert username en rol uit de sessie en vult deze aan met email,
|
||||
* author_name en author_email uit admin.json indien aanwezig.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return array|null Gebruikersdata als array, of null indien niet ingelogd.
|
||||
*/
|
||||
public function getCurrentUser(): ?array
|
||||
{
|
||||
if (!$this->isAuthenticated()) {
|
||||
@@ -186,8 +361,14 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role of the current user.
|
||||
* Returns the override role if set (for testing), otherwise the real role.
|
||||
* Geeft de huidige rol van de ingelogde gebruiker.
|
||||
*
|
||||
* Retourneert de override-rol indien ingesteld (voor test-doeleinden), anders
|
||||
* de werkelijke rol.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return string Rol-sleutel.
|
||||
*/
|
||||
public function getCurrentRole(): string
|
||||
{
|
||||
@@ -198,7 +379,11 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the real role of the current user (ignoring any override).
|
||||
* Geeft de werkelijke rol van de ingelogde gebruiker, eventuele override negerend.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return string Rol-sleutel.
|
||||
*/
|
||||
public function getRealRole(): string
|
||||
{
|
||||
@@ -206,7 +391,11 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has an active role override.
|
||||
* Controleert of de ingelogde gebruiker een actieve rol-override heeft.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return bool True indien een override-rol is ingesteld.
|
||||
*/
|
||||
public function hasRoleOverride(): bool
|
||||
{
|
||||
@@ -214,8 +403,16 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily switch to another role for testing purposes.
|
||||
* Only admin users can do this; the override cannot grant more than admin.
|
||||
* Wisselt tijdelijk naar een andere rol voor test-doeleinden.
|
||||
*
|
||||
* Alleen werkelijke admins kunnen wisselen; admins kunnen niet wisselen naar
|
||||
* 'admin' (zinloos) en enkel naar lagere rollen. Slaat de override op in de
|
||||
* sessie. De actie wordt gelogd.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $role Doel-rol-sleutel.
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function switchRole(string $role): array
|
||||
{
|
||||
@@ -240,7 +437,13 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the role override back to the real admin role.
|
||||
* Reset de rol-override terug naar de werkelijke admin-rol.
|
||||
*
|
||||
* Verwijdert de override uit de sessie indien aanwezig en logt de actie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function resetRole(): array
|
||||
{
|
||||
@@ -253,7 +456,15 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has permission to access a route.
|
||||
* Controleert of de huidige gebruiker een bepaalde route mag openen.
|
||||
*
|
||||
* Gebruikt de permissies van de huidige rol; de wildcard '*' geeft toegang
|
||||
* tot alles. Onbekende rollen krijgen enkel 'dashboard' en 'logout'.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $route Route-sleutel die gecontroleerd moet worden.
|
||||
* @return bool True indien de gebruiker de route mag benaderen.
|
||||
*/
|
||||
public function hasPermission(string $route): bool
|
||||
{
|
||||
@@ -268,7 +479,11 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available roles.
|
||||
* Geeft alle beschikbare rollen met hun leesbare labels.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return array<string, string> Map van rol-sleutel naar label.
|
||||
*/
|
||||
public static function getRoles(): array
|
||||
{
|
||||
@@ -276,13 +491,28 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role label.
|
||||
* Geeft het leesbare label voor een rol.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $role Rol-sleutel.
|
||||
* @return string Leesbaar label, of de rol-sleutel zelf indien onbekend.
|
||||
*/
|
||||
public static function getRoleLabel(string $role): string
|
||||
{
|
||||
return self::ROLE_LABELS[$role] ?? $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft het huidige CSRF-token, of genereert een nieuw token indien afwezig.
|
||||
*
|
||||
* Slaat een cryptografisch willekeurig token op in de sessie en retourneert
|
||||
* dit voor gebruik in admin-formulieren.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return string 64-tekens hexadecimaal CSRF-token.
|
||||
*/
|
||||
public function getCsrfToken(): string
|
||||
{
|
||||
if (!isset($_SESSION['admin_csrf_token'])) {
|
||||
@@ -291,11 +521,31 @@ class AdminAuth
|
||||
return $_SESSION['admin_csrf_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifieert een meegegeven CSRF-token tegen het token in de sessie.
|
||||
*
|
||||
* Gebruikt hash_equals om timing-attacks te voorkomen.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $token Token uit het formulier of verzoek.
|
||||
* @return bool True indien het token overeenkomt met het sessie-token.
|
||||
*/
|
||||
public function verifyCsrf(string $token): bool
|
||||
{
|
||||
return isset($_SESSION['admin_csrf_token']) && hash_equals($_SESSION['admin_csrf_token'], $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regeneert het CSRF-token in de sessie.
|
||||
*
|
||||
* Maakt een nieuw cryptografisch willekeurig token aan, nuttig na
|
||||
* privilege-wijzigingen of na het verwerken van een formulier.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function regenerateCsrfToken(): void
|
||||
{
|
||||
$_SESSION['admin_csrf_token'] = bin2hex(random_bytes(32));
|
||||
@@ -303,6 +553,16 @@ class AdminAuth
|
||||
|
||||
// --- User Management ---
|
||||
|
||||
/**
|
||||
* Geeft alle gebruikers met hun profiel- en rolinformatie.
|
||||
*
|
||||
* Levert een array geïndexeerd op gebruikersnaam, aangevuld met het
|
||||
* leesbare rol-label en eventuele profielvelden.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return array<string, array> Map van gebruikersnaam naar gebruikersdata.
|
||||
*/
|
||||
public function getUsers(): array
|
||||
{
|
||||
$users = [];
|
||||
@@ -321,6 +581,23 @@ class AdminAuth
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Voegt een nieuwe gebruiker toe aan de admin-configuratie.
|
||||
*
|
||||
* Controleert op dubbele gebruikersnamen, minimum wachtwoordlengte (8) en
|
||||
* geldigheid van de rol. Het wachtwoord wordt opgeslagen als bcrypt-hash.
|
||||
* Wijzigingen worden direct weggeschreven en gelogd.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @param string $password Wachtwoord (plaintext, minimaal 8 tekens).
|
||||
* @param string $role Rol-sleutel. Default 'admin'.
|
||||
* @param string $email E-mailadres van de gebruiker. Default ''.
|
||||
* @param string $authorName Naam voor auteur-metadata. Default ''.
|
||||
* @param string $authorEmail E-mail voor auteur-metadata. Default ''.
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function addUser(string $username, string $password, string $role = 'admin', string $email = '', string $authorName = '', string $authorEmail = ''): array
|
||||
{
|
||||
if ($this->findUser($username)) {
|
||||
@@ -348,7 +625,18 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the profile (email, author_name, author_email) of a user.
|
||||
* Werkt de profielvelden (email, author_name, author_email) van een gebruiker bij.
|
||||
*
|
||||
* Zoekt de gebruiker op naam en overschrijft de profielvelden, slaat de
|
||||
* configuratie op en logt de wijziging.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @param string $email E-mailadres. Default ''.
|
||||
* @param string $authorName Naam voor auteur-metadata. Default ''.
|
||||
* @param string $authorEmail E-mail voor auteur-metadata. Default ''.
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function updateUserProfile(string $username, string $email = '', string $authorName = '', string $authorEmail = ''): array
|
||||
{
|
||||
@@ -366,8 +654,17 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the role of an existing user.
|
||||
* A user can never change their own role (security guard).
|
||||
* Wijzigt de rol van een bestaande gebruiker.
|
||||
*
|
||||
* Een gebruiker kan nooit zijn eigen rol wijzigen (security guard). De rol
|
||||
* moet een bekende sleutel uit ROLE_PERMISSIONS zijn. Wijzigingen worden
|
||||
* opgeslagen en gelogd.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @param string $role Nieuwe rol-sleutel.
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function changeRole(string $username, string $role): array
|
||||
{
|
||||
@@ -388,6 +685,17 @@ class AdminAuth
|
||||
return ['success' => false, 'message' => 'Gebruiker niet gevonden.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verwijdert een gebruiker uit de admin-configuratie.
|
||||
*
|
||||
* Een gebruiker kan zichzelf niet verwijderen. De gebruikerslijst wordt
|
||||
* gefilterd, opgeslagen en de actie gelogd.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function deleteUser(string $username): array
|
||||
{
|
||||
if ($username === ($_SESSION['admin_user'] ?? '')) {
|
||||
@@ -403,6 +711,18 @@ class AdminAuth
|
||||
return ['success' => true, 'message' => 'Gebruiker verwijderd.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Zet een nieuw wachtwoord voor een gebruiker.
|
||||
*
|
||||
* Controleert de minimum wachtwoordlengte (8), hasht het wachtwoord met de
|
||||
* standaard PHP-hash en slaat de wijziging op.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @param string $newPassword Nieuw wachtwoord (plaintext, minimaal 8 tekens).
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function changePassword(string $username, string $newPassword): array
|
||||
{
|
||||
if (strlen($newPassword) < 8) {
|
||||
@@ -419,6 +739,19 @@ class AdminAuth
|
||||
return ['success' => false, 'message' => 'Gebruiker niet gevonden.'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wijzigt het eigen wachtwoord na verificatie van het huidige wachtwoord.
|
||||
*
|
||||
* Verifieert eerst het huidige wachtwoord tegen de opgeslagen hash, controleert
|
||||
* de lengte van het nieuwe wachtwoord (minimaal 8) en slaat de nieuwe hash op.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @param string $currentPassword Huidig wachtwoord (plaintext).
|
||||
* @param string $newPassword Nieuw wachtwoord (plaintext, minimaal 8 tekens).
|
||||
* @return array Resultaat met keys 'success' (bool) en 'message' (string).
|
||||
*/
|
||||
public function changeOwnPassword(string $username, string $currentPassword, string $newPassword): array
|
||||
{
|
||||
$user = $this->findUser($username);
|
||||
@@ -444,6 +777,14 @@ class AdminAuth
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
/**
|
||||
* Zoekt een gebruiker op gebruikersnaam in de admin-configuratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @return array|null Gebruikersdata als array, of null indien niet gevonden.
|
||||
*/
|
||||
private function findUser(string $username): ?array
|
||||
{
|
||||
foreach ($this->adminConfig['users'] ?? [] as $user) {
|
||||
@@ -454,6 +795,18 @@ class AdminAuth
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controleert of een gebruiker momenteel is vergrendeld door brute-force protection.
|
||||
*
|
||||
* Vergelijkt het aantal mislukte pogingen en de verstreken tijd sinds de
|
||||
* laatste poging met de geconfigureerde limieten. Verloopt de lockout dan
|
||||
* worden de pogingen gewist.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @return array Resultaat met keys 'locked' (bool) en optioneel 'remaining' (int, seconden).
|
||||
*/
|
||||
private function checkLockout(string $username): array
|
||||
{
|
||||
$attempts = $this->getFailedAttempts();
|
||||
@@ -477,6 +830,17 @@ class AdminAuth
|
||||
return ['locked' => false];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registreert een mislukte inlogpoging voor een gebruiker.
|
||||
*
|
||||
* Verhoogt de teller en actualiseert de timestamp van de laatste poging,
|
||||
* waarna de gegevens worden weggeschreven naar het lockout-bestand.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @return void
|
||||
*/
|
||||
private function recordFailedAttempt(string $username): void
|
||||
{
|
||||
$attempts = $this->getFailedAttempts();
|
||||
@@ -488,6 +852,16 @@ class AdminAuth
|
||||
file_put_contents($this->lockFile, json_encode($attempts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wist alle mislukte inlogpogingen voor een gebruiker.
|
||||
*
|
||||
* Verwijdert de gebruiker uit het lockout-bestand en slaat dit op.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @return void
|
||||
*/
|
||||
private function clearFailedAttempts(string $username): void
|
||||
{
|
||||
$attempts = $this->getFailedAttempts();
|
||||
@@ -496,13 +870,28 @@ class AdminAuth
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all failed login attempts for a user (public, used by CLI reset).
|
||||
* Wist de lockout voor een gebruiker (publiek, gebruikt door CLI reset).
|
||||
*
|
||||
* Wrapper rond clearFailedAttempts die ook van buiten de class kan worden
|
||||
* aangeroepen, bijvoorbeeld door een CLI-tool voor het resetten van vergrendelingen.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $username Gebruikersnaam.
|
||||
* @return void
|
||||
*/
|
||||
public function clearLockout(string $username): void
|
||||
{
|
||||
$this->clearFailedAttempts($username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leest alle mislukte inlogpogingen uit het lockout-bestand.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @return array<string, array{count: int, last_attempt: int}> Map van gebruikersnaam naar poging-data.
|
||||
*/
|
||||
private function getFailedAttempts(): array
|
||||
{
|
||||
if (!file_exists($this->lockFile)) {
|
||||
@@ -512,6 +901,18 @@ class AdminAuth
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Schrijft een bericht naar het logbestand met timestamp, level en IP-adres.
|
||||
*
|
||||
* Maakt de logdirectory aan indien nodig. Het IP-adres wordt via RequestLogger
|
||||
* bepaald indien beschikbaar, anders via REMOTE_ADDR met een fallback.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*
|
||||
* @param string $level Log-niveau (bijv. 'info', 'warning').
|
||||
* @param string $message Bericht dat gelogd moet worden.
|
||||
* @return void
|
||||
*/
|
||||
private function log(string $level, string $message): void
|
||||
{
|
||||
$logFile = $this->config['log_file'];
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"admi":{"count":1,"last_attempt":1771257322}}
|
||||
{"admi":{"count":1,"last_attempt":1771257322},"":{"count":4,"last_attempt":1787141227}}
|
||||
@@ -27,6 +27,14 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if generated_password %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6 class="alert-heading"><i class="bi bi-exclamation-triangle-fill"></i> Eerste installatie</h6>
|
||||
<p class="mb-2">Er is nog geen <code>admin.json</code> aanwezig. Een nieuw wachtwoord is gegenereerd voor de gebruiker <strong>admin</strong>:</p>
|
||||
<p class="mb-2"><code class="fs-5">{{ generated_password }}</code></p>
|
||||
<p class="mb-0 small">Log in met dit wachtwoord en wijzig het direct na login. Sla het veilig op; het wordt niet opnieuw getoond.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="/admin/login">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="mb-3">
|
||||
|
||||
@@ -13,24 +13,37 @@
|
||||
.editor-tree .tree-toggle.is-selected { background-color: rgba(13,110,253,.12); font-weight: 600; }
|
||||
.editor-tree .tree-toggle { cursor: pointer; user-select: none; width: 100%; text-align: left; background: transparent; border: 0; color: inherit; }
|
||||
.editor-tree .tree-chevron { flex: 0 0 auto; }
|
||||
.editor-tree .tree-chevron-btn { background: transparent; border: 0; color: inherit; padding: .2rem .4rem; line-height: 1.4; flex: 0 0 auto; cursor: pointer; }
|
||||
.editor-tree .tree-chevron-btn:hover { color: var(--bs-primary, #0d6efd); }
|
||||
.editor-tree .tree-name { flex: 1 1 auto; min-width: 0; }
|
||||
.editor-tree .tree-name.is-selected { background-color: var(--bs-primary-bg-subtle, #cfe2ff); font-weight: 600; }
|
||||
.editor-tree .tree-root-link { display: flex; align-items: center; gap: .3rem; padding: .25rem .4rem; border-radius: .25rem; text-decoration: none; color: inherit; font-size: .9rem; font-weight: 600; }
|
||||
.editor-tree .tree-root-link:hover { background-color: rgba(13,110,253,.08); }
|
||||
.editor-tree .tree-root-link.is-selected { background-color: var(--bs-primary-bg-subtle, #cfe2ff); }
|
||||
.editor-tree .tree-chevron-spacer { display: inline-block; width: .9rem; flex: 0 0 auto; }
|
||||
.editor-tree .badge-ext { font-size: .6rem; margin-left: auto; }
|
||||
.editor-tree .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.editor-tree .tree-delete-form { position: absolute; right: .25rem; top: 50%; transform: translateY(-50%); margin: 0; }
|
||||
.editor-tree .tree-delete-btn { font-size: .9rem; line-height: 1; padding: .15rem; color: #dc3545; background: transparent; border: 0; }
|
||||
.editor-tree .tree-delete-btn:hover { color: #b02a37; }
|
||||
.editor-tree .tree-move-btn { position: absolute; right: 1.75rem; top: 50%; transform: translateY(-50%); font-size: .9rem; line-height: 1; padding: .15rem; color: #6c757d; text-decoration: none; }
|
||||
.editor-tree .tree-move-btn:hover { color: #0d6efd; }
|
||||
.editor-tree li > .tree-link { padding-right: 3.5rem; }
|
||||
.editor-tree .tree-move-btn,
|
||||
.editor-tree .tree-delete-btn { font-size: .9rem; line-height: 1; padding: .15rem; color: #6c757d; text-decoration: none; background: transparent; border: 0; }
|
||||
.editor-tree .tree-move-btn:hover,
|
||||
.editor-tree .tree-delete-btn:hover { color: #0d6efd; }
|
||||
.editor-tree .tree-delete-btn:hover { color: #dc3545; }
|
||||
.editor-tree .tree-file-actions { position: absolute; right: .25rem; top: 50%; transform: translateY(-50%); display: flex; gap: .15rem; align-items: center; opacity: 0; transition: opacity .15s; }
|
||||
.editor-tree li:hover > .tree-file-actions { opacity: 1; }
|
||||
.editor-tree li > .tree-link { padding-right: 4rem; }
|
||||
/* Drag-and-drop states */
|
||||
.editor-tree li.tree-dragging { opacity: .5; }
|
||||
.editor-tree .tree-drag-over { background-color: rgba(13,110,253,.18) !important; outline: 2px dashed var(--bs-primary, #0d6efd); outline-offset: -2px; border-radius: .25rem; }
|
||||
/* Toast */
|
||||
.tree-toast { border-radius: .375rem; }
|
||||
.plugin-file-tree .card-body { max-height: 70vh; overflow-y: auto; }
|
||||
/* Directory action buttons (new file/dir, rename, delete) — content editor only */
|
||||
.tree-dir-row { position: relative; }
|
||||
.tree-dir-row .tree-toggle { padding-right: 7rem; }
|
||||
/* Directory action buttons (new file/dir, rename) — content editor only */
|
||||
.tree-dir-row { position: relative; display: flex; align-items: center; }
|
||||
.tree-dir-row > .tree-name { padding-right: 3rem; }
|
||||
.tree-dir-row > .tree-toggle { padding-right: 5rem; }
|
||||
.tree-dir-actions { position: absolute; right: .25rem; top: 50%; transform: translateY(-50%); display: flex; gap: .15rem; align-items: center; }
|
||||
.tree-dir-actions .btn-link { color: #6c757d; }
|
||||
.tree-dir-actions .btn-link:hover { color: #0d6efd; }
|
||||
.tree-dir-actions .tree-delete-btn { color: #dc3545; }
|
||||
.tree-dir-actions .tree-delete-btn:hover { color: #b02a37; }
|
||||
@media (max-width: 768px) {
|
||||
.editor-layout { flex-direction: column; }
|
||||
.plugin-file-tree { width: 100%; flex-basis: auto; max-height: 320px; }
|
||||
|
||||
@@ -6,25 +6,33 @@
|
||||
- files: array of nodes [{name, path, is_dir, extension, size, modified, children}]
|
||||
- relFile: currently selected relative file path (for active highlight + auto-expand)
|
||||
- editableExts: array of editable extensions (shown as .EXT badge)
|
||||
- csrf_token: CSRF token for delete forms
|
||||
- treeIdPrefix: prefix for collapse element ids (e.g. 'content-tree', 'plugin-tree', 'theme-tree')
|
||||
- treeRouteBase: admin route base used to build file links, e.g. '/admin/content'
|
||||
- treeRouteParams: extra query string for file links, e.g. 'plugin=MyPlugin&' or 'theme=default&' or '' (content)
|
||||
- treeDeleteRoute: admin route for file delete, e.g. '/admin/content-file-delete'
|
||||
- treeDeleteName: optional hidden field name for delete form (e.g. 'plugin' or 'theme'); emits no field when empty
|
||||
- treeDeleteValue: optional hidden field value for treeDeleteName (e.g. the plugin/theme name)
|
||||
- treeMoveRoute: admin route for file move, e.g. '/admin/plugins-file-move'
|
||||
- treeMoveParams: extra query string for move link, e.g. 'plugin=X&' or ''
|
||||
- treeDirActions: boolean; show per-folder new-file/new-dir/rename/delete buttons (content editor only)
|
||||
- treeDirActions: boolean; show per-folder new-file/new-dir/rename buttons (default true)
|
||||
- treeFileMove: boolean; show per-file move/rename pencil button (default true)
|
||||
- treeHideActionsFor: array of relative paths where move/delete actions are hidden (e.g. ['theme.json'])
|
||||
- treeDirSelectRoute: if set, folder names are links to ?dir=<path> (map-detail pane)
|
||||
- treeDirRenameRoute: if set, show a rename pencil per folder
|
||||
- treeDeleteBase: POST URL for file deletion (scope-specific)
|
||||
- treeScope: 'content' | 'theme' | 'plugin' (for tree-interactions.js)
|
||||
- treeMoveUrl: AJAX endpoint for drag-drop move (e.g. '/admin/tree-move')
|
||||
- treeScopeParam: extra query param string (e.g. 'plugin=MyPlugin&' or 'theme=default&' or '')
|
||||
|
||||
Icon maps are chosen automatically from the node extension. #}
|
||||
{% macro tree(nodes, opts) %}
|
||||
{% import _self as macros %}
|
||||
<ul class="editor-tree">
|
||||
{% for n in nodes %}
|
||||
{% set isActive = n.path == opts.relFile %}
|
||||
{% set containsActive = n.is_dir and opts.relFile starts with (n.path ~ '/') %}
|
||||
{# Een bestand is alleen actief wanneer er geen map geselecteerd is
|
||||
(een map-selectie opent het map-detail paneel, geen bestand). #}
|
||||
{% set isActive = n.path == opts.relFile and opts.selectedDir|default('') == '' %}
|
||||
{# Markeer de oudermap "bevat actieve selectie" voor auto-uitklappen:
|
||||
bij een geselecteerd bestand OF een geselecteerde (sub)map. #}
|
||||
{% set activePath = opts.selectedDir|default('') != '' ? opts.selectedDir : opts.relFile %}
|
||||
{% set containsActive = n.is_dir and activePath != '' and activePath starts with (n.path ~ '/') %}
|
||||
{% if n.is_dir %}
|
||||
{% set icon = 'bi-folder-fill text-warning' %}
|
||||
{% elseif n.extension == 'md' %}
|
||||
@@ -48,37 +56,42 @@
|
||||
{% else %}
|
||||
{% set icon = 'bi-file text-muted' %}
|
||||
{% endif %}
|
||||
<li>
|
||||
<li data-path="{{ n.path }}" data-isdir="{{ n.is_dir ? '1' : '0' }}">
|
||||
{% if n.is_dir %}
|
||||
{% set collapseId = opts.treeIdPrefix ~ '-' ~ n.path|replace({'/':'-','.':'-'}) %}
|
||||
<div class="tree-dir-row">
|
||||
<button type="button" class="tree-link tree-toggle btn-toggle {{ containsActive ? '' : 'collapsed' }}"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ collapseId }}"
|
||||
aria-expanded="{{ containsActive ? 'true' : 'false' }}"
|
||||
title="{{ n.path }}">
|
||||
<div class="tree-dir-row" data-dir="{{ n.path }}">
|
||||
{# Chevron: expand/collapse only (zonder navigatie) #}
|
||||
<button type="button" class="tree-chevron-btn" data-bs-toggle="collapse" data-bs-target="#{{ collapseId }}" aria-expanded="{{ containsActive ? 'true' : 'false' }}" title="{{ ta.expand_collapse|default('Uitklappen/inklappen') }}" aria-label="{{ ta.expand_collapse|default('Uitklappen/inklappen') }}">
|
||||
<i class="bi {{ containsActive ? 'bi-chevron-down' : 'bi-chevron-right' }} tree-chevron"></i>
|
||||
</button>
|
||||
{# Mapnaam: selecteer de map. Voor content gaat dit naar ?dir= (map-detail paneel rechts);
|
||||
voor plugins/themes is het een niet-navigerende toggle (legacy gedrag). #}
|
||||
{% if opts.treeDirSelectRoute %}
|
||||
<a href="{{ opts.treeDirSelectRoute }}{% if '?' in opts.treeDirSelectRoute %}&{% else %}?{% endif %}dir={{ n.path|url_encode }}" class="tree-link tree-name {{ containsActive ? 'is-active' : '' }} {{ opts.selectedDir == n.path ? 'is-selected' : '' }}" title="{{ n.path }}">
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<button type="button" class="tree-link tree-toggle btn-toggle {{ containsActive ? '' : 'collapsed' }}" data-bs-toggle="collapse" data-bs-target="#{{ collapseId }}" aria-expanded="{{ containsActive ? 'true' : 'false' }}" title="{{ n.path }}">
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if opts.treeDirActions %}
|
||||
<span class="tree-dir-actions">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newfile-btn" title="{{ ta.new_file|default('Nieuw bestand hier') }}" aria-label="{{ ta.new_file|default('Nieuw bestand') }}" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="{{ n.path }}">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newfile-btn" title="{{ ta.new_file|default('Nieuw bestand hier') }}" aria-label="{{ ta.new_file|default('Nieuw bestand') }}" data-bs-toggle="modal" data-bs-target="{{ opts.newFileModalId|default('#newFileModal') }}" data-in-dir="{{ n.path }}">
|
||||
<i class="bi bi-file-earmark-plus"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newdir-btn" title="{{ ta.new_folder|default('Nieuwe map') }}" aria-label="{{ ta.new_folder|default('Nieuwe map') }}" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ n.path }}">
|
||||
{% if opts.newDirModalId is defined and opts.newDirModalId %}
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newdir-btn" title="{{ ta.new_folder|default('Nieuwe map') }}" aria-label="{{ ta.new_folder|default('Nieuwe map') }}" data-bs-toggle="modal" data-bs-target="{{ opts.newDirModalId }}" data-in-dir="{{ n.path }}">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
</button>
|
||||
<a href="/admin/content-dir-rename-in?dir={{ n.path|url_encode }}" class="tree-rename-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
||||
{% endif %}
|
||||
{% if opts.treeDirRenameRoute %}
|
||||
<a href="{{ opts.treeDirRenameRoute }}{% if '?' in opts.treeDirRenameRoute %}&{% else %}?{% endif %}dir={{ n.path|url_encode }}" class="tree-rename-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/content-dir-delete-in" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_folder|default('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ opts.csrfToken }}">
|
||||
<input type="hidden" name="dir" value="{{ n.path }}">
|
||||
<button type="submit" class="btn btn-link btn-sm p-0 text-danger tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -99,22 +112,20 @@
|
||||
<span class="badge bg-secondary badge-ext">{{ n.extension|upper }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<span class="tree-file-actions">
|
||||
{% if n.path not in (opts.treeHideActionsFor ?? []) %}
|
||||
{% set showFileMove = opts.treeFileMove is defined ? opts.treeFileMove : true %}
|
||||
{% if showFileMove %}
|
||||
<a href="{{ opts.treeMoveRoute }}?{{ opts.treeMoveParams }}file={{ n.path|url_encode }}"
|
||||
class="tree-move-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
||||
class="tree-move-btn" title="{{ ta.rename|default('Hernoemen/Verplaatsen') }}" aria-label="{{ ta.rename|default('Hernoemen/Verplaatsen') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ opts.treeDeleteRoute }}" class="tree-delete-form" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ opts.csrfToken }}">
|
||||
{% if opts.treeDeleteName %}
|
||||
<input type="hidden" name="{{ opts.treeDeleteName }}" value="{{ opts.treeDeleteValue }}">
|
||||
{% endif %}
|
||||
<input type="hidden" name="file" value="{{ n.path }}">
|
||||
<button type="submit" class="btn btn-link btn-sm p-0 text-danger tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}" data-path="{{ n.path }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -129,29 +140,48 @@
|
||||
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="small text-muted fw-semibold"><i class="bi bi-folder2-open"></i> {{ treeHeader|default('Bestanden') }}</span>
|
||||
</div>
|
||||
<div class="card-body p-2" id="editorFileTree">
|
||||
<div class="card-body p-2" id="editorFileTree"
|
||||
data-csrf="{{ csrf_token|default('') }}"
|
||||
data-scope="{{ treeScope|default('content') }}"
|
||||
data-move-url="{{ treeMoveUrl|default('') }}"
|
||||
data-delete-base="{{ treeDeleteBase|default('') }}"
|
||||
data-new-file-modal="{{ newFileModalId|default('#newFileModal') }}"
|
||||
data-new-dir-modal="{{ newDirModalId|default('#newDirModal') }}"
|
||||
data-scope-param="{{ treeScopeParam|default('') }}"
|
||||
data-edit-base="{{ treeRouteBase|default('') }}">
|
||||
{% if treeDirSelectRoute is defined and treeDirSelectRoute is not empty %}
|
||||
{# Klikbare content-root: selecteert de root map (?dir= leeg).
|
||||
data-dir="" maakt de root ook een drag-drop doel (lege string = base dir). #}
|
||||
<a href="{{ treeDirSelectRoute }}" class="tree-link tree-root-link {{ selectedDir|default('') == '' ? 'is-selected' : '' }}" data-dir="" title="content">
|
||||
<i class="bi bi-folder2-open"></i>
|
||||
<span class="text-truncate">{{ treeRootLabel|default('content') }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if files is empty %}
|
||||
<div class="small text-muted p-2">{{ treeEmptyText|default('Geen bestanden gevonden.') }}</div>
|
||||
{% else %}
|
||||
{{ tree_macros.tree(files, {
|
||||
'relFile': relFile,
|
||||
'selectedDir': selectedDir|default(''),
|
||||
'editableExts': editableExts,
|
||||
'csrfToken': csrf_token,
|
||||
'treeIdPrefix': treeIdPrefix,
|
||||
'treeRouteBase': treeRouteBase,
|
||||
'treeRouteParams': treeRouteParams,
|
||||
'treeDeleteRoute': treeDeleteRoute,
|
||||
'treeDeleteName': treeDeleteName|default(''),
|
||||
'treeDeleteValue': treeDeleteValue|default(''),
|
||||
'treeMoveRoute': treeMoveRoute,
|
||||
'treeMoveParams': treeMoveParams,
|
||||
'treeDirActions': treeDirActions|default(false),
|
||||
'treeDirActions': treeDirActions is defined ? treeDirActions : true,
|
||||
'treeDirSelectRoute': treeDirSelectRoute|default(''),
|
||||
'treeDirRenameRoute': treeDirRenameRoute|default(''),
|
||||
'treeFileMove': treeFileMove is defined ? treeFileMove : true,
|
||||
'treeHideActionsFor': treeHideActionsFor|default([]),
|
||||
'newFileModalId': newFileModalId|default('#newFileModal'),
|
||||
'newDirModalId': newDirModalId|default(''),
|
||||
}) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/admin/assets/js/tree-interactions.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var tree = document.getElementById('editorFileTree');
|
||||
@@ -187,5 +217,10 @@
|
||||
var btn = findToggle(c);
|
||||
if (btn) expand(btn);
|
||||
});
|
||||
|
||||
// Make all tree <li> nodes draggable (for drag-and-drop move)
|
||||
Array.prototype.forEach.call(tree.querySelectorAll('li[data-path]'), function (li) {
|
||||
li.setAttribute('draggable', 'true');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -60,8 +60,9 @@
|
||||
<textarea name="content" id="editor-textarea" data-ext="{{ fileExt }}">{{ fileContent }}</textarea>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary" title="{{ ta.save_ctrl_s|default('Opslaan (Ctrl+S)') }}">
|
||||
<button type="submit" form="editor-form" class="btn btn-primary" title="{{ ta.save_ctrl_s|default('Opslaan (Ctrl+S)') }}">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
{% if isEditable %}
|
||||
@@ -70,10 +71,16 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/admin/content" class="btn btn-outline-secondary" id="back-btn">{{ ta.back|default('Terug') }}</a>
|
||||
</div>
|
||||
<form method="POST" action="/admin/content-delete" class="ms-auto" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="file" value="{{ file }}">
|
||||
<button type="submit" class="btn btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i> {{ ta.delete|default('Verwijderen') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
@@ -60,23 +60,105 @@
|
||||
'files': files,
|
||||
'relFile': relFile,
|
||||
'editableExts': editableExts,
|
||||
'csrf_token': csrf_token,
|
||||
'treeIdPrefix': 'content-tree',
|
||||
'treeHeader': ta.content_files|default('Content bestanden'),
|
||||
'treeEmptyText': ta.content_no_files|default('Geen bestanden gevonden.'),
|
||||
'treeRouteBase': '/admin/content',
|
||||
'treeRouteParams': '',
|
||||
'treeDeleteRoute': '/admin/content-file-delete',
|
||||
'treeDeleteName': '',
|
||||
'treeDeleteValue': '',
|
||||
'treeMoveRoute': '/admin/content-file-move',
|
||||
'treeMoveParams': '',
|
||||
'treeDirActions': true,
|
||||
'treeDirSelectRoute': '/admin/content',
|
||||
'treeDirRenameRoute': '',
|
||||
'treeFileMove': false,
|
||||
'treeHideActionsFor': [],
|
||||
'selectedDir': selectedDir|default(''),
|
||||
'treeRootLabel': 'content',
|
||||
'treeScope': 'content',
|
||||
'treeMoveUrl': '/admin/tree-move',
|
||||
'treeDeleteBase': '/admin/content-file-delete',
|
||||
'treeScopeParam': '',
|
||||
'newFileModalId': '#newFileModal',
|
||||
'newDirModalId': '#newDirModal',
|
||||
} %}
|
||||
|
||||
{# Editor main panel #}
|
||||
{# Editor / map-detail main panel #}
|
||||
<div class="editor-main">
|
||||
{% if showMapPaneel %}
|
||||
{# ── Map detail paneel ( rechts) ── tonen wanneer een map is geselecteerd #}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="/admin/content"><i class="bi bi-folder2-open"></i> content</a></li>
|
||||
{% if selectedDir %}
|
||||
{% set crumbPath = '' %}
|
||||
{% for part in selectedDir|split('/') %}
|
||||
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ part : part %}
|
||||
{% if loop.last %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item"><a href="/admin/content?dir={{ crumbPath|url_encode }}">{{ part }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<i class="bi bi-folder-fill text-warning"></i>
|
||||
<span class="fw-semibold">{{ selectedDirIsRoot ? 'content' : selectedDirName }}</span>
|
||||
{% if not selectedDirIsRoot %}
|
||||
<span class="badge bg-light text-dark ms-1">content/{{ selectedDir }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{# Mapnaam bewerkbaar + Opslaan = hernoem (geen apart potlood-icoon in de boom meer) #}
|
||||
<form method="POST" action="/admin/content?{% if selectedDir %}dir={{ selectedDir|url_encode }}{% endif %}" id="dir-rename-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="rename_dir">
|
||||
<input type="hidden" name="dir" value="{{ selectedDir }}">
|
||||
<label for="dirname" class="form-label">{{ ta.folder_name|default('Mapnaam') }}</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="dirname" name="newname" value="{{ selectedDirIsRoot ? '' : selectedDirName }}" placeholder="{{ ta.folder_name_placeholder|default('mapnaam') }}" {{ selectedDirIsRoot ? 'disabled' : '' }}>
|
||||
<button type="submit" class="btn btn-primary" {{ selectedDirIsRoot ? 'disabled' : '' }}>
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">{{ ta.folder_rename_help|default('Bewerk de mapnaam en klik opslaan om te hernoemen.') }}</div>
|
||||
</form>
|
||||
|
||||
{# Map acties: nieuw bestand / nieuwe map / upload — opereren op de geselecteerde map #}
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="{{ selectedDir }}" id="mapPaneelNewFileBtn">
|
||||
<i class="bi bi-file-earmark-plus"></i> {{ ta.new_file|default('Nieuw bestand') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ selectedDir }}" id="mapPaneelNewDirBtn">
|
||||
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success btn-sm" data-bs-toggle="collapse" data-bs-target="#contentUploadForm" id="mapPaneelUploadBtn">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
|
||||
</button>
|
||||
{% if not selectedDirIsRoot %}
|
||||
<form method="POST" action="/admin/content-dir-delete-in" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_folder|default('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="dir" value="{{ selectedDir }}">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-trash"></i> {{ ta.delete|default('Verwijderen') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Korte inhoud-samenvatting van de geselecteerde map #}
|
||||
<div class="text-muted small">
|
||||
<i class="bi bi-files"></i>
|
||||
{{ selectedDirCounts.files }} {{ ta.files|default('bestanden') }},
|
||||
{{ selectedDirCounts.dirs }} {{ ta.subfolders|default('submappen') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% if relFile %}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
@@ -149,15 +231,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary" title="{{ ta.save_ctrl_s|default('Opslaan (Ctrl+S)') }}">
|
||||
<button type="submit" form="editor-form" class="btn btn-primary" title="{{ ta.save_ctrl_s|default('Opslaan (Ctrl+S)') }}">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
<a href="/{{ currentLang }}{% if fileDir %}/{{ fileDir }}{% endif %}/{{ fileBaseName }}{% if fileExt != 'md' %}.{{ fileExt }}{% endif %}" target="_blank" class="btn btn-outline-info" title="{{ ta.open_new_tab|default('Open in nieuw tabblad') }}">
|
||||
<i class="bi bi-eye"></i> {{ ta.preview|default('Preview') }}
|
||||
</a>
|
||||
</div>
|
||||
<form method="POST" action="/admin/content-file-delete" class="ms-auto" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="file" value="{{ relFile }}">
|
||||
<button type="submit" class="btn btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i> {{ ta.delete|default('Verwijderen') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
@@ -165,11 +254,12 @@
|
||||
{% if relFile %}
|
||||
{{ ta.content_not_editable|default('Dit bestandstype kan niet in de editor bewerkt worden.') }}
|
||||
{% else %}
|
||||
{{ ta.content_select_file|default('Selecteer een bestand uit de zijbalk om te bewerken.') }}
|
||||
{{ ta.content_select_file|default('Selecteer een bestand of map uit de zijbalk.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}{# end showMapPaneel #}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -180,7 +270,7 @@
|
||||
<form method="POST" action="/admin/content">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="new_file">
|
||||
<input type="hidden" name="in_dir" id="newFileDir" value="">
|
||||
<input type="hidden" name="in_dir" id="newFileDir" value="" data-in-dir-field>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newFileModalLabel"><i class="bi bi-file-earmark-plus"></i> {{ ta.new_file_title|default('Nieuw bestand aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
@@ -217,7 +307,7 @@
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/content-dir-create-in">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="in_dir" id="newDirInDir" value="">
|
||||
<input type="hidden" name="in_dir" id="newDirInDir" value="" data-in-dir-field>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newDirModalLabel"><i class="bi bi-folder-plus"></i> {{ ta.new_folder_title|default('Nieuwe map aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
@@ -239,36 +329,31 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Pass the in-dir value from the tree button to the modal hidden fields
|
||||
// Mapselectie + upload/new file/new dir binding aan de geselecteerde map.
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var newFileModal = document.getElementById('newFileModal');
|
||||
var newDirModal = document.getElementById('newDirModal');
|
||||
var newFileBtnTop = document.getElementById('newFileBtnTop');
|
||||
var newDirBtnTop = document.getElementById('newDirBtnTop');
|
||||
var uploadDir = document.getElementById('contentUploadDir');
|
||||
|
||||
// Track the selected directory for the top toolbar buttons
|
||||
// Track the selected directory (server-side via ?dir=). This drives the
|
||||
// top toolbar buttons and the upload form target.
|
||||
var selectedDir = '{{ selectedDir|default('') }}';
|
||||
function updateTopButtons(dir) {
|
||||
selectedDir = dir || '';
|
||||
if (newFileBtnTop) newFileBtnTop.setAttribute('data-in-dir', selectedDir);
|
||||
if (newDirBtnTop) newDirBtnTop.setAttribute('data-in-dir', selectedDir);
|
||||
if (uploadDir) uploadDir.value = selectedDir;
|
||||
}
|
||||
updateTopButtons(selectedDir);
|
||||
|
||||
// Make folder rows selectable: clicking the folder name selects it
|
||||
var tree = document.getElementById('editorFileTree');
|
||||
if (tree) {
|
||||
// Chevron-kliks mogen niet doorbubbellen naar de mapnaam-link (die navigeert).
|
||||
tree.addEventListener('click', function (e) {
|
||||
var toggle = e.target.closest('.tree-toggle');
|
||||
if (!toggle) return;
|
||||
// Find the in-dir from the adjacent new-file button
|
||||
var row = toggle.closest('.tree-dir-row');
|
||||
if (!row) return;
|
||||
var newFileBtn = row.querySelector('.tree-newfile-btn');
|
||||
var dir = newFileBtn ? newFileBtn.getAttribute('data-in-dir') : '';
|
||||
updateTopButtons(dir);
|
||||
// Visually mark the selected folder
|
||||
tree.querySelectorAll('.tree-toggle.is-selected').forEach(function (el) { el.classList.remove('is-selected'); });
|
||||
toggle.classList.add('is-selected');
|
||||
var chevronBtn = e.target.closest('.tree-chevron-btn');
|
||||
if (chevronBtn) { e.stopPropagation(); }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -278,7 +363,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
var inDir = trigger ? trigger.getAttribute('data-in-dir') : selectedDir;
|
||||
var hidden = document.getElementById('newFileDir');
|
||||
if (hidden) hidden.value = inDir || '';
|
||||
// Show the in-dir prefix as a hint in the placeholder
|
||||
var input = document.getElementById('newFilenameInput');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
@@ -295,15 +379,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
});
|
||||
}
|
||||
|
||||
// Mark the initially selected folder (based on the opened file's directory)
|
||||
if (selectedDir && tree) {
|
||||
var initialBtn = tree.querySelector('.tree-newfile-btn[data-in-dir="' + selectedDir + '"]');
|
||||
if (initialBtn) {
|
||||
var initialToggle = initialBtn.closest('.tree-dir-row').querySelector('.tree-toggle');
|
||||
if (initialToggle) initialToggle.classList.add('is-selected');
|
||||
}
|
||||
}
|
||||
|
||||
// Layout select → update frontmatter (spiegel van content-edit.twig)
|
||||
var layoutSelect = document.getElementById('layout');
|
||||
if (layoutSelect) {
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal">
|
||||
<i class="bi bi-file-earmark-plus"></i> {{ ta.plugin_new_file|default('Nieuw bestand') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="newDirBtnTop" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ selectedDir|default('') }}">
|
||||
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
|
||||
</button>
|
||||
<a href="/admin/plugins" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
@@ -51,23 +54,85 @@
|
||||
'files': files,
|
||||
'relFile': relFile,
|
||||
'editableExts': editableExts,
|
||||
'csrf_token': csrf_token,
|
||||
'treeIdPrefix': 'plugin-tree',
|
||||
'treeHeader': ta.plugin_files|default('Plugin bestanden'),
|
||||
'treeEmptyText': ta.plugin_no_files|default('Geen bestanden gevonden.'),
|
||||
'treeRouteBase': '/admin/plugins-edit',
|
||||
'treeRouteParams': 'plugin=' ~ pluginName|url_encode ~ '&',
|
||||
'treeDeleteRoute': '/admin/plugins-file-delete',
|
||||
'treeDeleteName': 'plugin',
|
||||
'treeDeleteValue': pluginName,
|
||||
'treeMoveRoute': '/admin/plugins-file-move',
|
||||
'treeMoveParams': 'plugin=' ~ pluginName|url_encode ~ '&',
|
||||
'treeDirActions': false,
|
||||
'treeDirActions': true,
|
||||
'treeHideActionsFor': [],
|
||||
'treeDirSelectRoute': '/admin/plugins-edit?plugin=' ~ pluginName|url_encode,
|
||||
'treeDirRenameRoute': '/admin/plugins-dir-rename-in?plugin=' ~ pluginName|url_encode,
|
||||
'treeRootLabel': pluginName,
|
||||
'treeScope': 'plugin',
|
||||
'treeMoveUrl': '/admin/tree-move',
|
||||
'treeDeleteBase': '/admin/plugins-file-delete',
|
||||
'treeScopeParam': 'plugin=' ~ pluginName|url_encode ~ '&',
|
||||
'selectedDir': selectedDir|default(''),
|
||||
'newFileModalId': '#newFileModal',
|
||||
'newDirModalId': '#newDirModal',
|
||||
} %}
|
||||
|
||||
{# Editor main panel #}
|
||||
<div class="editor-main">
|
||||
{% if showMapPaneel %}
|
||||
{# Map detail paneel — tonen wanneer een map is geselecteerd #}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="/admin/plugins-edit?plugin={{ pluginName|url_encode }}"><i class="bi bi-folder2-open"></i> {{ pluginName }}</a></li>
|
||||
{% if selectedDir %}
|
||||
{% set crumbPath = '' %}
|
||||
{% for part in selectedDir|split('/') %}
|
||||
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ part : part %}
|
||||
{% if loop.last %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item"><a href="/admin/plugins-edit?plugin={{ pluginName|url_encode }}&dir={{ crumbPath|url_encode }}">{{ part }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<i class="bi bi-folder-fill text-warning"></i>
|
||||
<span class="fw-semibold">{{ selectedDirIsRoot ? pluginName : selectedDirName }}</span>
|
||||
{% if not selectedDirIsRoot %}
|
||||
<span class="badge bg-light text-dark ms-1">{{ pluginName }}/{{ selectedDir }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{# Map acties: nieuw bestand / nieuwe map / upload / verwijder #}
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="{{ selectedDir }}">
|
||||
<i class="bi bi-file-earmark-plus"></i> {{ ta.new_file|default('Nieuw bestand') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ selectedDir }}">
|
||||
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
|
||||
</button>
|
||||
{% if not selectedDirIsRoot %}
|
||||
<form method="POST" action="/admin/plugins-dir-delete-in" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_folder|default('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="plugin" value="{{ pluginName }}">
|
||||
<input type="hidden" name="dir" value="{{ selectedDir }}">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-trash"></i> {{ ta.delete|default('Verwijderen') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<i class="bi bi-files"></i>
|
||||
{{ selectedDirCounts.files }} {{ ta.files|default('bestanden') }},
|
||||
{{ selectedDirCounts.dirs }} {{ ta.subfolders|default('submappen') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% if relFile %}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
@@ -97,13 +162,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<button type="submit" form="editor-form" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
<a href="/admin/plugins" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
<form method="POST" action="/admin/plugins-file-delete" class="ms-auto" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="plugin" value="{{ pluginName }}">
|
||||
<input type="hidden" name="file" value="{{ relFile }}">
|
||||
<button type="submit" class="btn btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i> {{ ta.delete|default('Verwijderen') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
@@ -116,6 +189,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}{# end showMapPaneel #}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,6 +200,7 @@
|
||||
<form method="POST" action="/admin/plugins-edit?plugin={{ pluginName|url_encode }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="new_file">
|
||||
<input type="hidden" name="in_dir" value="" data-in-dir-field>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newFileModalLabel"><i class="bi bi-file-earmark-plus"></i> {{ ta.plugin_new_file_title|default('Nieuw bestand aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
@@ -145,6 +220,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# New directory modal #}
|
||||
<div class="modal fade" id="newDirModal" tabindex="-1" aria-labelledby="newDirModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/plugins-dir-create-in">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="plugin" value="{{ pluginName }}">
|
||||
<input type="hidden" name="in_dir" value="" data-in-dir-field>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newDirModalLabel"><i class="bi bi-folder-plus"></i> {{ ta.new_folder_title|default('Nieuwe map aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="dirnameInput" class="form-label">{{ ta.folder_name|default('Mapnaam') }}</label>
|
||||
<input type="text" class="form-control" id="dirnameInput" name="dirname" required autofocus>
|
||||
<div class="form-text">{{ ta.name_help|default('Alleen letters, cijfers, punten, underscores en streepjes.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal">
|
||||
<i class="bi bi-file-earmark-plus"></i> {{ ta.theme_new_file|default('Nieuw bestand') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="newDirBtnTop" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ selectedDir|default('') }}">
|
||||
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
|
||||
</button>
|
||||
<a href="/admin/theme" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
@@ -64,23 +67,88 @@
|
||||
'files': files,
|
||||
'relFile': relFile,
|
||||
'editableExts': editableExts,
|
||||
'csrf_token': csrf_token,
|
||||
'treeIdPrefix': 'theme-tree',
|
||||
'treeHeader': ta.theme_files|default('Thema bestanden'),
|
||||
'treeEmptyText': ta.theme_no_files|default('Geen bestanden gevonden.'),
|
||||
'treeRouteBase': '/admin/theme-edit',
|
||||
'treeRouteParams': 'theme=' ~ themeName|url_encode ~ '&',
|
||||
'treeDeleteRoute': '/admin/theme-file-delete',
|
||||
'treeDeleteName': 'theme',
|
||||
'treeDeleteValue': themeName,
|
||||
'treeMoveRoute': '/admin/theme-file-move',
|
||||
'treeMoveParams': 'theme=' ~ themeName|url_encode ~ '&',
|
||||
'treeDirActions': false,
|
||||
'treeDirActions': true,
|
||||
'treeHideActionsFor': ['theme.json'],
|
||||
'treeDirSelectRoute': '/admin/theme-edit?theme=' ~ themeName|url_encode,
|
||||
'treeDirRenameRoute': '/admin/theme-dir-rename-in?theme=' ~ themeName|url_encode,
|
||||
'treeRootLabel': themeName,
|
||||
'treeScope': 'theme',
|
||||
'treeMoveUrl': '/admin/tree-move',
|
||||
'treeDeleteBase': '/admin/theme-file-delete',
|
||||
'treeScopeParam': 'theme=' ~ themeName|url_encode ~ '&',
|
||||
'selectedDir': selectedDir|default(''),
|
||||
'newFileModalId': '#newFileModal',
|
||||
'newDirModalId': '#newDirModal',
|
||||
} %}
|
||||
|
||||
{# Editor main panel #}
|
||||
<div class="editor-main">
|
||||
{% if showMapPaneel %}
|
||||
{# Map detail paneel — tonen wanneer een map is geselecteerd #}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="/admin/theme-edit?theme={{ themeName|url_encode }}"><i class="bi bi-folder2-open"></i> {{ themeName }}</a></li>
|
||||
{% if selectedDir %}
|
||||
{% set crumbPath = '' %}
|
||||
{% for part in selectedDir|split('/') %}
|
||||
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ part : part %}
|
||||
{% if loop.last %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item"><a href="/admin/theme-edit?theme={{ themeName|url_encode }}&dir={{ crumbPath|url_encode }}">{{ part }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<i class="bi bi-folder-fill text-warning"></i>
|
||||
<span class="fw-semibold">{{ selectedDirIsRoot ? themeName : selectedDirName }}</span>
|
||||
{% if not selectedDirIsRoot %}
|
||||
<span class="badge bg-light text-dark ms-1">{{ themeName }}/{{ selectedDir }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{# Map acties: nieuw bestand / nieuwe map / upload / verwijder #}
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="{{ selectedDir }}">
|
||||
<i class="bi bi-file-earmark-plus"></i> {{ ta.new_file|default('Nieuw bestand') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ selectedDir }}">
|
||||
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-success btn-sm" data-bs-toggle="collapse" data-bs-target="#themeUploadForm">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
|
||||
</button>
|
||||
{% if not selectedDirIsRoot %}
|
||||
<form method="POST" action="/admin/theme-dir-delete-in" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_folder|default('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ themeName }}">
|
||||
<input type="hidden" name="dir" value="{{ selectedDir }}">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||
<i class="bi bi-trash"></i> {{ ta.delete|default('Verwijderen') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<i class="bi bi-files"></i>
|
||||
{{ selectedDirCounts.files }} {{ ta.files|default('bestanden') }},
|
||||
{{ selectedDirCounts.dirs }} {{ ta.subfolders|default('submappen') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% if relFile %}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
@@ -110,13 +178,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<button type="submit" form="editor-form" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
<a href="/admin/theme" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
{% if relFile != 'theme.json' %}
|
||||
<form method="POST" action="/admin/theme-file-delete" class="ms-auto" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ themeName }}">
|
||||
<input type="hidden" name="file" value="{{ relFile }}">
|
||||
<button type="submit" class="btn btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i> {{ ta.delete|default('Verwijderen') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
@@ -129,6 +207,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}{# end showMapPaneel #}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -139,6 +218,7 @@
|
||||
<form method="POST" action="/admin/theme-edit?theme={{ themeName|url_encode }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="new_file">
|
||||
<input type="hidden" name="in_dir" value="" data-in-dir-field>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newFileModalLabel"><i class="bi bi-file-earmark-plus"></i> {{ ta.theme_new_file_title|default('Nieuw bestand aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
@@ -158,6 +238,34 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# New directory modal #}
|
||||
<div class="modal fade" id="newDirModal" tabindex="-1" aria-labelledby="newDirModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/theme-dir-create-in">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ themeName }}">
|
||||
<input type="hidden" name="in_dir" value="" data-in-dir-field>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newDirModalLabel"><i class="bi bi-folder-plus"></i> {{ ta.new_folder_title|default('Nieuwe map aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="dirnameInput" class="form-label">{{ ta.folder_name|default('Mapnaam') }}</label>
|
||||
<input type="text" class="form-control" id="dirnameInput" name="dirname" required autofocus>
|
||||
<div class="form-text">{{ ta.name_help|default('Alleen letters, cijfers, punten, underscores en streepjes.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Natural Earth World Map SVG generator for CodePress CMS
|
||||
* Natural Earth World Map SVG generator voor CodePress CMS.
|
||||
*
|
||||
* Converts the Natural Earth 110m TopoJSON (public domain) into a plain SVG
|
||||
* where every country path carries its ISO 3166-1 alpha-2 code as the id,
|
||||
* so the admin statistics page can colour countries with plain CSS.
|
||||
* Zet de Natural Earth 110m TopoJSON (publiek domein) om in een plain SVG
|
||||
* waarin elk land-pad zijn ISO 3166-1 alpha-2 code als id meekrijgt, zodat
|
||||
* de admin-statistiekpagina landen met pure CSS kan inkleuren. Wordt via
|
||||
* de CLI uitgevoerd en schrijft public/assets/img/world-map.svg.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @package CodePress
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -66,6 +70,18 @@ function nameToAlpha2(): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Main generator-functie: bouwt de wereldkaart-SVG uit TopoJSON.
|
||||
*
|
||||
* Download (met caching) de countries-110m.json, decodeert delta-gecodeerde
|
||||
* arcs, projecteert op een equirectangular canvas met antimeridian-unwrap,
|
||||
* verzamelt paden per ISO alpha-2 code en schrijft de SVG naar
|
||||
* public/assets/img/world-map.svg.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param bool $verbose Of voortgang en statistieken naar stdout worden geschreven.
|
||||
* @return bool True bij succes, false bij een download- of parse-fout.
|
||||
*/
|
||||
function generateWorldMapSvg(bool $verbose = false): bool
|
||||
{
|
||||
$outPath = dirname(__DIR__) . '/public/assets/img/world-map.svg';
|
||||
@@ -104,6 +120,11 @@ function generateWorldMapSvg(bool $verbose = false): bool
|
||||
$scale = $topo['transform']['scale'];
|
||||
$translate = $topo['transform']['translate'];
|
||||
|
||||
/**
|
||||
* Delta-gecodeerde arcs decoderen naar plain lon/lat-punten.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Decode delta-encoded arcs into plain lon/lat pairs. Projection happens
|
||||
// later, after antimeridian handling, because that works in degrees.
|
||||
$decodedArcs = [];
|
||||
@@ -130,6 +151,14 @@ function generateWorldMapSvg(bool $verbose = false): bool
|
||||
$names = [];
|
||||
$skipped = [];
|
||||
|
||||
/**
|
||||
* Per land de TopoJSON-geometrie omzetten in SVG-paden.
|
||||
*
|
||||
* Bepaalt ISO alpha-2 (via numeric map of naam map), slaat Antarctica over,
|
||||
* bouwt Polygon/MultiPolygon-rings op en verzamelt paden per code.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
foreach ($topo['objects']['countries']['geometries'] as $geo) {
|
||||
$rawId = $geo['id'] ?? '';
|
||||
$countryName = $geo['properties']['name'] ?? 'Unknown';
|
||||
@@ -261,6 +290,15 @@ function generateWorldMapSvg(bool $verbose = false): bool
|
||||
$pathsByCode[$alpha2] .= ($pathsByCode[$alpha2] === '' ? '' : ' ') . $pathD;
|
||||
}
|
||||
|
||||
/**
|
||||
* SVG-document samenstellen en wegschrijven.
|
||||
*
|
||||
* Sorteert paden per code, formattert <path>-elementen met id, data-name,
|
||||
* class, d en <title>, voegt inline CSS toe en schrijft het resultaat naar
|
||||
* de output-map.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
ksort($pathsByCode);
|
||||
|
||||
$svgPaths = [];
|
||||
@@ -303,6 +341,14 @@ function generateWorldMapSvg(bool $verbose = false): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-entrypoint: wereldkaart-SVG genereren.
|
||||
*
|
||||
* Roept generateWorldMapSvg() aan en rapporteert het resultaat; bij een
|
||||
* fout wordt met exit(1) afgesloten.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
if (php_sapi_name() === 'cli') {
|
||||
echo "Wereldkaart SVG genereren uit Natural Earth TopoJSON...\n";
|
||||
if (generateWorldMapSvg(true)) {
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DB-IP Lite database downloader & binaire converter voor CodePress CMS.
|
||||
*
|
||||
* Downloadt de DB-IP Lite country-CSV (gzip) van download.db-ip.com, pakt
|
||||
* deze uit en zet de IP-ranges om in compacte binaire bestanden voor IPv4
|
||||
* en IPv6 (met 2-byte country codes), plus een meta.json met bron en
|
||||
* statistieken. Kan via de CLI of vanuit een admin-handler worden aangeroepen.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @package CodePress
|
||||
*/
|
||||
/**
|
||||
* DB-IP Lite database downloader & binary converter for CodePress CMS
|
||||
*/
|
||||
@@ -9,6 +20,16 @@ if (php_sapi_name() !== 'cli' && (!isset($_SESSION['admin_user']))) {
|
||||
// Can also be included from admin handler
|
||||
}
|
||||
|
||||
/**
|
||||
* GeoIP-database bijwerken: downloaden, uitpakken en converteren.
|
||||
*
|
||||
* Probeert maximaal drie recente maandelijkse DB-IP Lite releases. Bij
|
||||
* succes worden ipv4.bin en ipv6.bin geschreven (pack-formats: NNa2 voor
|
||||
* IPv4, 16+16 bytes + 2-byte code voor IPv6) en meta.json weggeschreven.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array Resultaat-array met keys: success, message en (optioneel) meta.
|
||||
*/
|
||||
function updateGeoIPDatabase(): array
|
||||
{
|
||||
$baseDir = dirname(__DIR__) . '/admin/storage/geoip';
|
||||
@@ -55,6 +76,14 @@ function updateGeoIPDatabase(): array
|
||||
$v4Count = 0;
|
||||
$v6Count = 0;
|
||||
|
||||
/**
|
||||
* CSV-records omzetten naar binaire IPv4-/IPv6-records.
|
||||
*
|
||||
* Elke geldige regel met start-, end-IP en 2-letterige country code wordt
|
||||
* weggeschreven naar het juiste binaire bestand met bijbehorend pack-formaat.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') continue;
|
||||
@@ -112,6 +141,13 @@ function updateGeoIPDatabase(): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI-entrypoint: DB-IP Lite database bijwerken.
|
||||
*
|
||||
* Roept updateGeoIPDatabase() aan en print het resultaatbericht.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['SCRIPT_FILENAME'])) {
|
||||
echo "DB-IP Lite database bijwerken...\n";
|
||||
$res = updateGeoIPDatabase();
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* CodePress CMS — Admin wachtwoord reset CLI
|
||||
* CodePress CMS — Admin wachtwoord reset CLI.
|
||||
*
|
||||
* Reset het wachtwoord van een admin-gebruiker via de command line. Genereert
|
||||
* een random wachtwoord wanneer er geen wordt meegegeven en wist tevens de
|
||||
* brute-force lockout voor de gebruiker, zodat direct inloggen weer mogelijk is.
|
||||
*
|
||||
* Gebruik:
|
||||
* php cli/reset-admin-password.php [gebruikersnaam] [nieuw_wachtwoord]
|
||||
@@ -12,8 +16,16 @@
|
||||
* Voorbeelden:
|
||||
* php cli/reset-admin-password.php admin
|
||||
* php cli/reset-admin-password.php admin MijnNieuweWachtwoord123
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @package CodePress
|
||||
*/
|
||||
|
||||
/**
|
||||
* CLI-only guard: dit script mag alleen via de CLI uitgevoerd worden.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
fwrite(STDERR, "Dit script kan alleen via de CLI uitgevoerd worden.\n");
|
||||
exit(1);
|
||||
@@ -24,6 +36,11 @@ require_once $rootDir . '/admin/src/AdminAuth.php';
|
||||
|
||||
$appConfig = require $rootDir . '/admin/config/app.php';
|
||||
|
||||
/**
|
||||
* Argumenten parsen en gebruik tonen bij ontbrekende gebruikersnaam.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Argumenten parsen
|
||||
$username = $argv[1] ?? '';
|
||||
$password = $argv[2] ?? '';
|
||||
@@ -39,6 +56,11 @@ if ($username === '') {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* AdminAuth aanmaken en controleren of de gebruiker bestaat.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// AdminAuth aanmaken (zonder sessie requirements)
|
||||
$auth = new AdminAuth($appConfig);
|
||||
|
||||
@@ -50,6 +72,11 @@ if (!isset($users[$username])) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wachtwoord genereren indien niet expliciet opgegeven.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Wachtwoord genereren als niet opgegeven
|
||||
if ($password === '') {
|
||||
$password = bin2hex(random_bytes(12));
|
||||
@@ -58,6 +85,13 @@ if ($password === '') {
|
||||
$generated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wachtwoord wijzigen en brute-force lockout wissen.
|
||||
*
|
||||
* Bij een mislukte wachtwoordwijziging wordt met exit(1) afgesloten.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Wachtwoord wijzigen
|
||||
$result = $auth->changePassword($username, $password);
|
||||
if (!$result['success']) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
+380
-158
@@ -3,36 +3,86 @@
|
||||
|
||||
|
||||
/**
|
||||
* CodePressCMS - Lightweight file-based content management system
|
||||
* Hoofdclass van het file-based CodePress CMS.
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* @author Edwin Noorlander
|
||||
* @version 1.0.0
|
||||
* @license MIT
|
||||
* @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
|
||||
* Initialiseer het CMS met de opgegeven configuratie.
|
||||
*
|
||||
* @param array $config Configuration array containing site settings
|
||||
* 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
|
||||
* Bouw een clean URL voor een pagina.
|
||||
*
|
||||
* @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
|
||||
* @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
|
||||
* Bouw een clean admin URL.
|
||||
*
|
||||
* @param string $route Admin route
|
||||
* @param array $params Additional query parameters
|
||||
* @return string 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
|
||||
* Bepaal de actieve taal vanuit het verzoek of de configuratie.
|
||||
*
|
||||
* @return string Current language code
|
||||
* @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
|
||||
* Verzamel alle beschikbare talen vanuit de language-directory.
|
||||
*
|
||||
* Each language is a subdirectory under language/ containing a site.php file.
|
||||
* Elke taal is een subdirectory onder language/ met een site.php-bestand.
|
||||
*
|
||||
* @return array Available languages with their codes and names
|
||||
* @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
|
||||
* Laad site-vertalingen voor de opgegeven taal.
|
||||
*
|
||||
* @param string $lang Language code
|
||||
* @return array Translations array
|
||||
* 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
|
||||
* Geef de native taalnaam voor een taalcode terug.
|
||||
*
|
||||
* @param string $langCode Language code
|
||||
* @return string Native language name
|
||||
* 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
|
||||
* Geef vertaalde tekst voor een sleutel terug.
|
||||
*
|
||||
* @param string $key Translation key
|
||||
* @return string Translated text
|
||||
* @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
|
||||
* Scan recursief een directory voor content-bestanden en mappen.
|
||||
*
|
||||
* @param string $dir Directory path to scan
|
||||
* @param string $prefix Relative path prefix
|
||||
* @return array Array of menu items
|
||||
* @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
|
||||
* Voer een zoekopdracht uit over alle content-bestanden.
|
||||
*
|
||||
* @param string $query Search query string
|
||||
* 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
|
||||
* Doorzoek recursief de bestanden in een directory naar de query.
|
||||
*
|
||||
* @param string $dir Directory to search in
|
||||
* @param string $prefix Relative path prefix
|
||||
* @param string $query Search 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
|
||||
* Maak een zoeksnippet met de gevonden query.
|
||||
*
|
||||
* @param string $content Full content to create snippet from
|
||||
* @param string $query Search query to highlight
|
||||
* @return string Formatted snippet
|
||||
* @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
|
||||
* Bepaal de content van de huidige pagina op basis van het verzoek.
|
||||
*
|
||||
* @return array Page data with title and content
|
||||
* 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
|
||||
* Verzamel bestandsinformatie inclusief aanmaak- en wijzigingsdatums.
|
||||
*
|
||||
* @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
|
||||
* @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
|
||||
* Format een bestandsgrootte in leesbare vorm (B, KB, MB, GB).
|
||||
*
|
||||
* @param int $bytes File size in bytes
|
||||
* @return string Formatted file size
|
||||
* @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
|
||||
* Bouw de content voor de zoekresultatenpagina.
|
||||
*
|
||||
* @return array Search results page data
|
||||
* @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
|
||||
* Parse YAML-frontmatter metadata uit de content.
|
||||
*
|
||||
* @param string $content Raw content
|
||||
* @return array Parsed metadata and content without meta block
|
||||
* 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
|
||||
* Parse Markdown-content naar HTML via 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
|
||||
* 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
|
||||
* Auto-link pagina-titels die in de content worden gevonden.
|
||||
*
|
||||
* @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
|
||||
* 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.
|
||||
* Verzamel alle content-entries (mappen + bestanden) uit de content-directory.
|
||||
*
|
||||
* @return array List of entries: ['path' => ..., 'title' => ..., 'type' => ...]
|
||||
* @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.
|
||||
* Scan recursief een directory voor content-entries.
|
||||
*
|
||||
* Returns a flat list of ['path' => relativePath, 'title' => ..., 'type' => ...].
|
||||
* 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
|
||||
* Format een bestands- of mapnaam tot een leesbare weergavenaam.
|
||||
*
|
||||
* @param string $filename Filename without extension
|
||||
* @return string Formatted display name
|
||||
* 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
|
||||
* Extract de paginatitel uit de content van een bestand.
|
||||
*
|
||||
* @param string $filePath Path to the file
|
||||
* @return string|null Extracted title or null if not found
|
||||
* 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
|
||||
* Parse een PHP-bestand en vang de output op.
|
||||
*
|
||||
* @param string $filePath Path to PHP file
|
||||
* @return array Parsed content with title and body
|
||||
* 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
|
||||
* Parse HTML-content en extraheer de titel.
|
||||
*
|
||||
* @param string $content Raw HTML content
|
||||
* @return array Parsed content with title and body
|
||||
* @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
|
||||
* Controleer of de content-directory leeg is.
|
||||
*
|
||||
* @return bool True if content directory is empty or doesn't exist
|
||||
* @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
|
||||
* Geef de content van een handleiding-pagina op basis van de gebruikerstaal.
|
||||
*
|
||||
* @return array Guide page data
|
||||
* 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
|
||||
* Genereer een directory-listingpagina.
|
||||
*
|
||||
* @param string $pagePath Relative path to directory
|
||||
* @param string $dirPath Absolute path to directory
|
||||
* @return array Directory listing page data
|
||||
* 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
|
||||
* Geef de content van de 404-foutpagina.
|
||||
*
|
||||
* @return array 404 page data
|
||||
* @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
|
||||
* Geef de opgebouwde menu-structuur terug.
|
||||
*
|
||||
* @return array Menu structure for navigation
|
||||
* @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
|
||||
* Genereer breadcrumb-navigatie-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
|
||||
* 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
|
||||
* Render menu-HTML met ondersteuning voor dropdowns.
|
||||
*
|
||||
* @param array $items Menu items to render
|
||||
* @param int $level Current nesting level
|
||||
* @return string Rendered menu HTML
|
||||
* 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
|
||||
* Geef de titel van de homepagina terug.
|
||||
*
|
||||
* @return string Homepage title
|
||||
* @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
|
||||
* Controleer of een map de actieve pagina bevat.
|
||||
*
|
||||
* @param array $children Array of child items
|
||||
* @return bool True if active page is found in children
|
||||
* @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/
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
+108
-32
@@ -1,27 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple Logger Class for CodePress CMS
|
||||
* Eenvoudige logger voor CodePress CMS.
|
||||
*
|
||||
* Provides structured logging with log levels and file output.
|
||||
* Voorziet in gestructureerde logging met loglevels en bestandsuitvoer.
|
||||
* Logregels worden naar een tekstbestand geschreven en, indien aanwezig,
|
||||
* tevens door de dynamische LogManager gerouteerd.
|
||||
*
|
||||
* @package CodePress
|
||||
* @version 1.0.0
|
||||
* @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
|
||||
* Initialiseert de logger.
|
||||
*
|
||||
* @param string $logFile Path to log file
|
||||
* @param bool $debugMode Enable debug logging
|
||||
* 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)
|
||||
* Logt een debug-boodschap (enkel in debug-modus).
|
||||
*
|
||||
* @param string $message Message to log
|
||||
* @param array $context Additional context
|
||||
* @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
|
||||
* Logt een informatieve boodschap.
|
||||
*
|
||||
* @param string $message Message to log
|
||||
* @param array $context Additional context
|
||||
* @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
|
||||
* Logt een waarschuwing.
|
||||
*
|
||||
* @param string $message Message to log
|
||||
* @param array $context Additional context
|
||||
* @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
|
||||
* Logt een fout.
|
||||
*
|
||||
* @param string $message Message to log
|
||||
* @param array $context Additional context
|
||||
* @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
|
||||
* Schrijft een logregel naar het bestand en routeert deze door LogManager.
|
||||
*
|
||||
* @param string $level Log level
|
||||
* @param string $message Message to log
|
||||
* @param array $context Additional context
|
||||
* 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
|
||||
* Geeft het pad naar het logbestand.
|
||||
*
|
||||
* @return string Log file path
|
||||
* @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
|
||||
* Verwijdert het logbestand.
|
||||
*
|
||||
* @return bool Success status
|
||||
* @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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
+56
-16
@@ -1,37 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* CodePress CMS Core Loader
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* The separation allows for:
|
||||
* - Clean public entry point (public/index.php)
|
||||
* - Reusable core components
|
||||
* - Proper class organization with PHPDoc documentation
|
||||
* @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();
|
||||
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* Router voor de PHP development-server: clean-URL support en statische bestanden.
|
||||
*
|
||||
* Wordt gebruikt via `php -S localhost:8080 cms/router.php`. Houdt de request-
|
||||
* URI tegen de public/-map voor statische bestanden, serveert theme-, admin-
|
||||
* theme- en plugin-assets buiten public/, en routeert /admin en taal-geprefixte
|
||||
* paden (/nl, /en) door naar admin.php respectievelijk index.php.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @package CodePress
|
||||
*/
|
||||
// Router file for PHP development server - clean URL support + static file serving
|
||||
|
||||
$requestUri = $_SERVER['REQUEST_URI'];
|
||||
@@ -19,6 +30,11 @@ $mimeTypes = [
|
||||
'json' => 'application/json',
|
||||
];
|
||||
|
||||
/**
|
||||
* Statische bestanden uit public/ serveren met juiste MIME-type.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Serve static files from public/
|
||||
$filePath = $publicDir . $path;
|
||||
if (is_file($filePath)) {
|
||||
@@ -30,6 +46,13 @@ if (is_file($filePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme-assets uit themes/<naam>/ serveren (bijv. /themes/default/js/theme.js).
|
||||
*
|
||||
* Path-traversal wordt afgedwongen via realpath() + prefix-controle.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Serve theme assets from the themes/ directory (e.g. /themes/default/js/theme.js)
|
||||
if (preg_match('#^/themes/([^/]+)/(.+)$#', $path, $m)) {
|
||||
$themeName = $m[1];
|
||||
@@ -50,6 +73,13 @@ if (preg_match('#^/themes/([^/]+)/(.+)$#', $path, $m)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin-theme-assets serveren (bijv. /admin/assets/css/bootstrap.min.js).
|
||||
*
|
||||
* Bronmap: admin/theme/default/assets/.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Serve admin theme assets (e.g. /admin/assets/css/bootstrap.min.js)
|
||||
// Served from admin/theme/default/assets/
|
||||
if (preg_match('#^/admin/assets/(.+)$#', $path, $m)) {
|
||||
@@ -70,6 +100,11 @@ if (preg_match('#^/admin/assets/(.+)$#', $path, $m)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin-assets serveren (bijv. /plugins/Navigation/assets/css/navigation.css).
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Serve plugin assets (e.g. /plugins/Navigation/assets/css/navigation.css)
|
||||
if (preg_match('#^/plugins/([^/]+)/assets/(.+)$#', $path, $m)) {
|
||||
$pluginName = $m[1];
|
||||
@@ -90,6 +125,13 @@ if (preg_match('#^/plugins/([^/]+)/assets/(.+)$#', $path, $m)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin-routes doorsturen: /admin/<route> → admin.php?route=<route>.
|
||||
*
|
||||
* Default route is 'dashboard' indien geen subpad opgegeven.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Admin routes: /admin/login → admin.php?route=login
|
||||
if (preg_match('#^/admin(?:/(.+))?$#', $path, $m)) {
|
||||
$_GET['route'] = $m[1] ?? 'dashboard';
|
||||
@@ -97,6 +139,14 @@ if (preg_match('#^/admin(?:/(.+))?$#', $path, $m)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Taal-geprefixte routes: /nl/<page> of /en/<page> → index.php?lang=...&page=...
|
||||
*
|
||||
* /nl/guide activeert de guide-flag met bestaande ?page=. Default valt
|
||||
* door naar index.php.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Language-prefixed routes: /nl/page/path → index.php?lang=nl&page=page/path
|
||||
if (preg_match('#^/(nl|en)(?:/(.+))?$#', $path, $m)) {
|
||||
$_GET['lang'] = $m[1];
|
||||
|
||||
@@ -5,11 +5,21 @@ CodePress uses a **tree view** (`/admin/content`) for content management: a file
|
||||
## Content editor
|
||||
|
||||
### File browser sidebar
|
||||
- Shows the nested file tree of `content/`
|
||||
- Shows the nested file tree of `content/`, with a clickable **content** root at the top
|
||||
- Hidden directories (`.bak`, `.git`) and dotfiles are skipped
|
||||
- Click a file to open it in the CodeMirror editor
|
||||
- Click a **file** to open it in the CodeMirror editor
|
||||
- Click a **folder** (the folder name) to select it — the **folder detail pane** appears on the right
|
||||
- The chevron (▶/▼) next to a folder expands/collapses it without selecting the folder
|
||||
- Folders containing the active file are auto-expanded
|
||||
- Per folder there are action buttons: new file, new folder, rename, delete
|
||||
- Per folder there are action buttons: new file (plus icon), new folder (folder-plus icon) — they operate on the selected folder
|
||||
- Per file there are action buttons: rename/move (pencil), delete (trash) — appear on hover
|
||||
|
||||
### Drag-and-drop
|
||||
- Drag a file or folder onto another folder to move it
|
||||
- Valid drop targets are highlighted during dragging
|
||||
- The move is performed via an AJAX call — the page reloads automatically on success
|
||||
- Security: a folder cannot be dropped into itself or a subfolder
|
||||
- Path-traversal protection: all paths stay within the content directory
|
||||
|
||||
### Editable file types
|
||||
`.md` (Markdown), `.php` (PHP), `.html` (HTML) — consistent with the existing content-edit page.
|
||||
@@ -56,26 +66,32 @@ Existing `{:width=...}` values are shown in the prompts. The size attributes are
|
||||
Images in `content/` are served via the `/-media/` endpoint (content/ lives outside the webroot). Markdown syntax `` with local URLs (relative like `image.jpg`, or absolute like `/content/_images/image.jpg`) is automatically rewritten to `/-media/...`. External URLs (`https://...`) are left untouched.
|
||||
|
||||
### Create a new file
|
||||
- Click **New file** (at the top or per folder in the sidebar)
|
||||
- Select the target folder in the sidebar (click the folder name), or use the root
|
||||
- Click **New file** (in the folder-detail pane or at the top)
|
||||
- Enter a path within content (e.g. `en.page` or `blog/en.post`)
|
||||
- Choose the file type (Markdown/PHP/HTML)
|
||||
- Subfolders are created automatically
|
||||
- Frontmatter with `layout`, `created`, `edited` is auto-generated
|
||||
- The new file is created in the selected folder
|
||||
|
||||
### Upload a file
|
||||
- Click **Upload** to upload files to `content/`
|
||||
- Select the target folder in the sidebar (click the folder name), or use the root
|
||||
- Click **Upload** (in the folder-detail pane or at the top) to upload files to the selected folder
|
||||
- Allowed: images, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD
|
||||
- Path-traversal protection: target dir must stay within `content/`
|
||||
- After the upload you return to the selected folder
|
||||
|
||||
### Move / delete a file
|
||||
- In the file tree each file has a move button (arrows icon) and a delete button (trash)
|
||||
- **Move**: choose a target folder from the dropdown listing all folders in content
|
||||
- **Delete**: with confirmation
|
||||
### Rename / delete a file
|
||||
- Open the file in the editor (click the file name in the sidebar)
|
||||
- **Rename**: edit the **Filename** field on the right above the editor and click Save — the file is renamed immediately
|
||||
- **Delete**: click the delete button (trash) next to the file in the sidebar, with confirmation
|
||||
|
||||
### Folder management
|
||||
- **New folder**: per folder in the sidebar or at the top
|
||||
- **Rename folder**: pencil icon per folder
|
||||
- **Delete folder**: trash icon per folder (only empty folders)
|
||||
- Click a folder in the sidebar to open the **folder-detail pane** on the right
|
||||
- **Rename folder**: edit the **Folder name** field in the folder-detail pane and click Save
|
||||
- **New file / new folder / upload into this folder**: use the buttons in the folder-detail pane — they operate on the selected folder
|
||||
- **Delete folder**: delete button in the folder-detail pane (only empty folders)
|
||||
- The content root is also selectable (click "content" at the top of the tree) — shows the folder-detail pane for the root (rename disabled)
|
||||
|
||||
### Layout selection
|
||||
On the editor page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
|
||||
|
||||
@@ -28,3 +28,44 @@ Only active plugins are shown in the admin sidebar.
|
||||
4. Enable the plugin
|
||||
|
||||
New plugins are content-plugins by default. To create a system plugin, add `"type": "system"` to `plugin.json`.
|
||||
|
||||
## Plugin editor
|
||||
|
||||
Via **Edit** (pencil icon) in the plugin overview you open the plugin editor (`/admin/plugins-edit?plugin=<name>`). It works the same as the content and theme editors.
|
||||
|
||||
### File browser sidebar
|
||||
- Shows the nested file tree of the plugin, with a clickable **plugin** root at the top
|
||||
- Click a file to open it in the CodeMirror editor
|
||||
- Click a folder to open the **folder detail pane** on the right (new file, new folder, upload, delete)
|
||||
- Folders containing the active file are auto-expanded
|
||||
- Per folder there are action buttons: new file, new folder — they operate on the selected folder
|
||||
- Per file there are action buttons: rename/move (pencil), delete (trash) — appear on hover
|
||||
|
||||
### Drag-and-drop
|
||||
- Drag a file or folder onto another folder to move it
|
||||
- Valid drop targets are highlighted during dragging
|
||||
- The move is performed via an AJAX call — the page reloads automatically on success
|
||||
- Protected plugins (like Navigation) cannot be edited/moved
|
||||
|
||||
### Editable file types
|
||||
`.php`, `.json`, `.md`, `.html`, `.css`, `.scss`, `.js`
|
||||
|
||||
### Create a new file / folder
|
||||
- Select the target folder in the sidebar (click the folder name), or use the root
|
||||
- Click **New file** or **New folder** (in the folder-detail pane or at the top)
|
||||
- For a new file: enter a path within the plugin (e.g. `helper.php` or `assets/css/extra.css`)
|
||||
- Subfolders are created automatically
|
||||
|
||||
### Move / delete a file
|
||||
- **Move**: drag the file onto another folder (drag-and-drop), or click the pencil icon
|
||||
- **Delete**: click the trash button next to the file in the sidebar, with confirmation
|
||||
|
||||
### Folder management
|
||||
- Click a folder in the sidebar to open the **folder-detail pane** on the right
|
||||
- **New file / new folder / upload into this folder**: use the buttons in the folder-detail pane
|
||||
- **Delete folder**: delete button in the folder-detail pane (only empty folders)
|
||||
|
||||
### Security
|
||||
- Protected plugins (Navigation) cannot be edited
|
||||
- All actions require a CSRF token
|
||||
- Path-traversal protection via `realpath()` + prefix check on the plugin dir
|
||||
@@ -21,17 +21,27 @@ In the theme overview you see per theme:
|
||||
Via **Edit** (pencil icon) in the theme overview you open the theme editor (`/admin/theme-edit?theme=<name>`). It works the same as the plugin editor:
|
||||
|
||||
### File browser sidebar
|
||||
- Shows the nested file tree of the theme
|
||||
- Shows the nested file tree of the theme, with a clickable **theme** root at the top
|
||||
- `assets/css_compiled/` is hidden (runtime artefact, read-only)
|
||||
- Click a file to open it in the CodeMirror editor
|
||||
- Click a folder to open the **folder detail pane** on the right (new file, new folder, upload, delete)
|
||||
- Folders containing the active file are auto-expanded
|
||||
- Per folder there are action buttons: new file, new folder — they operate on the selected folder
|
||||
- Per file there are action buttons: rename/move (pencil), delete (trash) — appear on hover
|
||||
|
||||
### Drag-and-drop
|
||||
- Drag a file or folder onto another folder to move it
|
||||
- Valid drop targets are highlighted during dragging
|
||||
- The move is performed via an AJAX call — the page reloads automatically on success
|
||||
- `theme.json` cannot be moved
|
||||
|
||||
### Editable file types
|
||||
`.twig`, `.json`, `.scss`, `.css`, `.js`, `.html`, `.md`, `.php`
|
||||
|
||||
### Create a new file
|
||||
- Click **New file**
|
||||
- Enter a path within the theme (e.g. `partials/header.twig` or `assets/scss/_variables.scss`)
|
||||
### Create a new file / folder
|
||||
- Select the target folder in the sidebar (click the folder name), or use the root
|
||||
- Click **New file** or **New folder** (in the folder-detail pane or at the top)
|
||||
- For a new file: enter a path within the theme (e.g. `partials/header.twig` or `assets/scss/_variables.scss`)
|
||||
- Subfolders are created automatically
|
||||
- Allowed: twig, json, scss, css, js, html, md, php
|
||||
- A stub is auto-generated (e.g. `{% extends 'base.twig' %}` for `.twig`)
|
||||
@@ -42,9 +52,14 @@ Via **Edit** (pencil icon) in the theme overview you open the theme editor (`/ad
|
||||
- Path-traversal protection: target dir must stay within `assets/`
|
||||
|
||||
### Move / delete a file
|
||||
- In the file tree each file has a move button (arrows icon) and a delete button (trash)
|
||||
- **Move**: choose a target folder from the dropdown listing all folders in the theme
|
||||
- **Delete**: with confirmation; `theme.json` cannot be deleted
|
||||
- **Move**: drag the file onto another folder (drag-and-drop), or click the pencil icon next to the file to go to the move form
|
||||
- **Delete**: click the trash button next to the file in the sidebar, with confirmation; `theme.json` cannot be deleted
|
||||
|
||||
### Folder management
|
||||
- Click a folder in the sidebar to open the **folder-detail pane** on the right
|
||||
- **New file / new folder / upload into this folder**: use the buttons in the folder-detail pane
|
||||
- **Delete folder**: delete button in the folder-detail pane (only empty folders)
|
||||
- The theme root is also selectable (click the theme name at the top of the tree)
|
||||
|
||||
### Compile SCSS from the editor
|
||||
- At the top of the editor there is a **Compile SCSS** button (only if `assets/scss/theme.scss` exists)
|
||||
|
||||
@@ -5,11 +5,21 @@ CodePress gebruikt een **boom weergave** (`/admin/content`) voor content-beheer:
|
||||
## Content editor
|
||||
|
||||
### Bestandsbrowser zijbalk
|
||||
- Toont de geneste bestandsboom van `content/`
|
||||
- Toont de geneste bestandsboom van `content/`, met een klikbare **content** root bovenaan
|
||||
- Verborgen mappen (`.bak`, `.git`) en dotfiles worden overgeslagen
|
||||
- Klik op een bestand om het te openen in de CodeMirror editor
|
||||
- Klik op een **bestand** om het te openen in de CodeMirror editor
|
||||
- Klik op een **map** (mapnaam) om de map te selecteren — rechts verschijnt het **map detail paneel**
|
||||
- De chevron (▶/▼) naast een map klapt deze in/uit zonder de map te selecteren
|
||||
- Mappen die het actieve bestand bevatten zijn automatisch uitgeklapt
|
||||
- Per map staan actie-knoppen: nieuw bestand, nieuwe map, hernoemen, verwijderen
|
||||
- Per map staan actie-knoppen: nieuw bestand (plus-icoon), nieuwe map (map-plus-icoon) — deze opereren op de geselecteerde map
|
||||
- Per bestand staan actie-knoppen: hernoemen/verplaatsen (potlood), verwijderen (prullenbak) — verschijnen bij hover
|
||||
|
||||
### Bestanden verslepen (drag-and-drop)
|
||||
- Sleep een bestand of map naar een andere map om het te verplaatsen
|
||||
- Tijdens het slepen worden geldige doel-mappen highlight getoond
|
||||
- De verplaatsing gebeurt via een AJAX-call — de pagina herlaadt automatisch na succes
|
||||
- Beveiliging: een map kan niet naar zichzelf of naar een submap verplaatst worden
|
||||
- Path-traversal bescherming: alle paden blijven binnen de content map
|
||||
|
||||
### Bewerkbare bestandstypen
|
||||
`.md` (Markdown), `.php` (PHP), `.html` (HTML) — consistent met de bestaande content-edit pagina.
|
||||
@@ -56,26 +66,32 @@ Bestaande `{:width=...}` waarden worden in de prompts getoond. De grootte-attrib
|
||||
Images in `content/` worden via de `/-media/` endpoint geserveerd (content/ staat buiten de webroot). De markdown syntax `` met lokale URLs (relatief zoals `image.jpg`, of absoluut zoals `/content/_images/image.jpg`) wordt automatisch herschreven naar `/-media/...`. Externe URLs (`https://...`) worden ongewijzigd gelaten.
|
||||
|
||||
### Nieuw bestand aanmaken
|
||||
- Klik op **Nieuw bestand** (bovenaan of per map in de zijbalk)
|
||||
- Selecteer de doelmap in de zijbalk (klik op de mapnaam), of gebruik de root
|
||||
- Klik op **Nieuw bestand** (in het map-detail paneel of bovenaan)
|
||||
- Geef een pad op binnen content (bijv. `nl.pagina` of `blog/nl.post`)
|
||||
- Kies het bestandstype (Markdown/PHP/HTML)
|
||||
- Submappen worden automatisch aangemaakt
|
||||
- Frontmatter met `layout`, `created`, `edited` wordt automatisch gegenereerd
|
||||
- Het nieuwe bestand wordt aangemaakt in de geselecteerde map
|
||||
|
||||
### Bestand uploaden
|
||||
- Klik op **Upload** om bestanden naar `content/` te uploaden
|
||||
- Selecteer de doelmap in de zijbalk (klik op de mapnaam), of gebruik de root
|
||||
- Klik op **Upload** (in het map-detail paneel of bovenaan) om bestanden naar de geselecteerde map te uploaden
|
||||
- Toegestaan: afbeeldingen, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD
|
||||
- Path-traversal bescherming: doelmap moet binnen `content/` blijven
|
||||
- Na de upload keer je terug naar de geselecteerde map
|
||||
|
||||
### Bestand verplaatsen / verwijderen
|
||||
- In de bestandsboom heeft elk bestand een verplaats-knop (pijlen-icoon) en een verwijder-knop (prullenbak)
|
||||
- **Verplaatsen**: kies een doelmap uit de dropdown met alle mappen in content
|
||||
- **Verwijderen**: met bevestiging
|
||||
### Bestand hernoemen / verwijderen
|
||||
- Open het bestand in de editor (klik op de bestandsnaam in de zijbalk)
|
||||
- **Hernoemen**: bewerk het **Bestandsnaam** veld rechts boven de editor en klik op Opslaan — het bestand wordt direct hernoemd
|
||||
- **Verwijderen**: klik op de verwijder-knop (prullenbak) naast het bestand in de zijbalk, met bevestiging
|
||||
|
||||
### Mappen beheer
|
||||
- **Nieuwe map**: per map in de zijbalk of bovenaan
|
||||
- **Map hernoemen**: potlood-icoon per map
|
||||
- **Map verwijderen**: prullenbak-icoon per map (alleen lege mappen)
|
||||
- Klik op een map in de zijbalk om het **map-detail paneel** rechts te openen
|
||||
- **Map hernoemen**: bewerk het **Mapnaam** veld in het map-detail paneel en klik op Opslaan
|
||||
- **Nieuw bestand / Nieuwe map / Uploaden in deze map**: gebruik de knoppen in het map-detail paneel — ze opereren op de geselecteerde map
|
||||
- **Map verwijderen**: verwijder-knop in het map-detail paneel (alleen lege mappen)
|
||||
- De content-root is ook selecteerbaar (klik op "content" bovenaan de boom) — toont het map-detail paneel voor de root (hernoemen uitgeschakeld)
|
||||
|
||||
### Layout selectie
|
||||
Op de editor-pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
|
||||
|
||||
@@ -28,3 +28,44 @@ Alleen actieve plugins worden getoond in de admin sidebar.
|
||||
4. Activeer de plugin
|
||||
|
||||
Nieuwe plugins zijn standaard content-plugins. Wil je een systeem-plugin maken, voeg dan `"type": "system"` toe aan `plugin.json`.
|
||||
|
||||
## Plugin editor
|
||||
|
||||
Via **Bewerken** (potlood-icoon) in het plugin-overzicht open je de plugin-editor (`/admin/plugins-edit?plugin=<naam>`). Deze werkt hetzelfde als de content- en thema-editors.
|
||||
|
||||
### Bestandsbrowser zijbalk
|
||||
- Toont de geneste bestandsboom van de plugin, met een klikbare **plugin** root bovenaan
|
||||
- Klik op een bestand om het te openen in de CodeMirror editor
|
||||
- Klik op een map om het **map detail paneel** rechts te openen (nieuw bestand, nieuwe map, uploaden, verwijderen)
|
||||
- Mappen die het actieve bestand bevatten zijn automatisch uitgeklapt
|
||||
- Per map staan actie-knoppen: nieuw bestand, nieuwe map — opereren op de geselecteerde map
|
||||
- Per bestand staan actie-knoppen: hernoemen/verplaatsen (potlood), verwijderen (prullenbak) — verschijnen bij hover
|
||||
|
||||
### Bestanden verslepen (drag-and-drop)
|
||||
- Sleep een bestand of map naar een andere map om het te verplaatsen
|
||||
- Tijdens het slepen worden geldige doel-mappen highlight getoond
|
||||
- De verplaatsing gebeurt via een AJAX-call — de pagina herlaadt automatisch na succes
|
||||
- Beschermde plugins (zoals Navigation) kunnen niet bewerkt/verplaatst worden
|
||||
|
||||
### Bewerkbare bestandstypen
|
||||
`.php`, `.json`, `.md`, `.html`, `.css`, `.scss`, `.js`
|
||||
|
||||
### Nieuw bestand / nieuwe map aanmaken
|
||||
- Selecteer de doelmap in de zijbalk (klik op de mapnaam), of gebruik de root
|
||||
- Klik op **Nieuw bestand** of **Nieuwe map** (in het map-detail paneel of bovenaan)
|
||||
- Bij nieuw bestand: geef een pad op binnen de plugin (bijv. `helper.php` of `assets/css/extra.css`)
|
||||
- Submappen worden automatisch aangemaakt
|
||||
|
||||
### Bestand verplaatsen / verwijderen
|
||||
- **Verplaatsen**: sleep het bestand naar een andere map (drag-and-drop), of klik op het potlood-icoon
|
||||
- **Verwijderen**: klik op de prullenbak-knop naast het bestand in de zijbalk, met bevestiging
|
||||
|
||||
### Mappen beheer
|
||||
- Klik op een map in de zijbalk om het **map-detail paneel** rechts te openen
|
||||
- **Nieuw bestand / Nieuwe map / Uploaden in deze map**: gebruik de knoppen in het map-detail paneel
|
||||
- **Map verwijderen**: verwijder-knop in het map-detail paneel (alleen lege mappen)
|
||||
|
||||
### Beveiliging
|
||||
- Beschermde plugins (Navigation) kunnen niet bewerkt worden
|
||||
- Alle acties vereisen CSRF token
|
||||
- Path-traversal bescherming via `realpath()` + prefix-check op de plugin-map
|
||||
@@ -21,17 +21,27 @@ In het thema-overzicht zie je per thema:
|
||||
Via **Bewerken** (potlood-icoon) in het thema-overzicht open je de thema-editor (`/admin/theme-edit?theme=<naam>`). Deze werkt hetzelfde als de plugin-editor:
|
||||
|
||||
### Bestandsbrowser zijbalk
|
||||
- Toont de geneste bestandsboom van het thema
|
||||
- Toont de geneste bestandsboom van het thema, met een klikbare **thema** root bovenaan
|
||||
- `assets/css_compiled/` wordt verborgen (runtime artefact, read-only)
|
||||
- Klik op een bestand om het te openen in de CodeMirror editor
|
||||
- Klik op een map om het **map detail paneel** rechts te openen (nieuw bestand, nieuwe map, uploaden, verwijderen)
|
||||
- Mappen die het actieve bestand bevatten zijn automatisch uitgeklapt
|
||||
- Per map staan actie-knoppen: nieuw bestand, nieuwe map — opereren op de geselecteerde map
|
||||
- Per bestand staan actie-knoppen: hernoemen/verplaatsen (potlood), verwijderen (prullenbak) — verschijnen bij hover
|
||||
|
||||
### Bestanden verslepen (drag-and-drop)
|
||||
- Sleep een bestand of map naar een andere map om het te verplaatsen
|
||||
- Tijdens het slepen worden geldige doel-mappen highlight getoond
|
||||
- De verplaatsing gebeurt via een AJAX-call — de pagina herlaadt automatisch na succes
|
||||
- `theme.json` kan niet verplaatst worden
|
||||
|
||||
### Bewerkbare bestandstypen
|
||||
`.twig`, `.json`, `.scss`, `.css`, `.js`, `.html`, `.md`, `.php`
|
||||
|
||||
### Nieuw bestand aanmaken
|
||||
- Klik op **Nieuw bestand**
|
||||
- Geef een pad op binnen het thema (bijv. `partials/header.twig` of `assets/scss/_variables.scss`)
|
||||
### Nieuw bestand / nieuwe map aanmaken
|
||||
- Selecteer de doelmap in de zijbalk (klik op de mapnaam), of gebruik de root
|
||||
- Klik op **Nieuw bestand** of **Nieuwe map** (in het map-detail paneel of bovenaan)
|
||||
- Bij nieuw bestand: geef een pad op binnen het thema (bijv. `partials/header.twig` of `assets/scss/_variables.scss`)
|
||||
- Submappen worden automatisch aangemaakt
|
||||
- Toegestaan: twig, json, scss, css, js, html, md, php
|
||||
- Een stub wordt automatisch gegenereerd (bijv. `{% extends 'base.twig' %}` voor `.twig`)
|
||||
@@ -42,9 +52,14 @@ Via **Bewerken** (potlood-icoon) in het thema-overzicht open je de thema-editor
|
||||
- Path-traversal bescherming: doelmap moet binnen `assets/` blijven
|
||||
|
||||
### Bestand verplaatsen / verwijderen
|
||||
- In de bestandsboom heeft elk bestand een verplaats-knop (pijlen-icoon) en een verwijder-knop (prullenbak)
|
||||
- **Verplaatsen**: kies een doelmap uit de dropdown met alle mappen in het thema
|
||||
- **Verwijderen**: met bevestiging; `theme.json` kan niet verwijderd worden
|
||||
- **Verplaatsen**: sleep het bestand naar een andere map (drag-and-drop), of klik op het potlood-icoon naast het bestand om naar het verplaats-formulier te gaan
|
||||
- **Verwijderen**: klik op de prullenbak-knop naast het bestand in de zijbalk, met bevestiging; `theme.json` kan niet verwijderd worden
|
||||
|
||||
### Mappen beheer
|
||||
- Klik op een map in de zijbalk om het **map-detail paneel** rechts te openen
|
||||
- **Nieuw bestand / Nieuwe map / Uploaden in deze map**: gebruik de knoppen in het map-detail paneel
|
||||
- **Map verwijderen**: verwijder-knop in het map-detail paneel (alleen lege mappen)
|
||||
- De thema-root is ook selecteerbaar (klik op de thema-naam bovenaan de boom)
|
||||
|
||||
### SCSS compileren vanuit de editor
|
||||
- Bovenaan de editor staat een **SCSS compileren** knop (alleen als `assets/scss/theme.scss` bestaat)
|
||||
|
||||
@@ -94,7 +94,7 @@ return [
|
||||
'content_files' => 'Content-Dateien',
|
||||
'content_no_files' => 'Keine Dateien gefunden.',
|
||||
'content_not_editable' => 'Dieser Dateityp kann im Editor nicht bearbeitet werden.',
|
||||
'content_select_file' => 'Wählen Sie eine Datei aus der Seitenleiste zum Bearbeiten.',
|
||||
'content_select_file' => 'Wählen Sie eine Datei oder einen Ordner aus der Seitenleiste.',
|
||||
'content_file_path' => 'Dateipfad innerhalb von content',
|
||||
'content_file_path_help' => 'Nur Buchstaben, Zahlen, Punkte, Unterstriche, Bindestriche und Schrägstriche. Unterordner werden automatisch erstellt. Die Erweiterung wird unten ausgewählt.',
|
||||
'content_upload_help' => 'Dateien werden in content/ hochgeladen. Erlaubt: Bilder, Video, Audio, PDF, ZIP, Office-Dokumente, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
@@ -120,6 +120,11 @@ return [
|
||||
'no_filter_results' => 'Keine Ergebnisse für diesen Filter.',
|
||||
'new_folder_title' => 'Neuen Ordner erstellen',
|
||||
'folder_name' => 'Ordnername',
|
||||
'folder_name_placeholder' => 'Ordnername',
|
||||
'folder_rename_help' => 'Bearbeiten Sie den Ordnernamen und klicken Sie auf Speichern zum Umbenennen.',
|
||||
'files' => 'Dateien',
|
||||
'subfolders' => 'Unterordner',
|
||||
'expand_collapse' => 'Ausklappen/Einklappen',
|
||||
'name_help' => 'Nur Buchstaben, Zahlen, Punkte, Unterstriche und Bindestriche.',
|
||||
|
||||
// Content edit / new
|
||||
|
||||
@@ -94,7 +94,7 @@ return [
|
||||
'content_files' => 'Content files',
|
||||
'content_no_files' => 'No files found.',
|
||||
'content_not_editable' => 'This file type cannot be edited in the editor.',
|
||||
'content_select_file' => 'Select a file from the sidebar to edit.',
|
||||
'content_select_file' => 'Select a file or folder from the sidebar.',
|
||||
'content_file_path' => 'File path within content',
|
||||
'content_file_path_help' => 'Only letters, numbers, dots, underscores, dashes and slashes. Subfolders are created automatically. The extension is chosen below.',
|
||||
'content_upload_help' => 'Files are uploaded to content/. Allowed: images, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
@@ -120,6 +120,11 @@ return [
|
||||
'no_filter_results' => 'No results for this filter.',
|
||||
'new_folder_title' => 'Create new folder',
|
||||
'folder_name' => 'Folder name',
|
||||
'folder_name_placeholder' => 'folder name',
|
||||
'folder_rename_help' => 'Edit the folder name and click save to rename it.',
|
||||
'files' => 'files',
|
||||
'subfolders' => 'subfolders',
|
||||
'expand_collapse' => 'Expand/collapse',
|
||||
'name_help' => 'Only letters, numbers, dots, underscores and dashes.',
|
||||
|
||||
// Content edit / new
|
||||
|
||||
@@ -96,7 +96,7 @@ return [
|
||||
'content_files' => 'Content bestanden',
|
||||
'content_no_files' => 'Geen bestanden gevonden.',
|
||||
'content_not_editable' => 'Dit bestandstype kan niet in de editor bewerkt worden.',
|
||||
'content_select_file' => 'Selecteer een bestand uit de zijbalk om te bewerken.',
|
||||
'content_select_file' => 'Selecteer een bestand of map uit de zijbalk.',
|
||||
'content_file_path' => 'Bestandspad binnen content',
|
||||
'content_file_path_help' => 'Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt. De extensie wordt hieronder gekozen.',
|
||||
'content_upload_help' => 'Bestanden worden geüpload naar content/. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
@@ -122,6 +122,11 @@ return [
|
||||
'no_filter_results' => 'Geen resultaten voor deze filter.',
|
||||
'new_folder_title' => 'Nieuwe map aanmaken',
|
||||
'folder_name' => 'Mapnaam',
|
||||
'folder_name_placeholder' => 'mapnaam',
|
||||
'folder_rename_help' => 'Bewerk de mapnaam en klik op opslaan om te hernoemen.',
|
||||
'files' => 'bestanden',
|
||||
'subfolders' => 'submappen',
|
||||
'expand_collapse' => 'Uitklappen/inklappen',
|
||||
'name_help' => 'Alleen letters, cijfers, punten, underscores en streepjes.',
|
||||
|
||||
// Content edit / new
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Dashboard plugin: toont CMS-, content- en plugin-overzicht in het beheer.
|
||||
*
|
||||
* Systeem-plugin die een overzichtspagina biedt met site-informatie,
|
||||
* content-statistieken en een lijst van geïnstalleerde plugins.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
class Dashboard
|
||||
{
|
||||
/**
|
||||
* Plugin-API instantie voor communicatie met de CMS-core.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var PluginAPIInterface|null
|
||||
*/
|
||||
private ?PluginAPIInterface $api = null;
|
||||
|
||||
/**
|
||||
* Stelt de Plugin-API instantie in.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param PluginAPIInterface $api De Plugin-API instantie.
|
||||
* @return void
|
||||
*/
|
||||
public function setAPI(PluginAPIInterface $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de plugin-configuratie terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> Plugin-configuratie met title, viewable en type.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -18,6 +45,12 @@ class Dashboard
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de admin-menu-items voor deze plugin.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<int, array<string, mixed>> Lijst met admin-menu-item configuraties.
|
||||
*/
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
return [
|
||||
@@ -33,6 +66,16 @@ class Dashboard
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verwerkt een admin-route en toont het dashboard.
|
||||
*
|
||||
* Geeft een HTML-weergave van site-informatie, content-statistieken
|
||||
* en een plugin-overzicht.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $action De uit te voeren actie.
|
||||
* @return string|null De HTML-uitvoer van het dashboard, of null.
|
||||
*/
|
||||
public function handleAdminRoute(string $action): ?string
|
||||
{
|
||||
$siteConfig = $this->getSiteConfig();
|
||||
@@ -147,6 +190,12 @@ class Dashboard
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt de site-configuratie op via de AdminPluginAPI.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> De site-configuratie, of een lege array.
|
||||
*/
|
||||
private function getSiteConfig(): array
|
||||
{
|
||||
if ($this->api === null) {
|
||||
@@ -159,6 +208,14 @@ class Dashboard
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bepaalt het type (system of content) van een plugin door het hoofdbestand te lezen.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $pluginDir Pad naar de plugin-map.
|
||||
* @param string $pluginName Naam van de plugin.
|
||||
* @return string Het plugin-type ('system' of 'content').
|
||||
*/
|
||||
private function getPluginType(string $pluginDir, string $pluginName): string
|
||||
{
|
||||
$pluginFile = $pluginDir . '/' . $pluginName . '.php';
|
||||
@@ -173,6 +230,16 @@ class Dashboard
|
||||
return 'content';
|
||||
}
|
||||
|
||||
/**
|
||||
* Telt het aantal bestanden in een map, eventueel gefilterd op extensie.
|
||||
*
|
||||
* Slaat verborgen en met '-' voorafgegane bestanden en mappen over.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $dir Pad naar de map die doorzocht moet worden.
|
||||
* @param string[] $extensions Optionele lijst van toegestane extensies.
|
||||
* @return int Het aantal gevonden bestanden.
|
||||
*/
|
||||
private function countFiles(string $dir, array $extensions = []): int
|
||||
{
|
||||
if (!is_dir($dir)) return 0;
|
||||
@@ -197,6 +264,15 @@ class Dashboard
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Telt het aantal submappen in een map, recursief.
|
||||
*
|
||||
* Slaat verborgen en met '-' voorafgegane mappen over.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $dir Pad naar de map die doorzocht moet worden.
|
||||
* @return int Het aantal gevonden submappen.
|
||||
*/
|
||||
private function countDirs(string $dir): int
|
||||
{
|
||||
if (!is_dir($dir)) return 0;
|
||||
@@ -215,6 +291,13 @@ class Dashboard
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berekent de totale bestandsgrootte in bytes van alle bestanden in een map.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $dir Pad naar de map die doorzocht moet worden.
|
||||
* @return int De totale grootte in bytes.
|
||||
*/
|
||||
private function dirSize(string $dir): int
|
||||
{
|
||||
if (!is_dir($dir)) return 0;
|
||||
@@ -230,6 +313,13 @@ class Dashboard
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formateert een byte-grootte naar een leesbare string met eenheid.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param int $bytes Het aantal bytes.
|
||||
* @return string De geformatteerde grootte met eenheid (bijv. '1.5 MB').
|
||||
*/
|
||||
private function formatSize(int $bytes): string
|
||||
{
|
||||
if ($bytes <= 0) return '0 B';
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* GeoIPInfo plugin: toont geografische informatie op basis van IP-adres.
|
||||
*
|
||||
* Systeem-plugin die een admin-pagina biedt met het IP-adres van de bezoeker
|
||||
* en het bijbehorende land via de GeoIP-dienst.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
class GeoIPInfo
|
||||
{
|
||||
/**
|
||||
* Plugin-API instantie voor communicatie met de CMS-core.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var PluginAPIInterface|null
|
||||
*/
|
||||
private ?PluginAPIInterface $api = null;
|
||||
|
||||
/**
|
||||
* Stelt de Plugin-API instantie in.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param PluginAPIInterface $api De Plugin-API instantie.
|
||||
* @return void
|
||||
*/
|
||||
public function setAPI(PluginAPIInterface $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de plugin-configuratie terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> Plugin-configuratie met title en type.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -18,7 +45,10 @@ class GeoIPInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin menu items.
|
||||
* Geeft de admin-menu-items voor deze plugin terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<int, array<string, mixed>> Lijst met admin-menu-item configuraties.
|
||||
*/
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
@@ -34,7 +64,10 @@ class GeoIPInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Register admin routes this plugin handles.
|
||||
* Geeft de admin-routes terug die deze plugin afhandelt.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<int, string> Lijst met admin-route namen.
|
||||
*/
|
||||
public function getAdminRoutes(): array
|
||||
{
|
||||
@@ -42,7 +75,13 @@ class GeoIPInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle admin route — render the admin page.
|
||||
* Verwerkt een admin-route en toont de GeoIP-info-pagina.
|
||||
*
|
||||
* Bepaalt het IP-adres van de bezoeker en toont het land en de vlag.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $route De af te handelen admin-route.
|
||||
* @return void
|
||||
*/
|
||||
public function handleAdminRoute(string $route): void
|
||||
{
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* HTMLBlock plugin: toont context-afhankelijke HTML-blokken in de zijbalk.
|
||||
*
|
||||
* Content-plugin die informatie over de huidige pagina toont,
|
||||
* inclusief taal, homepage-status, bestandsinformatie en snelle navigatie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
class HTMLBlock
|
||||
{
|
||||
/**
|
||||
* Plugin-configuratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
* Plugin-API instantie voor communicatie met de CMS-core.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var PluginAPIInterface|null
|
||||
*/
|
||||
private ?PluginAPIInterface $api = null;
|
||||
|
||||
/**
|
||||
* Initialiseert de plugin met standaardconfiguratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = [
|
||||
@@ -13,11 +39,27 @@ class HTMLBlock
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stelt de Plugin-API instantie in.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param PluginAPIInterface $api De Plugin-API instantie.
|
||||
* @return void
|
||||
*/
|
||||
public function setAPI(PluginAPIInterface $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genereert de HTML-inhoud voor de zijbalk.
|
||||
*
|
||||
* Toont informatie over de huidige pagina, taal, homepage-status,
|
||||
* bestandsinformatie en een snelle navigatielijst.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return string De opgebouwde HTML voor de zijbalk.
|
||||
*/
|
||||
public function getSidebarContent(): string
|
||||
{
|
||||
// Content plugin: translations follow the current content language.
|
||||
@@ -89,11 +131,24 @@ class HTMLBlock
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de plugin-configuratie terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> Plugin-configuratie.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stelt de plugin-configuratie in door deze te mergen met standaardwaarden.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param array<string, mixed> $config De configuratie die samengevoegd moet worden.
|
||||
* @return void
|
||||
*/
|
||||
public function setConfig(array $config): void
|
||||
{
|
||||
$this->config = array_merge($this->config, $config);
|
||||
|
||||
+50
-2
@@ -1,14 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Logs plugin: toont admin- en request-logs in het beheer.
|
||||
*
|
||||
* Systeem-plugin die logbestanden uitleest en in een tabel toont,
|
||||
* met tabbladen voor admin-logs en request-logs.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
class Logs
|
||||
{
|
||||
/**
|
||||
* Plugin-API instantie voor communicatie met de CMS-core.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var PluginAPIInterface|null
|
||||
*/
|
||||
private ?PluginAPIInterface $api = null;
|
||||
|
||||
/**
|
||||
* Stelt de Plugin-API instantie in.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param PluginAPIInterface $api De Plugin-API instantie.
|
||||
* @return void
|
||||
*/
|
||||
public function setAPI(PluginAPIInterface $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de plugin-configuratie terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> Plugin-configuratie met title, viewable en type.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -18,6 +45,12 @@ class Logs
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de admin-menu-items voor deze plugin terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<int, array<string, mixed>> Lijst met admin-menu-item configuraties.
|
||||
*/
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
@@ -38,7 +71,12 @@ class Logs
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this plugin's runtime config (defaults + overrides).
|
||||
* Haalt de runtime-configuratie van deze plugin op (standaardwaarden plus overrides).
|
||||
*
|
||||
* Leest standaardwaarden uit plugin.json en overschrijft deze met config.json.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> De samengevoegde plugin-configuratie.
|
||||
*/
|
||||
private function getPluginConfig(): array
|
||||
{
|
||||
@@ -64,6 +102,16 @@ class Logs
|
||||
return array_merge($defaults, $overrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verwerkt een admin-route en toont de logs-pagina.
|
||||
*
|
||||
* Leest de geselecteerde log, parset de regels en toont deze in een tabel
|
||||
* met tabbladen voor admin- en request-logs.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $action De uit te voeren actie.
|
||||
* @return string|null De HTML-uitvoer van de logs-pagina, of null.
|
||||
*/
|
||||
public function handleAdminRoute(string $action): ?string
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
@@ -78,7 +126,7 @@ class Logs
|
||||
};
|
||||
|
||||
$tab = $_GET['tab'] ?? 'admin';
|
||||
$logDir = dirname(__DIR__, 2) . '/admin/storage/logs';
|
||||
$logDir = ($this->api ? $this->api->getProjectRoot() : dirname(__DIR__, 2)) . '/admin/storage/logs';
|
||||
$logFile = $tab === 'requests' ? $logDir . '/requests.log' : $logDir . '/admin.log';
|
||||
|
||||
$logs = [];
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Navigation plugin: bouwt zijbalk-navigatie voor content en guide.
|
||||
*
|
||||
* Content-plugin die de navigatie opbouwt op basis van de mappenstructuur,
|
||||
* met ondersteuning voor guide-pagina's en reguliere content-pagina's.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
class Navigation
|
||||
{
|
||||
/**
|
||||
* Plugin-configuratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
private array $config;
|
||||
|
||||
/**
|
||||
* Plugin-API instantie voor communicatie met de CMS-core.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var PluginAPIInterface|null
|
||||
*/
|
||||
private ?PluginAPIInterface $api = null;
|
||||
|
||||
/**
|
||||
* Initialiseert de plugin met standaardconfiguratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = [
|
||||
@@ -14,23 +40,46 @@ class Navigation
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stelt de Plugin-API instantie in.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param PluginAPIInterface $api De Plugin-API instantie.
|
||||
* @return void
|
||||
*/
|
||||
public function setAPI(PluginAPIInterface $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de plugin-configuratie terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> Plugin-configuratie.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stelt de plugin-configuratie in door deze te mergen met standaardwaarden.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param array<string, mixed> $config De configuratie die samengevoegd moet worden.
|
||||
* @return void
|
||||
*/
|
||||
public function setConfig(array $config): void
|
||||
{
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to this plugin's CSS file (relative to web root).
|
||||
* Geeft het pad naar het CSS-bestand van deze plugin (relatief ten opzichte van de webroot).
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return string Het CSS-URL-pad.
|
||||
*/
|
||||
public function getCssUrl(): string
|
||||
{
|
||||
@@ -38,8 +87,13 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate sidebar navigation.
|
||||
* Detects whether we're on a guide page or content page and builds nav accordingly.
|
||||
* Genereert de zijbalk-navigatie.
|
||||
*
|
||||
* Detecteert of het om een guide-pagina of een content-pagina gaat en
|
||||
* bouwt de navigatie dienovereenkomstig op.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return string De HTML-navigatie, of een lege string in admin-context.
|
||||
*/
|
||||
public function getSidebarContent(): string
|
||||
{
|
||||
@@ -61,11 +115,17 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Build navigation for the guide section.
|
||||
* Bouwt de navigatie voor de guide-sectie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $lang De huidige taalcode.
|
||||
* @param string $currentPage De huidige pagina-pad.
|
||||
* @param bool $isAdmin Of dit in admin-context is.
|
||||
* @return string De opgebouwde HTML-navigatie, of een lege string.
|
||||
*/
|
||||
private function buildGuideNav(string $lang, string $currentPage, bool $isAdmin): string
|
||||
{
|
||||
$guideRoot = dirname(__DIR__, 2) . '/guide/' . $lang;
|
||||
$guideRoot = ($this->api ? $this->api->getProjectRoot() : dirname(__DIR__, 2)) . '/guide/' . $lang;
|
||||
if (!is_dir($guideRoot)) {
|
||||
return '';
|
||||
}
|
||||
@@ -79,11 +139,16 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Build navigation for the content section.
|
||||
* Bouwt de navigatie voor de content-sectie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $lang De huidige taalcode.
|
||||
* @param string $currentPage De huidige pagina-pad.
|
||||
* @return string De opgebouwde HTML-navigatie, of een lege string.
|
||||
*/
|
||||
private function buildContentNav(string $lang, string $currentPage): string
|
||||
{
|
||||
$contentRoot = dirname(__DIR__, 2) . '/content';
|
||||
$contentRoot = $this->api ? $this->api->getContentDir() : dirname(__DIR__, 2) . '/content';
|
||||
if (!is_dir($contentRoot)) {
|
||||
return '';
|
||||
}
|
||||
@@ -95,15 +160,17 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively build navigation from a directory structure.
|
||||
* Bouwt recursief navigatie op vanuit een mapstructuur.
|
||||
*
|
||||
* @param string $dir Current directory
|
||||
* @param string $relPath Relative path from root
|
||||
* @param string $currentPage Current page path
|
||||
* @param bool $isAdmin Whether this is admin context
|
||||
* @param string $mode 'guide' or 'content'
|
||||
* @param string $lang Current language
|
||||
* @param int $depth Nesting depth (0 = top level)
|
||||
* @since 2.6.4
|
||||
* @param string $dir Huidige map.
|
||||
* @param string $relPath Relatief pad vanaf de root.
|
||||
* @param string $currentPage Huidige pagina-pad.
|
||||
* @param bool $isAdmin Of dit in admin-context is.
|
||||
* @param string $mode Modus: 'guide' of 'content'.
|
||||
* @param string $lang Huidige taalcode.
|
||||
* @param int $depth Nesting-diepte (0 = topniveau). Standaard 0.
|
||||
* @return string De opgebouwde HTML-navigatie.
|
||||
*/
|
||||
private function buildNav(string $dir, string $relPath, string $currentPage, bool $isAdmin, string $mode, string $lang, int $depth = 0): string
|
||||
{
|
||||
@@ -188,7 +255,14 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the URL for a page.
|
||||
* Bouwt de URL voor een pagina-referentie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $pageRef De pagina-referentie.
|
||||
* @param string $lang De huidige taalcode.
|
||||
* @param bool $isAdmin Of dit in admin-context is.
|
||||
* @param string $mode Modus: 'guide' of 'content'.
|
||||
* @return string De opgebouwde URL.
|
||||
*/
|
||||
private function buildUrl(string $pageRef, string $lang, bool $isAdmin, string $mode): string
|
||||
{
|
||||
@@ -203,7 +277,13 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a nav item is the current page.
|
||||
* Controleert of een navigatie-item de huidige pagina is.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $pageRef De pagina-referentie van het item.
|
||||
* @param string $currentPage Het huidige pagina-pad.
|
||||
* @param string $mode Modus: 'guide' of 'content'.
|
||||
* @return bool True als het item actief is, anders false.
|
||||
*/
|
||||
private function isActive(string $pageRef, string $currentPage, string $mode): bool
|
||||
{
|
||||
@@ -215,7 +295,10 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available language codes.
|
||||
* Geeft de beschikbare taalcodes terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<int, string> Lijst met taalcodes.
|
||||
*/
|
||||
private function getLanguageCodes(): array
|
||||
{
|
||||
@@ -223,7 +306,11 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a slug into a readable title.
|
||||
* Formateert een slug naar een leesbare titel.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $slug De te formatteren slug.
|
||||
* @return string De geformatteerde titel.
|
||||
*/
|
||||
private function formatTitle(string $slug): string
|
||||
{
|
||||
@@ -233,8 +320,13 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the H1 title from a markdown/PHP/HTML file.
|
||||
* Falls back to formatTitle() if no H1 is found.
|
||||
* Haalt de H1-titel uit een markdown-, PHP- of HTML-bestand.
|
||||
*
|
||||
* Valt terug op formatTitle() wanneer geen H1 gevonden is.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $filePath Pad naar het bestand.
|
||||
* @return string De gevonden titel, of een lege string.
|
||||
*/
|
||||
private function getTitleFromFile(string $filePath): string
|
||||
{
|
||||
@@ -253,10 +345,12 @@ class Navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the navigation items as HTML.
|
||||
* Rendert de navigatie-items als HTML.
|
||||
*
|
||||
* @param array $items Navigation items
|
||||
* @param int $depth Nesting depth (0 = top level)
|
||||
* @since 2.6.4
|
||||
* @param array $items Navigatie-items.
|
||||
* @param int $depth Nesting-diepte (0 = topniveau). Standaard 0.
|
||||
* @return string De gegenereerde HTML.
|
||||
*/
|
||||
private function renderNav(array $items, int $depth = 0): string
|
||||
{
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Statistics plugin: toont analytics-statistieken in het beheer.
|
||||
*
|
||||
* Systeem-plugin die analytics-gegevens toont, zoals totaal aantal views,
|
||||
* unieke bezoekers, landen en top-pagina's.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
class Statistics
|
||||
{
|
||||
/**
|
||||
* Plugin-API instantie voor communicatie met de CMS-core.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @var PluginAPIInterface|null
|
||||
*/
|
||||
private ?PluginAPIInterface $api = null;
|
||||
|
||||
/**
|
||||
* Stelt de Plugin-API instantie in.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param PluginAPIInterface $api De Plugin-API instantie.
|
||||
* @return void
|
||||
*/
|
||||
public function setAPI(PluginAPIInterface $api): void
|
||||
{
|
||||
$this->api = $api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de plugin-configuratie terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> Plugin-configuratie met title, viewable en type.
|
||||
*/
|
||||
public function getConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -18,6 +45,12 @@ class Statistics
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Geeft de admin-menu-items voor deze plugin terug.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<int, array<string, mixed>> Lijst met admin-menu-item configuraties.
|
||||
*/
|
||||
public function getAdminMenu(): array
|
||||
{
|
||||
$config = $this->getPluginConfig();
|
||||
@@ -38,7 +71,12 @@ class Statistics
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this plugin's runtime config (defaults + overrides).
|
||||
* Haalt de runtime-configuratie van deze plugin op (standaardwaarden plus overrides).
|
||||
*
|
||||
* Leest standaardwaarden uit plugin.json en overschrijft deze met config.json.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return array<string, mixed> De samengevoegde plugin-configuratie.
|
||||
*/
|
||||
private function getPluginConfig(): array
|
||||
{
|
||||
@@ -64,6 +102,16 @@ class Statistics
|
||||
return array_merge($defaults, $overrides);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verwerkt een admin-route en toont de statistieken-pagina.
|
||||
*
|
||||
* Toont analytics-gegevens zoals views, unieke bezoekers, landen en
|
||||
* top-pagina's, mits analytics ingeschakeld is.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @param string $action De uit te voeren actie.
|
||||
* @return string|null De HTML-uitvoer van de statistieken-pagina, of null.
|
||||
*/
|
||||
public function handleAdminRoute(string $action): ?string
|
||||
{
|
||||
// System plugin: translations resolve against the admin language.
|
||||
@@ -182,6 +230,12 @@ class Statistics
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Haalt de Analytics-instantie op basis van de analytics-configuratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @return Analytics|null De Analytics-instantie, of null als analytics uitgeschakeld is.
|
||||
*/
|
||||
private function getAnalytics(): ?Analytics
|
||||
{
|
||||
if ($this->api === null) {
|
||||
|
||||
+1770
-107
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Asset-server voor productie: serveert statische bestanden uit themes/,
|
||||
* admin/theme/ en plugins/.
|
||||
*
|
||||
* Dit bestand wordt op productie gebruikt wanneer de PHP-dev-router niet
|
||||
* beschikbaar is. public/.htaccess herschrijft /themes/, /admin/assets/ en
|
||||
* /plugins/ URLs naar dit bestand. Paden worden via realpath() + prefix-
|
||||
* check afgedwongen binnen de juiste asset-map om path traversal te voorkomen.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @package CodePress
|
||||
*/
|
||||
/**
|
||||
* Asset server - serves static files from themes/, admin/theme/, and plugins/
|
||||
* This file is used on production servers where the PHP router is not available.
|
||||
@@ -25,6 +37,11 @@ $mimeTypes = [
|
||||
// Determine which asset directory to serve from
|
||||
$assetFile = null;
|
||||
|
||||
/**
|
||||
* /themes/<naam>/assets/...: theme-asset matchen en pad oplossen.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// /themes/<name>/assets/...
|
||||
if (preg_match('#^/themes/([^/]+)/assets/(.+)$#', $path, $m)) {
|
||||
$themeName = $m[1];
|
||||
@@ -32,12 +49,22 @@ if (preg_match('#^/themes/([^/]+)/assets/(.+)$#', $path, $m)) {
|
||||
$assetFile = $basePath . '/themes/' . $themeName . '/assets/' . $assetRel;
|
||||
$realBase = realpath($basePath . '/themes/' . $themeName . '/assets');
|
||||
}
|
||||
/**
|
||||
* /admin/assets/...: admin-theme-asset matchen (default admin theme).
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// /admin/assets/...
|
||||
elseif (preg_match('#^/admin/assets/(.+)$#', $path, $m)) {
|
||||
$assetRel = $m[1];
|
||||
$assetFile = $basePath . '/admin/theme/default/assets/' . $assetRel;
|
||||
$realBase = realpath($basePath . '/admin/theme/default/assets');
|
||||
}
|
||||
/**
|
||||
* /plugins/<naam>/assets/...: plugin-asset matchen en pad oplossen.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// /plugins/<name>/assets/...
|
||||
elseif (preg_match('#^/plugins/([^/]+)/assets/(.+)$#', $path, $m)) {
|
||||
$pluginName = $m[1];
|
||||
@@ -46,6 +73,14 @@ elseif (preg_match('#^/plugins/([^/]+)/assets/(.+)$#', $path, $m)) {
|
||||
$realBase = realpath($basePath . '/plugins/' . $pluginName . '/assets');
|
||||
}
|
||||
|
||||
/**
|
||||
* Matchende asset uitleveren met juiste MIME en cache-headers.
|
||||
*
|
||||
* Controleert dat het opgeloste pad binnen de asset-map blijft en een
|
||||
* regulier bestand is; anders volgt een 404.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
if ($assetFile && $realBase) {
|
||||
$realFile = realpath($assetFile);
|
||||
if ($realFile && strpos($realFile, $realBase) === 0 && is_file($realFile)) {
|
||||
|
||||
+102
-7
@@ -1,12 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Front-end entry point van CodePress CMS.
|
||||
*
|
||||
* Bootstrap de CMS-core, laadt de configuratie, stuurt security headers uit,
|
||||
* serveert media-/asset-/robots-requests, voert BotGuard- en rate-limit-
|
||||
* controles uit, registreert analytics- en logvermeldingen en render
|
||||
* uiteindelijk de pagina via CodePressCMS::render().
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @package CodePress
|
||||
*/
|
||||
require_once __DIR__ . '/../cms/core/index.php';
|
||||
|
||||
$config = include __DIR__ . '/../cms/core/config.php';
|
||||
|
||||
/**
|
||||
* Log-systeem initialiseren op basis van de logging-configuratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Initialize dynamic logging
|
||||
LogManager::init($config['logging'] ?? []);
|
||||
|
||||
/**
|
||||
* Security headers instellen voor alle front-end responses.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Security headers
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: SAMEORIGIN');
|
||||
@@ -15,6 +36,15 @@ header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header('Content-Security-Policy: default-src \'self\'; script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\'; img-src \'self\' data:; font-src \'self\';');
|
||||
header_remove('X-Powered-By');
|
||||
|
||||
/**
|
||||
* Media- en asset-serving via /-media/ en /-assets/ routes.
|
||||
*
|
||||
* Bepaalt het verzoekpad en de toegestane MIME-types, resolveert de
|
||||
* content-map dynamisch op basis van config['content_dir'] en serveert
|
||||
* bestanden buiten public/ met een pad-controle om traversal te blokkeren.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Serve media files from any content/ subdirectory via /-media/
|
||||
// Serve files from content/-assets/ via /-assets/ (backward compatible)
|
||||
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
|
||||
@@ -31,11 +61,15 @@ $mimeTypes = [
|
||||
'css' => 'text/css', 'js' => 'application/javascript',
|
||||
];
|
||||
|
||||
// Resolve the content directory once (absolute, from config) so media
|
||||
// serving follows a customised content_dir instead of a hardcoded path.
|
||||
$contentBase = realpath($config['content_dir']);
|
||||
$contentBase = $contentBase !== false ? $contentBase : rtrim($config['content_dir'], '/');
|
||||
|
||||
if (strpos($path, '/-media/') === 0) {
|
||||
$relative = ltrim(substr($path, 7), '/');
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
$filePath = $root . '/content/' . $relative;
|
||||
if (strpos($filePath, $root . '/content') === 0) {
|
||||
$filePath = $contentBase . '/' . $relative;
|
||||
if ($contentBase !== '' && strpos($filePath, $contentBase) === 0) {
|
||||
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $allowedExt) && file_exists($filePath) && !is_dir($filePath)) {
|
||||
if (isset($mimeTypes[$ext])) {
|
||||
@@ -50,18 +84,23 @@ if (strpos($path, '/-media/') === 0) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* /-assets/ route: serveert bestanden uit content/-assets/ of de content-
|
||||
* root, met backward-compatibiliteit en path-traversal-controle.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
if (strpos($path, '/-assets/') === 0) {
|
||||
$relative = ltrim(substr($path, 8), '/');
|
||||
$root = realpath(__DIR__ . '/..');
|
||||
|
||||
$servePath = null;
|
||||
$candidates = [
|
||||
$root . '/content/-assets/' . $relative,
|
||||
$root . '/content/' . $relative,
|
||||
$contentBase . '/-assets/' . $relative,
|
||||
$contentBase . '/' . $relative,
|
||||
];
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (strpos($candidate, $root . '/content') !== 0) continue;
|
||||
if ($contentBase === '' || strpos($candidate, $contentBase) !== 0) continue;
|
||||
if (file_exists($candidate) && !is_dir($candidate)) {
|
||||
$ext = strtolower(pathinfo($candidate, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, $allowedExt)) {
|
||||
@@ -92,6 +131,13 @@ if ($path === '/robots.txt') {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directe toegang tot /content/ blokkeren.
|
||||
*
|
||||
* Voorkomt dat ruwe content-bestanden via het web opgevraagd worden.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Block direct access to content files
|
||||
if (strpos($path, '/content/') === 0) {
|
||||
http_response_code(403);
|
||||
@@ -99,6 +145,16 @@ if (strpos($path, '/content/') === 0) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Security-checks en rate limiting uitvoeren.
|
||||
*
|
||||
* Bepaalt client-IP, BotGuard-controle, IP-blocklist en rate-limiter op
|
||||
* basis van de security-config. IP's op de allowed/uitgesloten lijsten
|
||||
* slaan de controles over. Het requestStatus wordt bijgehouden voor
|
||||
* latere logging en blocking.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Load RequestLogger for IP & request logging
|
||||
require_once __DIR__ . '/../cms/core/class/RequestLogger.php';
|
||||
|
||||
@@ -149,9 +205,22 @@ if (!$isAllowedIp) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CMS-instantie aanmaken met de geladen configuratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Instantiate CMS instance
|
||||
$cms = new CodePressCMS($config);
|
||||
|
||||
/**
|
||||
* Analytics en GeoIP-lookup uitvoeren.
|
||||
*
|
||||
* Land, IP-anonimisatie en analytics-instellingen worden bepaald op basis
|
||||
* van de analytics-configuratie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Analytics & GeoIP settings
|
||||
$analyticsSettings = $config['analytics'] ?? [];
|
||||
$geoCountry = null;
|
||||
@@ -160,11 +229,25 @@ if (!empty($analyticsSettings['enabled'])) {
|
||||
$geoCountry = $geoIp->lookupCountry($clientIp);
|
||||
}
|
||||
|
||||
/**
|
||||
* IP-anonimisatie toepassen voor opslag indien ingeschakeld.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Apply IP anonymization for storage if enabled
|
||||
$storedIp = !empty($analyticsSettings['anonymize_ip'])
|
||||
? RequestLogger::anonymizeIp($clientIp)
|
||||
: $clientIp;
|
||||
|
||||
/**
|
||||
* Pagina-view loggen (niet voor media-/assets-requests).
|
||||
*
|
||||
* Start sessie indien nodig, stelt huidige pagina/referrer vast, schrijft
|
||||
* een request-logregel, vuurt een LogManager-requestevent en registreert
|
||||
* geaggregeerde analytics (mits ingeschakeld en IP niet uitgesloten).
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Log page view (not for media/assets)
|
||||
if (!str_starts_with($path, '/-media/') && !str_starts_with($path, '/-assets/')) {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
@@ -203,6 +286,13 @@ if (!str_starts_with($path, '/-media/') && !str_starts_with($path, '/-assets/'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Geblokkeerde verzoeken afhandelen.
|
||||
*
|
||||
* Geeft 429 bij rate-limit en 403 bij andere blokkades, met noindex-headers.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
// Block request if status is not ok
|
||||
if ($requestStatus !== 'ok') {
|
||||
if ($requestStatus === 'blocked:ratelimit') {
|
||||
@@ -216,4 +306,9 @@ if ($requestStatus !== 'ok') {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagina renderen via de CodePressCMS-instantie.
|
||||
*
|
||||
* @since 2.6.4
|
||||
*/
|
||||
$cms->render();
|
||||
+2
-18
@@ -38,11 +38,6 @@ class InstalledVersions
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
@@ -320,12 +315,6 @@ class InstalledVersions
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,9 +343,7 @@ class InstalledVersions
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
@@ -364,16 +351,13 @@ class InstalledVersions
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
if (strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
|
||||
Vendored
-2
@@ -1,4 +1,3 @@
|
||||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@@ -18,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
Vendored
+3
-2
@@ -19,7 +19,8 @@ if ($issues) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
+13
-2
@@ -1,11 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* Versie-informatie voor CodePress CMS.
|
||||
*
|
||||
* Returnt een array met de huidige versie, releasedatum, codenaam, status,
|
||||
* systeemvereisten (PHP-versie en -extensies) en credits (auteur, licentie,
|
||||
* repository en contact). Wordt door de rest van het CMS gebruikt om de
|
||||
* versie te tonen en vereisten te valideren.
|
||||
*
|
||||
* @since 2.6.4
|
||||
* @package CodePress
|
||||
*/
|
||||
/**
|
||||
* CodePress CMS Version Information
|
||||
*/
|
||||
|
||||
return [
|
||||
'version' => '2.6.3',
|
||||
'release_date' => '2026-08-20',
|
||||
'version' => '2.6.4',
|
||||
'release_date' => '2026-08-26',
|
||||
'codename' => 'Lyra',
|
||||
'status' => 'stable',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user