Adds plugin management section, part of the admin page
parent
1ca0515ee1
commit
2a97539093
|
|
@ -4,34 +4,189 @@ namespace App\Core;
|
||||||
|
|
||||||
class PluginManager
|
class PluginManager
|
||||||
{
|
{
|
||||||
|
/** @var array<string, array{path: string, meta: array}> */
|
||||||
|
private static array $catalog = [];
|
||||||
|
|
||||||
|
/** @var array<string, array{path: string, meta: array}>> */
|
||||||
|
private static array $loaded = [];
|
||||||
|
|
||||||
|
/** @var array<string, array<int, string>> */
|
||||||
|
private static array $dependencyErrors = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads all enabled plugins from the given directory.
|
* Loads all enabled plugins from the given directory.
|
||||||
|
* Enforces declared dependencies before bootstrapping each plugin.
|
||||||
*
|
*
|
||||||
* @param string $pluginsDir
|
* @param string $pluginsDir
|
||||||
* @return array<string, array{path: string, meta: array}>
|
* @return array<string, array{path: string, meta: array}>
|
||||||
*/
|
*/
|
||||||
public static function load(string $pluginsDir): array
|
public static function load(string $pluginsDir): array
|
||||||
{
|
{
|
||||||
$enabled = [];
|
self::$catalog = self::scanCatalog($pluginsDir);
|
||||||
foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginPath) {
|
self::$loaded = [];
|
||||||
|
self::$dependencyErrors = [];
|
||||||
|
|
||||||
|
foreach (self::$catalog as $name => $info) {
|
||||||
|
if (empty($info['meta']['enabled'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self::resolve($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
$GLOBALS['plugin_dependency_errors'] = self::$dependencyErrors;
|
||||||
|
|
||||||
|
return self::$loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $pluginsDir
|
||||||
|
* @return array<string, array{path: string, meta: array}>
|
||||||
|
*/
|
||||||
|
private static function scanCatalog(string $pluginsDir): array
|
||||||
|
{
|
||||||
|
$catalog = [];
|
||||||
|
foreach (glob(rtrim($pluginsDir, '/'). '/*', GLOB_ONLYDIR) as $pluginPath) {
|
||||||
$manifest = $pluginPath . '/plugin.json';
|
$manifest = $pluginPath . '/plugin.json';
|
||||||
if (!file_exists($manifest)) {
|
if (!file_exists($manifest)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$meta = json_decode(file_get_contents($manifest), true);
|
$meta = json_decode(file_get_contents($manifest), true);
|
||||||
if (empty($meta['enabled'])) {
|
if (!is_array($meta)) {
|
||||||
continue;
|
$meta = [];
|
||||||
}
|
}
|
||||||
$name = basename($pluginPath);
|
$name = basename($pluginPath);
|
||||||
$enabled[$name] = [
|
$catalog[$name] = [
|
||||||
'path' => $pluginPath,
|
'path' => $pluginPath,
|
||||||
'meta' => $meta,
|
'meta' => $meta,
|
||||||
];
|
];
|
||||||
$bootstrap = $pluginPath . '/bootstrap.php';
|
}
|
||||||
if (file_exists($bootstrap)) {
|
|
||||||
include_once $bootstrap;
|
return $catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively resolves a plugin and its dependencies.
|
||||||
|
*/
|
||||||
|
private static function resolve(string $plugin, array $stack = []): bool
|
||||||
|
{
|
||||||
|
if (isset(self::$loaded[$plugin])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset(self::$catalog[$plugin])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($plugin, $stack, true)) {
|
||||||
|
self::$dependencyErrors[$plugin][] = 'Circular dependency detected: ' . implode(' -> ', array_merge($stack, [$plugin]));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = self::$catalog[$plugin]['meta'];
|
||||||
|
if (empty($meta['enabled'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dependencies = $meta['dependencies'] ?? [];
|
||||||
|
if (!is_array($dependencies)) {
|
||||||
|
$dependencies = [$dependencies];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stack[] = $plugin;
|
||||||
|
foreach ($dependencies as $dependency) {
|
||||||
|
$dependency = trim((string)$dependency);
|
||||||
|
if ($dependency === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset(self::$catalog[$dependency])) {
|
||||||
|
self::$dependencyErrors[$plugin][] = sprintf('Missing dependency "%s"', $dependency);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (empty(self::$catalog[$dependency]['meta']['enabled'])) {
|
||||||
|
self::$dependencyErrors[$plugin][] = sprintf('Dependency "%s" is disabled', $dependency);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!self::resolve($dependency, $stack)) {
|
||||||
|
self::$dependencyErrors[$plugin][] = sprintf('Dependency "%s" failed to load', $dependency);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $enabled;
|
array_pop($stack);
|
||||||
|
|
||||||
|
if (!empty(self::$dependencyErrors[$plugin])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bootstrap = self::$catalog[$plugin]['path'] . '/bootstrap.php';
|
||||||
|
if (file_exists($bootstrap)) {
|
||||||
|
include_once $bootstrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$loaded[$plugin] = self::$catalog[$plugin];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the scanned plugin catalog (enabled and disabled).
|
||||||
|
*
|
||||||
|
* @return array<string, array{path: string, meta: array}>
|
||||||
|
*/
|
||||||
|
public static function getCatalog(): array
|
||||||
|
{
|
||||||
|
return self::$catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all plugins that successfully loaded (dependencies satisfied).
|
||||||
|
*
|
||||||
|
* @return array<string, array{path: string, meta: array}>
|
||||||
|
*/
|
||||||
|
public static function getLoaded(): array
|
||||||
|
{
|
||||||
|
return self::$loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns dependency validation errors collected during load.
|
||||||
|
*
|
||||||
|
* @return array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
public static function getDependencyErrors(): array
|
||||||
|
{
|
||||||
|
return self::$dependencyErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a plugin's enabled flag back to its manifest.
|
||||||
|
*/
|
||||||
|
public static function setEnabled(string $plugin, bool $enabled): bool
|
||||||
|
{
|
||||||
|
if (!isset(self::$catalog[$plugin])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifestPath = self::$catalog[$plugin]['path'] . '/plugin.json';
|
||||||
|
if (!is_file($manifestPath) || !is_readable($manifestPath) || !is_writable($manifestPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($manifestPath);
|
||||||
|
$data = json_decode($raw ?: '', true);
|
||||||
|
if (!is_array($data)) {
|
||||||
|
$data = self::$catalog[$plugin]['meta'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['enabled'] = $enabled;
|
||||||
|
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||||
|
if (file_put_contents($manifestPath, $json, LOCK_EX) === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$catalog[$plugin]['meta'] = $data;
|
||||||
|
if (!$enabled && isset(self::$loaded[$plugin])) {
|
||||||
|
unset(self::$loaded[$plugin]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
require_once __DIR__ . '/../core/Maintenance.php';
|
require_once __DIR__ . '/../core/Maintenance.php';
|
||||||
require_once __DIR__ . '/../core/MigrationRunner.php';
|
require_once __DIR__ . '/../core/MigrationRunner.php';
|
||||||
|
require_once __DIR__ . '/../core/PluginManager.php';
|
||||||
require_once '../app/helpers/security.php';
|
require_once '../app/helpers/security.php';
|
||||||
include_once '../app/helpers/feedback.php';
|
include_once '../app/helpers/feedback.php';
|
||||||
|
|
||||||
|
|
@ -19,11 +20,11 @@ if (!Session::isValidSession()) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the user has admin permissions
|
||||||
$canAdmin = false;
|
$canAdmin = false;
|
||||||
if (isset($userId) && isset($userObject) && method_exists($userObject, 'hasRight')) {
|
if (isset($userId) && isset($userObject) && method_exists($userObject, 'hasRight')) {
|
||||||
$canAdmin = ($userId === 1) || (bool)$userObject->hasRight($userId, 'superuser');
|
$canAdmin = ($userId === 1) || (bool)$userObject->hasRight($userId, 'superuser');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$canAdmin) {
|
if (!$canAdmin) {
|
||||||
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
Feedback::flash('SECURITY', 'PERMISSION_DENIED');
|
||||||
header('Location: ' . $app_root);
|
header('Location: ' . $app_root);
|
||||||
|
|
@ -38,8 +39,10 @@ $sectionRegistry = [
|
||||||
'overview' => ['label' => 'Overview', 'position' => 100, 'hook' => null, 'type' => 'core'],
|
'overview' => ['label' => 'Overview', 'position' => 100, 'hook' => null, 'type' => 'core'],
|
||||||
'maintenance' => ['label' => 'Maintenance', 'position' => 200, 'hook' => null, 'type' => 'core'],
|
'maintenance' => ['label' => 'Maintenance', 'position' => 200, 'hook' => null, 'type' => 'core'],
|
||||||
'migrations' => ['label' => 'Migrations', 'position' => 300, 'hook' => null, 'type' => 'core'],
|
'migrations' => ['label' => 'Migrations', 'position' => 300, 'hook' => null, 'type' => 'core'],
|
||||||
|
'plugins' => ['label' => 'Plugins', 'position' => 400, 'hook' => null, 'type' => 'core'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Register sections for plugins
|
||||||
$registerSection = static function(array $section) use (&$sectionRegistry): void {
|
$registerSection = static function(array $section) use (&$sectionRegistry): void {
|
||||||
$key = strtolower(trim($section['key'] ?? ''));
|
$key = strtolower(trim($section['key'] ?? ''));
|
||||||
$label = trim((string)($section['label'] ?? ''));
|
$label = trim((string)($section['label'] ?? ''));
|
||||||
|
|
@ -108,7 +111,6 @@ foreach ($sectionRegistry as $key => $meta) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hooks section for plugins
|
|
||||||
$sectionStatePayload = \App\Core\HookDispatcher::applyFilters('admin.sections.state', [
|
$sectionStatePayload = \App\Core\HookDispatcher::applyFilters('admin.sections.state', [
|
||||||
'sections' => $sectionRegistry,
|
'sections' => $sectionRegistry,
|
||||||
'state' => [],
|
'state' => [],
|
||||||
|
|
@ -121,6 +123,76 @@ if (is_array($sectionStatePayload)) {
|
||||||
$sectionState = $sectionStatePayload['state'] ?? (is_array($sectionStatePayload) ? $sectionStatePayload : []);
|
$sectionState = $sectionStatePayload['state'] ?? (is_array($sectionStatePayload) ? $sectionStatePayload : []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get plugin catalog and list of loaded plugins
|
||||||
|
// with their dependencies
|
||||||
|
$pluginCatalog = \App\Core\PluginManager::getCatalog();
|
||||||
|
$pluginLoadedMap = \App\Core\PluginManager::getLoaded();
|
||||||
|
$pluginDependencyErrors = \App\Core\PluginManager::getDependencyErrors();
|
||||||
|
|
||||||
|
$normalizeDependencies = static function ($meta): array {
|
||||||
|
$deps = $meta['dependencies'] ?? [];
|
||||||
|
if (!is_array($deps)) {
|
||||||
|
$deps = $deps === null || $deps === '' ? [] : [$deps];
|
||||||
|
}
|
||||||
|
$deps = array_map('trim', $deps);
|
||||||
|
$deps = array_filter($deps, static function($dep) {
|
||||||
|
return $dep !== '';
|
||||||
|
});
|
||||||
|
return array_values(array_unique($deps));
|
||||||
|
};
|
||||||
|
|
||||||
|
$pluginDependentsIndex = [];
|
||||||
|
foreach ($pluginCatalog as $slug => $info) {
|
||||||
|
$deps = $normalizeDependencies($info['meta'] ?? []);
|
||||||
|
foreach ($deps as $dep) {
|
||||||
|
$pluginDependentsIndex[$dep][] = $slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build plugin admin map with details, state and dependencies
|
||||||
|
$pluginAdminMap = [];
|
||||||
|
foreach ($pluginCatalog as $slug => $info) {
|
||||||
|
$meta = $info['meta'] ?? [];
|
||||||
|
$name = trim((string)($meta['name'] ?? $slug));
|
||||||
|
$enabled = !empty($meta['enabled']);
|
||||||
|
$dependencies = $normalizeDependencies($meta);
|
||||||
|
$dependents = array_values($pluginDependentsIndex[$slug] ?? []);
|
||||||
|
$enabledDependents = array_values(array_filter($dependents, static function($depSlug) use ($pluginCatalog) {
|
||||||
|
return !empty($pluginCatalog[$depSlug]['meta']['enabled']);
|
||||||
|
}));
|
||||||
|
$missingDependencies = array_values(array_filter($dependencies, static function($depSlug) use ($pluginCatalog) {
|
||||||
|
return !isset($pluginCatalog[$depSlug]) || empty($pluginCatalog[$depSlug]['meta']['enabled']);
|
||||||
|
}));
|
||||||
|
|
||||||
|
$pluginAdminMap[$slug] = [
|
||||||
|
'slug' => $slug,
|
||||||
|
'name' => $name,
|
||||||
|
'version' => (string)($meta['version'] ?? ''),
|
||||||
|
'description' => (string)($meta['description'] ?? ''),
|
||||||
|
'enabled' => $enabled,
|
||||||
|
'loaded' => isset($pluginLoadedMap[$slug]),
|
||||||
|
'dependencies' => $dependencies,
|
||||||
|
'dependents' => $dependents,
|
||||||
|
'enabled_dependents' => $enabledDependents,
|
||||||
|
'missing_dependencies' => $missingDependencies,
|
||||||
|
'dependency_errors' => $pluginDependencyErrors[$slug] ?? [],
|
||||||
|
'can_enable' => !$enabled && empty($missingDependencies),
|
||||||
|
'can_disable' => $enabled && empty($enabledDependents),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pluginAdminList = array_values($pluginAdminMap);
|
||||||
|
usort($pluginAdminList, static function(array $a, array $b): int {
|
||||||
|
return strcmp(strtolower($a['name']), strtolower($b['name']));
|
||||||
|
});
|
||||||
|
|
||||||
|
$sectionState['plugins'] = [
|
||||||
|
'plugins' => $pluginAdminList,
|
||||||
|
'dependency_errors' => $pluginDependencyErrors,
|
||||||
|
'plugin_index' => $pluginAdminMap,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Prepare the DB migrations details
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
||||||
|
|
||||||
if ($postAction === 'read_migration') {
|
if ($postAction === 'read_migration') {
|
||||||
|
|
@ -193,6 +265,7 @@ if ($postAction !== '' && $postAction !== 'read_migration') {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Maintenance actions
|
||||||
if ($postAction === 'maintenance_on') {
|
if ($postAction === 'maintenance_on') {
|
||||||
$msg = trim($_POST['maintenance_message'] ?? '');
|
$msg = trim($_POST['maintenance_message'] ?? '');
|
||||||
\App\Core\Maintenance::enable($msg);
|
\App\Core\Maintenance::enable($msg);
|
||||||
|
|
@ -200,6 +273,7 @@ if ($postAction !== '' && $postAction !== 'read_migration') {
|
||||||
} elseif ($postAction === 'maintenance_off') {
|
} elseif ($postAction === 'maintenance_off') {
|
||||||
\App\Core\Maintenance::disable();
|
\App\Core\Maintenance::disable();
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Maintenance mode disabled.', true);
|
Feedback::flash('NOTICE', 'DEFAULT', 'Maintenance mode disabled.', true);
|
||||||
|
// DB migrations actions
|
||||||
} elseif ($postAction === 'migrate_up') {
|
} elseif ($postAction === 'migrate_up') {
|
||||||
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
||||||
$applied = $runner->applyPendingMigrations();
|
$applied = $runner->applyPendingMigrations();
|
||||||
|
|
@ -227,6 +301,37 @@ if ($postAction !== '' && $postAction !== 'read_migration') {
|
||||||
];
|
];
|
||||||
$_SESSION['migration_modal_open'] = $applied[0];
|
$_SESSION['migration_modal_open'] = $applied[0];
|
||||||
}
|
}
|
||||||
|
// Plugin actions
|
||||||
|
} elseif ($postAction === 'plugin_enable' || $postAction === 'plugin_disable') {
|
||||||
|
$slug = strtolower(trim($_POST['plugin'] ?? ''));
|
||||||
|
if ($slug === '' || !isset($pluginAdminMap[$slug])) {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', 'Unknown plugin specified.', false);
|
||||||
|
} else {
|
||||||
|
$pluginMeta = $pluginAdminMap[$slug];
|
||||||
|
if ($postAction === 'plugin_enable') {
|
||||||
|
if (!$pluginMeta['can_enable']) {
|
||||||
|
$reason = 'Resolve missing dependencies before enabling this plugin.';
|
||||||
|
if (!empty($pluginMeta['missing_dependencies'])) {
|
||||||
|
$reason = 'Enable required plugins first: ' . implode(', ', $pluginMeta['missing_dependencies']);
|
||||||
|
}
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', $reason, false);
|
||||||
|
} elseif (!\App\Core\PluginManager::setEnabled($slug, true)) {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', 'Failed to enable plugin. Check file permissions on plugin.json.', false);
|
||||||
|
} else {
|
||||||
|
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" enabled. Reload admin to finish loading it.', $pluginMeta['name']), true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!$pluginMeta['can_disable']) {
|
||||||
|
$reason = 'Disable dependent plugins first: ' . implode(', ', $pluginMeta['enabled_dependents']);
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', $reason, false);
|
||||||
|
} elseif (!\App\Core\PluginManager::setEnabled($slug, false)) {
|
||||||
|
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable plugin. Check file permissions on plugin.json.', false);
|
||||||
|
} else {
|
||||||
|
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" disabled.', $pluginMeta['name']), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Test migrations actions
|
||||||
} elseif ($postAction === 'create_test_migration') {
|
} elseif ($postAction === 'create_test_migration') {
|
||||||
$timestamp = date('Ymd_His');
|
$timestamp = date('Ymd_His');
|
||||||
$filename = $timestamp . '_test_migration.sql';
|
$filename = $timestamp . '_test_migration.sql';
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,143 @@ if (!empty($adminOverviewStatuses) && is_array($adminOverviewStatuses)) {
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
<?php elseif (($tabMeta['type'] ?? 'core') === 'core' && $sectionKey === 'plugins'): ?>
|
||||||
|
<?php
|
||||||
|
$pluginsState = $sectionState['plugins'] ?? [];
|
||||||
|
$pluginsList = $pluginsState['plugins'] ?? [];
|
||||||
|
$dependencyErrors = $pluginsState['dependency_errors'] ?? [];
|
||||||
|
$totalPlugins = count($pluginsList);
|
||||||
|
$enabledPlugins = count(array_filter($pluginsList, static function($plugin) {
|
||||||
|
return !empty($plugin['enabled']);
|
||||||
|
}));
|
||||||
|
$issuesPlugins = count(array_filter($pluginsList, static function($plugin) {
|
||||||
|
return !empty($plugin['dependency_errors']) || !$plugin['loaded'];
|
||||||
|
}));
|
||||||
|
?>
|
||||||
|
<div class="tm-admin-grid">
|
||||||
|
<article class="tm-admin-card">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<h2 class="tm-admin-card-title">Plugin overview</h2>
|
||||||
|
<p class="tm-admin-card-subtitle">Enable or disable functionality and review dependency health.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tm-hero-pill pill-primary">
|
||||||
|
<?= htmlspecialchars($enabledPlugins) ?> / <?= htmlspecialchars($totalPlugins) ?> enabled
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<?php if (!empty($dependencyErrors)): ?>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>Dependency issues detected.</strong> Resolve the following before enabling affected plugins:
|
||||||
|
<ul class="mb-0 mt-2">
|
||||||
|
<?php foreach ($dependencyErrors as $slug => $errors):
|
||||||
|
if (empty($errors)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<li><strong><?= htmlspecialchars($slug) ?>:</strong> <?= htmlspecialchars(implode('; ', $errors)) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (empty($pluginsList)): ?>
|
||||||
|
<p class="tm-admin-empty mb-0">No plugins detected in the plugins directory.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover tm-admin-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Plugin</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Depends on</th>
|
||||||
|
<th class="text-right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php
|
||||||
|
$pluginIndex = $pluginsState['plugin_index'] ?? [];
|
||||||
|
foreach ($pluginsList as $plugin):
|
||||||
|
$missingDeps = $plugin['missing_dependencies'] ?? [];
|
||||||
|
$depErrors = $plugin['dependency_errors'] ?? [];
|
||||||
|
$dependents = $plugin['dependents'] ?? [];
|
||||||
|
$enabledDependents = $plugin['enabled_dependents'] ?? [];
|
||||||
|
$statusBadges = [];
|
||||||
|
$statusBadges[] = $plugin['enabled']
|
||||||
|
? '<span class="badge text-uppercase" style="background-color:#198754;color:#fff;">Enabled</span>'
|
||||||
|
: '<span class="badge text-uppercase" style="background-color:#6c757d;color:#fff;">Disabled</span>';
|
||||||
|
if ($plugin['enabled'] && empty($depErrors) && $plugin['loaded']) {
|
||||||
|
$statusBadges[] = '<span class="badge text-uppercase" style="background-color:#0dcaf0;color:#052c65;">Loaded</span>';
|
||||||
|
}
|
||||||
|
if (!empty($missingDeps) || !empty($depErrors)) {
|
||||||
|
$statusBadges[] = '<span class="badge text-uppercase" style="background-color:#ffc107;color:#212529;">Issues</span>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><?= htmlspecialchars($plugin['name']) ?></strong>
|
||||||
|
<?php if (!empty($plugin['version'])): ?>
|
||||||
|
<span class="text-muted">v<?= htmlspecialchars($plugin['version']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($plugin['description'])): ?>
|
||||||
|
<p class="tm-admin-muted mb-0"><?= htmlspecialchars($plugin['description']) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?= implode(' ', $statusBadges) ?>
|
||||||
|
<?php if (!empty($depErrors)): ?>
|
||||||
|
<p class="tm-admin-muted text-warning mb-0"><?= htmlspecialchars(implode(' ', $depErrors)) ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if (!empty($plugin['dependencies'])): ?>
|
||||||
|
<ul class="tm-admin-inline-list">
|
||||||
|
<?php foreach ($plugin['dependencies'] as $dep):
|
||||||
|
$depMeta = $pluginIndex[$dep] ?? null;
|
||||||
|
$depStatusBadge = '';
|
||||||
|
if ($depMeta) {
|
||||||
|
$depStatusBadge = $depMeta['enabled']
|
||||||
|
? '<span class="badge" style="background-color:#198754;color:#fff;">OK</span>'
|
||||||
|
: '<span class="badge" style="background-color:#ffc107;color:#212529;">Off</span>';
|
||||||
|
if (!empty($depMeta['dependency_errors'])) {
|
||||||
|
$depStatusBadge = '<span class="badge" style="background-color:#dc3545;color:#fff;">Error</span>';
|
||||||
|
}
|
||||||
|
} elseif (in_array($dep, $missingDeps, true)) {
|
||||||
|
$depStatusBadge = '<span class="badge" style="background-color:#dc3545;color:#fff;">Missing</span>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<li>
|
||||||
|
<?= htmlspecialchars($dep) ?>
|
||||||
|
<?php if ($depStatusBadge !== ''): ?>
|
||||||
|
<span class="tm-admin-dep-status">(<?= $depStatusBadge ?>)</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<form method="post" class="d-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
||||||
|
<input type="hidden" name="section" value="plugins">
|
||||||
|
<input type="hidden" name="plugin" value="<?= htmlspecialchars($plugin['slug']) ?>">
|
||||||
|
<?php if ($plugin['enabled']): ?>
|
||||||
|
<input type="hidden" name="action" value="plugin_disable">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger" <?= $plugin['can_disable'] ? '' : 'disabled' ?>>Disable</button>
|
||||||
|
<?php else: ?>
|
||||||
|
<input type="hidden" name="action" value="plugin_enable">
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-success" <?= $plugin['can_enable'] ? '' : 'disabled' ?>>Enable</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
<?php elseif (!empty($tabMeta['hook'])): ?>
|
<?php elseif (!empty($tabMeta['hook'])): ?>
|
||||||
<?php
|
<?php
|
||||||
do_hook($tabMeta['hook'], [
|
do_hook($tabMeta['hook'], [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue