System plugin support: admin menu items, admin routes, API integration

- PluginManager: getAdminMenuItems(), handleAdminRoute(), getPluginType()
- admin.php: load PluginManager, pass plugin_admin_menu to Twig
- admin.php: route to plugin admin pages via handleAdminRoute()
- admin.twig: show 'Plugins' sidebar section for system plugins
- plugins-new.twig: choose content or system plugin type
- handlePluginsNew: generate proper template based on type (content/system)
- plugin-admin.twig: renders plugin output in admin layout
- GeoIPInfo: example system plugin (admin page with GeoIP info)
- System plugins: getAdminMenu(), getAdminRoutes(), handleAdminRoute()
- Content plugins: getSidebarContent() (unchanged)
This commit is contained in:
2026-08-11 18:06:17 +02:00
parent 6daa0a6f49
commit c2bcd7be22
11 changed files with 219 additions and 5 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 41 KiB

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}CodePress Admin{% endblock %}</title>
<link rel="icon" type="image/x-icon" href="/admin/assets/img/favicon.ico">
<link rel="stylesheet" href="/admin/assets/css/bootstrap.min.css">
<link rel="stylesheet" href="/admin/assets/css/bootstrap-icons.css">
<link rel="stylesheet" href="/admin/assets/css/style.css">
@@ -124,6 +125,17 @@
{% endif %}
{% endif %}
{% if plugin_admin_menu is not empty %}
<li class="nav-section">Plugins</li>
{% for item in plugin_admin_menu %}
<li class="nav-item">
<a class="nav-link {{ route == item.route ? 'active' : '' }}" href="/admin/{{ item.route }}">
<i class="bi {{ item.icon|default('bi-gear') }}"></i> {{ item.label }}
</a>
</li>
{% endfor %}
{% endif %}
{% if has_permission('guide') %}
<li class="nav-section">Help</li>
<li class="nav-item">
@@ -0,0 +1,7 @@
{% extends "layouts/admin.twig" %}
{% block title %}{{ route|capitalize }} - CodePress Admin{% endblock %}
{% block content %}
{{ plugin_content|raw }}
{% endblock %}
@@ -13,6 +13,21 @@
<input type="text" class="form-control" id="name" name="name" required autofocus>
<small class="form-text text-muted">Alleen letters, cijfers, underscores en streepjes.</small>
</div>
<div class="mb-3">
<label class="form-label">Plugin type</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="type" id="type-content" value="content" checked>
<label class="form-check-label" for="type-content">
<strong>Content plugin</strong> — Verschijnt in de sidebar op content pagina's
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="type" id="type-system" value="system">
<label class="form-check-label" for="type-system">
<strong>Systeem plugin</strong> — Voegt functionaliteit toe aan het CMS (admin menu, API)
</label>
</div>
</div>
</div>
<div class="card-footer bg-white">
<button type="submit" class="btn btn-primary">
@@ -21,4 +36,4 @@
<a href="/admin/plugins" class="btn btn-outline-secondary">Annuleren</a>
</div>
</form>
{% endblock %}
{% endblock %}
+78
View File
@@ -227,4 +227,82 @@ class PluginManager
}
return $urls;
}
/**
* Get admin menu items from system plugins.
* Each system plugin can register admin menu items via getAdminMenu().
* Returns an array of [label, route, icon] tuples.
*
* @return array Admin menu items
*/
public function getAdminMenuItems(): array
{
$items = [];
foreach ($this->plugins as $pluginName => $plugin) {
// Only system plugins provide admin menu items
if (method_exists($plugin, 'getConfig')) {
$config = $plugin->getConfig();
if (($config['type'] ?? 'content') !== 'system') {
continue;
}
}
if (method_exists($plugin, 'getAdminMenu')) {
$menuItems = $plugin->getAdminMenu();
if (is_array($menuItems)) {
foreach ($menuItems as $item) {
$items[] = $item;
}
}
}
}
return $items;
}
/**
* Handle an admin route for a system plugin.
* Called when a route matches a plugin's registered admin route.
* Returns the rendered content string, or null if no plugin handled it.
*
* @param string $route The admin route
* @return string|null Rendered content or null if not handled
*/
public function handleAdminRoute(string $route): ?string
{
foreach ($this->plugins as $pluginName => $plugin) {
if (method_exists($plugin, 'getConfig')) {
$config = $plugin->getConfig();
if (($config['type'] ?? 'content') !== 'system') {
continue;
}
}
if (method_exists($plugin, 'getAdminRoutes')) {
$routes = $plugin->getAdminRoutes();
if (is_array($routes) && in_array($route, $routes, true)) {
ob_start();
$plugin->handleAdminRoute($route);
return ob_get_clean();
}
}
}
return null;
}
/**
* Get a plugin's type (content or system).
*
* @param string $pluginName Plugin name
* @return string 'content', 'system', or 'content' if unknown
*/
public function getPluginType(string $pluginName): string
{
$pluginDir = $this->pluginsPath . '/' . $pluginName;
$jsonFile = $pluginDir . '/plugin.json';
if (file_exists($jsonFile)) {
$data = json_decode(file_get_contents($jsonFile), true);
return $data['type'] ?? 'content';
}
return 'content';
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
class GeoIPInfo
{
private ?CMSAPI $api = null;
public function setAPI(CMSAPI $api): void
{
$this->api = $api;
}
public function getConfig(): array
{
return [
'title' => 'GeoIP Info',
'type' => 'system',
];
}
/**
* Register admin menu items.
*/
public function getAdminMenu(): array
{
return [
['label' => 'GeoIP Info', 'route' => 'geoip-info', 'icon' => 'bi-globe2'],
];
}
/**
* Register admin routes this plugin handles.
*/
public function getAdminRoutes(): array
{
return ['geoip-info'];
}
/**
* Handle admin route — render the admin page.
*/
public function handleAdminRoute(string $route): void
{
$ip = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
$geoip = new GeoIP();
$countryCode = $geoip->lookupCountry($ip);
$country = GeoIP::getCountryName($countryCode);
$flag = GeoIP::getCountryFlagEmoji($countryCode);
echo '<h2 class="mb-4"><i class="bi bi-globe2"></i> GeoIP Info</h2>';
echo '<div class="card shadow-sm"><div class="card-body">';
echo '<table class="table table-sm">';
echo '<tr><td>IP adres</td><td><code>' . htmlspecialchars($ip) . '</code></td></tr>';
echo '<tr><td>Land</td><td>' . $flag . ' ' . htmlspecialchars($country) . '</td></tr>';
echo '</table>';
echo '</div></div>';
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"name": "GeoIPInfo",
"version": "1.0.0",
"author": "CodePress",
"description": "Systeem plugin voor GeoIP informatie in admin",
"type": "system"
}
+41 -3
View File
@@ -27,6 +27,8 @@ require_once __DIR__ . '/../cms/core/class/Cache.php';
require_once __DIR__ . '/../cms/core/class/GeoIP.php';
require_once __DIR__ . '/../cms/core/class/Analytics.php';
require_once __DIR__ . '/../cms/core/class/LogManager.php';
require_once __DIR__ . '/../cms/core/plugin/CMSAPI.php';
require_once __DIR__ . '/../cms/core/plugin/PluginManager.php';
// Initialize dynamic logging from site config
$siteConfigForLogging = file_exists($appConfig['config_json'])
@@ -62,6 +64,15 @@ $twig->addFunction(new \Twig\TwigFunction('role_label', function($role) {
return AdminAuth::getRoleLabel($role);
}));
// Initialize PluginManager for admin (system plugins provide admin menu items)
$siteConfigForPlugins = file_exists($appConfig['config_json'])
? (json_decode(file_get_contents($appConfig['config_json']), true) ?? [])
: [];
$enabledPluginsList = $siteConfigForPlugins['plugins']['enabled'] ?? $siteConfigForPlugins['enabled_plugins'] ?? [];
$adminPluginManager = new PluginManager(__DIR__ . '/../plugins', $enabledPluginsList);
$pluginAdminMenuItems = $adminPluginManager->getAdminMenuItems();
$twig->addGlobal('plugin_admin_menu', $pluginAdminMenuItems);
// Routing
$route = $_GET['route'] ?? '';
@@ -247,7 +258,22 @@ switch ($route) {
break;
default:
handleDashboard($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
// Check if a system plugin handles this route
$pluginContent = $adminPluginManager->handleAdminRoute($route);
if ($pluginContent !== null) {
echo $twig->render('pages/plugin-admin.twig', [
'user' => $user,
'route' => $route,
'csrf_token' => $csrf,
'sidebar_color' => getSidebarColor($appConfig),
'needs_editor' => false,
'message' => '',
'message_type' => 'info',
'plugin_content' => $pluginContent,
]);
} else {
handleDashboard($auth, $appConfig, $twig, $user, $csrf, $siteConfig);
}
}
// ============================================================================
@@ -1109,6 +1135,10 @@ function handlePluginsNew($auth, $config, $twig, $user, $csrf): void
$messageType = 'danger';
} else {
$pluginName = trim($_POST['name'] ?? '');
$pluginType = $_POST['type'] ?? 'content';
if (!in_array($pluginType, ['content', 'system'], true)) {
$pluginType = 'content';
}
if (!empty($pluginName)) {
$pluginName = preg_replace('/[^a-zA-Z0-9_-]/', '-', $pluginName);
$pluginsDir = $config['plugins_dir'];
@@ -1121,11 +1151,19 @@ function handlePluginsNew($auth, $config, $twig, $user, $csrf): void
'name' => ucfirst($pluginName),
'version' => '1.0.0',
'author' => $user['username'],
'type' => $pluginType,
];
file_put_contents($newPluginDir . '/plugin.json', json_encode($pluginJson, JSON_PRETTY_PRINT));
file_put_contents($newPluginDir . '/' . $pluginName . '.php', "<?php\n// Plugin: " . ucfirst($pluginName) . "\n\necho 'Hello from " . $pluginName . "';\n");
adminLog($config, 'info', $user['username'] . ' creëerde plugin ' . $pluginName);
// Generate plugin template based on type
if ($pluginType === 'system') {
$template = "<?php\n\nclass $pluginName\n{\n private ?CMSAPI \$api = null;\n\n public function setAPI(CMSAPI \$api): void\n {\n \$this->api = \$api;\n }\n\n public function getConfig(): array\n {\n return ['title' => ucfirst('$pluginName'), 'type' => 'system'];\n }\n\n // Register admin menu items\n public function getAdminMenu(): array\n {\n return [\n ['label' => ucfirst('$pluginName'), 'route' => '" . strtolower($pluginName) . "', 'icon' => 'bi-gear'],\n ];\n }\n\n // Register admin routes this plugin handles\n public function getAdminRoutes(): array\n {\n return ['" . strtolower($pluginName) . "'];\n }\n\n // Handle admin route\n public function handleAdminRoute(string \$route): void\n {\n // Render your admin page here\n echo '<h1>" . ucfirst($pluginName) . "</h1><p>Systeem plugin admin pagina.</p>';\n }\n}\n";
} else {
$template = "<?php\n\nclass $pluginName\n{\n private ?CMSAPI \$api = null;\n\n public function setAPI(CMSAPI \$api): void\n {\n \$this->api = \$api;\n }\n\n public function getConfig(): array\n {\n return ['title' => ucfirst('$pluginName'), 'type' => 'content'];\n }\n\n public function getSidebarContent(): string\n {\n return '<p>" . ucfirst($pluginName) . " sidebar content.</p>';\n }\n}\n";
}
file_put_contents($newPluginDir . '/' . $pluginName . '.php', $template);
adminLog($config, 'info', $user['username'] . ' creëerde ' . $pluginType . ' plugin ' . $pluginName);
header('Location: /admin/plugins-edit?plugin=' . urlencode($pluginName));
exit;
} else {
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+1 -1
View File
@@ -31,7 +31,7 @@
<link rel="me" href="{{ author_git }}">
<!-- Favicon -->
<link rel="icon" href="{{ theme_base_url }}/assets/img/favicon.ico" sizes="any">
<link rel="icon" type="image/x-icon" href="{{ theme_base_url }}/assets/img/favicon.ico">
<link rel="icon" type="image/svg+xml" href="{{ theme_base_url }}/assets/img/favicon.svg">
<link rel="apple-touch-icon" href="{{ theme_base_url }}/assets/img/favicon.svg">
<link rel="manifest" href="/manifest.json">
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB