'Requirements', 2 => 'Database', 3 => 'Server', 4 => 'Schema', 5 => 'Administrator', 6 => 'Finish', ); // --------------------------------------------------------------------------- // Paths // --------------------------------------------------------------------------- function install_root(): string { return dirname(__DIR__); } function install_lock_file(): string { return __DIR__ . '/' . INSTALL_LOCK; } function install_config_file(): string { return install_root() . '/config.local.php'; } // --------------------------------------------------------------------------- // The lock // --------------------------------------------------------------------------- /** * Why two conditions rather than one: * * The lock file alone is not enough - someone restoring a backup or unpacking * a fresh copy over an installed site would drop it and the installer would * happily reset the admin account. The database is the second opinion: if the * znote table already holds a row, this site is installed regardless of what * the filesystem says. * * Returns '' when the installer may run, or a reason when it may not. */ function install_locked_reason(): string { if (is_file(install_lock_file())) { return 'This site is already installed. Delete install/' . INSTALL_LOCK . ' if you really mean to run the installer again.'; } // A wizard already in progress must not be locked out by its own work: // step 4 creates the znote table, which is exactly what the check below // looks for. Step 2 has already warned if the database was not empty. if (!empty($_SESSION['install']) || install_max_step() > 1) { return ''; } $config = install_saved_config(); if (!$config) { return ''; } $link = @new mysqli($config['sqlHost'], $config['sqlUser'], $config['sqlPassword'], $config['sqlDatabase']); if ($link->connect_errno) { return ''; } $result = @$link->query('SELECT `id` FROM `znote` LIMIT 1'); $rows = ($result !== false) ? $result->num_rows : 0; $link->close(); if ($rows > 0) { return 'The database already contains a ZnoteX installation. The installer will not' . ' overwrite it. Delete the znote table first if that is really what you want.'; } return ''; } // --------------------------------------------------------------------------- // Wizard state // // Kept in the session, so a refresh does not lose the credentials typed two // steps ago. Nothing is written to disk until the final step. // --------------------------------------------------------------------------- function install_state(?array $merge = null): array { if (!isset($_SESSION['install'])) { $_SESSION['install'] = array(); } if ($merge !== null) { $_SESSION['install'] = array_merge($_SESSION['install'], $merge); } return $_SESSION['install']; } function install_get(string $key, $default = '') { $state = install_state(); return $state[$key] ?? $default; } function install_reset(): void { unset($_SESSION['install']); } /** Highest step reached, so someone cannot skip ahead by editing the URL. */ function install_max_step(?int $reached = null): int { if ($reached !== null && $reached > (int)($_SESSION['install_max'] ?? 1)) { $_SESSION['install_max'] = $reached; } return (int)($_SESSION['install_max'] ?? 1); } // --------------------------------------------------------------------------- // Config // --------------------------------------------------------------------------- /** Read config.local.php if it exists, else fall back to config.php values. */ function install_saved_config(): array { $keys = array('sqlHost', 'sqlUser', 'sqlPassword', 'sqlDatabase'); $out = array(); foreach (array(install_config_file(), install_root() . '/config.php') as $file) { if (!is_file($file)) { continue; } $config = array(); // Included in a function so it cannot pollute anything. @include $file; foreach ($keys as $key) { if (!isset($out[$key]) && isset($config[$key])) { $out[$key] = (string)$config[$key]; } } } return (count($out) === count($keys)) ? $out : array(); } /** A connection using the values collected so far, or null. */ function install_connect(?string &$error = null): ?mysqli { $link = @new mysqli( (string)install_get('sqlHost', '127.0.0.1'), (string)install_get('sqlUser'), (string)install_get('sqlPassword'), (string)install_get('sqlDatabase') ); if ($link->connect_errno) { $error = $link->connect_error; return null; } $link->set_charset('utf8mb4'); $link->query("SET collation_connection = 'utf8mb4_general_ci'"); return $link; } // --------------------------------------------------------------------------- // Checks // --------------------------------------------------------------------------- /** Requirements, as [label, ok, detail, fatal]. */ function install_requirements(): array { $checks = array(); $checks[] = array( 'PHP 8.1 or newer', PHP_VERSION_ID >= 80100, 'You are on PHP ' . PHP_VERSION, true, ); foreach (array('mysqli' => true, 'curl' => false, 'openssl' => false, 'gd' => false, 'zip' => false) as $ext => $fatal) { $checks[] = array( 'Extension: ' . $ext, extension_loaded($ext), $fatal ? 'Required' : 'Optional', $fatal, ); } $cache = install_root() . '/engine/cache'; $checks[] = array( 'engine/cache/ is writable', is_dir($cache) && is_writable($cache), $cache, true, ); $checks[] = array( 'The site root is writable', is_writable(install_root()), 'Needed to write config.local.php. You can also create it by hand at the last step.', false, ); $schema = install_root() . '/SQL/znote_schema.sql'; $checks[] = array( 'SQL/znote_schema.sql is present', is_file($schema), $schema, true, ); return $checks; } /** * Tables the OT server creates, which ZnoteX reads but never creates itself. * Their absence is what makes the installer refuse to go on. */ function install_server_tables(mysqli $link): array { $required = array('accounts', 'players'); $optional = array('guilds', 'houses', 'player_deaths', 'players_online'); $present = array(); $result = @$link->query('SHOW TABLES'); if ($result !== false) { while ($row = $result->fetch_array()) { $present[strtolower($row[0])] = true; } } $out = array('required' => array(), 'optional' => array(), 'ok' => true); foreach ($required as $table) { $found = isset($present[$table]); $out['required'][$table] = $found; if (!$found) { $out['ok'] = false; } } foreach ($optional as $table) { $out['optional'][$table] = isset($present[$table]); } $out['znote_installed'] = isset($present['znote']); return $out; } // --------------------------------------------------------------------------- // Small view helpers // --------------------------------------------------------------------------- function ih($value): string { return htmlspecialchars((string)($value ?? ''), ENT_QUOTES, 'UTF-8'); } function install_url(int $step): string { return 'index.php?step=' . $step; } function install_error(string $message): void { $_SESSION['install_error'] = $message; } function install_take_error(): string { $error = (string)($_SESSION['install_error'] ?? ''); unset($_SESSION['install_error']); return $error; } function install_csrf_token(): string { if (empty($_SESSION['install_csrf']) || !is_string($_SESSION['install_csrf'])) { $_SESSION['install_csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['install_csrf']; } function install_csrf_field(): string { return ''; } function install_csrf_validate(): bool { $posted = $_POST['install_csrf'] ?? null; $token = $_SESSION['install_csrf'] ?? null; if (!is_string($posted) || $posted === '' || !is_string($token) || $token === '') { return false; } $valid = hash_equals($token, $posted); if ($valid) { $_SESSION['install_csrf'] = bin2hex(random_bytes(32)); } return $valid; } function install_csrf_inject(string $html): string { if (stripos($html, ']*\bmethod\s*=\s*["\']?post["\']?)[^>]*>~i', static function (array $match): string { return $match[0] . "\n" . install_csrf_field(); }, $html ) ?? $html; }