diff --git a/includes/DatabaseInstaller.php b/includes/DatabaseInstaller.php index 3edb6c0..b67977c 100644 --- a/includes/DatabaseInstaller.php +++ b/includes/DatabaseInstaller.php @@ -278,9 +278,13 @@ class DatabaseInstaller { private static function splitSqlStatements(string $sql): array { $sql = preg_replace('/^\xEF\xBB\xBF/', '', $sql) ?? $sql; - $sql = preg_replace('/\/\*.*?\*\//s', '', $sql) ?? $sql; + $sql = preg_replace('/\/\*!\d+\s*(.*?)\*\//s', '$1', $sql) ?? $sql; + $sql = preg_replace('/\/\*(?!\!)(.*?)\*\//s', '', $sql) ?? $sql; + $sql = str_replace(["\r\n", "\r"], "\n", $sql); - $lines = preg_split('/\R/', $sql) ?: []; + // Do not use \R here. In byte mode it treats 0x85 as a newline, + // which corrupts UTF-8 Arabic seed data (for example حرف م = 0xD9 0x85). + $lines = explode("\n", $sql); $filteredLines = []; foreach ($lines as $line) { diff --git a/installation/index.php b/installation/index.php index 77e7a12..cef5dd1 100644 --- a/installation/index.php +++ b/installation/index.php @@ -45,6 +45,69 @@ function installationMigrationFiles(): array { return $files; } +function installationBuildDsn(string $host, ?string $dbName = null): string { + $normalizedHost = trim($host); + $port = null; + + if (substr_count($normalizedHost, ':') === 1) { + [$hostPart, $portPart] = explode(':', $normalizedHost, 2); + if ($hostPart !== '' && ctype_digit($portPart)) { + $normalizedHost = $hostPart; + $port = $portPart; + } + } + + $dsn = 'mysql:host=' . $normalizedHost; + if ($port !== null) { + $dsn .= ';port=' . $port; + } + if ($dbName !== null && $dbName !== '') { + $dsn .= ';dbname=' . $dbName; + } + + return $dsn . ';charset=utf8mb4'; +} + +function installationQuoteIdentifier(string $identifier): string { + return '`' . str_replace('`', '``', $identifier) . '`'; +} + +function installationBuildConfigContent(string $host, string $name, string $user, string $pass): string { + return sprintf( + <<<'PHP' + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + if (session_status() === PHP_SESSION_NONE && !headers_sent()) { + @session_start(); + } + if (isset($_SESSION['outlet_id'])) { + $pdo->exec("SET @session_outlet_id = " . (int)$_SESSION['outlet_id']); + } else { + $pdo->exec("SET @session_outlet_id = 0"); + } + } + return $pdo; +} + +PHP, + var_export($host, true), + var_export($name, true), + var_export($user, true), + var_export($pass, true) + ); +} + // Step 1: Requirements if ($step === 1) { $requirements = [ @@ -82,22 +145,41 @@ if ($step === 1) { // Step 2: Database if ($step === 2 && $_SERVER['REQUEST_METHOD'] === 'POST') { - $host = $_POST['db_host'] ?? ''; - $name = $_POST['db_name'] ?? ''; - $user = $_POST['db_user'] ?? ''; - $pass = $_POST['db_pass'] ?? ''; + $host = trim((string) ($_POST['db_host'] ?? '')); + $name = trim((string) ($_POST['db_name'] ?? '')); + $user = trim((string) ($_POST['db_user'] ?? '')); + $pass = (string) ($_POST['db_pass'] ?? ''); - try { - $pdo = new PDO("mysql:host=$host;dbname=$name", $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); - - // Save to config.php - $configContent = " PDO::ERRMODE_EXCEPTION,\n PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,\n ]);\n }\n return \$pdo;\n}\n"; - file_put_contents(__DIR__ . '/../db/config.php', $configContent); - - header("Location: index.php?step=3"); - exit; - } catch (PDOException $e) { - $error = "Database Connection Failed: " . $e->getMessage(); + if ($host === '' || $name === '' || $user === '') { + $error = 'Database host, name, and user are required.'; + } else { + try { + $pdoOptions = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]; + + try { + $pdo = new PDO(installationBuildDsn($host, $name), $user, $pass, $pdoOptions); + } catch (PDOException $connectionException) { + $pdo = new PDO(installationBuildDsn($host), $user, $pass, $pdoOptions); + $pdo->exec('CREATE DATABASE IF NOT EXISTS ' . installationQuoteIdentifier($name) . ' CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); + } + + $configPath = __DIR__ . '/../db/config.php'; + $bytesWritten = file_put_contents($configPath, installationBuildConfigContent($host, $name, $user, $pass)); + if ($bytesWritten === false) { + throw new RuntimeException('Unable to write db/config.php'); + } + + require_once __DIR__ . '/../includes/DatabaseInstaller.php'; + DatabaseInstaller::install(); + + header("Location: index.php?step=3"); + exit; + } catch (Throwable $e) { + $error = "Database Setup Failed: " . $e->getMessage(); + } } } @@ -110,7 +192,11 @@ if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') { try { require_once __DIR__ . '/../includes/DatabaseInstaller.php'; - DatabaseInstaller::install(); + if (!DatabaseInstaller::isInstalled()) { + DatabaseInstaller::install(); + } else { + DatabaseInstaller::ensureCurrentSchema(); + } $pdo = db(); // Ensure Admin Role exists (might be in schema but just in case) @@ -160,6 +246,7 @@ if ($step === 4) { + Installation - Step <?= $step ?> @@ -247,9 +334,10 @@ if ($step === 4) {
- +
+
This step creates the database tables and imports the base schema before you add the admin account.
Step 3: Super Admin Setup