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>