1 <div align="center"> 2 3 <img src="https://img.shields.io/badge/PHP-8.0+-777BB4?style=for-the-badge&logo=php&logoColor=white" alt="PHP 8.0+"> 4 <img src="https://img.shields.io/badge/SQLite-003B57?style=for-the-badge&logo=sqlite&logoColor=white" alt="SQLite"> 5 <img src="https://img.shields.io/badge/MySQL-4479A1?style=for-the-badge&logo=mysql&logoColor=white" alt="MySQL"> 6 <img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="MIT License"> 7 <img src="https://img.shields.io/badge/Zero-Dependencies-orange?style=for-the-badge" alt="Zero Dependencies"> 8 9 <br><br> 10 11 ``` 12 ███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗ 13 ████╗ ██║██╔════╝╚██╗██╔╝██║ ██║██╔════╝ 14 ██╔██╗ ██║█████╗ ╚███╔╝ ██║ ██║███████╗ 15 ██║╚██╗██║██╔══╝ ██╔██╗ ██║ ██║╚════██║ 16 ██║ ╚████║███████╗██╔╝ ██╗╚██████╔╝███████║ 17 ╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ 18 ``` 19 20 ### A modern, full-featured discussion platform built with pure PHP 8 21 22 **No frameworks · No npm · No Docker · Just upload and run** 23 24 [Features](#-features) · [Quick Start](#-quick-start) · [Installation](#-installation) · [Configuration](#️-configuration) · [Addons](#-addon-system) · [API](#-api-reference) · [FAQ](#-faq) 25 26 </div> 27 28 --- 29 30 ## ✨ Features 31 32 ### 💬 Forum Core 33 - **Categories** with icons, colours, sub-categories, and per-role permissions 34 - **Topics & threaded replies** with pagination 35 - **Full Markdown editor** — Google Docs-style toolbar with SVG icons, Write/Preview tabs 36 - **Syntax-highlighted code blocks** via Prism.js (200+ languages, lazy-loaded only when needed) 37 - **Styled blockquotes** with gradient left border 38 - **Post permalinks** — every post gets `#post-{id}` + a 🔗 copy-link button (pagination-aware) 39 - **Inline image upload** — paste, drag-drop, or file picker directly in the editor 40 - **Media auto-embeds** — paste a URL and it becomes a player (14 platforms) 41 - **@mentions** with live autocomplete 42 - **Live search** — finds topics AND post content, links directly to the matching post on the correct page 43 44 ### 🔐 Roles & Permissions 45 46 | Role | Level | Can Do | 47 |---|---|---| 48 | **Guest** | 0 | Read public categories | 49 | **Member** | 10 | Post, reply, like, message, friend | 50 | **Moderator** | 20 | + Pin/close topics, edit any post | 51 | **Admin** | 30 | Full access + admin panel | 52 53 **Per-category permissions** — set independently for reading, posting, and replying: 54 55 | Permission | Options | 56 |---|---| 57 | Who can **read** | 🌐 Everyone · 👤 Members · 🛡️ Moderators+ · 👑 Admins | 58 | Who can **post topics** | Same four options | 59 | Who can **reply** | Same four options | 60 61 ### ⭐ Karma System 62 63 Eight progressive tiers earned through activity: 64 65 | Tier | Points | Icon | 66 |---|---|---| 67 | Newcomer | 0–9 | 🌱 | 68 | Member | 10–49 | 💬 | 69 | Regular | 50–99 | ⭐ | 70 | Contributor | 100–249 | 🌟 | 71 | Veteran | 250–499 | 🔥 | 72 | Expert | 500–999 | 💎 | 73 | Elite | 1000–2499 | 👑 | 74 | Legend | 2500+ | 🏆 | 75 76 Admins can manually adjust karma (Add / Subtract / Set) with an optional reason that notifies the user. 77 78 ### 📬 Private Messages 79 - Inbox/Sent with unread badges 80 - Conversation threads displayed as chat bubbles 81 - Read receipts (✓ sent · ✓✓ read) 82 - Online status indicator (green if active in last 5 min) 83 - Live user search autocomplete 84 85 ### 🔍 Search 86 - **Topics tab** — title matches 87 - **Posts tab** — content matches, jumps directly to the exact post on the correct page 88 - **Users tab** — username + bio search 89 - Live header dropdown shows topic + post results simultaneously 90 91 ### 🧩 Addon System 92 Extend the forum by dropping a folder into `addons/` and clicking Activate. No core file edits needed. Full PHP API access with 9 event hooks. 93 94 ### 🔒 Security 95 - CSRF tokens on all forms and AJAX 96 - bcrypt password hashing (cost 12) 97 - Math captcha (admin toggle, separate for posts and new topics) 98 - Rate limiting with live countdown 99 - Auto-generated `.htaccess` protection for `data/` and `uploads/` 100 - Security headers: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, HSTS on HTTPS 101 - All SQL via PDO prepared statements 102 103 ### 🗄️ Database Support 104 105 | Driver | Version | 106 |---|---| 107 | **SQLite** | 3.x — zero configuration, single file | 108 | **MySQL** | 5.7+ | 109 | **MariaDB** | 10.3+ | 110 111 Schema migrates automatically on every request — update files and existing installs upgrade themselves. 112 113 --- 114 115 ## 🚀 Quick Start 116 117 ### Shared Hosting (5 minutes) 118 119 ```bash 120 # 1. Upload to your server 121 scp -r forum-clean/ user@host:~/public_html/forum/ 122 123 # 2. Set directory permissions 124 chmod 755 data/ public/uploads/ public/uploads/avatars/ 125 126 # 3. Visit the installer 127 # https://yoursite.com/forum/install/ 128 129 # 4. Complete the 3-step wizard, then remove /install/ 130 rm -rf install/ 131 ``` 132 133 ### Local Development 134 135 ```bash 136 # PHP built-in server — SQLite, zero config 137 cd forum-clean/ 138 php -S localhost:8080 139 # open http://localhost:8080/install/ 140 ``` 141 142 ### Docker (Apache) 143 144 ```dockerfile 145 # Dockerfile 146 FROM php:8.2-apache 147 RUN docker-php-ext-install pdo pdo_sqlite 148 RUN a2enmod rewrite 149 COPY forum-clean/ /var/www/html/ 150 RUN chown -R www-data:www-data /var/www/html/data \ 151 /var/www/html/public/uploads 152 ``` 153 154 ```bash 155 docker build -t nexus-forum . 156 docker run -p 8080:80 nexus-forum 157 # open http://localhost:8080/install/ 158 ``` 159 160 --- 161 162 ## 📦 Installation 163 164 ### Requirements 165 166 | Item | Minimum | Notes | 167 |---|---|---| 168 | PHP | **8.0** | 8.2+ recommended | 169 | PDO | Required | `pdo_sqlite` or `pdo_mysql` | 170 | GD | Optional | For image thumbnails | 171 | Web server | Apache or Nginx | See configs below | 172 | Disk | 10 MB | Plus user uploads | 173 174 ### Step-by-step 175 176 **1 — Upload files** 177 178 The forum works at any URL path: 179 - `https://yoursite.com/` 180 - `https://yoursite.com/forum/` 181 - `https://yoursite.com/community/board/` 182 183 The `BASE` path is auto-detected. No `.env` changes needed. 184 185 **2 — Set permissions** 186 187 ```bash 188 chmod 755 data/ 189 chmod 755 public/uploads/ 190 chmod 755 public/uploads/avatars/ 191 ``` 192 193 **3 — Run the web installer** 194 195 Visit `/install/` — the 3-step wizard: 196 197 | Step | What happens | 198 |---|---| 199 | **1 — Requirements** | Checks PHP version, extensions, directory permissions | 200 | **2 — Database** | Choose SQLite or MySQL, enter site name + admin credentials | 201 | **3 — Done** | Writes config, runs migration, shows security checklist | 202 203 **4 — Post-install (automatic)** 204 205 The installer automatically creates: 206 - `data/.htaccess` — denies all web access to the database directory 207 - `public/uploads/.htaccess` — blocks PHP execution in uploads folder 208 - `data/db_config.php` → `chmod 0640` 209 - `data/forum.db` → `chmod 0640` (SQLite only) 210 - `data/installed.lock` — prevents re-running the installer 211 212 ### MySQL Setup 213 214 ```sql 215 CREATE DATABASE nexus_forum 216 CHARACTER SET utf8mb4 217 COLLATE utf8mb4_unicode_ci; 218 219 CREATE USER 'nexus'@'localhost' IDENTIFIED BY 'your_strong_password'; 220 GRANT ALL PRIVILEGES ON nexus_forum.* TO 'nexus'@'localhost'; 221 FLUSH PRIVILEGES; 222 ``` 223 224 Then select "MySQL / MariaDB" in the installer. 225 226 --- 227 228 ## ⚙️ Configuration 229 230 ### Apache 231 232 ```apache 233 <VirtualHost *:80> 234 ServerName forum.yoursite.com 235 DocumentRoot /var/www/nexus-forum 236 237 <Directory /var/www/nexus-forum> 238 AllowOverride All 239 Require all granted 240 </Directory> 241 242 # Protect database directory 243 <Directory /var/www/nexus-forum/data> 244 Require all denied 245 </Directory> 246 </VirtualHost> 247 ``` 248 249 ### Nginx 250 251 ```nginx 252 server { 253 listen 80; 254 server_name forum.yoursite.com; 255 root /var/www/nexus-forum; 256 index index.php; 257 258 # Block sensitive paths 259 location ~ ^/(data|includes)/ { 260 deny all; 261 return 404; 262 } 263 location ~ \.(db|sqlite|lock)$ { 264 deny all; 265 return 404; 266 } 267 # Block PHP execution in uploads 268 location ~ ^/public/uploads/.*\.php$ { 269 deny all; 270 } 271 272 location ~ \.php$ { 273 fastcgi_pass unix:/var/run/php/php8.2-fpm.sock; 274 include fastcgi_params; 275 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 276 } 277 278 location / { 279 try_files $uri $uri/ =404; 280 } 281 } 282 ``` 283 284 ### Admin Settings Panel 285 286 Visit **Admin → Settings** to configure: 287 288 | Setting | Description | 289 |---|---| 290 | Site name & description | Header and `<title>` | 291 | Topics / Posts per page | Pagination sizes | 292 | Post captcha | Math captcha on replies (spam protection) | 293 | Topic captcha | Math captcha on new topics | 294 | Rate limiting | Seconds between posts | 295 | Max upload size | Image upload limit | 296 | Registration | Open or closed | 297 298 --- 299 300 ## 📁 Project Structure 301 302 ``` 303 nexus-forum/ 304 │ 305 ├── 📁 addons/ # Drop addon folders here 306 │ └── example-hello-world/ # Sample addon (see Addon docs) 307 │ 308 ├── 📁 admin/ # Admin panel 309 │ ├── index.php # Dashboard 310 │ ├── users.php # User list 311 │ ├── user.php # Edit user + karma manager 312 │ ├── categories.php # Categories + role permissions 313 │ ├── topics.php # Topic moderation 314 │ ├── settings.php # Site settings 315 │ ├── themes.php # Theme switching 316 │ └── addons.php # Addon manager + developer docs 317 │ 318 ├── 📁 api/ # JSON endpoints (POST) 319 │ ├── reply.php # Post a reply 320 │ ├── edit.php # Edit a post 321 │ ├── delete.php # Delete a post 322 │ ├── like.php # Like / unlike 323 │ ├── upload.php # Image upload 324 │ ├── notifications.php # Mark read 325 │ ├── friend.php # Friend requests 326 │ ├── karma.php # Admin karma adjust 327 │ ├── chat.php # Private message actions 328 │ ├── search.php # Live search (topics + posts) 329 │ ├── search_users.php # User autocomplete 330 │ └── topic_action.php # Pin / close / delete topic 331 │ 332 ├── 📁 auth/ # login · register · logout 333 ├── 📁 data/ # Created by installer (not web-accessible) 334 ├── 📁 forum/ # category · topic · new-topic · search 335 │ 336 ├── 📁 includes/ # Core library (not web-accessible) 337 │ ├── bootstrap.php # Loads everything, boots addons 338 │ ├── config.php # Path detection, security headers 339 │ ├── db.php # PDO multi-driver DB class 340 │ ├── functions.php # All helpers 341 │ ├── markdown.php # Markdown + embed renderer 342 │ └── addons.php # AddonManager class 343 │ 344 ├── 📁 install/ # DELETE after setup 345 ├── 📁 messages/ # inbox · compose · view 346 │ 347 ├── 📁 public/ 348 │ ├── css/main.css # ~2400 lines — full design system 349 │ ├── js/app.js # ~1000 lines — all client JS 350 │ └── uploads/ # User images (PHP execution blocked) 351 │ 352 ├── 📁 users/ # profile · edit · search 353 │ 354 ├── 📁 views/partials/ 355 │ ├── layout.php # Header, sidebar, nav 356 │ ├── layout_end.php # Footer, Prism.js loader, app.js 357 │ ├── admin_layout.php # Admin sidebar 358 │ └── editor_toolbar.php # Reusable Markdown toolbar (SVG icons) 359 │ 360 └── index.php # Homepage 361 ``` 362 363 --- 364 365 ## 📺 Media Embeds 366 367 Paste any of these URLs alone on a line in a post and it auto-embeds as a player: 368 369 | Platform | Supported | 370 |---|---| 371 | YouTube | Videos, Shorts, YouTube Music | 372 | Vimeo | Videos | 373 | Twitch | Live streams, VODs | 374 | Dailymotion | Videos | 375 | Streamable | Clips | 376 | Rumble | Videos | 377 | Spotify | Tracks, albums, playlists, podcast episodes, artist pages | 378 | SoundCloud | Tracks | 379 | Loom | Screen recordings | 380 | CodePen | Pens | 381 | JSFiddle | Fiddles | 382 | Twitter / X | Tweets | 383 | TED Talks | Talks | 384 | Bandcamp | Tracks | 385 386 --- 387 388 ## 🧩 Addon System 389 390 ### Installing 391 392 1. Drop the addon folder into `addons/` 393 2. **Admin → Addons → ▶ Activate** 394 395 ### Creating an Addon 396 397 **`nexus-addon.json`** — manifest (required) 398 399 ```json 400 { 401 "name": "My Addon", 402 "description": "What this addon does.", 403 "version": "1.0.0", 404 "author": "Your Name", 405 "url": "https://yoursite.com", 406 "hooks": ["after_topic_created", "render_post_footer"], 407 "requires": { "nexus": ">=14" } 408 } 409 ``` 410 411 **`main.php`** — entry point (required) 412 413 ```php 414 <?php 415 // Runs on every request when the addon is active 416 417 // Inject HTML below every post 418 addon_on('render_post_footer', function(array $post): string { 419 return '<div class="my-badge">✓ Verified</div>'; 420 }); 421 422 // React to new topics 423 addon_on('after_topic_created', function(array $data): void { 424 // $data: topic_id, title, slug, category_id, user_id 425 // Call external webhook, send Slack message, etc. 426 // file_get_contents('https://hooks.example.com?title=' . urlencode($data['title'])); 427 }); 428 429 // Filter post HTML before display 430 addon_on('render_post_content', function(string $html): string { 431 return str_replace(':-)', '😊', $html); 432 }); 433 ``` 434 435 **`install.php`** — runs on activation (optional) 436 437 ```php 438 <?php 439 DB::connect()->exec("CREATE TABLE IF NOT EXISTS my_log ( 440 id INTEGER PRIMARY KEY AUTOINCREMENT, 441 message TEXT NOT NULL, 442 created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP 443 )"); 444 cfg_set('my_addon_active', '1'); 445 ``` 446 447 **`uninstall.php`** — runs on deactivation (optional) 448 449 ```php 450 <?php 451 cfg_set('my_addon_active', '0'); 452 // DB::run("DROP TABLE IF EXISTS my_log"); // uncomment to clean up 453 ``` 454 455 ### Hook Reference 456 457 | Hook | Data passed | Return | Fires when | 458 |---|---|---|---| 459 | `after_topic_created` | `array` {topic_id, title, slug, category_id, user_id} | void | Topic saved | 460 | `after_reply_saved` | `array` {post_id, topic_id, user_id} | void | Reply posted | 461 | `after_user_registered` | `array` {user_id, username, email} | void | Registration | 462 | `render_post_content` | `string` HTML | `string` HTML | Before post output | 463 | `render_post_footer` | `array` post row | `string` HTML | Below post body | 464 | `render_topic_header` | `array` topic row | `string` HTML | Above topic | 465 | `user_karma_changed` | `array` {user_id, old, new, by} | void | Karma adjusted | 466 | `admin_nav_items` | `array` items | `array` | Admin sidebar | 467 | `before_page_head` | `string` HTML | `string` HTML | Inside `<head>` | 468 469 ### Addon PHP API 470 471 ```php 472 // Database 473 DB::rows("SELECT * FROM topics WHERE category_id=?", [$catId]); 474 DB::row("SELECT * FROM users WHERE id=?", [$uid]); 475 DB::insert("INSERT INTO my_log (message) VALUES (?)", [$msg]); 476 DB::run("UPDATE my_table SET col=? WHERE id=?", [$val, $id]); 477 DB::val("SELECT COUNT(*) FROM posts WHERE topic_id=?", [$tid]); 478 479 // Current user 480 global $USER; // array or null 481 482 // Settings 483 $val = cfg('site_name', 'My Forum'); 484 cfg_set('my_key', 'my_value'); 485 486 // Notifications 487 add_notification($userId, 'my_type', ['key' => 'value']); 488 489 // Karma 490 add_karma($userId, 10); // add 10 points 491 492 // URL helpers 493 $url = u('forum/topic.php?slug=' . urlencode($slug)); 494 $assetUrl = asset('js/app.js'); 495 ``` 496 497 --- 498 499 ## 🎨 Theming 500 501 All design tokens are CSS custom properties in `public/css/main.css`: 502 503 ```css 504 :root { 505 /* Brand colours */ 506 --blue: #3b82f6; 507 --blue-d: #2563eb; 508 --blue-l: #eff6ff; 509 --green: #22c55e; 510 --red: #ef4444; 511 --purple: #8b5cf6; 512 513 /* Surfaces */ 514 --bg: #f1f5f9; /* page background */ 515 --surface: #ffffff; /* cards */ 516 --border: #e2e8f0; /* borders */ 517 --border-l: #f1f5f9; /* light borders */ 518 519 /* Text */ 520 --text: #0f172a; 521 --muted: #64748b; 522 --faint: #94a3b8; 523 524 /* Typography */ 525 --font: 'Inter', -apple-system, sans-serif; 526 --mono: 'JetBrains Mono', 'Fira Code', monospace; 527 528 /* Sizing */ 529 --r: 6px; /* border radius */ 530 --r-lg: 10px; 531 --r-xl: 16px; 532 --header: 56px; 533 --sidebar: 220px; 534 } 535 ``` 536 537 Override any variable in a custom stylesheet, or inject one via the `before_page_head` addon hook. 538 539 --- 540 541 ## 🔑 API Reference 542 543 All endpoints accept `POST` (or `GET` for search) and expect a `csrf` parameter from the `NX.csrf` global. 544 545 | Endpoint | Auth | Description | 546 |---|---|---| 547 | `POST /api/reply.php` | Member | Post a reply (`slug`, `content`) | 548 | `POST /api/edit.php` | Author/Admin | Edit post (`post_id`, `content`) | 549 | `POST /api/delete.php` | Author/Admin | Delete post (`post_id`) | 550 | `POST /api/like.php` | Member | Like/unlike (`post_id`) | 551 | `POST /api/upload.php` | Member | Upload image (`file`) → `{url}` | 552 | `POST /api/topic_action.php` | Mod/Admin | Pin/close/delete topic | 553 | `POST /api/friend.php` | Member | Friend actions (`action`, `other_id`) | 554 | `POST /api/karma.php` | Admin | Adjust karma (`user_id`, `amount`, `op`) | 555 | `POST /api/notifications.php` | Member | Mark notifications read | 556 | `GET /api/search.php?q=` | Public | Live search → `{topics, posts}` | 557 | `GET /api/search_users.php?q=` | Public | User autocomplete → `[{id, username, avatar}]` | 558 | `POST /api/chat.php` | Member | DM actions (`action`: send/poll/load/conversations) | 559 560 **Quick example — posting a reply:** 561 562 ```javascript 563 const fd = new FormData(); 564 fd.append('slug', 'my-topic-slug'); 565 fd.append('content', 'My reply content here.'); 566 fd.append('csrf', NX.csrf); // NX is the global config object 567 568 const res = await fetch(NX.base + '/api/reply.php', { method: 'POST', body: fd }); 569 const data = await res.json(); 570 // Success: { ok: true, post: { id, content, post_num, created_at, username, ... } } 571 // Error: { error: "message", ... } 572 ``` 573 574 --- 575 576 ## 🧰 Developer Reference 577 578 ### Helper Functions 579 580 | Function | Returns | Description | 581 |---|---|---| 582 | `e($val)` | `string` | `htmlspecialchars()` — always use when outputting user data | 583 | `u($path)` | `string` | URL with BASE prefix | 584 | `asset($path)` | `string` | Public asset URL | 585 | `go($path)` | never | Redirect | 586 | `post($key, $default)` | `mixed` | `$_POST[$key] ?? $default` | 587 | `get($key, $default)` | `mixed` | `$_GET[$key] ?? $default` | 588 | `cfg($key, $default)` | `string` | Read a setting (cached) | 589 | `cfg_set($key, $value)` | void | Write a setting | 590 | `sanitise($input)` | `string` | Strip HTML/PHP/scripts from user input | 591 | `must_login()` | void | Redirect if not authenticated | 592 | `must_admin()` | void | Redirect if not admin | 593 | `is_admin()` | `bool` | Check admin role | 594 | `current_user()` | `?array` | Current user row or null | 595 | `csrf_input()` | `string` | `<input type="hidden" name="csrf" value="...">` | 596 | `csrf_ok()` | `bool` | Validate CSRF token | 597 | `render_post($raw)` | `string` | Render Markdown + embeds to HTML | 598 | `add_karma($uid, $pts)` | void | Add (or subtract) karma points | 599 | `karma_tier($karma)` | `array` | Tier name, icon, colour, progress | 600 | `add_notification($uid, $type, $data)` | void | Queue a notification | 601 | `can_read_category($cat)` | `bool` | Read permission check | 602 | `can_post_topic($cat)` | `bool` | Post permission check | 603 | `can_reply_topic($cat)` | `bool` | Reply permission check | 604 | `unique_slug($title, $table)` | `string` | Generate a unique URL slug | 605 | `rate_check($uid, $type)` | `array` | `{ok, wait}` | 606 | `rate_record($uid, $type)` | void | Record a rate-limited action | 607 | `addon_hook($hook, $data)` | `mixed` | Fire an addon hook | 608 609 ### Database Class 610 611 ```php 612 DB::rows($sql, $params) // array of rows 613 DB::row($sql, $params) // one row or null 614 DB::insert($sql, $params) // int lastInsertId 615 DB::run($sql, $params) // PDOStatement 616 DB::val($sql, $params) // scalar or null 617 DB::now() // cross-driver: NOW() or datetime('now') 618 DB::isMysql() // bool 619 DB::insertIgnore($table, $cols, $vals) // cross-driver INSERT IGNORE 620 DB::upsert($table, $keyCol, $valCol, $key, $val) // cross-driver upsert 621 ``` 622 623 ### Adding a New Page 624 625 ```php 626 <?php 627 require_once __DIR__ . '/../includes/bootstrap.php'; 628 must_login(); // or must_admin(), or omit for public pages 629 630 $PAGE_TITLE = 'My Page'; 631 include __DIR__ . '/../views/partials/layout.php'; 632 ?> 633 634 <h1>Hello, <?= e($USER['username']) ?></h1> 635 <p>Your karma: <?= (int)$USER['karma'] ?></p> 636 637 <?php include __DIR__ . '/../views/partials/layout_end.php'; ?> 638 ``` 639 640 --- 641 642 ## 🛡️ Security Checklist 643 644 After going live: 645 646 - [ ] **Delete `install/`** — prevents re-installation 647 - [ ] **Verify `data/.htaccess`** — should deny all HTTP access (auto-created) 648 - [ ] **Verify `public/uploads/.htaccess`** — should block `.php` execution (auto-created) 649 - [ ] **Use HTTPS** — HSTS header is sent automatically when detected 650 - [ ] **MySQL users** — grant only `SELECT`, `INSERT`, `UPDATE`, `DELETE` (not `DROP`) 651 - [ ] **Enable captcha** — Admin → Settings → Post Captcha / Topic Captcha 652 - [ ] **Set rate limits** — Admin → Settings → Rate Limiting 653 654 --- 655 656 ## ❓ FAQ 657 658 <details> 659 <summary><strong>Can I run this on shared hosting without shell access?</strong></summary> 660 661 Yes. Shared hosting is the primary target. Everything is configured through the web installer. No Composer, npm, or shell access required. 662 </details> 663 664 <details> 665 <summary><strong>Do I need a separate database server?</strong></summary> 666 667 No. SQLite works out of the box with zero configuration — the database is a single file in `data/`. You can switch to MySQL/MariaDB any time by re-running the installer. 668 </details> 669 670 <details> 671 <summary><strong>How do I upgrade to a new version?</strong></summary> 672 673 Replace all files except `data/`. The schema migration runs automatically on the first page load after the update, adding any new columns safely with `IF NOT EXISTS` / `information_schema` checks. 674 </details> 675 676 <details> 677 <summary><strong>How do I reset a forgotten admin password?</strong></summary> 678 679 Run from the command line in your forum directory: 680 681 ```bash 682 php -r " 683 require 'includes/bootstrap.php'; 684 \$hash = password_hash('new_password_here', PASSWORD_BCRYPT, ['cost' => 12]); 685 DB::run('UPDATE users SET password=? WHERE role=?', [\$hash, 'admin']); 686 echo 'Password reset successfully.'; 687 " 688 ``` 689 </details> 690 691 <details> 692 <summary><strong>Can I use this behind a reverse proxy / load balancer?</strong></summary> 693 694 Yes. The `BASE` path is auto-detected from `DOCUMENT_ROOT` vs `SCRIPT_FILENAME`. No `.env` changes needed. For HTTPS detection behind a proxy, ensure the proxy sets `X-Forwarded-Proto: https`. 695 </details> 696 697 <details> 698 <summary><strong>How do I back up the forum?</strong></summary> 699 700 **SQLite:** Copy `data/forum.db` and `data/db_config.php`. 701 702 **MySQL:** `mysqldump nexus_forum > backup.sql` 703 704 Also back up `public/uploads/` for user images. 705 </details> 706 707 <details> 708 <summary><strong>Why no Composer / npm?</strong></summary> 709 710 The goal is maximum deployability. Any server running PHP 8 with PDO can run Nexus — no package manager, no build step, no Node.js. The only optional CDN dependency is Prism.js for syntax highlighting, which is lazy-loaded only when a code block is on the page. 711 </details> 712 713 --- 714 715 ## 🤝 Contributing 716 717 1. Fork the repository 718 2. Create a branch: `git checkout -b feature/my-feature` 719 3. Make your changes — test on **both SQLite and MySQL** 720 4. Syntax check: `find . -name "*.php" | xargs php -l` 721 5. Submit a pull request 722 723 ### Code Guidelines 724 725 - **PHP 8.0+** — use `match`, arrow functions, named arguments freely 726 - **No raw SQL interpolation** — always use PDO prepared statements 727 - **Always `e()` user output** — never echo user data unescaped 728 - **Cross-driver SQL** — test on both SQLite and MySQL; use `DB::insertIgnore()`, `DB::upsert()`, `DB::now()` for portability 729 - **No external dependencies** — no Composer packages, no npm, no build step 730 731 --- 732 733 ## 📄 License 734 735 MIT License — free to use, modify, and distribute. 736 737 --- 738 739 <div align="center"> 740 741 **Nexus Discussion** 742 743 *Built with PHP 8 · PDO · Vanilla JS* 744 745 *No frameworks · No build steps · No Docker required* 746 747 **[⬆ Back to top](#nexus-discussion)** 748 749 </div>