(string)($nav['label'] ?? ''), 'url' => (string)($nav['url'] ?? ''), 'roles' => array_values(array_filter((array)($nav['roles'] ?? []))), 'icon' => (string)($nav['icon'] ?? ''), 'slug' => (string)($plugin['slug'] ?? ''), ]; } $items = array_values(array_filter($items, static function (array $item): bool { if ($item['label'] === '' || $item['url'] === '') { return false; } $slug = trim((string)($item['slug'] ?? '')); if ($slug !== '' && Auth::check() && !Permissions::can(Auth::role(), 'plugin.' . $slug)) { return false; } return true; })); $order = [ 'artists' => 10, 'releases' => 20, 'store' => 30, 'advanced-reporting' => 40, 'support' => 50, ]; usort($items, static function (array $a, array $b) use ($order): int { $aSlug = (string)($a['slug'] ?? ''); $bSlug = (string)($b['slug'] ?? ''); $aOrder = $order[$aSlug] ?? 1000; $bOrder = $order[$bSlug] ?? 1000; if ($aOrder === $bOrder) { return strcasecmp((string)($a['label'] ?? ''), (string)($b['label'] ?? '')); } return $aOrder <=> $bOrder; }); return $items; } public static function register(Router $router): void { foreach (self::enabled() as $plugin) { $entry = (string)($plugin['entry'] ?? 'plugin.php'); $entryPath = rtrim((string)($plugin['path'] ?? ''), '/') . '/' . $entry; if (!is_file($entryPath)) { continue; } $handler = require $entryPath; if (is_callable($handler)) { $handler($router); } } } public static function toggle(string $slug, bool $enabled): void { $db = Database::get(); if (!$db instanceof PDO) { return; } try { $stmt = $db->prepare("UPDATE ac_plugins SET is_enabled = :enabled, updated_at = NOW() WHERE slug = :slug"); $stmt->execute([ ':enabled' => $enabled ? 1 : 0, ':slug' => $slug, ]); } catch (Throwable $e) { } self::sync(); if (!$enabled) { $plugin = null; foreach (self::$plugins as $item) { if (($item['slug'] ?? '') === $slug) { $plugin = $item; break; } } if ($plugin && !empty($plugin['pages'])) { self::removeNavLinks($db, (array)$plugin['pages']); } } } public static function sync(): void { $filesystem = self::scanFilesystem(); $db = Database::get(); $dbRows = []; if ($db instanceof PDO) { try { $db->exec(" CREATE TABLE IF NOT EXISTS ac_plugins ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, slug VARCHAR(120) NOT NULL UNIQUE, name VARCHAR(200) NOT NULL, version VARCHAR(50) NOT NULL DEFAULT '0.0.0', is_enabled TINYINT(1) NOT NULL DEFAULT 0, installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); $stmt = $db->query("SELECT slug, is_enabled FROM ac_plugins"); $dbRows = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : []; } catch (Throwable $e) { $dbRows = []; } } $dbMap = []; foreach ($dbRows as $row) { $dbMap[(string)$row['slug']] = (int)$row['is_enabled']; } foreach ($filesystem as $slug => $plugin) { $plugin['is_enabled'] = (bool)($dbMap[$slug] ?? ($plugin['default_enabled'] ?? false)); $filesystem[$slug] = $plugin; if ($db instanceof PDO && !isset($dbMap[$slug])) { try { $stmt = $db->prepare(" INSERT INTO ac_plugins (slug, name, version, is_enabled) VALUES (:slug, :name, :version, :enabled) "); $stmt->execute([ ':slug' => $slug, ':name' => (string)($plugin['name'] ?? $slug), ':version' => (string)($plugin['version'] ?? '0.0.0'), ':enabled' => $plugin['is_enabled'] ? 1 : 0, ]); } catch (Throwable $e) { } } if ($db instanceof PDO && $plugin['is_enabled']) { $thisPages = $plugin['pages'] ?? []; if (is_array($thisPages) && $thisPages) { self::ensurePages($db, $thisPages); } } } self::$plugins = array_values($filesystem); } private static function scanFilesystem(): array { if (self::$path === '' || !is_dir(self::$path)) { return []; } $plugins = []; foreach (glob(self::$path . '/*/plugin.json') as $manifestPath) { $dir = dirname($manifestPath); $slug = basename($dir); $raw = file_get_contents($manifestPath); $decoded = json_decode($raw ?: '', true); if (!is_array($decoded)) { $decoded = []; } $plugins[$slug] = [ 'slug' => $slug, 'name' => (string)($decoded['name'] ?? $slug), 'version' => (string)($decoded['version'] ?? '0.0.0'), 'description' => (string)($decoded['description'] ?? ''), 'author' => (string)($decoded['author'] ?? ''), 'admin_nav' => is_array($decoded['admin_nav'] ?? null) ? $decoded['admin_nav'] : null, 'pages' => is_array($decoded['pages'] ?? null) ? $decoded['pages'] : [], 'entry' => (string)($decoded['entry'] ?? 'plugin.php'), 'default_enabled' => (bool)($decoded['default_enabled'] ?? false), 'path' => $dir, ]; } return $plugins; } private static function ensurePages(PDO $db, array $pages): void { foreach ($pages as $page) { if (!is_array($page)) { continue; } $slug = trim((string)($page['slug'] ?? '')); $title = trim((string)($page['title'] ?? '')); $content = (string)($page['content_html'] ?? ''); if ($slug === '' || $title === '') { continue; } try { $stmt = $db->prepare("SELECT id FROM ac_pages WHERE slug = :slug LIMIT 1"); $stmt->execute([':slug' => $slug]); if ($stmt->fetch()) { continue; } $insert = $db->prepare(" INSERT INTO ac_pages (title, slug, content_html, is_published, is_home, is_blog_index) VALUES (:title, :slug, :content_html, 1, 0, 0) "); $insert->execute([ ':title' => $title, ':slug' => $slug, ':content_html' => $content, ]); } catch (Throwable $e) { } } } private static function removeNavLinks(PDO $db, array $pages): void { foreach ($pages as $page) { if (!is_array($page)) { continue; } $slug = trim((string)($page['slug'] ?? '')); if ($slug === '') { continue; } $url = '/' . ltrim($slug, '/'); try { $stmt = $db->prepare("DELETE FROM ac_nav_links WHERE url = :url"); $stmt->execute([':url' => $url]); } catch (Throwable $e) { } } } }