xgit simple git

nexus

nexus

clone git clone https://kb.hax.al/nexus

includes/addons.php


1<?php
2/**
3 * Nexus Forum — Addon System
4 *
5 * Two hook patterns:
6 *
7 *  AddonManager::fire($hook, $data)
8 *    FILTER pattern — each callback receives the return value of the previous.
9 *    Use for transforming a single value (e.g. render_post_content: HTML string in/out).
10 *
11 *  AddonManager::collect($hook, $context)
12 *    COLLECTOR pattern — every callback receives the same $context array.
13 *    Non-empty string returns are accumulated and joined.
14 *    Use for output hooks where multiple addons each contribute HTML
15 *    (e.g. render_post_footer, render_topic_header, before_page_head).
16 */
17if (!defined('NEXUS')) exit('Forbidden');
18 
19class AddonManager {
20    private static array  $hooks   = [];
21    private static array  $loaded  = [];
22    private static ?array $active  = null;
23 
24    /* ── Boot all active addons ─────────────────────────────── */
25    public static function boot(): void {
26        foreach (self::activeList() as $slug) {
27            $main = ROOT . '/addons/' . $slug . '/main.php';
28            if (file_exists($main) && !isset(self::$loaded[$slug])) {
29                try {
30                    require_once $main;
31                    self::$loaded[$slug] = true;
32                } catch (Throwable $e) {
33                    error_log("Addon [$slug] boot error: " . $e->getMessage());
34                }
35            }
36        }
37    }
38 
39    /* ── Register a hook callback ────────────────────────────── */
40    public static function on(string $hook, callable $cb, int $priority = 10): void {
41        self::$hooks[$hook][$priority][] = $cb;
42    }
43 
44    /* ── FILTER: chain callbacks, each receives previous return ─ */
45    public static function fire(string $hook, mixed $data = null): mixed {
46        if (empty(self::$hooks[$hook])) return $data;
47        $buckets = self::$hooks[$hook];
48        ksort($buckets);
49        foreach ($buckets as $callbacks) {
50            foreach ($callbacks as $cb) {
51                try {
52                    $result = $cb($data);
53                    if ($result !== null) $data = $result;
54                } catch (Throwable $e) {
55                    error_log("Addon hook [$hook] filter error: " . $e->getMessage());
56                }
57            }
58        }
59        return $data;
60    }
61 
62    /* ── COLLECTOR: all callbacks receive same $context ──────── */
63    /* Each callback should return '' or an HTML string to append. */
64    public static function collect(string $hook, mixed $context = null): string {
65        if (empty(self::$hooks[$hook])) return '';
66        $buckets = self::$hooks[$hook];
67        ksort($buckets);
68        $output = '';
69        foreach ($buckets as $callbacks) {
70            foreach ($callbacks as $cb) {
71                try {
72                    $result = $cb($context);
73                    if (is_string($result) && $result !== '') {
74                        $output .= $result;
75                    }
76                } catch (Throwable $e) {
77                    error_log("Addon hook [$hook] collect error: " . $e->getMessage());
78                }
79            }
80        }
81        return $output;
82    }
83 
84    public static function hasHook(string $hook): bool {
85        return !empty(self::$hooks[$hook]);
86    }
87 
88    /* ── Active addon list ──────────────────────────────────── */
89    public static function activeList(): array {
90        if (self::$active !== null) return self::$active;
91        try {
92            $val = DB::val("SELECT value FROM settings WHERE `key`='active_addons'");
93            self::$active = $val ? (json_decode($val, true) ?: []) : [];
94        } catch (Throwable $e) {
95            self::$active = [];
96        }
97        return self::$active;
98    }
99 
100    /* ── Scan all installed addons ──────────────────────────── */
101    public static function all(): array {
102        $dir    = ROOT . '/addons';
103        $active = self::activeList();
104        $addons = [];
105        if (!is_dir($dir)) return [];
106        foreach (scandir($dir) as $slug) {
107            if ($slug[0] === '.') continue;
108            $manifest = $dir . '/' . $slug . '/nexus-addon.json';
109            if (!file_exists($manifest)) continue;
110            $info = json_decode(file_get_contents($manifest), true);
111            if (!$info || empty($info['name'])) continue;
112            $addons[$slug] = array_merge([
113                'slug' => $slug, 'name' => $slug, 'description' => '',
114                'version' => '1.0.0', 'author' => 'Unknown', 'url' => '',
115                'hooks' => [], 'requires' => [],
116            ], $info, [
117                'slug'   => $slug,
118                'active' => in_array($slug, $active),
119            ]);
120        }
121        return $addons;
122    }
123 
124    /* ── Activate / deactivate ──────────────────────────────── */
125    public static function activate(string $slug): bool {
126        $active = self::activeList();
127        if (in_array($slug, $active)) return true;
128        $install = ROOT . '/addons/' . $slug . '/install.php';
129        if (file_exists($install)) {
130            try { require $install; } catch (Throwable $e) {
131                error_log("Addon [$slug] install error: " . $e->getMessage());
132                return false;
133            }
134        }
135        $active[] = $slug;
136        self::saveActive($active);
137        self::$active = $active;
138        return true;
139    }
140 
141    public static function deactivate(string $slug): bool {
142        $active = self::activeList();
143        if (!in_array($slug, $active)) return true;
144        $uninstall = ROOT . '/addons/' . $slug . '/uninstall.php';
145        if (file_exists($uninstall)) {
146            try { require $uninstall; } catch (Throwable $e) {
147                error_log("Addon [$slug] uninstall error: " . $e->getMessage());
148            }
149        }
150        $active = array_values(array_filter($active, fn($s) => $s !== $slug));
151        self::saveActive($active);
152        self::$active = $active;
153        return true;
154    }
155 
156    private static function saveActive(array $active): void {
157        DB::upsert('settings', 'key', 'value', 'active_addons', json_encode(array_values($active)));
158    }
159}
160 
161/* ── Global convenience helpers ─────────────────────────────── */
162 
163/** FILTER hook — transforms a value through all callbacks */
164function addon_hook(string $hook, mixed $data = null): mixed {
165    return AddonManager::fire($hook, $data);
166}
167 
168/** COLLECTOR hook — every callback gets same $context, returns accumulated HTML */
169function addon_collect(string $hook, mixed $context = null): string {
170    return AddonManager::collect($hook, $context);
171}
172 
173/** Register a callback on a hook */
174function addon_on(string $hook, callable $cb, int $priority = 10): void {
175    AddonManager::on($hook, $cb, $priority);
176}