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) ─── */
8define('NEXUS', true);
9define('ROOT',  dirname(__DIR__));
10define('DATA',  ROOT . '/data');
11define('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, '/');
18define('BASE', $_b === '/' ? '' : $_b);
19unset($_dr, $_rp, $_b);
20 
21// Already installed → redirect
22if (file_exists(DATA . '/installed.lock')) {
23    header('Location: ' . BASE . '/');
24    exit;
25}
26 
27// Load DB class only (no bootstrap, no session)
28require_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 ───────────────────────────────── */
52if ($_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 ────────────────────────────────────────── */
241function 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>
618function 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>