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  */
17 if (!defined('NEXUS')) exit('Forbidden');
18 
19 class 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 */
164 function 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 */
169 function addon_collect(string $hook, mixed $context = null): string {
170     return AddonManager::collect($hook, $context);
171 }
172 
173 /** Register a callback on a hook */
174 function addon_on(string $hook, callable $cb, int $priority = 10): void {
175     AddonManager::on($hook, $cb, $priority);
176 }