CourseHub installer
- = $e($msg) ?>
$a) { if (str_starts_with($a, '--videos=')) { $videosRoot = substr($a, strlen('--videos=')); unset($args[$i]); } } $args = array_values($args); $adminUser = $args[0] ?? ''; $adminPass = $args[1] ?? ''; $appUser = ''; $appPass = ''; $run = true; } else { $adminUser = trim($_POST['admin_user'] ?? ''); $adminPass = $_POST['admin_pass'] ?? ''; $appUser = trim($_POST['app_user'] ?? ''); $appPass = $_POST['app_pass'] ?? ''; $videosRoot = trim($_POST['videos_root'] ?? ''); $run = $_SERVER['REQUEST_METHOD'] === 'POST'; } if (!$run) { renderPage(null, false, ''); exit; } // -- Run the installation ---------------------------------------------------- [$done, $steps, $notice] = performInstall($adminUser, $adminPass, $appUser, $appPass, $videosRoot); if ($done && @file_put_contents($lockFile, date('c') . "\n") === false) { $steps[] = [false, 'Could not write .install.lock — the installer will stay re-runnable. ' . 'Delete or protect install.php on a production server.']; } respond($steps, $done, $notice); // ----------------------------------------------------------------------------- function performInstall(string $adminUser, string $adminPass, string $appUser, string $appPass, string $videosRoot): array { $steps = []; $notice = ''; try { if (!extension_loaded('pdo_mysql')) { throw new RuntimeException('PHP extension pdo_mysql is not loaded.'); } $opts = [PDO::ATTR_ERRMODE => 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 . '".']; } } // Videos folder: optionally persist a new path into config.php, then // make sure the effective path exists. $effectiveRoot = VIDEOS_ROOT; if ($videosRoot !== '' && $videosRoot !== VIDEOS_ROOT) { $cfgFile = __DIR__ . '/config.php'; $cfg = file_get_contents($cfgFile); $new = preg_replace( "/define\('VIDEOS_ROOT',\s*'[^']*'\);/", "define('VIDEOS_ROOT', '" . addslashes($videosRoot) . "');", $cfg, 1, $replaced ); if ($replaced === 1 && @file_put_contents($cfgFile, $new) !== false) { $effectiveRoot = $videosRoot; $steps[] = [true, 'Set VIDEOS_ROOT in config.php to ' . $videosRoot . '.']; } else { $steps[] = [false, 'Could not update config.php — set VIDEOS_ROOT to ' . $videosRoot . ' manually.']; } } if (!is_dir($effectiveRoot)) { if (@mkdir($effectiveRoot, 0755, true)) { $steps[] = [true, 'Created videos folder ' . $effectiveRoot . '.']; } else { $steps[] = [false, 'Warning: videos folder ' . $effectiveRoot . ' does not exist ' . 'and could not be created — create it (or fix VIDEOS_ROOT 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'); ?>