xgit simple git

nexus

nexus

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

admin/addons.php

1 <?php
2 require_once __DIR__ . '/../includes/bootstrap.php';
3 must_admin();
4 $PAGE_TITLE = 'Addons';
5 $ADMIN_PAGE = 'addons';
6 $flash = null;
7 
8 if ($_SERVER['REQUEST_METHOD'] === 'POST' && csrf_ok()) {
9     $action = post('action');
10     $slug   = preg_replace('/[^a-zA-Z0-9_\-]/', '', post('slug'));
11 
12     if ($action === 'activate' && $slug) {
13         if (AddonManager::activate($slug)) {
14             $flash = ['ok', "Addon «{$slug}» activated."];
15         } else {
16             $flash = ['err', "Failed to activate «{$slug}». Check error logs."];
17         }
18     } elseif ($action === 'deactivate' && $slug) {
19         AddonManager::deactivate($slug);
20         $flash = ['ok', "Addon «{$slug}» deactivated."];
21     }
22 }
23 
24 $addons = AddonManager::all();
25 include __DIR__ . '/../views/partials/admin_layout.php';
26 ?>
27 
28 <?php if ($flash): ?>
29   <div class="alert <?= $flash[0] ?>"><?= e($flash[1]) ?></div>
30 <?php endif; ?>
31 
32 <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
33   <div>
34     <h1 style="font-size:1.3rem;font-weight:700;margin:0">🧩 Addons</h1>
35     <p style="color:var(--muted);font-size:14px;margin:4px 0 0">Extend your forum with addons. Place addon folders in <code>addons/</code>.</p>
36   </div>
37   <a href="#developer-docs" class="btn-ghost btn-sm">📖 Developer Docs</a>
38 </div>
39 
40 <?php if (empty($addons)): ?>
41   <div class="acard">
42     <div class="acard-body" style="text-align:center;padding:40px">
43       <div style="font-size:3rem;margin-bottom:12px">🧩</div>
44       <h2 style="font-size:1.1rem;margin-bottom:8px">No addons installed</h2>
45       <p style="color:var(--muted);font-size:14px;margin-bottom:16px">
46         Place addon folders in <code><?= e(ROOT) ?>/addons/</code> and they will appear here.
47       </p>
48       <a href="#developer-docs" class="btn-primary">Create Your First Addon →</a>
49     </div>
50   </div>
51 <?php else: ?>
52   <div style="display:flex;flex-direction:column;gap:14px;margin-bottom:28px">
53     <?php foreach ($addons as $slug => $addon): ?>
54       <div class="acard addon-card <?= $addon['active'] ? 'addon-active' : '' ?>">
55         <div class="addon-card-inner">
56           <div class="addon-info">
57             <div class="addon-header">
58               <span class="addon-name"><?= e($addon['name']) ?></span>
59               <span class="addon-version">v<?= e($addon['version']) ?></span>
60               <?php if ($addon['active']): ?>
61                 <span class="addon-status-badge active">● Active</span>
62               <?php else: ?>
63                 <span class="addon-status-badge inactive">○ Inactive</span>
64               <?php endif; ?>
65             </div>
66             <p class="addon-desc"><?= e($addon['description']) ?></p>
67             <div class="addon-meta">
68               <span>By <strong><?= e($addon['author']) ?></strong></span>
69               <?php if ($addon['url']): ?>
70                 <a href="<?= e($addon['url']) ?>" target="_blank" rel="noopener" style="font-size:12px">🔗 Website</a>
71               <?php endif; ?>
72               <span class="addon-slug-tag"><code><?= e($slug) ?></code></span>
73               <?php if (!empty($addon['hooks'])): ?>
74                 <span style="font-size:12px;color:var(--muted)">
75                   Hooks: <?= implode(', ', array_map('htmlspecialchars', $addon['hooks'])) ?>
76                 </span>
77               <?php endif; ?>
78             </div>
79           </div>
80           <div class="addon-actions">
81             <form method="POST">
82               <?= csrf_input() ?>
83               <input type="hidden" name="slug" value="<?= e($slug) ?>">
84               <?php if ($addon['active']): ?>
85                 <input type="hidden" name="action" value="deactivate">
86                 <button class="btn-warn btn-sm" onclick="return confirm('Deactivate this addon?')">
87                   ⏸ Deactivate
88                 </button>
89               <?php else: ?>
90                 <input type="hidden" name="action" value="activate">
91                 <button class="btn-ok btn-sm">▶ Activate</button>
92               <?php endif; ?>
93             </form>
94           </div>
95         </div>
96       </div>
97     <?php endforeach; ?>
98   </div>
99 <?php endif; ?>
100 
101 <!-- ── Developer Documentation ─────────────────────── -->
102 <div class="acard" id="developer-docs">
103   <div class="acard-head">
104     <h2>📖 Developer Documentation — How to Create an Addon</h2>
105   </div>
106   <div class="acard-body addon-docs">
107 
108     <h3>Overview</h3>
109     <p>Addons extend Nexus Forum without modifying core files. Each addon is a folder inside <code>addons/</code> containing at minimum a <code>nexus-addon.json</code> manifest and a <code>main.php</code> entry point.</p>
110 
111     <h3>Directory Structure</h3>
112     <pre><code>addons/
113 └── your-addon-slug/
114     ├── nexus-addon.json   ← Required: manifest
115     ├── main.php           ← Required: entry point, registers hooks
116     ├── install.php        ← Optional: runs on first activation
117     ├── uninstall.php      ← Optional: runs on deactivation
118     └── assets/            ← Optional: CSS, JS, images</code></pre>
119 
120     <h3>1. The Manifest (<code>nexus-addon.json</code>)</h3>
121     <pre><code>{
122   "name":        "My Addon",
123   "description": "A short description of what this addon does.",
124   "version":     "1.0.0",
125   "author":      "Your Name",
126   "url":         "https://yoursite.com/addon",
127   "hooks": [
128     "after_post_saved",
129     "render_post_footer"
130   ],
131   "requires": {
132     "nexus": ">=14"
133   }
134 }</code></pre>
135 
136     <h3>2. The Entry Point (<code>main.php</code>)</h3>
137     <p>Use <code>addon_on($hook, $callback)</code> to register listeners. Hooks fire in priority order (default 10). Lower number = runs first.</p>
138     <pre><code>&lt;?php
139 // main.php — loaded once per request if addon is active
140 
141 // Example: add a footer line after every post
142 addon_on('render_post_footer', function(array $post): string {
143     return '&lt;div class="custom-footer"&gt;Addon says hi on post #' . $post['id'] . '&lt;/div&gt;';
144 });
145 
146 // Example: run code when a new topic is created
147 addon_on('after_topic_created', function(array $data): void {
148     // $data has: topic_id, title, slug, category_id, user_id
149     // Do something: call external API, log it, etc.
150     error_log('New topic: ' . $data['title']);
151 });
152 
153 // Example: filter post content before display
154 addon_on('render_post_content', function(string $html): string {
155     // Transform HTML before output
156     return str_replace(':-)', '😊', $html);
157 });</code></pre>
158 
159     <h3>3. Available Hooks</h3>
160     <table class="atable" style="margin-top:8px">
161       <thead><tr><th>Hook</th><th>Receives</th><th>Returns</th><th>When</th></tr></thead>
162       <tbody>
163         <tr><td><code>after_topic_created</code></td><td><code>array</code> topic data</td><td><em>void</em></td><td>After a new topic is saved</td></tr>
164         <tr><td><code>after_reply_saved</code></td><td><code>array</code> post data</td><td><em>void</em></td><td>After a reply is posted</td></tr>
165         <tr><td><code>after_user_registered</code></td><td><code>array</code> user data</td><td><em>void</em></td><td>After a new user registers</td></tr>
166         <tr><td><code>render_post_content</code></td><td><code>string</code> HTML</td><td><code>string</code> HTML</td><td>Before post content is output (filter)</td></tr>
167         <tr><td><code>render_post_footer</code></td><td><code>array</code> post row</td><td><code>string</code> HTML</td><td>Below each post body</td></tr>
168         <tr><td><code>render_topic_header</code></td><td><code>array</code> topic row</td><td><code>string</code> HTML</td><td>Above topic content</td></tr>
169         <tr><td><code>admin_nav_items</code></td><td><code>array</code> nav items</td><td><code>array</code></td><td>Admin sidebar links</td></tr>
170         <tr><td><code>user_karma_changed</code></td><td><code>array</code> {user_id, old, new}</td><td><em>void</em></td><td>When karma is adjusted</td></tr>
171         <tr><td><code>before_page_head</code></td><td><code>string</code> HTML</td><td><code>string</code> HTML</td><td>Inside &lt;head&gt; on every page</td></tr>
172       </tbody>
173     </table>
174 
175     <h3>4. Install / Uninstall Scripts</h3>
176     <pre><code>&lt;?php
177 // install.php — runs once when admin activates the addon
178 // Use this to create tables, insert settings, etc.
179 DB::connect()->exec("
180     CREATE TABLE IF NOT EXISTS my_addon_logs (
181         id         INTEGER PRIMARY KEY AUTOINCREMENT,
182         message    TEXT,
183         created_at TEXT DEFAULT CURRENT_TIMESTAMP
184     )
185 ");
186 cfg_set('my_addon_enabled', '1');</code></pre>
187 
188     <pre><code>&lt;?php
189 // uninstall.php — runs when admin deactivates the addon
190 // Clean up tables and settings (optional — you may want to keep data)
191 // DB::run("DROP TABLE IF EXISTS my_addon_logs");
192 cfg_set('my_addon_enabled', '0');</code></pre>
193 
194     <h3>5. Accessing Nexus APIs</h3>
195     <p>All Nexus helpers are available inside addon code:</p>
196     <pre><code>// Database
197 $rows = DB::rows("SELECT * FROM topics WHERE category_id=?", [1]);
198 DB::insert("INSERT INTO my_addon_logs (message) VALUES (?)", ['hello']);
199 
200 // Users
201 global $USER;  // currently logged-in user (or null)
202 
203 // Settings
204 $siteName = cfg('site_name', 'My Forum');
205 cfg_set('my_addon_setting', 'value');
206 
207 // Notifications
208 add_notification($userId, 'my_addon', ['key' => 'value']);
209 
210 // Karma
211 add_karma($userId, 5);  // award 5 karma points
212 
213 // URL helpers
214 $url = u('forum/topic.php?slug=hello');  // respects BASE path</code></pre>
215 
216     <h3>6. Example Addon</h3>
217     <p>A complete working example is included at <code>addons/example-hello-world/</code>.</p>
218   </div>
219 </div>
220 
221 <style>
222 .addon-card { transition:box-shadow .18s; }
223 .addon-card.addon-active { border-color:var(--green); }
224 .addon-card-inner { display:flex; align-items:flex-start; gap:16px; justify-content:space-between; flex-wrap:wrap; }
225 .addon-info { flex:1; min-width:0; }
226 .addon-header { display:flex; align-items:center; gap:10px; margin-bottom:6px; flex-wrap:wrap; }
227 .addon-name { font-size:15px; font-weight:700; }
228 .addon-version { font-size:11px; background:var(--bg); border:1px solid var(--border); border-radius:8px; padding:1px 7px; color:var(--muted); }
229 .addon-status-badge { font-size:11px; font-weight:700; padding:2px 8px; border-radius:8px; }
230 .addon-status-badge.active   { background:#f0fdf4; color:#166534; border:1px solid #bbf7d0; }
231 .addon-status-badge.inactive { background:#f8fafc; color:#94a3b8; border:1px solid #e2e8f0; }
232 .addon-desc { font-size:14px; color:var(--muted); margin:0 0 8px; }
233 .addon-meta { display:flex; align-items:center; gap:14px; font-size:13px; color:var(--muted); flex-wrap:wrap; }
234 .addon-slug-tag code { background:var(--bg); padding:1px 6px; border-radius:4px; font-size:12px; border:1px solid var(--border); }
235 .addon-actions { flex-shrink:0; }
236 .addon-docs h3 { font-size:14px; font-weight:700; margin:20px 0 8px; color:var(--text); }
237 .addon-docs p  { font-size:14px; color:var(--muted); margin-bottom:10px; line-height:1.7; }
238 .addon-docs pre { background:#1e293b; color:#e2e8f0; padding:16px; border-radius:var(--r); overflow-x:auto; margin:8px 0 16px; font-size:13px; line-height:1.6; }
239 .addon-docs code { font-family:var(--mono); font-size:13px; }
240 .addon-docs p code { background:var(--bg); border:1px solid var(--border); padding:1px 6px; border-radius:4px; }
241 </style>
242 
243 <?php include __DIR__ . '/../views/partials/admin_layout_end.php'; ?>