CourseHub installer
- = $e($msg) ?>
PDO::ERRMODE_EXCEPTION]; $dsnServer = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';charset=utf8mb4'; $useAdmin = $adminUser !== ''; $pdo = new PDO($dsnServer, $useAdmin ? $adminUser : DB_USER, $useAdmin ? $adminPass : DB_PASS, $opts); $steps[] = [true, 'Connected to MySQL at ' . DB_HOST . ':' . DB_PORT . ' as ' . ($useAdmin ? $adminUser : DB_USER) . '.']; // Database $dbQuoted = '`' . str_replace('`', '', DB_NAME) . '`'; $pdo->exec("CREATE DATABASE IF NOT EXISTS $dbQuoted CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); $steps[] = [true, 'Database "' . DB_NAME . '" is ready.']; // App DB user (only possible with admin privileges) if ($useAdmin) { foreach (['localhost', '127.0.0.1'] as $host) { $u = $pdo->quote(DB_USER) . '@' . $pdo->quote($host); $pdo->exec("CREATE USER IF NOT EXISTS $u IDENTIFIED BY " . $pdo->quote(DB_PASS)); $pdo->exec("GRANT ALL PRIVILEGES ON $dbQuoted.* TO $u"); } $pdo->exec('FLUSH PRIVILEGES'); $steps[] = [true, 'App DB user "' . DB_USER . '" created/updated and granted access.']; } // Schema $sql = @file_get_contents(__DIR__ . '/schema.sql'); if ($sql === false) { throw new RuntimeException('Cannot read schema.sql.'); } $pdo->exec("USE $dbQuoted"); $applied = 0; foreach (splitSqlStatements($sql) as $stmt) { if (preg_match('/^\s*(CREATE\s+DATABASE|USE)\b/i', $stmt)) { continue; // installer already handled DB creation/selection } $pdo->exec($stmt); $applied++; } $tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN); $steps[] = [true, "Applied $applied schema statements — tables: " . implode(', ', $tables) . '.']; // Verify the app credentials from config.php actually work $dsnDb = $dsnServer . ';dbname=' . DB_NAME; try { $app = new PDO($dsnDb, DB_USER, DB_PASS, $opts); $steps[] = [true, 'Verified config.php credentials — the app can connect.']; } catch (PDOException $e) { throw new RuntimeException('The app credentials in config.php cannot connect (' . $e->getMessage() . '). Fix DB_USER/DB_PASS, or re-run with database ' . 'admin credentials so the installer can create the user.'); } // Optional first login account if ($appUser !== '') { if (!preg_match('/^[A-Za-z0-9_.-]{3,50}$/', $appUser)) { throw new RuntimeException('Account username must be 3-50 characters (letters, digits, _ . -).'); } if (strlen($appPass) < 6) { throw new RuntimeException('Account password must be at least 6 characters.'); } $stmt = $app->prepare('SELECT id FROM users WHERE username = ?'); $stmt->execute([$appUser]); if ($stmt->fetch()) { $steps[] = [true, 'Account "' . $appUser . '" already exists — left unchanged.']; } else { $app->prepare('INSERT INTO users (username, password_hash) VALUES (?, ?)') ->execute([$appUser, password_hash($appPass, PASSWORD_DEFAULT)]); $steps[] = [true, 'Created login account "' . $appUser . '".']; } } if (!is_dir(VIDEOS_ROOT)) { $steps[] = [false, 'Warning: VIDEOS_ROOT (' . VIDEOS_ROOT . ') does not exist yet — ' . 'set it in config.php before scanning.']; } $notice = 'Installation complete. Open the app, log in, and use "Rescan library" ' . '(or run: php scan.php) to index your videos.'; return [true, $steps, $notice]; } catch (Throwable $e) { $steps[] = [false, $e->getMessage()]; return [false, $steps, '']; } } /** Split a .sql file into statements (strips -- comments; no procedure support). */ function splitSqlStatements(string $sql): array { $lines = array_filter( explode("\n", $sql), fn($l) => !preg_match('/^\s*--/', $l) ); $stmts = array_map('trim', explode(';', implode("\n", $lines))); return array_values(array_filter($stmts, fn($s) => $s !== '')); } function renderPage(?array $steps, bool $done, string $notice): void { $e = fn($s) => htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); ?>