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 = [];
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
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§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
|
// 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§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', [
|
$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';
|
||||||
|
|
|
||||||
|
|
@ -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?';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue