diff --git a/.gitignore b/.gitignore index d8834ab..e99ed3e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ config.php # Video library lives outside version control videos/ + +# Written by install.php after a successful install +.install.lock diff --git a/install.php b/install.php new file mode 100644 index 0000000..5244ce5 --- /dev/null +++ b/install.php @@ -0,0 +1,256 @@ + 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)
+
+
+
+
+
+ +
+
+ +
+ + +