1<?php
2/**
3 * Nexus Forum — Web Installer
4 * Supports: SQLite3, MySQL, MariaDB
5 */
6
7
8define('NEXUS', true);
9define('ROOT', dirname(__DIR__));
10define('DATA', ROOT . '/data');
11define('UPLOADS', ROOT . '/public/uploads');
12
13
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
22if (file_exists(DATA . '/installed.lock')) {
23 header('Location: ' . BASE . '/');
24 exit;
25}
26
27
28require_once ROOT . '/includes/db.php';
29
30
31$step = (int)($_GET['step'] ?? 1);
32$errs = [];
33$info = [];
34$selDb = $_POST['db_type'] ?? 'sqlite';
35
36
37$hasSQLite = extension_loaded('pdo_sqlite') || in_array('sqlite', PDO::getAvailableDrivers());
38$hasMySQL = extension_loaded('pdo_mysql') || in_array('mysql', PDO::getAvailableDrivers());
39
40
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
52if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['install_submit'])) {
53
54
55 $selDb = $_POST['db_type'] ?? 'sqlite';
56
57
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
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
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
101 if (!$errs) {
102 try {
103
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
124 DB::init();
125 $db = DB::connect();
126
127
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
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
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
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
194 foreach ([DATA, UPLOADS, UPLOADS . '/avatars'] as $dir) {
195 if (!is_dir($dir)) {
196 mkdir($dir, 0750, true);
197 }
198 }
199
200
201
202 file_put_contents(DATA . '/.htaccess', "Order Deny,Allow\nDeny from all\n");
203
204
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
214 if (!DB::isMysql() && file_exists(DATA . '/forum.db')) {
215 @chmod(DATA . '/forum.db', 0640);
216 }
217
218
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
227 $step = 3;
228 $info = ['username' => $adminUser, 'db' => $selDb];
229
230 } catch (Throwable $ex) {
231 $errs[] = 'Installation error: ' . htmlspecialchars($ex->getMessage());
232
233 if (isset($cfgPath) && file_exists($cfgPath)) {
234 @unlink($cfgPath);
235 }
236 }
237 }
238}
239
240
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,
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:
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:
281 color:
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:
291 .logo-sub { font-size: 13px; color:
292
293
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:
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:
305 .step.active .step-n { background:
306 .step.todo .step-n { background:
307 .step-label { font-size: 11px; font-weight: 600; color:
308 .step.active .step-label { color:
309 .step.done .step-label { color:
310
311
312 .req-row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-bottom: 1px solid
313 .req-row:last-child { border-bottom: none; }
314 .ok { color:
315 .fail { color:
316
317
318 .db-switch { display: flex; border: 2px solid
319 .db-btn {
320 flex: 1; padding: 14px 12px; text-align: center; cursor: pointer;
321 background:
322 font-size: 14px; font-weight: 600; color:
323 transition: all .2s; line-height: 1.4;
324 }
325 .db-btn:first-child { border-right: 2px solid
326 .db-btn.active { background:
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
332 .notice { border-radius: 8px; padding: 11px 14px; font-size: 13px; margin-bottom: 14px; }
333 .notice-green { background:
334 .notice-yellow { background:
335 .notice-blue { background:
336
337
338 .fsection { margin-bottom: 22px; }
339 .fsection legend, .fsection-title {
340 display: block; font-weight: 700; font-size: 13px; color:
341 padding-bottom: 8px; border-bottom: 2px solid
342 }
343 .fg { margin-bottom: 14px; }
344 .fg label { display: block; font-size: 13px; font-weight: 600; color:
345 .fg label small { font-weight: 400; color:
346 .fi {
347 width: 100%; padding: 9px 12px; border: 1px solid
348 font-size: 14px; font-family: inherit; color:
349 }
350 .fi:focus { border-color:
351 .grid-2 { display: grid; grid-template-columns: 1fr 90px; gap: 10px; }
352
353
354 .btn-install {
355 display: flex; align-items: center; justify-content: center; gap: 8px;
356 width: 100%; padding: 13px; background:
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:
361 .btn-next {
362 display: flex; align-items: center; justify-content: center;
363 width: 100%; padding: 12px; background:
364 text-decoration: none; font-size: 15px; font-weight: 600; transition: all .2s;
365 }
366 .btn-next:hover { background:
367
368
369 .alert-err { background:
370
371
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:
375 .success-hero p { color:
376 .info-table { background:
377 .info-row { display: flex; justify-content: space-between; align-items: center; padding: 11px 16px; border-bottom: 1px solid
378 .info-row:last-child { border-bottom: none; }
379 .info-row .label { font-weight: 600; color:
380 .info-row code { background:
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): ?>
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): ?>
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): ?>
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
620 document.getElementById('dbTypeHidden').value = type;
621
622
623 document.getElementById('btnSQLite').className = 'db-btn' + (type === 'sqlite' ? ' active' : '');
624 document.getElementById('btnMySQL').className = 'db-btn' + (type === 'mysql' ? ' active' : '');
625
626
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>