xgit simple git

nexus

nexus

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

admin/addons.php


1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3must_admin();
4$PAGE_TITLE = 'Addons';
5$ADMIN_PAGE = 'addons';
6$flash = null;
7 
8if ($_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();
25include __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
142addon_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
147addon_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
154addon_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.
179DB::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");
186cfg_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");
192cfg_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]);
198DB::insert("INSERT INTO my_addon_logs (message) VALUES (?)", ['hello']);
199 
200// Users
201global $USER;  // currently logged-in user (or null)
202 
203// Settings
204$siteName = cfg('site_name', 'My Forum');
205cfg_set('my_addon_setting', 'value');
206 
207// Notifications
208add_notification($userId, 'my_addon', ['key' => 'value']);
209 
210// Karma
211add_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'; ?>