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'); ?> CourseHub installer

CourseHub installer

Open CourseHub
Database admin credentials (optional)
Needed only on first install, to create the database and the app DB user (). Used once, never stored.
First login account (optional)