From 5edc929c131a3798b038e27cf4a87ec57ca92e93 Mon Sep 17 00:00:00 2001 From: Edwin Noorlander Date: Tue, 18 Aug 2026 13:49:52 +0000 Subject: [PATCH] v2.6.1d (Lyra): Plugin i18n, plugin editor vernieuwd, media invoegen Plugin internationalisatie: plugins hebben eigen language/ mappen. Systeem plugins volgen admin taal (admin.php), content plugins volgen content taal (site.php). Fallback chain: geselecteerd -> plugin default_language -> CMS default. PluginManager/AdminPluginAPI/CMSAPI uitgebreid met getPluginTranslations()/t(). plugin.json settings ondersteunen label_key/help_key/option_label_key. Plugin uniformiteit: alle 6 plugins hebben uniforme structuur (README.md, assets/.gitkeep, language/nl|en/). plugins/README.md herschreven. guide plugin-development.md (NL+EN) volledig herschreven. Plugin editor vernieuwd: geneste bestandsbrowser zijbalk, nieuw bestand aanmaken, uploaden naar assets/, verwijderen en verplaatsen. Nieuwe routes: plugins-file-upload, plugins-file-delete, plugins-file-move. Path-traversal bescherming + protected plugins geblokkeerd. Media invoegen in editor: nieuw /admin/media-list JSON endpoint + herbruikbare _media-modal.twig include. Plugin-context scant assets/ map. editor-toolbar.js modeMap uitgebreid voor css/scss/js/json. Plugin overzicht knoppen: alleen iconen met title/aria-label. Tests: pentest 30/30, WCAG 2.1 AA 25/25. --- README.md | 38 +- TODO.md | 38 +- admin/src/AdminAuth.php | 63 ++ .../theme/default/assets/js/editor-toolbar.js | 9 +- admin/theme/default/views/layouts/admin.twig | 70 +- .../default/views/pages/_media-modal.twig | 200 ++++ .../default/views/pages/content-edit.twig | 16 + .../default/views/pages/plugin-config.twig | 102 ++- .../default/views/pages/plugins-edit.twig | 230 ++++- .../views/pages/plugins-move-form.twig | 39 + admin/theme/default/views/pages/plugins.twig | 20 +- .../theme/default/views/pages/users-edit.twig | 18 + cms/core/class/CodePressCMS.php | 8 +- cms/core/class/ContentAPI.php | 17 + cms/core/plugin/AdminPluginAPI.php | 55 ++ cms/core/plugin/CMSAPI.php | 61 ++ cms/core/plugin/PluginAPIInterface.php | 23 + cms/core/plugin/PluginManager.php | 133 ++- docs/release-notes/v2.6.1d.md | 173 ++++ guide/en/codepress-developer/architectuur.md | 16 +- .../codepress-developer/plugin-development.md | 240 ++++- guide/nl/codepress-developer/architectuur.md | 16 +- .../codepress-developer/plugin-development.md | 248 ++++- language/de/admin.php | 16 + language/en/admin.php | 39 + language/nl/admin.php | 39 + plugins/Dashboard/Dashboard.php | 46 +- plugins/Dashboard/README.md | 39 + plugins/Dashboard/assets/.gitkeep | 0 plugins/Dashboard/language/en/admin.php | 34 + plugins/Dashboard/language/nl/admin.php | 34 + plugins/Dashboard/plugin.json | 4 +- plugins/GeoIPInfo/GeoIPInfo.php | 23 +- plugins/GeoIPInfo/README.md | 38 + plugins/GeoIPInfo/assets/.gitkeep | 0 plugins/GeoIPInfo/language/en/admin.php | 10 + plugins/GeoIPInfo/language/nl/admin.php | 10 + plugins/GeoIPInfo/plugin.json | 6 +- plugins/HTMLBlock/HTMLBlock.php | 22 +- plugins/HTMLBlock/README.md | 121 +-- plugins/HTMLBlock/assets/.gitkeep | 0 plugins/HTMLBlock/language/en/site.php | 15 + plugins/HTMLBlock/language/nl/site.php | 15 + plugins/HTMLBlock/plugin.json | 4 +- plugins/Logs/Logs.php | 68 +- plugins/Logs/README.md | 49 + plugins/Logs/assets/.gitkeep | 0 plugins/Logs/language/en/admin.php | 32 + plugins/Logs/language/nl/admin.php | 32 + plugins/Logs/plugin.json | 40 +- plugins/Navigation/README.md | 47 + plugins/Navigation/assets/.gitkeep | 0 plugins/Navigation/language/en/site.php | 7 + plugins/Navigation/language/nl/site.php | 7 + plugins/Navigation/plugin.json | 4 +- plugins/README.md | 155 ++-- plugins/Statistics/README.md | 51 ++ plugins/Statistics/Statistics.php | 71 +- plugins/Statistics/assets/.gitkeep | 0 plugins/Statistics/language/en/admin.php | 32 + plugins/Statistics/language/nl/admin.php | 32 + plugins/Statistics/plugin.json | 33 +- public/admin.php | 857 +++++++++++++++++- version.php | 4 +- 64 files changed, 3503 insertions(+), 366 deletions(-) create mode 100644 admin/theme/default/views/pages/_media-modal.twig create mode 100644 admin/theme/default/views/pages/plugins-move-form.twig create mode 100644 docs/release-notes/v2.6.1d.md create mode 100644 plugins/Dashboard/README.md create mode 100644 plugins/Dashboard/assets/.gitkeep create mode 100644 plugins/Dashboard/language/en/admin.php create mode 100644 plugins/Dashboard/language/nl/admin.php create mode 100644 plugins/GeoIPInfo/README.md create mode 100644 plugins/GeoIPInfo/assets/.gitkeep create mode 100644 plugins/GeoIPInfo/language/en/admin.php create mode 100644 plugins/GeoIPInfo/language/nl/admin.php create mode 100644 plugins/HTMLBlock/assets/.gitkeep create mode 100644 plugins/HTMLBlock/language/en/site.php create mode 100644 plugins/HTMLBlock/language/nl/site.php create mode 100644 plugins/Logs/README.md create mode 100644 plugins/Logs/assets/.gitkeep create mode 100644 plugins/Logs/language/en/admin.php create mode 100644 plugins/Logs/language/nl/admin.php create mode 100644 plugins/Navigation/README.md create mode 100644 plugins/Navigation/assets/.gitkeep create mode 100644 plugins/Navigation/language/en/site.php create mode 100644 plugins/Navigation/language/nl/site.php create mode 100644 plugins/Statistics/README.md create mode 100644 plugins/Statistics/assets/.gitkeep create mode 100644 plugins/Statistics/language/en/admin.php create mode 100644 plugins/Statistics/language/nl/admin.php diff --git a/README.md b/README.md index 8f72bd2..46d3382 100644 --- a/README.md +++ b/README.md @@ -210,12 +210,15 @@ codepress/ │ │ ├── partials/ # Header, navigation, footer │ │ └── assets/ # SCSS, CSS, JS, img ├── plugins/ # Plugins -│ ├── HTMLBlock/ # Voorbeeld sidebar plugin -│ └── Navigation/ # Essentiële navigatie plugin (beschermd) -│ ├── Navigation.php # Plugin code -│ ├── plugin.json # Plugin metadata -│ ├── assets/scss/ # Plugin SCSS bron -│ └── assets/css/ # Plugin CSS +│ ├── Dashboard/ # Systeem plugin (admin taal) +│ ├── HTMLBlock/ # Content plugin (content taal) +│ ├── Navigation/ # Essentiële navigatie plugin (beschermd) +│ └── Statistics/ # Systeem plugin (admin taal) +│ ├── Statistics.php # Plugin code +│ ├── plugin.json # Plugin metadata + instellingen +│ ├── README.md # Plugin documentatie +│ ├── assets/ # Plugin CSS/JS/SCSS +│ └── language/ # Plugin vertalingen (nl/, en/) ├── content/ # Website content (.md, .php, .html) ├── public/ # Web root │ ├── index.php # Website entry point @@ -281,14 +284,33 @@ codepress/ ### Plugin structuur +Elke plugin heeft een uniforme structuur: + ``` plugins/MijnPlugin/ ├── MijnPlugin.php # Plugin code (naam = pluginnaam) -├── plugin.json # Plugin metadata +├── plugin.json # Plugin metadata + instellingen +├── README.md # Plugin documentatie ├── assets/scss/ # Plugin SCSS bron -└── assets/css/ # Plugin CSS (na compilatie) +├── assets/css/ # Plugin CSS (na compilatie) +└── language/ # Plugin vertalingen (i18n) + ├── nl/admin.php # NL admin labels (systeem plugins) + └── en/admin.php # EN admin labels ``` +- **Systeem plugins** volgen de admin-taal (`language//admin.php`) +- **Content plugins** volgen de content-taal (`language//site.php`) +- Fallback chain: geselecteerde taal → plugin `default_language` → CMS site default + +Zie `guide/nl/codepress-developer/plugin-development.md` voor uitgebreide documentatie. + +### Plugin editor + +De admin plugin-editor (`/admin/plugins-edit`) biedt een volledige bestandsbeheer-omgeving: +- Geneste bestandsbrowser zijbalk (alle bestanden in de plugin-map) +- Nieuw bestand aanmaken, uploaden naar assets/, verwijderen en verplaatsen +- CodeMirror editor voor .php, .json, .md, .html, .css, .scss, .js bestanden + ### Essentiële plugins De **Navigation** plugin is een essentiële plugin en kan niet worden gedeactiveerd, bewerkt of verwijderd. Deze plugin genereert automatisch de zijbalknavigatie voor handleidingen en content. diff --git a/TODO.md b/TODO.md index 21d6af7..886a2d6 100644 --- a/TODO.md +++ b/TODO.md @@ -1,15 +1,27 @@ # TODO ## Voor elke versie verhoging. Deze nooit weghalen. -- [ ] Pentest controles uitgevoerd -- [ ] WCAG 2.1 AA accessibility tests -- [ ] Volledige git commit maken. -- [ ] Verslag maken voor opdrcht gever -- [ ] Na convormatie versie verhogen -- [ ] Bij Voltooid versie ophoging aanmaken en deze allemaal unvinken voor volgende ronde. +- [x] Pentest controles uitgevoerd +- [x] WCAG 2.1 AA accessibility tests +- [x] Volledige git commit maken. +- [x] Verslag maken voor opdrcht gever +- [x] Na convormatie versie verhogen +- [x] Bij Voltooid versie ophoging aanmaken en deze allemaal unvinken voor volgende ronde. -## Te doen ⏳ +## Bug fixes +- [x] admin rollen werkt niet goed, dashboard geeft een 404, de plugin's moeten aangege welke rol nodig is. +- [x] user admin mag nooit zijn eigen rol weizigen. +- [x] Na in loggen moet de admin zijn rol binnen de /admin kunnen veranderen om andere rollen te kunnen testen +- [x] in een andere rol kan de gebruiker niet zijn eigen wachtwoord veranderen. +- [x] Bij het aanmaken van een pagina in de content moet de auteur naam en email in de meta komen te staan. Deze moeten dus ook door middel van de api aan de twig bestanden doorgeven kunnen worden +- [x] Admin taal niet volledig geïntegreerd met plugins: plugins hebben nu eigen language/ mappen (nl/en). Systeem plugins volgen admin taal (admin.php), content plugins volgen content taal (site.php). Fallback chain: geselecteerd → plugin default_language → CMS default → leeg. PluginManager + AdminPluginAPI + CMSAPI uitgebreid met getPluginTranslations()/t(). plugin.json settings ondersteunen label_key/help_key/option_label_key. +- [x] Plugin uniformiteit: alle 6 plugins hebben nu een uniforme structuur (.php, plugin.json, README.md, assets/.gitkeep, language/nl|en/). plugins/README.md herschreven. guide plugin-development.md (NL+EN) volledig bijgewerkt met i18n + admin integratie + instellingen-schema. guide architectuur.md (NL+EN) bijgewerkt met plugin language/ map. +- [x] Plugin editor toont alleen de .php maar een plugin kan uit meerdere bestanden bestaan: plugins-edit pagina heeft nu een bestandsbrowser zijbalk die alle bestanden in de plugin-map toont (.php, .json, .md, .html, .css, .scss, .js) inclusief language/ en assets/ submappen. Nieuw-bestand knop met modal. Path-traversal bescherming via realpath + prefix check + segment-guard. scanPluginFiles() helper toegevoegd. editor-toolbar.js modeMap uitgebreid voor css/scss/js/json. Vertaalstrings toegevoegd aan nl/en/de admin.php. +- [x] Plugin editor uploaden en verwijderen: plugins-edit pagina heeft nu een Upload knop (bestanden naar assets/ van de plugin) en per-bestand verwijder-knop in de bestandsboom. Twee nieuwe routes: plugins-file-upload en plugins-file-delete. Path-traversal bescherming + protected plugins geblokkeerd + dotfiles geweigerd. Vertaalstrings toegevoegd. +- [x] Plugin editor bestand verplaatsen: plugins-edit pagina heeft nu per-bestand een verplaats-knop in de bestandsboom. Nieuwe route plugins-file-move + template plugins-move-form.twig (dropdown met mappen binnen de plugin, plugin root als optie, huidige map uitgesloten). Path-traversal bescherming + protected plugins geblokkeerd. + +## Nice to have - [ ] Multidomein implementeren (elk domein = eigen thema + content, één admin + site name) - [ ] Fase 1: Config-resolutie - [ ] For apache instants domein settings /public/noorlander.info/ or /public/mycode.name/ @@ -49,6 +61,18 @@ - [x] Pentest controles uitgevoerd (30/30 tests geslaagd — 0 vulnerabilities) - [x] WCAG 2.1 AA accessibility tests (25/25 tests geslaagd — 100% compliance, test-script verbeterd met min/max checks en grep -E) +## v2.6.1d (2026-08-18) ✅ +- [x] Pentest controles uitgevoerd (30/30) +- [x] WCAG 2.1 AA accessibility tests (25/25) +- [x] Plugin internationalisatie (i18n): plugins hebben eigen language/ mappen, systeem plugins volgen admin taal, content plugins volgen content taal, fallback chain +- [x] Plugin uniformiteit: alle 6 plugins hebben uniforme structuur (README.md, assets/.gitkeep, language/nl|en/) +- [x] Plugin editor vernieuwd: bestandsbrowser zijbalk (geneste boom), nieuw bestand aanmaken, uploaden, verwijderen, verplaatsen +- [x] Media invoegen in editor: nieuw /admin/media-list JSON endpoint + herbruikbare _media-modal.twig include; plugin-context scant assets/ map +- [x] Plugin overzicht knoppen: alleen iconen met title/aria-label +- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt (plugin-development.md NL+EN volledig herschreven, architectuur.md NL+EN bijgewerkt) +- [x] Verslag gemaakt (docs/release-notes/v2.6.1d.md) +- [x] Versie verhoogd naar 2.6.1d + ## v2.6.1c (2026-08-17) ✅ - [x] Pentest controles uitgevoerd (30/30) - [x] WCAG 2.1 AA accessibility tests (25/25) diff --git a/admin/src/AdminAuth.php b/admin/src/AdminAuth.php index 20b0c74..f4886d3 100644 --- a/admin/src/AdminAuth.php +++ b/admin/src/AdminAuth.php @@ -187,12 +187,71 @@ class AdminAuth /** * Get the role of the current user. + * Returns the override role if set (for testing), otherwise the real role. */ public function getCurrentRole(): string { + if (isset($_SESSION['admin_role_override'])) { + return $_SESSION['admin_role_override']; + } return $_SESSION['admin_role'] ?? 'admin'; } + /** + * Get the real role of the current user (ignoring any override). + */ + public function getRealRole(): string + { + return $_SESSION['admin_role'] ?? 'admin'; + } + + /** + * Check if the current user has an active role override. + */ + public function hasRoleOverride(): bool + { + return isset($_SESSION['admin_role_override']); + } + + /** + * Temporarily switch to another role for testing purposes. + * Only admin users can do this; the override cannot grant more than admin. + */ + public function switchRole(string $role): array + { + if (!$this->isAuthenticated()) { + return ['success' => false, 'message' => 'Niet ingelogd.']; + } + // Only real admins can switch roles + $realRole = $this->getRealRole(); + if ($realRole !== 'admin') { + return ['success' => false, 'message' => 'Alleen admins kunnen van rol wisselen.']; + } + if (!isset(self::ROLE_PERMISSIONS[$role])) { + return ['success' => false, 'message' => 'Ongeldige rol.']; + } + // Admins cannot switch to 'admin' (no point) — only to lower roles for testing + if ($role === 'admin') { + return ['success' => false, 'message' => 'Je bent al admin.']; + } + $_SESSION['admin_role_override'] = $role; + $this->log('info', "Rol-switch: {$_SESSION['admin_user']} -> {$role} (test)"); + return ['success' => true, 'message' => 'Rol gewijzigd naar ' . self::getRoleLabel($role) . '.']; + } + + /** + * Reset the role override back to the real admin role. + */ + public function resetRole(): array + { + if (isset($_SESSION['admin_role_override'])) { + unset($_SESSION['admin_role_override']); + $this->log('info', "Rol-switch gereset: {$_SESSION['admin_user']}"); + return ['success' => true, 'message' => 'Rol teruggezet naar admin.']; + } + return ['success' => false, 'message' => 'Geen rol-override actief.']; + } + /** * Check if the current user has permission to access a route. */ @@ -308,9 +367,13 @@ class AdminAuth /** * Change the role of an existing user. + * A user can never change their own role (security guard). */ public function changeRole(string $username, string $role): array { + if ($username === ($_SESSION['admin_user'] ?? '')) { + return ['success' => false, 'message' => 'Je kunt je eigen rol niet wijzigen.']; + } if (!isset(self::ROLE_PERMISSIONS[$role])) { return ['success' => false, 'message' => 'Ongeldige rol.']; } diff --git a/admin/theme/default/assets/js/editor-toolbar.js b/admin/theme/default/assets/js/editor-toolbar.js index 3210f43..be96fdf 100644 --- a/admin/theme/default/assets/js/editor-toolbar.js +++ b/admin/theme/default/assets/js/editor-toolbar.js @@ -7,7 +7,7 @@ var ext = textarea.dataset.ext || 'md'; var form = document.getElementById('editor-form') || textarea.closest('form'); - var modeMap = { md: 'markdown', html: 'htmlmixed', php: 'php' }; + var modeMap = { md: 'markdown', html: 'htmlmixed', php: 'php', css: 'css', scss: 'css', js: 'javascript', json: 'javascript' }; var editor = CodeMirror.fromTextArea(textarea, { mode: modeMap[ext] || 'markdown', @@ -146,8 +146,11 @@ insert(editor, '/**\n * \n */', 7); break; case 'media': - var modal = new bootstrap.Modal(document.getElementById('mediaModal')); - if (modal) modal.show(); + var modalEl = document.getElementById('mediaModal'); + if (modalEl && window.bootstrap) { + var modal = bootstrap.Modal.getOrCreateInstance(modalEl); + modal.show(); + } break; } } diff --git a/admin/theme/default/views/layouts/admin.twig b/admin/theme/default/views/layouts/admin.twig index bfafdfa..d59ff06 100644 --- a/admin/theme/default/views/layouts/admin.twig +++ b/admin/theme/default/views/layouts/admin.twig @@ -41,7 +41,7 @@
{{ user.username|default('') }} -
{{ role_label(user_role)|default('') }} +
{{ role_label(user_role)|default('') }}{% if has_role_override %} {{ ta.role_testing|default('Test') }}{% endif %}
+ {# Role switch modal (only rendered for real admins) #} + {% if user_real_role == 'admin' %} + + {% endif %} +
{% if message is defined and message %}
+ {% if needs_editor %} + {% include 'pages/_media-modal.twig' %} + {% endif %} + {% if needs_editor %} diff --git a/admin/theme/default/views/pages/_media-modal.twig b/admin/theme/default/views/pages/_media-modal.twig new file mode 100644 index 0000000..af66aae --- /dev/null +++ b/admin/theme/default/views/pages/_media-modal.twig @@ -0,0 +1,200 @@ +{# Reusable media modal, included by admin.twig when needs_editor is true. + The editor toolbar "media" button opens this modal. It fetches the list + of media files from /admin/media-list (JSON) and, on click, inserts a + snippet into the active CodeMirror editor at the cursor. + + Scope: + - Default (content editor): fetches /admin/media-list + - Plugin editor (mediaPluginName set): fetches /admin/media-list?plugin= + which scans plugins//assets/ instead of content/. + + The snippet format depends on the editor's current mode (data-ext): + - markdown (md): ![alt](url) + - html/php: name + - other (css, json, js, ...): the raw URL #} + + + \ No newline at end of file diff --git a/admin/theme/default/views/pages/content-edit.twig b/admin/theme/default/views/pages/content-edit.twig index 4cb9018..2386b2f 100644 --- a/admin/theme/default/views/pages/content-edit.twig +++ b/admin/theme/default/views/pages/content-edit.twig @@ -40,6 +40,22 @@ {% endif %} {% endif %} + {% if isEditable and (currentAuthorName or currentAuthorEmail or currentCreated) %} +
+
+ + +
+
+ + +
+
+ + +
+
+ {% endif %} {% if isEditable %}
diff --git a/admin/theme/default/views/pages/plugin-config.twig b/admin/theme/default/views/pages/plugin-config.twig index 05a15b2..5b5c4ad 100644 --- a/admin/theme/default/views/pages/plugin-config.twig +++ b/admin/theme/default/views/pages/plugin-config.twig @@ -3,18 +3,98 @@ {% block title %}{{ ta.plugin_config|default('Plugin Configuratie: ') }}{{ pluginName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %} {% block content %} -

{{ ta.plugin_config|default('Plugin Configuratie: ') }}{{ pluginName }}

+
-
- -
- +
+
+
+
+ {{ ta.plugin_settings|default('Instellingen') }} +
+
+ {% if settingsSchema is empty %} +

{{ ta.plugin_no_settings|default('Deze plugin heeft geen instelbare configuratie.') }}

+ {% else %} + + + {% for setting in settingsSchema %} + {% set settingLabel = setting.resolved_label|default('') ? setting.resolved_label : (setting.label|default(setting.key)) %} + {% set settingHelp = setting.resolved_help|default('') ? setting.resolved_help : (setting.help|default('')) %} + {% set settingOptions = setting.resolved_options is not null ? setting.resolved_options : (setting.options|default([])) %} +
+ + {% set currentValue = resolvedConfig[setting.key]|default(setting.default) %} + {% if setting.type == 'select' %} + + {% elseif setting.type == 'multi-select' %} +
+ {% for optValue, optLabel in settingOptions %} +
+ + +
+ {% endfor %} +
+ {% elseif setting.type == 'checkbox' %} +
+ + +
+ {% elseif setting.type == 'number' %} + + {% else %} + + {% endif %} + {% if settingHelp %} + {{ settingHelp }} + {% endif %} +
+ {% endfor %} +
+ + {{ ta.cancel|default('Annuleren') }} +
+ + {% endif %} +
+
- {% endblock %} \ No newline at end of file diff --git a/admin/theme/default/views/pages/plugins-edit.twig b/admin/theme/default/views/pages/plugins-edit.twig index 07197a3..dbac80f 100644 --- a/admin/theme/default/views/pages/plugins-edit.twig +++ b/admin/theme/default/views/pages/plugins-edit.twig @@ -2,36 +2,234 @@ {% block title %}{{ ta.plugin_edit|default('Plugin bewerken: ') }}{{ pluginName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %} +{# Recursively render the plugin file tree as a nested, collapsible list. + A folder is expanded by default when the currently selected file lives + somewhere inside it, so the user always sees where their file is. #} +{% macro tree(nodes, pluginName, relFile, editableExts, csrfToken) %} +{% import _self as macros %} +
    + {% for n in nodes %} + {% set isActive = n.path == relFile %} + {# A folder contains the active file when relFile starts with n.path + '/' #} + {% set containsActive = n.is_dir and relFile starts with (n.path ~ '/') %} + {# Icon per type #} + {% set icon = n.is_dir ? 'bi-folder-fill text-warning' : (n.extension == 'php' ? 'bi-file-code text-success' : (n.extension == 'json' ? 'bi-filetype-json text-secondary' : (n.extension == 'md' ? 'bi-file-text text-primary' : (n.extension in ['css','scss'] ? 'bi-filetype-css text-info' : (n.extension == 'js' ? 'bi-filetype-js text-warning' : (n.extension == 'html' ? 'bi-file-earmark text-info' : 'bi-file text-muted')))))) %} +
  • + {% if n.is_dir %} + {% set collapseId = 'plugin-tree-' ~ n.path|replace({'/':'-','.':'-'}) %} + +
    + {% if n.children is not empty %} + {{ macros.tree(n.children, pluginName, relFile, editableExts, csrfToken) }} + {% endif %} +
    + {% else %} + + + + {{ n.name }} + {% if n.extension in editableExts %} + {{ n.extension|upper }} + {% endif %} + + + + +
    + + + + +
    + {% endif %} +
  • + {% endfor %} +
+{% endmacro %} +{% import _self as tree_macros %} + {% block extra_css %} + {% endblock %} {% block content %} -
-

{{ ta.plugin_edit|default('Plugin bewerken: ') }}{{ pluginName }}

- - {{ ta.back|default('Terug') }} - +
+

{{ ta.plugin_edit|default('Plugin bewerken: ') }}{{ pluginName }}

+
+ + + + {{ ta.back|default('Terug') }} + +
-
- +{# Upload form (collapsed by default) #} +
-
-
- + + + + +
+ + + {{ ta.plugin_upload_help|default('Bestanden worden geüpload naar assets/ van deze plugin. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD.') }} +
+ + +
+
+
+ +
+ {# File tree sidebar #} +
+
+
+ {{ ta.plugin_files|default('Plugin bestanden') }} +
+
+ {% if files is empty %} +
{{ ta.plugin_no_files|default('Geen bestanden gevonden.') }}
+ {% else %} + {{ tree_macros.tree(files, pluginName, relFile, editableExts, csrf_token) }} + {% endif %}
-
- - {{ ta.cancel|default('Annuleren') }} + + {# Editor main panel #} +
+ {% if relFile %} + + {% endif %} + + {% if isEditable %} +
+ +
+
+
+
+ +
+
+
+
+ + {{ ta.cancel|default('Annuleren') }} +
+
+ {% else %} +
+
+ + {% if relFile %} + {{ ta.plugin_not_editable|default('Dit bestandstype kan niet in de editor bewerkt worden.') }} + {% else %} + {{ ta.plugin_select_file|default('Selecteer een bestand uit de zijbalk om te bewerken.') }} + {% endif %} +
+
+ {% endif %}
- +
+ +{# New file modal #} + {% endblock %} {% block extra_js %} diff --git a/admin/theme/default/views/pages/plugins-move-form.twig b/admin/theme/default/views/pages/plugins-move-form.twig new file mode 100644 index 0000000..addaf53 --- /dev/null +++ b/admin/theme/default/views/pages/plugins-move-form.twig @@ -0,0 +1,39 @@ +{% extends "layouts/admin.twig" %} + +{% block title %}{{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %} + +{% block content %} +
+

{{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }}

+ + {{ ta.back|default('Terug') }} + +
+ +
+ + + +
+
+ {{ ta.move_prefix|default('Verplaats') }} {{ itemName }} + {% if itemDir %}({{ pluginName }}/{{ itemDir }}){% else %}({{ pluginName }}/){% endif %} + {{ ta.move_to|default('naar:') }} +
+
+ + +
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/admin/theme/default/views/pages/plugins.twig b/admin/theme/default/views/pages/plugins.twig index 78d3a2d..59f4fdf 100644 --- a/admin/theme/default/views/pages/plugins.twig +++ b/admin/theme/default/views/pages/plugins.twig @@ -38,31 +38,31 @@

@@ -84,6 +95,12 @@ {{ target_user.role_label|default(target_user.role) }}

+ {% if is_self %} + + {% else %}
@@ -100,6 +117,7 @@ {{ ta.change|default('Wijzigen') }}
+ {% endif %}
diff --git a/cms/core/class/CodePressCMS.php b/cms/core/class/CodePressCMS.php index ec66f2b..e8a2e7f 100644 --- a/cms/core/class/CodePressCMS.php +++ b/cms/core/class/CodePressCMS.php @@ -48,8 +48,10 @@ class CodePressCMS { // Initialize plugin manager (files already loaded in cms/core/index.php) $enabledPlugins = $this->config['enabled_plugins'] ?? []; - $this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins', $enabledPlugins); + $siteDefaultLang = $this->config['language']['default'] ?? 'nl'; + $this->pluginManager = new PluginManager(__DIR__ . '/../../../plugins', $enabledPlugins, $siteDefaultLang); $api = new CMSAPI($this); + $api->setPluginManager($this->pluginManager); $this->pluginManager->setAPI($api); $this->buildMenu(); @@ -1378,6 +1380,10 @@ class CodePressCMS { 'author_name' => $this->config['author']['name'] ?? 'CodePress Developer', 'author_website' => $this->normalizeUrl($this->config['author']['website'] ?? '') ?: '#', 'author_git' => $this->config['author']['git'] ?? '', + // Per-page author metadata (from frontmatter, if present) + 'page_author_name' => htmlspecialchars($page['metadata']['author_name'] ?? ''), + 'page_author_email' => htmlspecialchars($page['metadata']['author_email'] ?? ''), + 'page_created' => htmlspecialchars($page['metadata']['created'] ?? ''), 'seo_description' => $this->config['seo']['description'] ?? 'CodePress CMS - Lightweight file-based content management system', 'seo_keywords' => $this->config['seo']['keywords'] ?? 'cms, php, content management, file-based', 'block_ai_bots' => !empty($this->config['security']['block_ai_bots']), diff --git a/cms/core/class/ContentAPI.php b/cms/core/class/ContentAPI.php index d0ff27f..5439354 100644 --- a/cms/core/class/ContentAPI.php +++ b/cms/core/class/ContentAPI.php @@ -241,4 +241,21 @@ class ContentAPI { return isset($_GET['search']); } + + /** + * Get the author metadata for the current page. + * Returns author_name, author_email and created from the page frontmatter. + * + * @return array Author metadata with keys: author_name, author_email, created + */ + public function getPageAuthor(): array + { + $page = $this->cms->getPage(); + $metadata = $page['metadata'] ?? []; + return [ + 'author_name' => $metadata['author_name'] ?? '', + 'author_email' => $metadata['author_email'] ?? '', + 'created' => $metadata['created'] ?? '', + ]; + } } diff --git a/cms/core/plugin/AdminPluginAPI.php b/cms/core/plugin/AdminPluginAPI.php index a79b939..b120f6e 100644 --- a/cms/core/plugin/AdminPluginAPI.php +++ b/cms/core/plugin/AdminPluginAPI.php @@ -8,11 +8,24 @@ class AdminPluginAPI implements PluginAPIInterface { private array $config; private string $projectRoot; + private string $adminLanguage; + private ?PluginManager $pluginManager = null; public function __construct(array $siteConfig, string $projectRoot = '') { $this->config = $siteConfig; $this->projectRoot = $projectRoot !== '' ? $projectRoot : dirname(__DIR__, 3); + $this->adminLanguage = $siteConfig['admin_language'] + ?? ($siteConfig['language']['default'] ?? 'nl'); + } + + /** + * Inject the admin PluginManager so plugins can resolve their own + * translations through the same fallback chain. + */ + public function setPluginManager(PluginManager $pm): void + { + $this->pluginManager = $pm; } /** @@ -79,4 +92,46 @@ class AdminPluginAPI implements PluginAPIInterface } return ['version' => '0.0.0']; } + + /** + * Get the active admin language code (e.g. 'nl', 'en'). + * System plugins should use this to resolve their translations. + */ + public function getAdminLanguage(): string + { + return $this->adminLanguage; + } + + /** + * Get the translations for a plugin in the admin context. + * + * Fallback chain (handled by PluginManager): + * requested language -> plugin default_language -> empty array. + * + * @param string $pluginName Plugin directory name + * @param string|null $lang Override language; defaults to the active admin language + * @return array Translations [key => value] + */ + public function getPluginTranslations(string $pluginName, ?string $lang = null): array + { + if ($this->pluginManager === null) { + return []; + } + $lang = $lang ?? $this->adminLanguage; + return $this->pluginManager->getPluginTranslations($pluginName, $lang, 'admin'); + } + + /** + * Translate a single key for a plugin in the 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 + */ + public function t(string $key, string $pluginName, ?string $lang = null): string + { + $t = $this->getPluginTranslations($pluginName, $lang); + return $t[$key] ?? $key; + } } \ No newline at end of file diff --git a/cms/core/plugin/CMSAPI.php b/cms/core/plugin/CMSAPI.php index 0d402e0..de74927 100644 --- a/cms/core/plugin/CMSAPI.php +++ b/cms/core/plugin/CMSAPI.php @@ -3,11 +3,21 @@ class CMSAPI implements PluginAPIInterface { private CodePressCMS $cms; + private ?PluginManager $pluginManager = null; 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. + */ + public function setPluginManager(PluginManager $pm): void + { + $this->pluginManager = $pm; + } /** * Get current page information @@ -220,4 +230,55 @@ class CMSAPI implements PluginAPIInterface { return $this->cms->getAllPageTitles(); } + + /** + * Get the author metadata for the current page. + * Returns author_name, author_email and created from the page frontmatter. + * + * @return array Author metadata with keys: author_name, author_email, created + */ + public function getPageAuthor(): array + { + $page = $this->cms->getPage(); + $metadata = $page['metadata'] ?? []; + return [ + 'author_name' => $metadata['author_name'] ?? '', + 'author_email' => $metadata['author_email'] ?? '', + 'created' => $metadata['created'] ?? '', + ]; + } + + /** + * Get the translations for a plugin in the front-end context. + * + * Content plugins follow the current content language. Fallback chain + * (handled by PluginManager): requested language -> plugin + * default_language -> empty array. + * + * @param string $pluginName Plugin directory name + * @param string|null $lang Override language; defaults to the current content language + * @return array Translations [key => value] + */ + public function getPluginTranslations(string $pluginName, ?string $lang = null): array + { + if ($this->pluginManager === null) { + return []; + } + $lang = $lang ?? $this->cms->currentLanguage; + return $this->pluginManager->getPluginTranslations($pluginName, $lang, 'site'); + } + + /** + * Translate a single key for a plugin in the 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 + */ + public function t(string $key, string $pluginName, ?string $lang = null): string + { + $t = $this->getPluginTranslations($pluginName, $lang); + return $t[$key] ?? $key; + } } \ No newline at end of file diff --git a/cms/core/plugin/PluginAPIInterface.php b/cms/core/plugin/PluginAPIInterface.php index a8834b8..e59272d 100644 --- a/cms/core/plugin/PluginAPIInterface.php +++ b/cms/core/plugin/PluginAPIInterface.php @@ -7,4 +7,27 @@ interface PluginAPIInterface { public function getConfig(string $key, $default = null); + + /** + * Get the translations for a plugin in the current context. + * + * 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. + * + * @param string $pluginName Plugin directory name + * @param string|null $lang Override language (optional) + * @return array Translations [key => value] + */ + public function getPluginTranslations(string $pluginName, ?string $lang = null): array; + + /** + * Translate a single key for a plugin in the current 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 + */ + public function t(string $key, string $pluginName, ?string $lang = null): string; } \ No newline at end of file diff --git a/cms/core/plugin/PluginManager.php b/cms/core/plugin/PluginManager.php index 2f25761..3ded2c7 100644 --- a/cms/core/plugin/PluginManager.php +++ b/cms/core/plugin/PluginManager.php @@ -8,13 +8,24 @@ class PluginManager private array $enabledPlugins = []; private array $actions = []; private array $filters = []; + private string $siteDefaultLanguage = 'nl'; - public function __construct(string $pluginsPath, array $enabledPlugins = []) + public function __construct(string $pluginsPath, array $enabledPlugins = [], string $siteDefaultLanguage = 'nl') { $this->pluginsPath = $pluginsPath; $this->enabledPlugins = $enabledPlugins; + $this->siteDefaultLanguage = $siteDefaultLanguage !== '' ? $siteDefaultLanguage : 'nl'; $this->loadPlugins(); } + + /** + * Set the CMS site default language. Used as a fallback when a plugin + * does not declare its own `default_language` in plugin.json. + */ + public function setSiteDefaultLanguage(string $lang): void + { + $this->siteDefaultLanguage = $lang !== '' ? $lang : 'nl'; + } public function setAPI($api): void { @@ -124,6 +135,123 @@ class PluginManager 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. + * + * @param string $pluginName Plugin name (directory name) + * @return array Resolved config: [key => value, ...] + */ + public function getPluginConfig(string $pluginName): array + { + $pluginDir = $this->pluginsPath . '/' . $pluginName; + $pluginJsonFile = $pluginDir . '/plugin.json'; + $configJsonFile = $pluginDir . '/config.json'; + + $defaults = []; + if (file_exists($pluginJsonFile)) { + $pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? []; + foreach ($pluginJson['settings'] ?? [] as $setting) { + if (isset($setting['key'])) { + $defaults[$setting['key']] = $setting['default'] ?? null; + } + } + } + + $overrides = []; + if (file_exists($configJsonFile)) { + $overrides = json_decode(file_get_contents($configJsonFile), true) ?? []; + } + + return array_merge($defaults, $overrides); + } + + /** + * Get the default (fallback) language declared by a plugin. + * + * Resolved from (in order): + * 1. plugin.json `default_language` + * 2. CMS site config `language.default` + * 3. 'nl' + * + * @param string $pluginName Plugin directory name + * @return string Language code (e.g. 'nl', 'en') + */ + public function getPluginDefaultLanguage(string $pluginName): string + { + $pluginJsonFile = $this->pluginsPath . '/' . $pluginName . '/plugin.json'; + if (file_exists($pluginJsonFile)) { + $pluginJson = json_decode(file_get_contents($pluginJsonFile), true) ?? []; + if (!empty($pluginJson['default_language'])) { + return (string)$pluginJson['default_language']; + } + } + return $this->siteDefaultLanguage ?? 'nl'; + } + + /** + * Load the translations for a plugin for the requested language. + * + * Fallback chain: requested language -> plugin default language -> empty 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] + */ + public function getPluginTranslations(string $pluginName, string $lang, string $context = 'admin'): array + { + $langDir = $this->pluginsPath . '/' . $pluginName . '/language'; + if (!is_dir($langDir)) { + return []; + } + + // Try requested language first + $file = $langDir . '/' . $lang . '/' . $context . '.php'; + if (file_exists($file)) { + $t = include $file; + if (is_array($t)) { + return $t; + } + } + + // Fallback to the plugin's default language + $defaultLang = $this->getPluginDefaultLanguage($pluginName); + if ($defaultLang !== $lang) { + $fallbackFile = $langDir . '/' . $defaultLang . '/' . $context . '.php'; + if (file_exists($fallbackFile)) { + $t = include $fallbackFile; + if (is_array($t)) { + return $t; + } + } + } + + return []; + } + + /** + * Collect translations for every loaded plugin for the requested language. + * + * Returns an associative array keyed by plugin name: + * ['Statistics' => [...], 'Logs' => [...], ...] + * + * @param string $lang Requested language code + * @param string $context 'admin' or 'site' + * @return array Plugin translations keyed by plugin name + */ + public function getAllPluginTranslations(string $lang, string $context = 'admin'): array + { + $all = []; + foreach ($this->plugins as $pluginName => $plugin) { + // System plugins follow the admin language; content plugins follow the content language. + // The caller decides the context; here we just load for every plugin. + $all[$pluginName] = $this->getPluginTranslations($pluginName, $lang, $context); + } + return $all; + } + public function isPluginViewable(object $plugin): bool { if (method_exists($plugin, 'getConfig')) { @@ -236,7 +364,7 @@ class PluginManager * Find which plugin handles a given admin route. * * @param string $route The admin route (e.g. 'statistics' or 'statistics/details') - * @return array|null ['plugin' => name, 'action' => action] or null + * @return array|null ['plugin' => name, 'action' => action, 'permission' => required permission] or null */ public function resolveAdminRoute(string $route): ?array { @@ -248,6 +376,7 @@ class PluginManager return [ 'plugin' => $item['plugin'] ?? '', 'action' => $action, + 'permission' => $item['permission'] ?? 'plugins', ]; } } diff --git a/docs/release-notes/v2.6.1d.md b/docs/release-notes/v2.6.1d.md new file mode 100644 index 0000000..1eb687d --- /dev/null +++ b/docs/release-notes/v2.6.1d.md @@ -0,0 +1,173 @@ +# CodePress CMS v2.6.1d — Release Notes + +**Release datum:** 2026-08-18 +**Codename:** Lyra +**Status:** stable + +--- + +## Samenvatting + +Deze release richt zich volledig op **plugin-verbeteringen**: plugin internationalisatie (i18n), plugin uniformiteit, en een volledig vernieuwde plugin-editor met bestandsbrowser, uploaden, verwijderen en verplaatsen van bestanden binnen een plugin. + +--- + +## Nieuwe features + +### 1. Plugin internationalisatie (i18n) + +Plugins hebben nu hun eigen `language/` map met vertalingen, geïntegreerd met de admin-taal en content-taal. + +- **Systeem plugins** (`type: "system"`) volgen de geselecteerde **admin taal** — vertalingen in `language//admin.php` +- **Content plugins** (`type: "content"`) volgen de geselecteerde **content taal** — vertalingen in `language//site.php` +- **Fallback chain**: geselecteerde taal → plugin `default_language` (uit `plugin.json`) → CMS site default → lege array (sleutel wordt ongewijzigd getoond) + +**Core uitbreidingen:** +- `PluginManager`: `getPluginDefaultLanguage()`, `getPluginTranslations()`, `getAllPluginTranslations()` met fallback chain +- `PluginAPIInterface`: `getPluginTranslations()` + `t()` toegevoegd (contract voor beide API's) +- `AdminPluginAPI`: `getAdminLanguage()`, `getPluginTranslations()`, `t()` (systeem plugins) +- `CMSAPI`: `getPluginTranslations()`, `t()` (content plugins) + +**Admin integratie:** +- `public/admin.php` laadt alle plugin-vertalingen als Twig global `ta_plugins` + functions `tap()` en `plugin_menu_label()` +- `handlePluginsConfig()` resolveert `label_key`/`help_key`/`option_label_key` via plugin-vertalingen voor instellingen-labels +- Admin sidebar toont vertaalde plugin-menu labels via `plugin_menu_label()` + +**Alle 6 plugins bijgewerkt:** +- Dashboard, GeoIPInfo, Logs, Statistics (systeem): `language/nl/admin.php` + `language/en/admin.php` +- HTMLBlock, Navigation (content): `language/nl/site.php` + `language/en/site.php` +- `plugin.json`: `default_language: "nl"` toegevoegd +- Logs en Statistics: instellingen-labels via `label_key`/`help_key` in plaats van hardcoded tekst +- Alle hardcoded Nederlandse strings in `handleAdminRoute()`/`getSidebarContent()` vervangen door vertaalde waarden + +### 2. Plugin uniformiteit + +Elke plugin heeft nu een uniforme structuur: + +``` +plugins// +├── .php # Hoofd plugin class +├── plugin.json # Metadata + instellingen +├── README.md # Plugin documentatie (uniform format) +├── assets/ # CSS/JS/SCSS (.gitkeep als leeg) +│ └── ... +└── language/ # Vertalingen + ├── nl/ + └── en/ +``` + +- Per plugin een `README.md` met uniform format: plugin type, bestandsstructuur, functies, instellingen, taal support +- `plugins/README.md` herschreven als korte algemene intro met plugin-overzicht tabel +- `guide/nl|en/codepress-developer/plugin-development.md` volledig herschreven (structuur, plugin.json velden incl. `default_language`, settings schema met `label_key`/`help_key`, API methodes incl. `getPluginTranslations`/`t`, admin integratie, taal support sectie) +- `guide/nl|en/codepress-developer/architectuur.md` bijgewerkt met plugin `language/` map + +### 3. Plugin editor volledig vernieuwd + +De plugin-editor (`/admin/plugins-edit`) toonde alleen het `.php` bestand. Nu is het een volledige bestandsbeheer-omgeving. + +#### Bestandsbrowser zijbalk +- Geneste, inklapbare boomstructuur van alle bestanden in de plugin-map (`.php`, `.json`, `.md`, `.html`, `.css`, `.scss`, `.js`) inclusief `language/` en `assets/` submappen +- Mappen die het actieve bestand bevatten worden automatisch uitgeklapt +- Iconen per bestandstype (PHP, JSON, MD, CSS, JS, HTML, mappen) +- Bootstrap collapse via `