Bug: beschermde/essentiële plugins (Dashboard, Navigation) konden niet meer geactiveerd worden als ze uit enabled_plugins raakten. Core forceert nu laden van essential plugins (plugin.json essential: true); admin-UI toont ze altijd als Actief; toggle-handler staat aanzetten wél toe, uitzetten blijft geblokkeerd; isProtectedPlugin() dekt nu ook essential.
619 lines
20 KiB
PHP
619 lines
20 KiB
PHP
<?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.5
|
|
*/
|
|
class PluginManager
|
|
{
|
|
/**
|
|
* Geladen plugin-instanties, geïndexeerd op plugin-naam.
|
|
*
|
|
* @since 2.6.5
|
|
* @var array<string,object>
|
|
*/
|
|
private array $plugins = [];
|
|
|
|
/**
|
|
* Het absolute pad naar de pluginsdirectory.
|
|
*
|
|
* @since 2.6.5
|
|
* @var string
|
|
*/
|
|
private string $pluginsPath;
|
|
|
|
/**
|
|
* De geïnjecteerde plugin-API-instantie (CMSAPI of AdminPluginAPI).
|
|
*
|
|
* @since 2.6.5
|
|
* @var PluginAPIInterface|null
|
|
*/
|
|
private $api = null;
|
|
|
|
/**
|
|
* Lijst met ingeschakelde plugin-namen.
|
|
*
|
|
* @since 2.6.5
|
|
* @var array<int,string>
|
|
*/
|
|
private array $enabledPlugins = [];
|
|
|
|
/**
|
|
* Geregistreerde action-hooks, gegroepeerd per hook-naam en prioriteit.
|
|
*
|
|
* @since 2.6.5
|
|
* @var array<string,array<int,array<int,callable>>>
|
|
*/
|
|
private array $actions = [];
|
|
|
|
/**
|
|
* Geregistreerde filter-hooks, gegroepeerd per hook-naam en prioriteit.
|
|
*
|
|
* @since 2.6.5
|
|
* @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.5
|
|
* @var string
|
|
*/
|
|
private string $siteDefaultLanguage = 'nl';
|
|
|
|
/**
|
|
* Construeer een PluginManager en laadt direct de ingeschakelde plugins.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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;
|
|
$this->enabledPlugins = $enabledPlugins;
|
|
$this->siteDefaultLanguage = $siteDefaultLanguage !== '' ? $siteDefaultLanguage : 'nl';
|
|
$this->loadPlugins();
|
|
}
|
|
|
|
/**
|
|
* 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.5
|
|
*
|
|
* @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.5
|
|
*
|
|
* @param PluginAPIInterface $api De plugin-API-instantie.
|
|
* @return void
|
|
*/
|
|
public function setAPI($api): void
|
|
{
|
|
$this->api = $api;
|
|
|
|
foreach ($this->plugins as $plugin) {
|
|
if (method_exists($plugin, 'setAPI')) {
|
|
$plugin->setAPI($api);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Controleer of een plugin als 'essential' is gemarkeerd in zijn plugin.json.
|
|
*
|
|
* Essential plugins worden altijd geladen, ongeacht de enabled_plugins-lijst,
|
|
* en kunnen niet worden uitgeschakeld, bewerkt of verwijderd via de admin.
|
|
*
|
|
* @since 2.6.6
|
|
*
|
|
* @param string $pluginName Plugin-naam (directorynaam).
|
|
* @return bool True indien de plugin in plugin.json essential: true heeft.
|
|
*/
|
|
public function isEssentialPlugin(string $pluginName): bool
|
|
{
|
|
$pluginJsonFile = $this->pluginsPath . '/' . $pluginName . '/plugin.json';
|
|
if (!file_exists($pluginJsonFile)) {
|
|
return false;
|
|
}
|
|
$pluginJson = json_decode(file_get_contents($pluginJsonFile), true);
|
|
return is_array($pluginJson) && ($pluginJson['essential'] ?? false) === true;
|
|
}
|
|
|
|
/**
|
|
* Laad alle ingeschakelde plugins uit de pluginsdirectory.
|
|
*
|
|
* Essential plugins (plugin.json `essential: true`) worden altijd geladen,
|
|
* ook als ze niet in enabled_plugins staan. Per plugin wordt het hoofdbestand
|
|
* (<naam>.php) geïncludeerd, de plugin-class geïnstantieerd, en action- en
|
|
* filter-hooks automatisch geregistreerd.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return void
|
|
*/
|
|
private function loadPlugins(): void
|
|
{
|
|
if (!is_dir($this->pluginsPath)) {
|
|
return;
|
|
}
|
|
|
|
$pluginDirs = glob($this->pluginsPath . '/*', GLOB_ONLYDIR);
|
|
|
|
foreach ($pluginDirs as $pluginDir) {
|
|
$pluginName = basename($pluginDir);
|
|
|
|
// Essential plugins are always loaded, regardless of enabled_plugins.
|
|
$isEnabled = in_array($pluginName, $this->enabledPlugins, true);
|
|
if (!$isEnabled && !$this->isEssentialPlugin($pluginName)) {
|
|
continue;
|
|
}
|
|
// Keep the effective enabled list consistent for getEnabledPlugins()/isEnabled().
|
|
if (!$isEnabled) {
|
|
$this->enabledPlugins[] = $pluginName;
|
|
}
|
|
|
|
$pluginFile = $pluginDir . '/' . $pluginName . '.php';
|
|
|
|
if (file_exists($pluginFile)) {
|
|
require_once $pluginFile;
|
|
|
|
$className = $pluginName;
|
|
if (class_exists($className)) {
|
|
$this->plugins[$pluginName] = new $className();
|
|
|
|
// Auto-register hooks from plugin methods
|
|
$hookMethods = ['onPageLoad', 'onBeforeRender', 'onAfterRender', 'onSearch', 'onMenuBuild'];
|
|
foreach ($hookMethods as $hook) {
|
|
if (method_exists($this->plugins[$pluginName], $hook)) {
|
|
$this->addAction($hook, [$this->plugins[$pluginName], $hook]);
|
|
}
|
|
}
|
|
|
|
// Register filter methods
|
|
$filterMethods = ['onContentFilter', 'onTitleFilter', 'onMenuFilter'];
|
|
foreach ($filterMethods as $filter) {
|
|
if (method_exists($this->plugins[$pluginName], $filter)) {
|
|
$this->addFilter($filter, [$this->plugins[$pluginName], $filter]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registreer een action-callback voor een hook.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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.5
|
|
*
|
|
* @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.5
|
|
*
|
|
* @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;
|
|
ksort($this->actions[$hook]);
|
|
foreach ($this->actions[$hook] as $callbacks) {
|
|
foreach ($callbacks as $callback) {
|
|
$callback(...$args);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pas alle geregistreerde filter-callbacks toe op een waarde.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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;
|
|
ksort($this->filters[$hook]);
|
|
foreach ($this->filters[$hook] as $callbacks) {
|
|
foreach ($callbacks as $callback) {
|
|
$value = $callback($value, ...$args);
|
|
}
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Haal een specifieke plugin-instantie op.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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.5
|
|
*
|
|
* @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.5
|
|
*
|
|
* @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.5
|
|
*
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* Haal de runtime-configuratie van een plugin op.
|
|
*
|
|
* Standaardwaarden uit plugin.json `settings` worden samengevoegd met
|
|
* overrides uit het config.json van de plugin.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @param string $pluginName Plugin-naam (directorynaam).
|
|
* @return array<string,mixed> Opgeloste configuratie als [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);
|
|
}
|
|
|
|
/**
|
|
* Haal de standaard (fallback) taal van een plugin op.
|
|
*
|
|
* Resolved in volgorde van:
|
|
* 1. plugin.json `default_language`
|
|
* 2. CMS site-config `language.default`
|
|
* 3. 'nl'
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @param string $pluginName Plugin-directorynaam.
|
|
* @return string Taalcode (bijv. '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';
|
|
}
|
|
|
|
/**
|
|
* Laad de vertalingen voor een plugin voor de aangevraagde taal.
|
|
*
|
|
* Fallback-keten: aangevraagde taal -> standaardtaal van de plugin -> lege array.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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
|
|
{
|
|
$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 [];
|
|
}
|
|
|
|
/**
|
|
* Verzamel vertalingen voor alle geladen plugins voor de aangevraagde taal.
|
|
*
|
|
* Geeft een associatieve array terug, geïndexeerd op plugin-naam:
|
|
* ['Statistics' => [...], 'Logs' => [...], ...]
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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
|
|
{
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* Controleer of een plugin viewable is.
|
|
*
|
|
* Een plugin is viewable tenzij zijn configuratie 'viewable' expliciet op false zet.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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')) {
|
|
$config = $plugin->getConfig();
|
|
return !isset($config['viewable']) || $config['viewable'] !== false;
|
|
}
|
|
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.5
|
|
*
|
|
* @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 = '';
|
|
|
|
foreach ($this->plugins as $pluginName => $plugin) {
|
|
if (!$this->isPluginViewable($plugin) || !method_exists($plugin, 'getSidebarContent')) {
|
|
continue;
|
|
}
|
|
|
|
if ($allowedPlugins !== null && !in_array($pluginName, $allowedPlugins, true)) {
|
|
continue;
|
|
}
|
|
|
|
$content = $plugin->getSidebarContent();
|
|
if (trim($content) === '') {
|
|
continue;
|
|
}
|
|
|
|
$title = 'Plugin';
|
|
if (method_exists($plugin, 'getConfig')) {
|
|
$config = $plugin->getConfig();
|
|
$title = $config['title'] ?? 'Plugin';
|
|
}
|
|
|
|
$sidebarContent .= '
|
|
<div class="card mb-3">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">' . htmlspecialchars($title) . '</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
' . $content . '
|
|
</div>
|
|
</div>';
|
|
}
|
|
|
|
return $sidebarContent;
|
|
}
|
|
|
|
/**
|
|
* Haal CSS-URL's op van alle ingeschakelde plugins die getCssUrl() implementeren.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return array<int,string> Lijst met CSS-URL's.
|
|
*/
|
|
public function getPluginCssUrls(): array
|
|
{
|
|
$urls = [];
|
|
foreach ($this->plugins as $pluginName => $plugin) {
|
|
if (method_exists($plugin, 'getCssUrl')) {
|
|
$url = $plugin->getCssUrl();
|
|
if (!empty($url)) {
|
|
$urls[] = $url;
|
|
}
|
|
}
|
|
}
|
|
return $urls;
|
|
}
|
|
|
|
/**
|
|
* Verzamel admin-menu-items van alle plugins die getAdminMenu() implementeren.
|
|
*
|
|
* Elke plugin levert [['route' => 'plugin-name/action', 'label' => 'Label', 'icon' => 'bi-icon']].
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @return array<int,array<string,mixed>> Admin-menu-items van alle plugins.
|
|
*/
|
|
public function getAdminMenuItems(): array
|
|
{
|
|
$items = [];
|
|
foreach ($this->plugins as $pluginName => $plugin) {
|
|
if (method_exists($plugin, 'getAdminMenu')) {
|
|
$pluginItems = $plugin->getAdminMenu();
|
|
if (is_array($pluginItems)) {
|
|
foreach ($pluginItems as $item) {
|
|
$items[] = $item;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Dispatch een admin-route naar de plugin die deze afhandelt.
|
|
*
|
|
* Plugins implementeren handleAdminRoute(string $action): ?string
|
|
* (geeft gerenderde HTML of null terug).
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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
|
|
{
|
|
$plugin = $this->getPlugin($pluginName);
|
|
if ($plugin === null) {
|
|
return null;
|
|
}
|
|
if (method_exists($plugin, 'handleAdminRoute')) {
|
|
return $plugin->handleAdminRoute($action);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Bepaal welke plugin een opgegeven admin-route afhandelt.
|
|
*
|
|
* @since 2.6.5
|
|
*
|
|
* @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
|
|
{
|
|
foreach ($this->getAdminMenuItems() as $item) {
|
|
$itemRoute = $item['route'] ?? '';
|
|
// Check if the requested route starts with this plugin's route
|
|
if ($route === $itemRoute || str_starts_with($route, $itemRoute . '/')) {
|
|
$action = substr($route, strlen($itemRoute) + 1);
|
|
return [
|
|
'plugin' => $item['plugin'] ?? '',
|
|
'action' => $action,
|
|
'permission' => $item['permission'] ?? 'plugins',
|
|
];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
} |