Refactors admin/plugins to use only DB. Adds buttons and modal info page

main
Yasen Pramatarov 2026-01-19 11:31:34 +02:00
parent 81e665644c
commit 064614f73f
3 changed files with 638 additions and 35 deletions

View File

@ -27,7 +27,7 @@ class PluginManager
self::$dependencyErrors = []; self::$dependencyErrors = [];
foreach (self::$catalog as $name => $info) { foreach (self::$catalog as $name => $info) {
if (empty($info['meta']['enabled'])) { if (!self::isEnabled($name)) {
continue; continue;
} }
self::resolve($name); self::resolve($name);
@ -83,7 +83,7 @@ class PluginManager
} }
$meta = self::$catalog[$plugin]['meta']; $meta = self::$catalog[$plugin]['meta'];
if (empty($meta['enabled'])) { if (!self::isEnabled($plugin)) {
return false; return false;
} }
@ -102,7 +102,7 @@ class PluginManager
self::$dependencyErrors[$plugin][] = sprintf('Missing dependency "%s"', $dependency); self::$dependencyErrors[$plugin][] = sprintf('Missing dependency "%s"', $dependency);
continue; continue;
} }
if (empty(self::$catalog[$dependency]['meta']['enabled'])) { if (!self::isEnabled($dependency)) {
self::$dependencyErrors[$plugin][] = sprintf('Dependency "%s" is disabled', $dependency); self::$dependencyErrors[$plugin][] = sprintf('Dependency "%s" is disabled', $dependency);
continue; continue;
} }
@ -157,7 +157,8 @@ class PluginManager
} }
/** /**
* Persists a plugin's enabled flag back to its manifest. * Persists a plugin's enabled flag to the database settings table.
* Note: This method no longer requires write access to plugin.json files.
*/ */
public static function setEnabled(string $plugin, bool $enabled): bool public static function setEnabled(string $plugin, bool $enabled): bool
{ {
@ -165,28 +166,139 @@ class PluginManager
return false; return false;
} }
$manifestPath = self::$catalog[$plugin]['path'] . '/plugin.json'; global $db;
if (!is_file($manifestPath) || !is_readable($manifestPath) || !is_writable($manifestPath)) { if (!$db instanceof PDO) {
return false; return false;
} }
$raw = file_get_contents($manifestPath); try {
$data = json_decode($raw ?: '', true); // Update or insert plugin setting in database
if (!is_array($data)) { $stmt = $db->prepare(
$data = self::$catalog[$plugin]['meta']; 'INSERT INTO settings (`key`, `value`, updated_at)
} VALUES (:key, :value, NOW())
ON DUPLICATE KEY UPDATE `value` = :value, updated_at = NOW()'
);
$key = 'plugin_enabled_' . $plugin;
$value = $enabled ? '1' : '0';
$stmt->execute([':key' => $key, ':value' => $value]);
$data['enabled'] = $enabled; // Clear loaded cache if disabling
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL; if (!$enabled && isset(self::$loaded[$plugin])) {
if (file_put_contents($manifestPath, $json, LOCK_EX) === false) { unset(self::$loaded[$plugin]);
}
return true;
} catch (PDOException $e) {
// Log the actual error for debugging
error_log('PluginManager::setEnabled failed for ' . $plugin . ': ' . $e->getMessage());
return false;
}
}
/**
* Check if a plugin is enabled from database settings.
*/
public static function isEnabled(string $plugin): bool
{
if (!isset(self::$catalog[$plugin])) {
return false; return false;
} }
self::$catalog[$plugin]['meta'] = $data; global $db;
if (!$enabled && isset(self::$loaded[$plugin])) { if ($db instanceof PDO) {
unset(self::$loaded[$plugin]); try {
$stmt = $db->prepare('SELECT `value` FROM settings WHERE `key` = :key LIMIT 1');
$key = 'plugin_enabled_' . $plugin;
$stmt->execute([':key' => $key]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if ($result !== false) {
return $result['value'] === '1';
}
} catch (PDOException $e) {
// Log error but return false
error_log('PluginManager::isEnabled database error for ' . $plugin . ': ' . $e->getMessage());
}
} }
return true; // Default to disabled if no database entry or database unavailable
return false;
}
/**
* Install plugin by running its migrations.
*/
public static function install(string $plugin): bool
{
if (!isset(self::$catalog[$plugin])) {
return false;
}
$pluginPath = self::$catalog[$plugin]['path'];
$bootstrapPath = $pluginPath . '/bootstrap.php';
if (!file_exists($bootstrapPath)) {
return false;
}
try {
// Include bootstrap to run migrations
include_once $bootstrapPath;
// Look for migration function
$migrationFunction = str_replace('-', '_', $plugin) . '_ensure_tables';
if (function_exists($migrationFunction)) {
$migrationFunction();
return true;
}
return false;
} catch (Throwable $e) {
return false;
}
}
/**
* Purge plugin by dropping its tables and removing settings.
*/
public static function purge(string $plugin): bool
{
if (!isset(self::$catalog[$plugin])) {
return false;
}
global $db;
if (!$db instanceof PDO) {
return false;
}
try {
// First disable the plugin
self::setEnabled($plugin, false);
// Remove plugin settings
$stmt = $db->prepare('DELETE FROM settings WHERE `key` LIKE :pattern');
$stmt->execute([':pattern' => 'plugin_enabled_' . $plugin]);
// Drop plugin-specific tables (user_pro_* tables for this plugin)
$stmt = $db->prepare('SHOW TABLES LIKE "user_pro_%"');
$stmt->execute();
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
foreach ($tables as $table) {
// Check if this table belongs to the plugin by checking its migration file
$migrationFile = self::$catalog[$plugin]['path'] . '/migrations/create_' . $plugin . '_tables.sql';
if (file_exists($migrationFile)) {
$migrationContent = file_get_contents($migrationFile);
if (strpos($migrationContent, $table) !== false) {
$db->exec("DROP TABLE IF EXISTS `$table`");
}
}
}
return true;
} catch (Throwable $e) {
return false;
}
} }
} }

View File

@ -154,21 +154,42 @@ $pluginAdminMap = [];
foreach ($pluginCatalog as $slug => $info) { foreach ($pluginCatalog as $slug => $info) {
$meta = $info['meta'] ?? []; $meta = $info['meta'] ?? [];
$name = trim((string)($meta['name'] ?? $slug)); $name = trim((string)($meta['name'] ?? $slug));
$enabled = !empty($meta['enabled']); $enabled = \App\Core\PluginManager::isEnabled($slug); // Use database setting
$dependencies = $normalizeDependencies($meta); $dependencies = $normalizeDependencies($meta);
$dependents = array_values($pluginDependentsIndex[$slug] ?? []); $dependents = array_values($pluginDependentsIndex[$slug] ?? []);
$enabledDependents = array_values(array_filter($dependents, static function($depSlug) use ($pluginCatalog) { $enabledDependents = array_values(array_filter($dependents, static function($depSlug) {
return !empty($pluginCatalog[$depSlug]['meta']['enabled']); return \App\Core\PluginManager::isEnabled($depSlug); // Use database setting
})); }));
$missingDependencies = array_values(array_filter($dependencies, static function($depSlug) use ($pluginCatalog) { $missingDependencies = array_values(array_filter($dependencies, static function($depSlug) use ($pluginCatalog) {
return !isset($pluginCatalog[$depSlug]) || empty($pluginCatalog[$depSlug]['meta']['enabled']); return !isset($pluginCatalog[$depSlug]) || !\App\Core\PluginManager::isEnabled($depSlug); // Use database setting
})); }));
// Check for migration files and existing tables
$migrationFiles = glob($info['path'] . '/migrations/*.sql');
$hasMigration = !empty($migrationFiles);
$existingTables = [];
if ($hasMigration && isset($db) && $db instanceof PDO) {
$stmt = $db->query("SHOW TABLES");
$allTables = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
foreach ($migrationFiles as $migrationFile) {
$migrationContent = file_get_contents($migrationFile);
foreach ($allTables as $table) {
if (strpos($migrationContent, $table) !== false) {
$existingTables[] = $table;
}
}
}
$existingTables = array_unique($existingTables);
}
$pluginAdminMap[$slug] = [ $pluginAdminMap[$slug] = [
'slug' => $slug, 'slug' => $slug,
'name' => $name, 'name' => $name,
'version' => (string)($meta['version'] ?? ''), 'version' => (string)($meta['version'] ?? ''),
'description' => (string)($meta['description'] ?? ''), 'description' => (string)($meta['description'] ?? ''),
'path' => $info['path'],
'enabled' => $enabled, 'enabled' => $enabled,
'loaded' => isset($pluginLoadedMap[$slug]), 'loaded' => isset($pluginLoadedMap[$slug]),
'dependencies' => $dependencies, 'dependencies' => $dependencies,
@ -178,6 +199,9 @@ foreach ($pluginCatalog as $slug => $info) {
'dependency_errors' => $pluginDependencyErrors[$slug] ?? [], 'dependency_errors' => $pluginDependencyErrors[$slug] ?? [],
'can_enable' => !$enabled && empty($missingDependencies), 'can_enable' => !$enabled && empty($missingDependencies),
'can_disable' => $enabled && empty($enabledDependents), 'can_disable' => $enabled && empty($enabledDependents),
'has_migration' => $hasMigration,
'existing_tables' => $existingTables,
'can_install' => $hasMigration && empty($existingTables),
]; ];
} }
@ -316,21 +340,98 @@ if ($postAction !== '' && $postAction !== 'read_migration') {
} }
Feedback::flash('ERROR', 'DEFAULT', $reason, false); Feedback::flash('ERROR', 'DEFAULT', $reason, false);
} elseif (!\App\Core\PluginManager::setEnabled($slug, true)) { } elseif (!\App\Core\PluginManager::setEnabled($slug, true)) {
Feedback::flash('ERROR', 'DEFAULT', 'Failed to enable plugin. Check file permissions on plugin.json.', false); Feedback::flash('ERROR', 'DEFAULT', 'Failed to enable plugin. Check database connection and error logs.', false);
} else { } else {
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" enabled. Reload admin to finish loading it.', $pluginMeta['name']), true); // Automatically install plugin tables when enabling
$installResult = \App\Core\PluginManager::install($slug);
if ($installResult) {
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" enabled and installed successfully.', $pluginMeta['name']), true);
} else {
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" enabled but installation failed. Check migration files.', $pluginMeta['name']), true);
}
} }
} else { } else {
if (!$pluginMeta['can_disable']) { if (!$pluginMeta['can_disable']) {
$reason = 'Disable dependent plugins first: ' . implode(', ', $pluginMeta['enabled_dependents']); $reason = 'Disable dependent plugins first: ' . implode(', ', $pluginMeta['enabled_dependents']);
Feedback::flash('ERROR', 'DEFAULT', $reason, false); Feedback::flash('ERROR', 'DEFAULT', $reason, false);
} elseif (!\App\Core\PluginManager::setEnabled($slug, false)) { } elseif (!\App\Core\PluginManager::setEnabled($slug, false)) {
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable plugin. Check file permissions on plugin.json.', false); Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable plugin. Check database connection and error logs.', false);
} else { } else {
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" disabled.', $pluginMeta['name']), true); Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" disabled.', $pluginMeta['name']), true);
} }
} }
} }
// Plugin install action
} elseif ($postAction === 'plugin_install') {
$slug = strtolower(trim($_POST['plugin'] ?? ''));
if ($slug === '' || !isset($pluginAdminMap[$slug])) {
Feedback::flash('ERROR', 'DEFAULT', 'Unknown plugin specified.', false);
} else {
if (\App\Core\PluginManager::install($slug)) {
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" installed successfully.', $pluginAdminMap[$slug]['name']), true);
} else {
Feedback::flash('ERROR', 'DEFAULT', 'Plugin installation failed. Check migration files.', false);
}
}
// Plugin purge action
} elseif ($postAction === 'plugin_purge') {
$slug = strtolower(trim($_POST['plugin'] ?? ''));
if ($slug === '' || !isset($pluginAdminMap[$slug])) {
Feedback::flash('ERROR', 'DEFAULT', 'Unknown plugin specified.', false);
} else {
if (\App\Core\PluginManager::purge($slug)) {
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" purged successfully. All data and tables removed.', $pluginAdminMap[$slug]['name']), true);
} else {
Feedback::flash('ERROR', 'DEFAULT', 'Plugin purge failed. Check database permissions.', false);
}
}
// Plugin check action
} elseif ($postAction === 'plugin_check') {
$slug = strtolower(trim($_POST['plugin'] ?? ''));
if ($slug === '' || !isset($pluginAdminMap[$slug])) {
Feedback::flash('ERROR', 'DEFAULT', 'Unknown plugin specified.', false);
} else {
// Redirect to plugin check page
header('Location: ' . $app_root . '?page=admin&section=plugins&action=plugin_check_page&plugin=' . urlencode($slug));
exit;
}
// Plugin migration test actions
} elseif ($postAction === 'test_plugin_migrations') {
$slug = strtolower(trim($_POST['plugin'] ?? ''));
if ($slug === '' || !isset($pluginAdminMap[$slug])) {
Feedback::flash('ERROR', 'DEFAULT', 'Unknown plugin specified.', false);
} else {
try {
$pluginPath = $pluginAdminMap[$slug]['path'];
$bootstrapPath = $pluginPath . '/bootstrap.php';
if (!file_exists($bootstrapPath)) {
Feedback::flash('ERROR', 'DEFAULT', 'Plugin has no bootstrap file.', false);
} else {
// Load plugin bootstrap in isolation to test migrations
$migrationFunctions = [];
$bootstrapContent = file_get_contents($bootstrapPath);
// Check for migration functions
if (strpos($bootstrapContent, '_ensure_tables') !== false) {
// Temporarily include bootstrap to test migrations
include_once $bootstrapPath;
$migrationFunctionName = str_replace('-', '_', $slug) . '_ensure_tables';
if (function_exists($migrationFunctionName)) {
$migrationFunctionName();
Feedback::flash('NOTICE', 'DEFAULT', sprintf('Plugin "%s" migrations executed successfully.', $pluginAdminMap[$slug]['name']), true);
} else {
Feedback::flash('ERROR', 'DEFAULT', 'Plugin migration function not found.', false);
}
} else {
Feedback::flash('ERROR', 'DEFAULT', 'Plugin has no migration function.', false);
}
}
} catch (Throwable $e) {
Feedback::flash('ERROR', 'DEFAULT', 'Migration test failed: ' . $e->getMessage(), false);
}
}
// Test migrations actions // Test migrations actions
} elseif ($postAction === 'create_test_migration') { } elseif ($postAction === 'create_test_migration') {
$timestamp = date('Ymd_His'); $timestamp = date('Ymd_His');
@ -432,6 +533,139 @@ try {
$migration_error = $e->getMessage(); $migration_error = $e->getMessage();
} }
// Generate CSRF token early for all templates
$csrf_token = $security->generateCsrfToken();
// Handle plugin check page
if ($queryAction === 'plugin_check_page' && isset($_GET['plugin'])) {
// Simple test for JSON response
if (isset($_GET['test'])) {
header('Content-Type: application/json');
echo json_encode(['test' => 'working', 'timestamp' => time()]);
exit;
}
// Debug: Log request details
error_log('Plugin check request: ' . print_r([
'action' => $queryAction,
'plugin' => $_GET['plugin'],
'ajax' => isset($_SERVER['HTTP_X_REQUESTED_WITH']),
'ajax_header' => $_SERVER['HTTP_X_REQUESTED_WITH'] ?? 'not set',
'content_type' => $_SERVER['CONTENT_TYPE'] ?? 'not set',
'request_method' => $_SERVER['REQUEST_METHOD'] ?? 'not set'
], true));
// Start output buffering to catch any unwanted output
ob_start();
// Disable error display for JSON responses
$originalErrorReporting = error_reporting();
$originalDisplayErrors = ini_get('display_errors');
$isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') ||
isset($_GET['ajax']);
if ($isAjax) {
error_reporting(0);
ini_set('display_errors', 0);
}
$pluginSlug = strtolower(trim($_GET['plugin']));
if (!isset($pluginAdminMap[$pluginSlug])) {
if ($isAjax) {
ob_end_clean(); // Clear and end output buffer
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Unknown plugin specified.']);
exit;
}
Feedback::flash('ERROR', 'DEFAULT', 'Unknown plugin specified.', false);
header('Location: ' . $app_root . '?page=admin&section=plugins');
exit;
}
$pluginInfo = $pluginAdminMap[$pluginSlug];
$checkResults = [];
try {
// Check plugin files exist
$migrationFiles = glob($pluginInfo['path'] . '/migrations/*.sql');
$hasMigration = !empty($migrationFiles);
$checkResults['files'] = [
'manifest' => file_exists($pluginInfo['path'] . '/plugin.json'),
'bootstrap' => file_exists($pluginInfo['path'] . '/bootstrap.php'),
'migration' => $hasMigration,
];
// Check database tables
global $db;
$pluginTables = [];
if ($db instanceof PDO) {
$stmt = $db->query("SHOW TABLES");
$allTables = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
if ($hasMigration) {
// Check each migration file for table references
foreach ($migrationFiles as $migrationFile) {
$migrationContent = file_get_contents($migrationFile);
foreach ($allTables as $table) {
if (strpos($migrationContent, $table) !== false) {
$pluginTables[] = $table;
}
}
}
$pluginTables = array_unique($pluginTables);
}
}
$checkResults['tables'] = $pluginTables;
// Check plugin functions
$bootstrapPath = $pluginInfo['path'] . '/bootstrap.php';
if (file_exists($bootstrapPath)) {
include_once $bootstrapPath;
$migrationFunction = str_replace('-', '_', $pluginSlug) . '_ensure_tables';
$checkResults['functions'] = [
'migration' => function_exists($migrationFunction),
];
}
} catch (Throwable $e) {
$checkResults['error'] = $e->getMessage();
}
// Handle AJAX request
if ($isAjax) {
// Restore error reporting
error_reporting($originalErrorReporting);
ini_set('display_errors', $originalDisplayErrors);
ob_end_clean(); // Clear and end output buffer
header('Content-Type: application/json');
header('Cache-Control: no-cache, must-revalidate');
$jsonData = json_encode([
'success' => true,
'pluginInfo' => $pluginInfo,
'checkResults' => $checkResults,
'csrf_token' => $csrf_token,
'app_root' => $app_root
]);
error_log('JSON response: ' . $jsonData);
echo $jsonData;
exit;
}
// Restore error reporting for non-AJAX requests
error_reporting($originalErrorReporting);
ini_set('display_errors', $originalDisplayErrors);
// Include check page template for non-AJAX requests
include '../app/templates/admin_plugin_check.php';
exit;
}
$overviewPillsPayload = \App\Core\HookDispatcher::applyFilters('admin.overview.pills', [ $overviewPillsPayload = \App\Core\HookDispatcher::applyFilters('admin.overview.pills', [
'pills' => [], 'pills' => [],
'sections' => $sectionRegistry, 'sections' => $sectionRegistry,
@ -456,6 +690,7 @@ if (is_array($overviewStatusesPayload)) {
$adminOverviewStatuses = $overviewStatusesPayload['statuses'] ?? (is_array($overviewStatusesPayload) ? $overviewStatusesPayload : []); $adminOverviewStatuses = $overviewStatusesPayload['statuses'] ?? (is_array($overviewStatusesPayload) ? $overviewStatusesPayload : []);
} }
$csrf_token = $security->generateCsrfToken(); // Get any new feedback messages
include_once '../app/helpers/feedback.php';
include '../app/templates/admin.php'; include '../app/templates/admin.php';

View File

@ -22,6 +22,18 @@ if (!empty($modal_to_open)) {
$preselectModalId = 'migrationModal' . md5($modal_to_open); $preselectModalId = 'migrationModal' . md5($modal_to_open);
} }
?>
<style>
.tooltip {
font-size: 0.75rem;
}
.tooltip-inner {
font-size: 0.75rem;
max-width: 300px;
}
</style>
<?php
$tabs = $adminTabs ?? []; $tabs = $adminTabs ?? [];
if (empty($tabs)) { if (empty($tabs)) {
$tabs = [ $tabs = [
@ -459,18 +471,87 @@ if (!empty($adminOverviewStatuses) && is_array($adminOverviewStatuses)) {
<?php endif; ?> <?php endif; ?>
</td> </td>
<td class="text-right"> <td class="text-right">
<form method="post" class="d-inline"> <div class="btn-group" role="group">
<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']): ?> <?php if ($plugin['enabled']): ?>
<input type="hidden" name="action" value="plugin_disable"> <form method="post" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-danger" <?= $plugin['can_disable'] ? '' : 'disabled' ?>>Disable</button> <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']) ?>">
<input type="hidden" name="action" value="plugin_disable">
<?php if ($plugin['can_disable']): ?>
<span data-toggle="tooltip" data-placement="top" title="Disable this plugin">
<button type="submit" class="btn btn-sm btn-outline-danger">Disable</button>
</span>
<?php else: ?>
<span data-toggle="tooltip" data-placement="top"
title="<?= htmlspecialchars('Cannot disable: ' . (count($plugin['enabled_dependents']) > 0 ? 'Required by: ' . implode(', ', $plugin['enabled_dependents']) : 'Plugin has active dependents')) ?>">
<button type="button" class="btn btn-sm btn-outline-danger" disabled>Disable</button>
</span>
<?php endif; ?>
</form>
<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']) ?>">
<input type="hidden" name="action" value="plugin_purge">
<?php if ($plugin['can_disable']): ?>
<span data-toggle="tooltip" data-placement="top" title="Remove all plugin data and tables">
<button type="submit" class="btn btn-sm btn-outline-warning" onclick="return confirm('Are you sure? This will permanently delete all plugin data and tables!')">Purge</button>
</span>
<?php else: ?>
<span data-toggle="tooltip" data-placement="top"
title="<?= htmlspecialchars('Cannot purge: ' . (count($plugin['enabled_dependents']) > 0 ? 'Required by: ' . implode(', ', $plugin['enabled_dependents']) : 'Plugin has active dependents')) ?>">
<button type="button" class="btn btn-sm btn-outline-warning" disabled>Purge</button>
</span>
<?php endif; ?>
</form>
<?php else: ?> <?php else: ?>
<input type="hidden" name="action" value="plugin_enable"> <form method="post" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-success" <?= $plugin['can_enable'] ? '' : 'disabled' ?>>Enable</button> <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']) ?>">
<input type="hidden" name="action" value="plugin_enable">
<?php if ($plugin['can_enable']): ?>
<span data-toggle="tooltip" data-placement="top" title="Enable this plugin">
<button type="submit" class="btn btn-sm btn-outline-success">Enable</button>
</span>
<?php else: ?>
<span data-toggle="tooltip" data-placement="top"
title="<?= htmlspecialchars('Cannot enable: ' . (count($plugin['missing_dependencies']) > 0 ? 'Missing dependencies: ' . implode(', ', $plugin['missing_dependencies']) : 'Plugin has dependency issues')) ?>">
<button type="button" class="btn btn-sm btn-outline-success" disabled>Enable</button>
</span>
<?php endif; ?>
</form>
<?php if (file_exists($pluginAdminMap[$plugin['slug']]['path'] . '/bootstrap.php')): ?>
<?php if ($plugin['can_install']): ?>
<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']) ?>">
<input type="hidden" name="action" value="plugin_install">
<span data-toggle="tooltip" data-placement="top" title="Install plugin database tables">
<button type="submit" class="btn btn-sm btn-outline-primary">Install</button>
</span>
</form>
<?php else: ?>
<?php if ($plugin['has_migration']): ?>
<span data-toggle="tooltip" data-placement="top" title="Tables already exist">
<button type="button" class="btn btn-sm btn-outline-primary" disabled>Install</button>
</span>
<?php else: ?>
<span data-toggle="tooltip" data-placement="top" title="No migration files found">
<button type="button" class="btn btn-sm btn-outline-primary" disabled>Install</button>
</span>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?> <?php endif; ?>
</form> <?php if (file_exists($pluginAdminMap[$plugin['slug']]['path'] . '/bootstrap.php')): ?>
<span data-toggle="tooltip" data-placement="top" title="Check plugin health and status">
<button type="button" class="btn btn-sm btn-outline-secondary" data-toggle="modal" data-target="#pluginCheckModal<?= htmlspecialchars($plugin['slug']) ?>">Check</button>
</span>
<?php endif; ?>
</div>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
@ -564,8 +645,183 @@ endif; ?>
<input type="hidden" name="filename" value=""> <input type="hidden" name="filename" value="">
</form> </form>
<!-- Plugin Check Modals -->
<?php foreach ($pluginAdminList as $plugin): ?>
<?php if (file_exists($plugin['path'] . '/bootstrap.php')): ?>
<?php
$modalId = 'pluginCheckModal' . htmlspecialchars($plugin['slug']);
$checkResults = [];
try {
// Check plugin files exist
$checkResults['files'] = [
'manifest' => file_exists($plugin['path'] . '/plugin.json'),
'bootstrap' => file_exists($plugin['path'] . '/bootstrap.php'),
'migration' => file_exists($plugin['path'] . '/migrations/create_' . $plugin['slug'] . '_tables.sql'),
];
// Check database tables
global $db;
if ($db instanceof PDO) {
$stmt = $db->query("SHOW TABLES LIKE 'user_pro_%'");
$allTables = $stmt->fetchAll(PDO::FETCH_COLUMN, 0);
$migrationFile = $plugin['path'] . '/migrations/create_' . $plugin['slug'] . '_tables.sql';
if (file_exists($migrationFile)) {
$migrationContent = file_get_contents($migrationFile);
$pluginTables = [];
foreach ($allTables as $table) {
if (strpos($migrationContent, $table) !== false) {
$pluginTables[] = $table;
}
}
$checkResults['tables'] = $pluginTables;
}
}
// Check plugin functions
$bootstrapPath = $plugin['path'] . '/bootstrap.php';
if (file_exists($bootstrapPath)) {
include_once $bootstrapPath;
$migrationFunction = str_replace('-', '_', $plugin['slug']) . '_ensure_tables';
$checkResults['functions'] = [
'migration' => function_exists($migrationFunction),
];
}
} catch (Throwable $e) {
$checkResults['error'] = $e->getMessage();
}
?>
<div class="modal fade" id="<?= $modalId ?>" tabindex="-1" aria-labelledby="<?= $modalId ?>Label" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="<?= $modalId ?>Label">Plugin Health Check: <?= htmlspecialchars($plugin['name']) ?></h5>
<button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="card-title mb-0">File System Check</h6>
</div>
<div class="card-body">
<?php foreach ($checkResults['files'] ?? [] as $file => $exists): ?>
<div class="d-flex justify-content-between align-items-center mb-2">
<span><?= htmlspecialchars(ucfirst($file)) ?></span>
<span class="badge bg-<?= $exists ? 'success' : 'danger' ?>">
<?= $exists ? 'Exists' : 'Missing' ?>
</span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="card-title mb-0">Database Tables</h6>
</div>
<div class="card-body">
<?php if (!empty($checkResults['tables'])): ?>
<?php foreach ($checkResults['tables'] as $table): ?>
<div class="d-flex justify-content-between align-items-center mb-2">
<span><?= htmlspecialchars($table) ?></span>
<span class="badge bg-success">Present</span>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="text-muted mb-0">No plugin tables found.</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="card-title mb-0">Functions Check</h6>
</div>
<div class="card-body">
<?php foreach ($checkResults['functions'] ?? [] as $func => $exists): ?>
<div class="d-flex justify-content-between align-items-center mb-2">
<span><?= htmlspecialchars($func) ?>()</span>
<span class="badge bg-<?= $exists ? 'success' : 'danger' ?>">
<?= $exists ? 'Available' : 'Missing' ?>
</span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="card-title mb-0">Plugin Information</h6>
</div>
<div class="card-body">
<div class="small">
<div class="mb-1"><strong>Name:</strong> <?= htmlspecialchars($plugin['name']) ?></div>
<div class="mb-1"><strong>Version:</strong> <?= htmlspecialchars($plugin['version'] ?? 'N/A') ?></div>
<div class="mb-1"><strong>Enabled:</strong> <span class="badge bg-<?= $plugin['enabled'] ? 'success' : 'secondary' ?>"><?= $plugin['enabled'] ? 'Yes' : 'No' ?></span></div>
<div class="mb-1"><strong>Dependencies:</strong> <?= !empty($plugin['dependencies']) ? htmlspecialchars(implode(', ', $plugin['dependencies'])) : 'None' ?></div>
</div>
</div>
</div>
</div>
</div>
<?php if (isset($checkResults['error'])): ?>
<div class="alert alert-danger mt-3">
<strong>Error:</strong> <?= htmlspecialchars($checkResults['error']) ?>
</div>
<?php endif; ?>
</div>
<div class="modal-footer">
<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']) ?>">
<input type="hidden" name="action" value="plugin_install">
<button type="submit" class="btn btn-primary">Install Tables</button>
</form>
<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']) ?>">
<input type="hidden" name="action" value="test_plugin_migrations">
<button type="submit" class="btn btn-info">Test Migrations</button>
</form>
<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']) ?>">
<input type="hidden" name="action" value="plugin_purge">
<button type="submit" class="btn btn-warning" onclick="return confirm('Are you sure? This will permanently delete all plugin data and tables!')">Purge Plugin</button>
</form>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
<form method="post" id="tm-admin-hidden-read-migration" class="d-none">
<input type="hidden" name="action" value="read_migration">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
<input type="hidden" name="filename" value="">
</form>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Initialize tooltips
if (typeof $ !== 'undefined' && $.fn.tooltip) {
$('[data-toggle="tooltip"]').tooltip();
}
document.querySelectorAll('form.tm-confirm').forEach((form) => { document.querySelectorAll('form.tm-confirm').forEach((form) => {
form.addEventListener('submit', (event) => { form.addEventListener('submit', (event) => {
const message = form.getAttribute('data-confirm') || 'Are you sure?'; const message = form.getAttribute('data-confirm') || 'Are you sure?';