259 lines
8.7 KiB
PHP
259 lines
8.7 KiB
PHP
|
|
<?php
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Core\Services;
|
||
|
|
|
||
|
|
use Core\Http\Router;
|
||
|
|
use PDO;
|
||
|
|
use Throwable;
|
||
|
|
|
||
|
|
class Plugins
|
||
|
|
{
|
||
|
|
private static string $path = '';
|
||
|
|
private static array $plugins = [];
|
||
|
|
|
||
|
|
public static function init(string $path): void
|
||
|
|
{
|
||
|
|
self::$path = rtrim($path, '/');
|
||
|
|
self::sync();
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function all(): array
|
||
|
|
{
|
||
|
|
return self::$plugins;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function enabled(): array
|
||
|
|
{
|
||
|
|
return array_values(array_filter(self::$plugins, static function (array $plugin): bool {
|
||
|
|
return (bool)($plugin['is_enabled'] ?? false);
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function isEnabled(string $slug): bool
|
||
|
|
{
|
||
|
|
foreach (self::$plugins as $plugin) {
|
||
|
|
if ((string)($plugin['slug'] ?? '') === $slug) {
|
||
|
|
return (bool)($plugin['is_enabled'] ?? false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function adminNav(): array
|
||
|
|
{
|
||
|
|
$items = [];
|
||
|
|
foreach (self::enabled() as $plugin) {
|
||
|
|
$nav = $plugin['admin_nav'] ?? null;
|
||
|
|
if (!is_array($nav)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
$items[] = [
|
||
|
|
'label' => (string)($nav['label'] ?? ''),
|
||
|
|
'url' => (string)($nav['url'] ?? ''),
|
||
|
|
'roles' => array_values(array_filter((array)($nav['roles'] ?? []))),
|
||
|
|
'icon' => (string)($nav['icon'] ?? ''),
|
||
|
|
'slug' => (string)($plugin['slug'] ?? ''),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
return 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;
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
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) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|