xgit simple git

nexus

nexus

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

install/index.php

1 <?php
2 /**
3  * Nexus Forum — Web Installer
4  * Supports: SQLite3, MySQL, MariaDB
5  */
6 
7 /* ── Bootstrap (standalone — does not use bootstrap.php) ─── */
8 define('NEXUS', true);
9 define('ROOT',  dirname(__DIR__));
10 define('DATA',  ROOT . '/data');
11 define('UPLOADS', ROOT . '/public/uploads');
12 
13 // Detect web root offset (so the forum works at /forum/, /app/forum/, etc.)
14 $_dr = rtrim(str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT'] ?? ''), '/');
15 $_rp = str_replace('\\', '/', ROOT);
16 $_b  = str_replace($_dr, '', $_rp);
17 $_b  = '/' . trim($_b, '/');
18 define('BASE', $_b === '/' ? '' : $_b);
19 unset($_dr, $_rp, $_b);
20 
21 // Already installed → redirect
22 if (file_exists(DATA . '/installed.lock')) {
23     header('Location: ' . BASE . '/');
24     exit;
25 }
26 
27 // Load DB class only (no bootstrap, no session)
28 require_once ROOT . '/includes/db.php';
29 
30 /* ── State ──────────────────────────────────────────────── */
31 $step      = (int)($_GET['step'] ?? 1);
32 $errs      = [];
33 $info      = [];
34 $selDb     = $_POST['db_type'] ?? 'sqlite';
35 
36 // Check which PDO drivers are available
37 $hasSQLite = extension_loaded('pdo_sqlite')  || in_array('sqlite', PDO::getAvailableDrivers());
38 $hasMySQL  = extension_loaded('pdo_mysql')   || in_array('mysql',  PDO::getAvailableDrivers());
39 
40 /* ── Requirements ───────────────────────────────────────── */
41 $reqs = [
42     'PHP 8.0+'                => version_compare(PHP_VERSION, '8.0', '>='),
43     'PDO extension'           => extension_loaded('PDO'),
44     'Cryptographic random'    => function_exists('random_int'),
45     'JSON support'            => function_exists('json_encode'),
46     'SQLite3 or MySQL driver' => $hasSQLite || $hasMySQL,
47     'data/ directory writable'=> !is_dir(DATA) ? is_writable(ROOT) : is_writable(DATA),
48 ];
49 $allOk = !in_array(false, $reqs);
50 
51 /* ── Step 2: process form ───────────────────────────────── */
52 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['install_submit'])) {
53 
54     // Re-read db choice from POST
55     $selDb = $_POST['db_type'] ?? 'sqlite';
56 
57     // Validate common fields
58     $siteName   = trim($_POST['site_name']   ?? '');
59     $siteDesc   = trim($_POST['site_desc']   ?? '');
60     $adminUser  = trim($_POST['admin_user']  ?? '');
61     $adminEmail = trim($_POST['admin_email'] ?? '');
62     $adminPass  = $_POST['admin_pass']  ?? '';
63     $adminPass2 = $_POST['admin_pass2'] ?? '';
64 
65     if (!$siteName)                                             $errs[] = 'Site name is required.';
66     if (!preg_match('/^[a-zA-Z0-9_\-]{3,30}$/', $adminUser))  $errs[] = 'Admin username must be 3–30 alphanumeric characters (letters, numbers, _ -)';
67     if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL))       $errs[] = 'Admin email is not valid.';
68     if (strlen($adminPass) < 8)                                $errs[] = 'Admin password must be at least 8 characters.';
69     if ($adminPass !== $adminPass2)                            $errs[] = 'Passwords do not match.';
70 
71     // Validate DB-specific fields
72     $dbHost = trim($_POST['db_host'] ?? '127.0.0.1');
73     $dbPort = max(1, min(65535, (int)($_POST['db_port'] ?? 3306)));
74     $dbName = trim($_POST['db_name'] ?? '');
75     $dbUser = trim($_POST['db_user'] ?? '');
76     $dbPass = $_POST['db_pass'] ?? '';
77 
78     if ($selDb === 'mysql') {
79         if (!$hasMySQL)   $errs[] = 'PDO MySQL driver is not available on this server.';
80         if (!$dbName)     $errs[] = 'Database name is required for MySQL.';
81         if (!$dbUser)     $errs[] = 'Database username is required for MySQL.';
82 
83         // Test connection before proceeding
84         if (!$errs) {
85             try {
86                 $testPdo = new PDO(
87                     "mysql:host={$dbHost};port={$dbPort};dbname={$dbName};charset=utf8mb4",
88                     $dbUser, $dbPass,
89                     [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 5]
90                 );
91                 unset($testPdo);
92             } catch (PDOException $e) {
93                 $errs[] = 'Could not connect to MySQL: ' . htmlspecialchars($e->getMessage());
94             }
95         }
96     } else {
97         if (!$hasSQLite) $errs[] = 'PDO SQLite driver is not available on this server. Please choose MySQL.';
98     }
99 
100     // All good — run installation
101     if (!$errs) {
102         try {
103             /* 1. Write db_config.php */
104             $cfgPath = ROOT . '/includes/db_config.php';
105             if ($selDb === 'mysql') {
106                 $cfg = "<?php\n"
107                      . "if (!defined('NEXUS')) { http_response_code(403); exit('Forbidden'); }\n"
108                      . "define('DB_DRIVER',  'mysql');\n"
109                      . "define('DB_HOST',    " . var_export($dbHost,        true) . ");\n"
110                      . "define('DB_PORT',    " . var_export((string)$dbPort, true) . ");\n"
111                      . "define('DB_NAME',    " . var_export($dbName,         true) . ");\n"
112                      . "define('DB_USER',    " . var_export($dbUser,         true) . ");\n"
113                      . "define('DB_PASS',    " . var_export($dbPass,         true) . ");\n"
114                      . "define('DB_CHARSET', 'utf8mb4');\n";
115             } else {
116                 $cfg = "<?php\n"
117                      . "if (!defined('NEXUS')) { http_response_code(403); exit('Forbidden'); }\n"
118                      . "define('DB_DRIVER', 'sqlite');\n";
119             }
120             file_put_contents($cfgPath, $cfg);
121             @chmod($cfgPath, 0640);
122 
123             /* 2. Build schema */
124             DB::init();
125             $db = DB::connect();
126 
127             /* 3. Insert settings */
128             $upsert = DB::isMysql()
129                 ? 'INSERT INTO settings (`key`,`value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)'
130                 : 'INSERT OR REPLACE INTO settings (key,value) VALUES (?,?)';
131             $si = $db->prepare($upsert);
132             $settingsToInsert = [
133                 'site_name'              => $siteName,
134                 'site_desc'              => $siteDesc ?: 'A community forum',
135                 'allow_reg'              => '1',
136                 'topics_per_page'        => '30',
137                 'posts_per_page'         => '20',
138                 'rate_limit_enabled'     => '0',
139                 'rate_limit_count'       => '3',
140                 'rate_limit_window'      => '60',
141                 'post_captcha_enabled'   => '0',
142                 'topic_captcha_enabled'  => '0',
143             ];
144             foreach ($settingsToInsert as $k => $v) {
145                 $si->execute([$k, $v]);
146             }
147 
148             /* 4. Create admin user */
149             $adminId = DB::insert(
150                 'INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, ?)',
151                 [$adminUser, $adminEmail, password_hash($adminPass, PASSWORD_BCRYPT, ['cost' => 12]), 'admin']
152             );
153 
154             /* 5. Create default categories */
155             $catStmt = $db->prepare('INSERT INTO categories (name, slug, description, color, icon, position) VALUES (?, ?, ?, ?, ?, ?)');
156             $defaultCats = [
157                 ['Announcements', 'announcements', 'Important updates from the team',  '#ef4444', '📢', 1],
158                 ['General',       'general',       'Talk about anything',               '#3b82f6', '💬', 2],
159                 ['Support',       'support',       'Get help from the community',       '#10b981', '🛟', 3],
160                 ['Ideas',         'ideas',         'Share your suggestions',            '#8b5cf6', '💡', 4],
161                 ['Showcase',      'showcase',      'Show off your projects',            '#f59e0b', '🌟', 5],
162             ];
163             foreach ($defaultCats as $cat) {
164                 $catStmt->execute($cat);
165             }
166 
167             /* 6. Create welcome topic in General */
168             $generalId = (int) DB::val("SELECT id FROM categories WHERE slug = 'general'");
169             if ($generalId) {
170                 $welcomeBody = "# Welcome to {$siteName}!\n\n"
171                     . "This is your new community forum. Here's how to get started:\n\n"
172                     . "## Quick Start\n\n"
173                     . "- Browse the categories in the left sidebar\n"
174                     . "- Click **+ New Topic** to start a discussion\n"
175                     . "- Type **@username** in a post to mention someone\n"
176                     . "- Paste a YouTube, Vimeo, or Spotify URL on its own line to auto-embed it\n"
177                     . "- Like posts to give ⭐ Karma to helpful members\n\n"
178                     . "Enjoy the community! 👋";
179 
180                 $nowExpr = DB::isMysql() ? 'NOW()' : "datetime('now')";
181                 $topicId = DB::insert(
182                     "INSERT INTO topics (title, slug, category_id, user_id, pinned, last_post_at)
183                      VALUES (?, ?, ?, ?, 1, {$nowExpr})",
184                     ["Welcome to {$siteName}!", 'welcome', $generalId, $adminId]
185                 );
186                 DB::insert(
187                     'INSERT INTO posts (topic_id, user_id, content, post_num) VALUES (?, ?, ?, 1)',
188                     [$topicId, $adminId, $welcomeBody]
189                 );
190                 DB::run('UPDATE categories SET topic_count = 1, post_count = 1 WHERE id = ?', [$generalId]);
191             }
192 
193             /* 7. Create directories */
194             foreach ([DATA, UPLOADS, UPLOADS . '/avatars'] as $dir) {
195                 if (!is_dir($dir)) {
196                     mkdir($dir, 0750, true);
197                 }
198             }
199 
200             /* 8. Security hardening */
201             // Block web access to data/
202             file_put_contents(DATA . '/.htaccess', "Order Deny,Allow\nDeny from all\n");
203 
204             // Block PHP execution in uploads/
205             file_put_contents(UPLOADS . '/.htaccess',
206                 "Options -Indexes -ExecCGI\n"
207                 . "<FilesMatch \"\\.(?i:php|phtml|php3|php4|php5|php7|phar|cgi|pl|sh|exe)$\">\n"
208                 . "  Order Allow,Deny\n"
209                 . "  Deny from all\n"
210                 . "</FilesMatch>\n"
211             );
212 
213             // Secure the SQLite file
214             if (!DB::isMysql() && file_exists(DATA . '/forum.db')) {
215                 @chmod(DATA . '/forum.db', 0640);
216             }
217 
218             /* 9. Write installed.lock */
219             file_put_contents(DATA . '/installed.lock', json_encode([
220                 'installed_at' => date('c'),
221                 'db_driver'    => $selDb,
222                 'php_version'  => PHP_VERSION,
223             ]));
224             @chmod(DATA . '/installed.lock', 0640);
225 
226             /* Done */
227             $step = 3;
228             $info = ['username' => $adminUser, 'db' => $selDb];
229 
230         } catch (Throwable $ex) {
231             $errs[] = 'Installation error: ' . htmlspecialchars($ex->getMessage());
232             // Roll back config file if install failed
233             if (isset($cfgPath) && file_exists($cfgPath)) {
234                 @unlink($cfgPath);
235             }
236         }
237     }
238 }
239 
240 /* ── HTML helpers ────────────────────────────────────────── */
241 function esc(mixed $v): string {
242     return htmlspecialchars((string)$v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
243 }
244 
245 ?><!DOCTYPE html>
246 <html lang="en">
247 <head>
248   <meta charset="UTF-8">
249   <meta name="viewport" content="width=device-width, initial-scale=1.0">
250   <title>Install — Nexus Forum</title>
251   <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
252   <link rel="stylesheet" href="<?= BASE ?>/public/css/main.css">
253   <style>
254     body {
255       background: linear-gradient(135deg, #1e40af 0%, #0f172a 100%);
256       min-height: 100vh;
257       display: flex;
258       align-items: center;
259       justify-content: center;
260       padding: 24px;
261       font-family: 'Inter', sans-serif;
262     }
263     .card {
264       background: #fff;
265       border-radius: 16px;
266       padding: 40px;
267       max-width: 580px;
268       width: 100%;
269       box-shadow: 0 25px 60px rgba(0,0,0,.3);
270     }
271     .logo-row {
272       display: flex;
273       align-items: center;
274       gap: 12px;
275       margin-bottom: 32px;
276     }
277     .logo-mark {
278       width: 46px;
279       height: 46px;
280       background: #3b82f6;
281       color: #fff;
282       border-radius: 12px;
283       display: flex;
284       align-items: center;
285       justify-content: center;
286       font-size: 24px;
287       font-weight: 700;
288       flex-shrink: 0;
289     }
290     .logo-text { font-size: 20px; font-weight: 700; color: #0f172a; }
291     .logo-sub  { font-size: 13px; color: #64748b; }
292 
293     /* Steps */
294     .steps { display: flex; margin-bottom: 32px; }
295     .step  { flex: 1; text-align: center; position: relative; }
296     .step::after { content: ''; position: absolute; top: 14px; left: 50%; width: 100%; height: 2px; background: #e2e8f0; z-index: 0; }
297     .step:last-child::after { display: none; }
298     .step-n {
299       width: 30px; height: 30px; border-radius: 50%;
300       display: flex; align-items: center; justify-content: center;
301       font-size: 13px; font-weight: 700; margin: 0 auto 6px;
302       position: relative; z-index: 1;
303     }
304     .step.done .step-n { background: #22c55e; color: #fff; }
305     .step.active .step-n { background: #3b82f6; color: #fff; box-shadow: 0 0 0 4px #dbeafe; }
306     .step.todo .step-n  { background: #e2e8f0; color: #94a3b8; }
307     .step-label { font-size: 11px; font-weight: 600; color: #94a3b8; }
308     .step.active .step-label { color: #3b82f6; }
309     .step.done .step-label   { color: #22c55e; }
310 
311     /* Req table */
312     .req-row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-bottom: 1px solid #f1f5f9; font-size: 14px; }
313     .req-row:last-child { border-bottom: none; }
314     .ok   { color: #22c55e; font-weight: 700; }
315     .fail { color: #ef4444; font-weight: 700; }
316 
317     /* DB switcher */
318     .db-switch { display: flex; border: 2px solid #e2e8f0; border-radius: 10px; overflow: hidden; margin-bottom: 18px; }
319     .db-btn {
320       flex: 1; padding: 14px 12px; text-align: center; cursor: pointer;
321       background: #f8fafc; border: none; font-family: inherit;
322       font-size: 14px; font-weight: 600; color: #64748b;
323       transition: all .2s; line-height: 1.4;
324     }
325     .db-btn:first-child { border-right: 2px solid #e2e8f0; }
326     .db-btn.active { background: #3b82f6; color: #fff; }
327     .db-btn .db-sub { font-size: 11px; font-weight: 400; opacity: .75; display: block; margin-top: 2px; }
328     .db-panel { display: none; }
329     .db-panel.visible { display: block; }
330 
331     /* Notices */
332     .notice { border-radius: 8px; padding: 11px 14px; font-size: 13px; margin-bottom: 14px; }
333     .notice-green  { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }
334     .notice-yellow { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }
335     .notice-blue   { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e40af; }
336 
337     /* Form sections */
338     .fsection { margin-bottom: 22px; }
339     .fsection legend, .fsection-title {
340       display: block; font-weight: 700; font-size: 13px; color: #374151;
341       padding-bottom: 8px; border-bottom: 2px solid #f1f5f9; margin-bottom: 14px; width: 100%;
342     }
343     .fg { margin-bottom: 14px; }
344     .fg label { display: block; font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 5px; }
345     .fg label small { font-weight: 400; color: #94a3b8; }
346     .fi {
347       width: 100%; padding: 9px 12px; border: 1px solid #d1d5db; border-radius: 8px;
348       font-size: 14px; font-family: inherit; color: #111827; outline: none; transition: border-color .18s;
349     }
350     .fi:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,.12); }
351     .grid-2 { display: grid; grid-template-columns: 1fr 90px; gap: 10px; }
352 
353     /* Buttons */
354     .btn-install {
355       display: flex; align-items: center; justify-content: center; gap: 8px;
356       width: 100%; padding: 13px; background: #3b82f6; color: #fff; border: none;
357       border-radius: 10px; font-size: 16px; font-weight: 700; cursor: pointer;
358       font-family: inherit; transition: all .2s; margin-top: 6px;
359     }
360     .btn-install:hover { background: #2563eb; transform: translateY(-1px); }
361     .btn-next {
362       display: flex; align-items: center; justify-content: center;
363       width: 100%; padding: 12px; background: #3b82f6; color: #fff; border-radius: 10px;
364       text-decoration: none; font-size: 15px; font-weight: 600; transition: all .2s;
365     }
366     .btn-next:hover { background: #2563eb; text-decoration: none; color: #fff; }
367 
368     /* Error alert */
369     .alert-err { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; border-radius: 8px; padding: 12px 16px; font-size: 14px; margin-bottom: 18px; }
370 
371     /* Success */
372     .success-hero { text-align: center; padding: 10px 0 20px; }
373     .success-hero .emoji { font-size: 4rem; line-height: 1; margin-bottom: 12px; }
374     .success-hero h2 { font-size: 1.5rem; font-weight: 800; color: #0f172a; margin-bottom: 6px; }
375     .success-hero p  { color: #64748b; font-size: 15px; }
376     .info-table { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 0; margin-bottom: 20px; overflow: hidden; }
377     .info-row { display: flex; justify-content: space-between; align-items: center; padding: 11px 16px; border-bottom: 1px solid #f1f5f9; font-size: 14px; }
378     .info-row:last-child { border-bottom: none; }
379     .info-row .label { font-weight: 600; color: #374151; }
380     .info-row code { background: #e2e8f0; padding: 2px 8px; border-radius: 5px; font-size: 13px; }
381     .security-list { list-style: none; padding: 0; margin: 8px 0 0; }
382     .security-list li { display: flex; align-items: flex-start; gap: 8px; font-size: 13px; margin-bottom: 6px; line-height: 1.4; }
383   </style>
384 </head>
385 <body>
386 <div class="card">
387 
388   <!-- Logo -->
389   <div class="logo-row">
390     <div class="logo-mark">N</div>
391     <div>
392       <div class="logo-text">Nexus Forum</div>
393       <div class="logo-sub">Installation Wizard</div>
394     </div>
395   </div>
396 
397   <!-- Steps indicator -->
398   <div class="steps">
399     <?php
400     $stepDefs = ['Requirements', 'Configure', 'Complete'];
401     foreach ($stepDefs as $i => $label):
402       $n = $i + 1;
403       $cls = $step > $n ? 'done' : ($step === $n ? 'active' : 'todo');
404     ?>
405       <div class="step <?= $cls ?>">
406         <div class="step-n"><?= $step > $n ? '✓' : $n ?></div>
407         <div class="step-label"><?= $label ?></div>
408       </div>
409     <?php endforeach; ?>
410   </div>
411 
412   <?php if ($step === 1): /* ─── STEP 1: Requirements ─── */ ?>
413 
414     <h2 style="font-size:1.25rem;font-weight:700;margin-bottom:6px">System Check</h2>
415     <p style="color:#64748b;font-size:14px;margin-bottom:20px">Checking your server before we begin.</p>
416 
417     <?php foreach ($reqs as $label => $ok): ?>
418       <div class="req-row">
419         <span><?= esc($label) ?></span>
420         <span class="<?= $ok ? 'ok' : 'fail' ?>"><?= $ok ? '✓ OK' : '✗ FAIL' ?></span>
421       </div>
422     <?php endforeach; ?>
423 
424     <div style="margin-top:8px;padding:10px 0;border-top:1px solid #f1f5f9">
425       <div class="req-row">
426         <span>SQLite3 (PDO)</span>
427         <span class="<?= $hasSQLite ? 'ok' : 'fail' ?>"><?= $hasSQLite ? '✓ Available' : '✗ Not available' ?></span>
428       </div>
429       <div class="req-row">
430         <span>MySQL / MariaDB (PDO)</span>
431         <span class="<?= $hasMySQL ? 'ok' : 'fail' ?>"><?= $hasMySQL ? '✓ Available' : '✗ Not available' ?></span>
432       </div>
433     </div>
434 
435     <div style="margin-top:22px">
436       <?php if ($allOk): ?>
437         <a href="?step=2" class="btn-next">Continue to Configuration →</a>
438       <?php else: ?>
439         <div class="alert-err">Please fix the issues above, then reload this page.</div>
440       <?php endif; ?>
441     </div>
442 
443   <?php elseif ($step === 2): /* ─── STEP 2: Configure ─── */ ?>
444 
445     <h2 style="font-size:1.25rem;font-weight:700;margin-bottom:6px">Configure Your Forum</h2>
446     <p style="color:#64748b;font-size:14px;margin-bottom:22px">Fill in all fields below and click <strong>Install</strong>.</p>
447 
448     <?php if ($errs): ?>
449       <div class="alert-err">
450         <?php foreach ($errs as $e): ?>
451           <div>• <?= esc($e) ?></div>
452         <?php endforeach; ?>
453       </div>
454     <?php endif; ?>
455 
456     <form method="POST" action="?step=2">
457       <input type="hidden" name="install_submit" value="1">
458       <input type="hidden" name="db_type" id="dbTypeHidden" value="<?= esc($selDb) ?>">
459 
460       <!-- Database selection -->
461       <div class="fsection">
462         <span class="fsection-title">1. Database Engine</span>
463 
464         <div class="db-switch">
465           <button type="button" class="db-btn <?= $selDb !== 'mysql' ? 'active' : '' ?>"
466                   id="btnSQLite" onclick="switchDb('sqlite')">
467             🗃️ SQLite3
468             <span class="db-sub">Easiest — no setup needed</span>
469           </button>
470           <button type="button" class="db-btn <?= $selDb === 'mysql' ? 'active' : '' ?>"
471                   id="btnMySQL" onclick="switchDb('mysql')">
472             🐬 MySQL / MariaDB
473             <span class="db-sub">Recommended for production</span>
474           </button>
475         </div>
476 
477         <!-- SQLite panel -->
478         <div id="panelSQLite" class="db-panel <?= $selDb !== 'mysql' ? 'visible' : '' ?>">
479           <?php if ($hasSQLite): ?>
480             <div class="notice notice-green">
481               ✓ SQLite will be automatically created at <code>data/forum.db</code>. No extra configuration needed.
482             </div>
483           <?php else: ?>
484             <div class="notice notice-yellow">
485               ⚠️ PDO SQLite is not available on this server. Please switch to MySQL.
486             </div>
487           <?php endif; ?>
488         </div>
489 
490         <!-- MySQL panel -->
491         <div id="panelMySQL" class="db-panel <?= $selDb === 'mysql' ? 'visible' : '' ?>">
492           <?php if ($hasMySQL): ?>
493             <div class="grid-2">
494               <div class="fg">
495                 <label>Host</label>
496                 <input type="text" name="db_host" class="fi" value="<?= esc($_POST['db_host'] ?? '127.0.0.1') ?>" placeholder="127.0.0.1">
497               </div>
498               <div class="fg">
499                 <label>Port</label>
500                 <input type="number" name="db_port" class="fi" value="<?= esc($_POST['db_port'] ?? '3306') ?>" min="1" max="65535">
501               </div>
502             </div>
503             <div class="fg">
504               <label>Database Name</label>
505               <input type="text" name="db_name" class="fi" value="<?= esc($_POST['db_name'] ?? '') ?>" placeholder="nexus_forum">
506             </div>
507             <div class="fg">
508               <label>Database Username</label>
509               <input type="text" name="db_user" class="fi" value="<?= esc($_POST['db_user'] ?? '') ?>" placeholder="nexus_user">
510             </div>
511             <div class="fg">
512               <label>Database Password</label>
513               <input type="password" name="db_pass" class="fi" placeholder="Your database password">
514             </div>
515             <div class="notice notice-blue" style="font-size:12px">
516               <strong>Create the database first if you haven't:</strong><br>
517               <code style="font-size:11px">CREATE DATABASE nexus_forum CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;</code>
518             </div>
519           <?php else: ?>
520             <div class="notice notice-yellow">⚠️ PDO MySQL is not available on this server.</div>
521           <?php endif; ?>
522         </div>
523       </div>
524 
525       <!-- Site details -->
526       <div class="fsection">
527         <span class="fsection-title">2. Site Details</span>
528         <div class="fg">
529           <label>Site Name <span style="color:#ef4444">*</span></label>
530           <input type="text" name="site_name" class="fi" required
531                  value="<?= esc($_POST['site_name'] ?? 'My Forum') ?>" placeholder="My Community">
532         </div>
533         <div class="fg">
534           <label>Description <small>(optional)</small></label>
535           <input type="text" name="site_desc" class="fi"
536                  value="<?= esc($_POST['site_desc'] ?? '') ?>" placeholder="A place for great discussions">
537         </div>
538       </div>
539 
540       <!-- Admin account -->
541       <div class="fsection">
542         <span class="fsection-title">3. Admin Account</span>
543         <div class="fg">
544           <label>Username <span style="color:#ef4444">*</span> <small>(letters, numbers, _ -)</small></label>
545           <input type="text" name="admin_user" class="fi" required
546                  value="<?= esc($_POST['admin_user'] ?? '') ?>" placeholder="admin"
547                  minlength="3" maxlength="30" autocomplete="off">
548         </div>
549         <div class="fg">
550           <label>Email Address <span style="color:#ef4444">*</span></label>
551           <input type="email" name="admin_email" class="fi" required
552                  value="<?= esc($_POST['admin_email'] ?? '') ?>" placeholder="admin@example.com">
553         </div>
554         <div class="fg">
555           <label>Password <span style="color:#ef4444">*</span> <small>(min. 8 characters)</small></label>
556           <input type="password" name="admin_pass" class="fi" required
557                  minlength="8" placeholder="Choose a strong password" autocomplete="new-password">
558         </div>
559         <div class="fg">
560           <label>Confirm Password <span style="color:#ef4444">*</span></label>
561           <input type="password" name="admin_pass2" class="fi" required
562                  minlength="8" placeholder="Repeat your password" autocomplete="new-password">
563         </div>
564       </div>
565 
566       <button type="submit" class="btn-install">
567         🚀 Install Nexus Forum
568       </button>
569     </form>
570 
571   <?php elseif ($step === 3): /* ─── STEP 3: Done ─── */ ?>
572 
573     <div class="success-hero">
574       <div class="emoji">🎉</div>
575       <h2>Installation Complete!</h2>
576       <p>Your forum is ready. Log in with your admin credentials below.</p>
577     </div>
578 
579     <div class="info-table">
580       <div class="info-row">
581         <span class="label">Forum URL</span>
582         <a href="<?= BASE ?>/"><?= BASE ?: '/' ?></a>
583       </div>
584       <div class="info-row">
585         <span class="label">Admin Panel</span>
586         <a href="<?= BASE ?>/admin/"><?= BASE ?>/admin/</a>
587       </div>
588       <div class="info-row">
589         <span class="label">Admin Username</span>
590         <code><?= esc($info['username'] ?? '') ?></code>
591       </div>
592       <div class="info-row">
593         <span class="label">Database</span>
594         <span><?= ($info['db'] ?? 'sqlite') === 'mysql' ? '🐬 MySQL / MariaDB' : '🗃️ SQLite3' ?></span>
595       </div>
596     </div>
597 
598     <div class="notice notice-blue">
599       <strong>🔒 Post-Install Security Checklist</strong>
600       <ul class="security-list">
601         <li>✅ <code>data/.htaccess</code> configured — web access blocked automatically</li>
602         <li>✅ <code>uploads/.htaccess</code> configured — PHP execution blocked automatically</li>
603         <li>⚠️ <strong>Delete or restrict the <code>install/</code> directory</strong> — it's no longer needed</li>
604         <li>⚠️ Enable HTTPS on your web server</li>
605         <?php if (($info['db'] ?? '') !== 'mysql'): ?>
606           <li>📦 Back up <code>data/forum.db</code> regularly</li>
607         <?php endif; ?>
608       </ul>
609     </div>
610 
611     <a href="<?= BASE ?>/" class="btn-next" style="margin-top:8px">Visit Your Forum →</a>
612 
613   <?php endif; ?>
614 
615 </div>
616 
617 <script>
618 function switchDb(type) {
619   // Update hidden input
620   document.getElementById('dbTypeHidden').value = type;
621 
622   // Update button styles
623   document.getElementById('btnSQLite').className = 'db-btn' + (type === 'sqlite' ? ' active' : '');
624   document.getElementById('btnMySQL').className  = 'db-btn' + (type === 'mysql'  ? ' active' : '');
625 
626   // Show/hide panels
627   document.getElementById('panelSQLite').className = 'db-panel' + (type === 'sqlite' ? ' visible' : '');
628   document.getElementById('panelMySQL').className  = 'db-panel' + (type === 'mysql'  ? ' visible' : '');
629 }
630 </script>
631 </body>
632 </html>