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.
This commit is contained in:
2026-08-18 13:49:52 +00:00
parent 10c72de859
commit 5edc929c13
64 changed files with 3503 additions and 366 deletions
@@ -2,14 +2,24 @@
## Plugin structure
Each plugin has its own folder under `plugins/`. The folder name must match the main plugin class name.
```
plugins/MyPlugin/
├── MyPlugin.php # Main plugin class (name = folder name)
├── plugin.json # Plugin metadata
├── config.json # Optional configuration
── assets/ # Optional CSS/JS
├── css/
── scss/
├── MyPlugin.php # Main plugin class (name = folder name)
├── plugin.json # Plugin metadata + settings schema
├── README.md # Plugin documentation
── config.json # Optional runtime configuration (overrides defaults)
├── assets/ # Optional CSS/JS/SCSS
── css/
│ └── scss/
└── language/ # Translations
├── nl/
│ ├── admin.php # Admin labels (system plugins)
│ └── site.php # Front-end labels (content plugins)
└── en/
├── admin.php
└── site.php
```
## plugin.json
@@ -22,7 +32,9 @@ plugins/MyPlugin/
"description": "Description",
"type": "content",
"essential": false,
"hasConfig": false
"hasConfig": false,
"default_language": "nl",
"settings": []
}
```
@@ -37,6 +49,54 @@ plugins/MyPlugin/
| `type` | `"system"` or `"content"` | System (blue badge) or content (green badge) |
| `essential` | boolean | Essential plugins cannot be edited/deleted |
| `hasConfig` | boolean | Shows a Config button in admin |
| `default_language` | string | Fallback language for plugin translations (e.g. `nl`) |
| `settings` | array | Settings schema (see below) |
## Settings schema
When `hasConfig: true`, define settings in the `settings` array:
```json
{
"settings": [
{
"key": "max_items",
"type": "number",
"default": 10,
"label_key": "setting_max_items",
"help_key": "setting_max_items_help"
},
{
"key": "required_roles",
"type": "multi-select",
"default": ["admin"],
"options": {
"admin": "Admin",
"content-manager": "Content Manager"
},
"label_key": "setting_required_roles",
"help_key": "setting_required_roles_help",
"option_label_key": "role_options"
}
]
}
```
### Per-setting fields
| Field | Description |
|-------|-------------|
| `key` | Setting key (stored in `config.json`) |
| `type` | `text`, `checkbox`, `number`, `select`, `multi-select` |
| `default` | Default value |
| `label` | Hardcoded label (fallback when no `label_key` or when translation missing) |
| `help` | Hardcoded help text (fallback when no `help_key`) |
| `label_key` | Key in the plugin's `language/<lang>/admin.php` for a translated label |
| `help_key` | Key in the plugin's `language/<lang>/admin.php` for translated help text |
| `option_label_key` | Key pointing to an array of translated option labels for `select`/`multi-select` |
| `options` | Options for `select`/`multi-select` (`{value: label}`) |
The admin loads defaults from `plugin.json` and overrides them with values from `config.json`. The plugin reads the resolved values via `PluginManager::getPluginConfig()` or its own `getPluginConfig()` method.
## Plugin class example
@@ -63,8 +123,11 @@ class MyPlugin
public function getSidebarContent(): string
{
// Fetch plugin translations (content plugin = front-end language)
$t = $this->api ? $this->api->getPluginTranslations('MyPlugin') : [];
$title = $this->api ? $this->api->getCurrentPageTitle() : '';
return '<p>Current page: ' . htmlspecialchars($title) . '</p>';
$label = $t['current_page'] ?? 'Current page';
return '<p>' . htmlspecialchars($label) . ': ' . htmlspecialchars($title) . '</p>';
}
}
```
@@ -76,20 +139,122 @@ The plugin class is automatically loaded by `PluginManager` when the plugin is l
The API is injected via `setAPI()`, not via a static method. In the front-end context this is a `CMSAPI` instance, in the admin context an `AdminPluginAPI` instance. Both implement `PluginAPIInterface`.
```php
// Front-end API (CMSAPI)
// Front-end API (CMSAPI) - for content plugins
$this->api->getCurrentPageTitle();
$this->api->getMenu();
$this->api->getConfig('site_title');
$this->api->getCurrentLanguage();
$this->api->isHomepage();
$this->api->createUrl('about-us');
$this->api->getPluginTranslations('MyPlugin'); // front-end language
$this->api->t('current_page', 'MyPlugin'); // translation helper
// Admin API (AdminPluginAPI)
// Admin API (AdminPluginAPI) - for system plugins
$this->api->getConfig('analytics.enabled');
$this->api->getContentDir();
$this->api->getEnabledPlugins();
$this->api->getAdminLanguage(); // active admin language
$this->api->getPluginTranslations('MyPlugin'); // admin language
$this->api->t('menu_label', 'MyPlugin'); // translation helper
```
## Language support (i18n)
Each plugin can provide translations in `language/<lang>/`. System plugins use `admin.php`, content plugins use `site.php`.
### Language file format
`language/en/admin.php`:
```php
<?php
return [
// Menu
'menu_label' => 'My Plugin',
// Page
'page_title' => 'My Plugin',
'current_page' => 'Current page',
// Settings (label_key/help_key point here)
'setting_max_items' => 'Maximum number of items',
'setting_max_items_help' => 'Number of items shown.',
'role_options' => [
'admin' => 'Admin',
'content-manager' => 'Content Manager',
],
];
```
### Fallback chain
Plugin translations are resolved in this order:
1. **Selected language**`language/<selected>/admin.php` (or `site.php`)
2. **Plugin default_language**`plugin.json` `default_language` (e.g. `nl`)
3. **CMS site default**`config.language.default`
4. **Empty array** — the key is shown unchanged
### System vs content plugins
- **System plugins** (`type: "system"`) follow the selected **admin language** (`config.admin_language`). Translations live in `language/<lang>/admin.php`.
- **Content plugins** (`type: "content"`) follow the selected **content language** (from the URL or `config.language.default`). Translations live in `language/<lang>/site.php`.
### Fetching translations in a plugin
```php
// In handleAdminRoute() or getSidebarContent():
$t = $this->api->getPluginTranslations('MyPlugin');
$tr = function (string $key) use ($t): string {
return $t[$key] ?? $key;
};
echo htmlspecialchars($tr('page_title'));
```
Or use the single-key helper:
```php
echo htmlspecialchars($this->api->t('page_title', 'MyPlugin'));
```
### Translating the menu label
In `getAdminMenu()`, add `label_key`. The admin sidebar shows the translation via `plugin_menu_label()`:
```php
public function getAdminMenu(): array
{
return [
[
'plugin' => 'MyPlugin',
'route' => 'my-plugin',
'label' => 'My Plugin', // fallback
'label_key' => 'menu_label', // points to language/<lang>/admin.php
'icon' => 'bi-puzzle',
'section' => 'general', // or 'system'
'permission' => 'my-plugin',
],
];
}
```
### Translating setting labels
In `plugin.json` `settings`, use `label_key`/`help_key`/`option_label_key` instead of hardcoded `label`/`help`:
```json
{
"key": "max_items",
"label_key": "setting_max_items",
"help_key": "setting_max_items_help",
"type": "number",
"default": 10
}
```
The admin's `handlePluginsConfig()` resolves these via the plugin translations; if a translation is missing it falls back to `label`/`help` from `plugin.json`.
## Hooks
Plugins can implement the following methods for automatic hook registration:
@@ -115,25 +280,72 @@ Plugins can add custom admin pages via `getAdminMenu()` and `handleAdminRoute()`
```php
public function getAdminMenu(): array
{
$config = $this->getPluginConfig();
$requiredRoles = $config['required_roles'] ?? ['admin'];
return [
[
'plugin' => 'MyPlugin',
'route' => 'my-plugin',
'label' => 'My Plugin',
'label_key' => 'menu_label',
'icon' => 'bi-puzzle',
'section' => 'general', // or 'system'
'section' => 'general', // or 'system'
'permission' => 'my-plugin',
'required_roles' => $requiredRoles,
],
];
}
public function handleAdminRoute(string $action): ?string
{
return '<h2>My Plugin admin page</h2>';
$t = $this->api ? $this->api->getPluginTranslations('MyPlugin') : [];
$tr = function (string $key) use ($t): string {
return $t[$key] ?? $key;
};
return '<h2>' . htmlspecialchars($tr('page_title')) . '</h2>';
}
```
Only plugins listed in `enabled_plugins` are shown in the admin sidebar.
### Runtime config in a plugin
Plugins can read their runtime config (defaults from `plugin.json` `settings` + overrides from `config.json`):
```php
private function getPluginConfig(): array
{
$pluginDir = dirname(__DIR__);
$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);
}
```
Or via the shared method on PluginManager:
```php
$config = $this->pluginManager->getPluginConfig('MyPlugin');
```
## Adding CSS
Plugins can provide a CSS URL via `getCssUrl()`:
@@ -143,4 +355,6 @@ public function getCssUrl(): string
{
return '/plugins/MyPlugin/assets/css/style.css';
}
```
```
The URL is passed to the front-end template via `plugin_css_urls`.