383 lines
17 KiB
PHP
383 lines
17 KiB
PHP
<?php
|
|
session_start();
|
|
|
|
$lockFile = __DIR__ . '/../installed.lock';
|
|
if (file_exists($lockFile)) {
|
|
die("Installation already completed. Please remove 'installed.lock' if you want to re-install.");
|
|
}
|
|
|
|
$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
|
|
$error = '';
|
|
$success = '';
|
|
|
|
function installationMigrationSortKey(string $filePath): string {
|
|
$basename = basename($filePath);
|
|
|
|
return match ($basename) {
|
|
'20260318_add_outlet_id_to_purchases.sql' => '20260318_10_add_outlet_id_to_purchases.sql',
|
|
'20260318_create_outlets_table.sql' => '20260318_20_create_outlets_table.sql',
|
|
'20260318_multi_outlet_schema.sql' => '20260318_30_multi_outlet_schema.sql',
|
|
'20260318_local_definitions.sql' => '20260318_40_local_definitions.sql',
|
|
'20260318_user_outlets_table.sql' => '20260318_50_user_outlets_table.sql',
|
|
default => $basename,
|
|
};
|
|
}
|
|
|
|
function installationLegacyNumberedMigrationFile(string $filePath): bool {
|
|
// Skip obsolete pre-date-based migrations from older packaged builds.
|
|
return preg_match('/^\d{1,7}_/', basename($filePath)) === 1;
|
|
}
|
|
|
|
function installationMigrationFiles(): array {
|
|
$files = array_merge(
|
|
glob(__DIR__ . '/../db/migrations/*.sql') ?: [],
|
|
glob(__DIR__ . '/../db/migrations/*.php') ?: []
|
|
);
|
|
|
|
$files = array_values(array_filter($files, static function (string $filePath): bool {
|
|
return !installationLegacyNumberedMigrationFile($filePath);
|
|
}));
|
|
|
|
usort($files, static function (string $left, string $right): int {
|
|
return strnatcasecmp(installationMigrationSortKey($left), installationMigrationSortKey($right));
|
|
});
|
|
|
|
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'
|
|
<?php
|
|
define('DB_HOST', %s);
|
|
define('DB_NAME', %s);
|
|
define('DB_USER', %s);
|
|
define('DB_PASS', %s);
|
|
|
|
function db() {
|
|
static $pdo;
|
|
if (!$pdo) {
|
|
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
|
PDO::ATTR_ERRMODE => 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 = [
|
|
'php_version' => [
|
|
'name' => 'PHP Version (>= 8.0)',
|
|
'status' => version_compare(PHP_VERSION, '8.0.0', '>='),
|
|
'current' => PHP_VERSION
|
|
],
|
|
'pdo_mysql' => [
|
|
'name' => 'PDO MySQL Extension',
|
|
'status' => extension_loaded('pdo_mysql'),
|
|
'current' => extension_loaded('pdo_mysql') ? 'Loaded' : 'Missing'
|
|
],
|
|
'curl' => [
|
|
'name' => 'cURL Extension (Required for Licensing)',
|
|
'status' => extension_loaded('curl'),
|
|
'current' => extension_loaded('curl') ? 'Loaded' : 'Missing'
|
|
],
|
|
'config_writable' => [
|
|
'name' => 'db/config.php Writable',
|
|
'status' => is_writable(__DIR__ . '/../db/config.php'),
|
|
'current' => is_writable(__DIR__ . '/../db/config.php') ? 'Yes' : 'No'
|
|
],
|
|
'root_writable' => [
|
|
'name' => 'Root Directory Writable',
|
|
'status' => is_writable(__DIR__ . '/..'),
|
|
'current' => is_writable(__DIR__ . '/..') ? 'Yes' : 'No'
|
|
]
|
|
];
|
|
$allOk = true;
|
|
foreach ($requirements as $req) {
|
|
if (!$req['status']) $allOk = false;
|
|
}
|
|
}
|
|
|
|
// Step 2: Database
|
|
if ($step === 2 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$host = trim((string) ($_POST['db_host'] ?? ''));
|
|
$name = trim((string) ($_POST['db_name'] ?? ''));
|
|
$user = trim((string) ($_POST['db_user'] ?? ''));
|
|
$pass = (string) ($_POST['db_pass'] ?? '');
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 3: Super Admin
|
|
if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
require_once __DIR__ . '/../db/config.php';
|
|
$adminUser = $_POST['admin_user'] ?? '';
|
|
$adminPass = $_POST['admin_pass'] ?? '';
|
|
$adminEmail = $_POST['admin_email'] ?? '';
|
|
|
|
try {
|
|
require_once __DIR__ . '/../includes/DatabaseInstaller.php';
|
|
if (!DatabaseInstaller::isInstalled()) {
|
|
DatabaseInstaller::install();
|
|
} else {
|
|
DatabaseInstaller::ensureCurrentSchema();
|
|
}
|
|
$pdo = db();
|
|
|
|
// Ensure Admin Role exists (might be in schema but just in case)
|
|
$stmt = $pdo->prepare("SELECT id FROM role_groups WHERE name = 'Administrator'");
|
|
$stmt->execute();
|
|
$role = $stmt->fetch();
|
|
if (!$role) {
|
|
$pdo->exec("INSERT INTO role_groups (name) VALUES ('Administrator')");
|
|
$roleId = $pdo->lastInsertId();
|
|
$pdo->exec("INSERT INTO role_permissions (role_id, permission) VALUES ($roleId, 'all')");
|
|
} else {
|
|
$roleId = $role['id'];
|
|
// Also ensure 'all' permission exists for it
|
|
$stmt = $pdo->prepare("SELECT id FROM role_permissions WHERE role_id = ? AND permission = 'all'");
|
|
$stmt->execute([$roleId]);
|
|
if (!$stmt->fetch()) {
|
|
$pdo->exec("INSERT INTO role_permissions (role_id, permission) VALUES ($roleId, 'all')");
|
|
}
|
|
}
|
|
|
|
// Insert Admin User (Use ON DUPLICATE KEY UPDATE to handle existing user from schema)
|
|
$hashedPass = password_hash($adminPass, PASSWORD_DEFAULT);
|
|
$stmt = $pdo->prepare("INSERT INTO users (username, password, email, group_id, status)
|
|
VALUES (?, ?, ?, ?, 'active')
|
|
ON DUPLICATE KEY UPDATE
|
|
password = VALUES(password),
|
|
email = VALUES(email),
|
|
group_id = VALUES(group_id),
|
|
status = 'active'");
|
|
$stmt->execute([$adminUser, $hashedPass, $adminEmail, $roleId]);
|
|
|
|
header("Location: index.php?step=4");
|
|
exit;
|
|
} catch (Exception $e) {
|
|
$error = "Setup Failed: " . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
// Step 4: Finish
|
|
if ($step === 4) {
|
|
file_put_contents($lockFile, date('Y-m-d H:i:s'));
|
|
}
|
|
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<title>Installation - Step <?= $step ?></title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
<style>
|
|
body { background: #f0f2f5; font-family: 'Inter', sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
|
|
.install-container { width: 100%; max-width: 600px; padding: 20px; }
|
|
.card { border: none; border-radius: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.08); overflow: hidden; }
|
|
.card-header { background: linear-gradient(135deg, #6e8efb, #a777e3); color: white; border: none; padding: 30px; text-align: center; }
|
|
.step-indicator { display: flex; justify-content: space-between; margin-bottom: 30px; position: relative; }
|
|
.step-indicator::before { content: ''; position: absolute; top: 15px; left: 0; right: 0; height: 2px; background: #e0e0e0; z-index: 1; }
|
|
.step-dot { width: 32px; height: 32px; background: #fff; border: 2px solid #e0e0e0; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 2; font-weight: bold; font-size: 14px; color: #999; }
|
|
.step-dot.active { border-color: #6e8efb; color: #6e8efb; background: #fff; box-shadow: 0 0 10px rgba(110, 142, 251, 0.3); }
|
|
.step-dot.completed { background: #6e8efb; border-color: #6e8efb; color: #fff; }
|
|
.btn-primary { background: linear-gradient(135deg, #6e8efb, #a777e3); border: none; border-radius: 12px; padding: 12px 24px; font-weight: 600; }
|
|
.form-control { border-radius: 12px; padding: 12px; border: 1px solid #dee2e6; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="install-container">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="fw-bold mb-0">System Installation</h3>
|
|
<p class="mb-0 opacity-75">Configure your application in minutes</p>
|
|
</div>
|
|
<div class="card-body p-4 p-md-5">
|
|
<div class="step-indicator">
|
|
<div class="step-dot <?= $step >= 1 ? ($step > 1 ? 'completed' : 'active') : '' ?>"><?= $step > 1 ? '<i class="bi bi-check"></i>' : '1' ?></div>
|
|
<div class="step-dot <?= $step >= 2 ? ($step > 2 ? 'completed' : 'active') : '' ?>"><?= $step > 2 ? '<i class="bi bi-check"></i>' : '2' ?></div>
|
|
<div class="step-dot <?= $step >= 3 ? ($step > 3 ? 'completed' : 'active') : '' ?>"><?= $step > 3 ? '<i class="bi bi-check"></i>' : '3' ?></div>
|
|
<div class="step-dot <?= $step >= 4 ? 'completed' : '' ?>">4</div>
|
|
</div>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-danger d-flex align-items-center mb-4">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
|
<div><?= $error ?></div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($step === 1): ?>
|
|
<h5 class="fw-bold mb-4">Step 1: Check Requirements</h5>
|
|
<div class="list-group list-group-flush mb-4">
|
|
<?php foreach ($requirements as $req): ?>
|
|
<div class="list-group-item d-flex justify-content-between align-items-center py-3">
|
|
<div>
|
|
<h6 class="mb-0 fw-semibold"><?= $req['name'] ?></h6>
|
|
<small class="text-muted">Current: <?= $req['current'] ?></small>
|
|
</div>
|
|
<?php if ($req['status']): ?>
|
|
<span class="badge bg-success-subtle text-success rounded-pill px-3">Passed</span>
|
|
<?php else: ?>
|
|
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">Failed</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php if ($allOk): ?>
|
|
<div class="d-grid">
|
|
<a href="index.php?step=2" class="btn btn-primary">Next: Database Setup</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="alert alert-warning small">Please fix the issues above to continue.</div>
|
|
<div class="d-grid">
|
|
<button onclick="window.location.reload()" class="btn btn-secondary">Retry Check</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php elseif ($step === 2): ?>
|
|
<h5 class="fw-bold mb-4">Step 2: Database Configuration</h5>
|
|
<form method="POST">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Database Host</label>
|
|
<input type="text" name="db_host" class="form-control" value="127.0.0.1" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Database Name</label>
|
|
<input type="text" name="db_name" class="form-control" placeholder="e.g. admin_db" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Database User</label>
|
|
<input type="text" name="db_user" class="form-control" placeholder="e.g. root" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Database Password</label>
|
|
<input type="password" name="db_pass" class="form-control" placeholder="Enter password">
|
|
</div>
|
|
<div class="d-grid mt-4">
|
|
<button type="submit" class="btn btn-primary">Verify, Save & Import Schema</button>
|
|
</div>
|
|
</form>
|
|
<div class="alert alert-info small mt-3 mb-0">This step creates the database tables and imports the base schema before you add the admin account.</div>
|
|
|
|
<?php elseif ($step === 3): ?>
|
|
<h5 class="fw-bold mb-4">Step 3: Super Admin Setup</h5>
|
|
<form method="POST">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Admin Username</label>
|
|
<input type="text" name="admin_user" class="form-control" placeholder="e.g. admin" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Admin Email</label>
|
|
<input type="email" name="admin_email" class="form-control" placeholder="admin@example.com" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">Admin Password</label>
|
|
<input type="password" name="admin_pass" class="form-control" placeholder="Minimum 6 characters" required minlength="6">
|
|
</div>
|
|
<div class="d-grid mt-4">
|
|
<button type="submit" class="btn btn-primary">Finish Installation</button>
|
|
</div>
|
|
</form>
|
|
|
|
<?php elseif ($step === 4): ?>
|
|
<div class="text-center py-4">
|
|
<div class="display-1 text-success mb-4">
|
|
<i class="bi bi-check-circle-fill"></i>
|
|
</div>
|
|
<h4 class="fw-bold">Ready to Launch!</h4>
|
|
<p class="text-muted">Installation completed successfully. Your system is now secure and ready to use.</p>
|
|
<div class="alert alert-info small mt-4">
|
|
<i class="bi bi-info-circle me-2"></i>
|
|
For security, the <code>installed.lock</code> file has been created.
|
|
</div>
|
|
<div class="d-grid mt-5">
|
|
<a href="../index.php" class="btn btn-primary">Go to Dashboard</a>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|