Refactors admin/plugins to use only DB. Adds buttons and modal info page
parent
81e665644c
commit
064614f73f
|
|
@ -27,7 +27,7 @@ class PluginManager
|
|||
self::$dependencyErrors = [];
|
||||
|
||||
foreach (self::$catalog as $name => $info) {
|
||||
if (empty($info['meta']['enabled'])) {
|
||||
if (!self::isEnabled($name)) {
|
||||
continue;
|
||||
}
|
||||
self::resolve($name);
|
||||
|
|
@ -83,7 +83,7 @@ class PluginManager
|
|||
}
|
||||
|
||||
$meta = self::$catalog[$plugin]['meta'];
|
||||
if (empty($meta['enabled'])) {
|
||||
if (!self::isEnabled($plugin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ class PluginManager
|
|||
self::$dependencyErrors[$plugin][] = sprintf('Missing dependency "%s"', $dependency);
|
||||
continue;
|
||||
}
|
||||
if (empty(self::$catalog[$dependency]['meta']['enabled'])) {
|
||||
if (!self::isEnabled($dependency)) {
|
||||
self::$dependencyErrors[$plugin][] = sprintf('Dependency "%s" is disabled', $dependency);
|
||||
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
|
||||
{
|
||||
|
|
@ -165,28 +166,139 @@ class PluginManager
|
|||
return false;
|
||||
}
|
||||
|
||||
$manifestPath = self::$catalog[$plugin]['path'] . '/plugin.json';
|
||||
if (!is_file($manifestPath) || !is_readable($manifestPath) || !is_writable($manifestPath)) {
|
||||
global $db;
|
||||
if (!$db instanceof PDO) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$raw = file_get_contents($manifestPath);
|
||||
$data = json_decode($raw ?: '', true);
|
||||
if (!is_array($data)) {
|
||||
$data = self::$catalog[$plugin]['meta'];
|
||||
}
|
||||
try {
|
||||
// Update or insert plugin setting in database
|
||||
$stmt = $db->prepare(
|
||||
'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;
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
if (file_put_contents($manifestPath, $json, LOCK_EX) === false) {
|
||||
// Clear loaded cache if disabling
|
||||
if (!$enabled && isset(self::$loaded[$plugin])) {
|
||||
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;
|
||||
}
|
||||
|
||||
self::$catalog[$plugin]['meta'] = $data;
|
||||
if (!$enabled && isset(self::$loaded[$plugin])) {
|
||||
unset(self::$loaded[$plugin]);
|
||||
global $db;
|
||||
if ($db instanceof PDO) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
return true;
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,21 +154,42 @@ $pluginAdminMap = [];
|
|||
foreach ($pluginCatalog as $slug => $info) {
|
||||
$meta = $info['meta'] ?? [];
|
||||
$name = trim((string)($meta['name'] ?? $slug));
|
||||
$enabled = !empty($meta['enabled']);
|
||||
$enabled = \App\Core\PluginManager::isEnabled($slug); // Use database setting
|
||||
$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']);
|
||||
$enabledDependents = array_values(array_filter($dependents, static function($depSlug) {
|
||||
return \App\Core\PluginManager::isEnabled($depSlug); // Use database setting
|
||||
}));
|
||||
$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] = [
|
||||
'slug' => $slug,
|
||||
'name' => $name,
|
||||
'version' => (string)($meta['version'] ?? ''),
|
||||
'description' => (string)($meta['description'] ?? ''),
|
||||
'path' => $info['path'],
|
||||
'enabled' => $enabled,
|
||||
'loaded' => isset($pluginLoadedMap[$slug]),
|
||||
'dependencies' => $dependencies,
|
||||
|
|
@ -178,6 +199,9 @@ foreach ($pluginCatalog as $slug => $info) {
|
|||
'dependency_errors' => $pluginDependencyErrors[$slug] ?? [],
|
||||
'can_enable' => !$enabled && empty($missingDependencies),
|
||||
'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);
|
||||
} 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 {
|
||||
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 {
|
||||
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);
|
||||
Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable plugin. Check database connection and error logs.', false);
|
||||
} else {
|
||||
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§ion=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
|
||||
} elseif ($postAction === 'create_test_migration') {
|
||||
$timestamp = date('Ymd_His');
|
||||
|
|
@ -432,6 +533,139 @@ try {
|
|||
$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§ion=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', [
|
||||
'pills' => [],
|
||||
'sections' => $sectionRegistry,
|
||||
|
|
@ -456,6 +690,7 @@ if (is_array($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';
|
||||
|
|
|
|||
|
|
@ -22,6 +22,18 @@ if (!empty($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 ?? [];
|
||||
if (empty($tabs)) {
|
||||
$tabs = [
|
||||
|
|
@ -459,18 +471,87 @@ if (!empty($adminOverviewStatuses) && is_array($adminOverviewStatuses)) {
|
|||
<?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']) ?>">
|
||||
<div class="btn-group" role="group">
|
||||
<?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>
|
||||
<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_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: ?>
|
||||
<input type="hidden" name="action" value="plugin_enable">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success" <?= $plugin['can_enable'] ? '' : 'disabled' ?>>Enable</button>
|
||||
<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_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; ?>
|
||||
</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>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -564,8 +645,183 @@ endif; ?>
|
|||
<input type="hidden" name="filename" value="">
|
||||
</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>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Initialize tooltips
|
||||
if (typeof $ !== 'undefined' && $.fn.tooltip) {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
}
|
||||
|
||||
document.querySelectorAll('form.tm-confirm').forEach((form) => {
|
||||
form.addEventListener('submit', (event) => {
|
||||
const message = form.getAttribute('data-confirm') || 'Are you sure?';
|
||||
|
|
|
|||
Loading…
Reference in New Issue