Файл: new_top/install/index.php
Строк: 391
<?php
/**
* New Top System — Installation Wizard
* Multi-step installer. Index lives in project root.
*
* Важно: обработка POST выполняется ДО любого вывода HTML,
* иначе header('Location: ...') не сработает.
*/
declare(strict_types=1);
$configFile = dirname(__DIR__) . '/core/config/database.php';
if (is_file($configFile) && !isset($_GET['force'])) {
http_response_code(403);
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Install</title></head><body style="font-family:sans-serif;background:#0b0f1a;color:#eee;padding:40px">';
echo '<h1>Установка уже выполнена</h1><p>Удалите каталог <code>install/</code> с сервера.</p>';
echo '<p><a href="/" style="color:#5b8def">На главную</a></p><?= pwa_register() ?>
</body></html>';
exit;
}
session_start();
define('INSTALL_ROOT', dirname(__DIR__));
define('INSTALL_DIR', __DIR__);
define('MIN_PHP', '7.4.0');
if (file_exists(INSTALL_ROOT . '/core/config/database.php') && empty($_GET['force'])) {
header('Location: ../');
exit('Система уже установлена. Для принудительной переустановки используйте ?force=1');
}
$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
$errors = [];
function checkRequirements(): array
{
$checks = [];
$checks['php_version'] = [
'name' => 'Версия PHP ≥ ' . MIN_PHP,
'ok' => version_compare(PHP_VERSION, MIN_PHP, '>='),
'value' => PHP_VERSION,
];
$checks['pdo'] = [
'name' => 'Расширение PDO',
'ok' => extension_loaded('pdo'),
'value' => extension_loaded('pdo') ? 'установлено' : 'отсутствует',
];
$checks['pdo_mysql'] = [
'name' => 'PDO MySQL',
'ok' => extension_loaded('pdo_mysql'),
'value' => extension_loaded('pdo_mysql') ? 'установлено' : 'отсутствует',
];
$checks['gd'] = [
'name' => 'GD (для счётчиков-изображений)',
'ok' => extension_loaded('gd'),
'value' => extension_loaded('gd') ? 'установлено' : 'отсутствует (рекомендуется)',
];
$checks['mbstring'] = [
'name' => 'mbstring',
'ok' => extension_loaded('mbstring'),
'value' => extension_loaded('mbstring') ? 'установлено' : 'отсутствует',
];
$checks['writable_core'] = [
'name' => 'Запись в /core/config/',
'ok' => is_writable(INSTALL_ROOT . '/core') || @mkdir(INSTALL_ROOT . '/core/config', 0755, true),
'value' => is_writable(INSTALL_ROOT . '/core') ? 'доступна' : 'нет прав',
];
$checks['writable_cache'] = [
'name' => 'Запись в /core/cache/',
'ok' => is_dir(INSTALL_ROOT . '/core/cache') || @mkdir(INSTALL_ROOT . '/core/cache', 0755, true),
'value' => (is_dir(INSTALL_ROOT . '/core/cache') && is_writable(INSTALL_ROOT . '/core/cache')) ? 'доступна' : 'нет прав',
];
return $checks;
}
/**
* Выполнить SQL-схему по отдельным запросам (PDO часто не поддерживает multi-query).
*/
function execSchema(PDO $pdo, string $sql): void
{
// Удаляем комментарии вида -- ...
$sql = preg_replace('/^--.*$/m', '', $sql);
// Разбиваем по ; с учётом того, что внутри строк ; маловероятны в нашей схеме
$parts = preg_split('/;s*[rn]+/', $sql);
foreach ($parts as $part) {
$part = trim($part);
if ($part === '' || $part === ';') {
continue;
}
// Добавляем ; если нужно
if (substr($part, -1) !== ';') {
$part .= ';';
}
$pdo->exec($part);
}
}
function generateConfigFile(array $cfg, string $siteTitle): string
{
$host = addslashes($cfg['db_host']);
$name = addslashes($cfg['db_name']);
$user = addslashes($cfg['db_user']);
$pass = addslashes($cfg['db_pass']);
$csrf = bin2hex(random_bytes(16));
$cron = bin2hex(random_bytes(16));
$date = date('Y-m-d H:i:s');
return <<<PHP
<?php
/**
* Database configuration — generated by installer
* Generated: {$date}
*/
declare(strict_types=1);
return [
'db' => [
'host' => '{$host}',
'dbname' => '{$name}',
'username' => '{$user}',
'password' => '{$pass}',
'charset' => 'utf8mb4',
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
'site' => [
'title' => '{$siteTitle}',
'url' => '',
],
'security' => [
'session_name' => 'TOPSESSID',
'csrf_key' => '{$csrf}',
'cron_key' => '{$cron}',
'trust_proxy' => false, // true only behind trusted reverse proxy
],
];
PHP;
}
function renderHeader(string $title): void
{
echo '<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>' . htmlspecialchars($title) . ' — Установка Top System</title>
<style>
:root {
--bg:#0b0f1a; --surface:#1a2234; --surface2:#222c42; --border:rgba(255,255,255,.08);
--text:#e8eef8; --muted:#8b9bb8; --primary:#5b8def; --accent:#00d4aa; --danger:#ff6b7a;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.5;
background-image:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(91,141,239,.18),transparent);min-height:100vh;padding:24px 12px}
.wrapper{max-width:520px;margin:0 auto;background:var(--surface);border:1px solid var(--border);border-radius:20px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,.35)}
.header{background:linear-gradient(135deg,#1a2744,#243656);padding:24px 22px;border-bottom:1px solid var(--border)}
.header h1{font-size:1.3rem;font-weight:700}
.header p{color:var(--muted);font-size:.88rem;margin-top:4px}
.content{padding:22px}
.steps{display:flex;gap:6px;margin-bottom:22px}
.step{flex:1;text-align:center;padding:8px 4px;background:#121826;border-radius:999px;font-size:.7rem;font-weight:700;color:var(--muted);border:1px solid var(--border)}
.step.active{background:linear-gradient(135deg,#5b8def,#3d6fd8);color:#fff;border-color:transparent}
.step.done{background:rgba(0,212,170,.15);color:var(--accent);border-color:rgba(0,212,170,.3)}
.card{background:#121826;border:1px solid var(--border);border-radius:14px;padding:18px;margin-bottom:16px}
.check-item{display:flex;justify-content:space-between;align-items:center;padding:11px 0;border-bottom:1px solid var(--border);font-size:.88rem;gap:12px}
.check-item:last-child{border-bottom:none}
.ok{color:var(--accent);font-weight:700}.fail{color:var(--danger);font-weight:700}
label{display:block;margin-bottom:6px;font-weight:600;font-size:.8rem;color:var(--muted)}
input{width:100%;padding:12px 14px;border:1px solid var(--border);border-radius:10px;font-size:1rem;margin-bottom:14px;background:#0b0f1a;color:var(--text)}
input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(91,141,239,.25)}
.btn{display:inline-flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#5b8def,#3d6fd8);color:#fff;border:none;padding:12px 22px;border-radius:999px;font-size:.92rem;font-weight:700;cursor:pointer;text-decoration:none;box-shadow:0 4px 16px rgba(91,141,239,.35)}
.btn-secondary{background:#222c42;box-shadow:none;border:1px solid var(--border);color:var(--text)}
.error{background:rgba(255,107,122,.12);border:1px solid rgba(255,107,122,.35);color:#ffb3bb;padding:12px 14px;border-radius:10px;margin-bottom:14px;font-size:.9rem}
.success{background:rgba(0,212,170,.1);border:1px solid rgba(0,212,170,.3);color:#7dffd4;padding:12px 14px;border-radius:10px;margin-bottom:14px;font-size:.9rem}
.footer{text-align:center;padding:14px;color:var(--muted);font-size:.75rem;border-top:1px solid var(--border)}
.actions{display:flex;justify-content:space-between;gap:10px;margin-top:8px;flex-wrap:wrap}
code{background:#0b0f1a;padding:2px 6px;border-radius:4px;font-size:.85em}
ul{margin:12px 0 0 18px;color:var(--muted);font-size:.9rem}
</style>
</head>
<body>
<div class="wrapper">
<div class="header">
<h1>Установка Top System</h1>
<p>Современная система рейтинга сайтов со счётчиками</p>
</div>
<div class="content">';
}
function renderFooter(): void
{
echo '</div><div class="footer">New Top System © ' . date('Y') . '</div></div></body></html>';
}
function installRedirect(int $toStep): void
{
// Абсолютный путь относительно текущего скрипта — надёжнее, чем ?step=
$target = 'index.php?step=' . $toStep;
if (!headers_sent()) {
header('Location: ' . $target);
exit;
}
// Fallback, если заголовки уже отправлены
echo '<script>location.replace(' . json_encode($target) . ');</script>';
echo '<noscript><meta http-equiv="refresh" content="0;url=' . htmlspecialchars($target) . '"></noscript>';
exit;
}
// =====================================================================
// STEP 1 — только проверка, редиректов после POST нет
// =====================================================================
if ($step === 1) {
renderHeader('Проверка окружения');
$checks = checkRequirements();
$allOk = true;
echo '<div class="steps">
<div class="step active">1. Окружение</div>
<div class="step">2. База данных</div>
<div class="step">3. Администратор</div>
<div class="step">4. Завершение</div>
</div><div class="card">';
foreach ($checks as $c) {
$isGd = (strpos($c['name'], 'GD') !== false);
if (!$c['ok'] && !$isGd) {
$allOk = false;
}
echo '<div class="check-item"><span>' . htmlspecialchars($c['name']) . '</span>
<span class="' . ($c['ok'] ? 'ok' : 'fail') . '">' . htmlspecialchars($c['value']) . '</span></div>';
}
echo '</div>';
if ($allOk) {
echo '<div class="success">Основные проверки пройдены. Можно продолжать.</div>';
echo '<div class="actions"><div></div><a href="?step=2" class="btn">Далее →</a></div>';
} else {
echo '<div class="error">Обнаружены критические проблемы. Устраните их и обновите страницу.</div>';
echo '<div class="actions"><a href="?step=1" class="btn btn-secondary">Обновить</a></div>';
}
renderFooter();
exit;
}
// =====================================================================
// STEP 2 — сначала POST (запись в БД + редирект), потом HTML
// =====================================================================
if ($step === 2) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$dbHost = trim($_POST['db_host'] ?? 'localhost');
$dbName = trim($_POST['db_name'] ?? '');
$dbUser = trim($_POST['db_user'] ?? '');
$dbPass = $_POST['db_pass'] ?? '';
if ($dbName === '' || $dbUser === '') {
$errors[] = 'Имя базы данных и пользователь обязательны.';
} elseif (!preg_match('/^[a-zA-Z0-9._-]+$/', $dbHost)) {
$errors[] = 'Некорректный хост БД.';
} else {
try {
$dsn = "mysql:host={$dbHost};charset=utf8mb4";
$pdo = new PDO($dsn, $dbUser, $dbPass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// Экранируем имя БД для идентификатора
if (!preg_match('/^[a-zA-Z0-9_]+$/', $dbName)) {
throw new RuntimeException('Имя базы данных может содержать только латинские буквы, цифры и подчёркивание.');
}
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$pdo->exec("USE `{$dbName}`");
$schemaPath = INSTALL_ROOT . '/sql/schema.sql';
$schema = file_get_contents($schemaPath);
if ($schema === false) {
throw new RuntimeException('Не удалось прочитать файл sql/schema.sql');
}
execSchema($pdo, $schema);
$_SESSION['install'] = [
'db_host' => $dbHost,
'db_name' => $dbName,
'db_user' => $dbUser,
'db_pass' => $dbPass,
];
// Редирект ДО любого HTML
installRedirect(3);
} catch (Throwable $e) {
$errors[] = 'Ошибка: ' . $e->getMessage();
}
}
}
// Только если редиректа не было — рисуем форму
renderHeader('Настройка базы данных');
echo '<div class="steps">
<div class="step done">1. Окружение</div>
<div class="step active">2. База данных</div>
<div class="step">3. Администратор</div>
<div class="step">4. Завершение</div>
</div>';
foreach ($errors as $e) {
echo '<div class="error">' . htmlspecialchars($e) . '</div>';
}
$vHost = htmlspecialchars($_POST['db_host'] ?? 'localhost');
$vName = htmlspecialchars($_POST['db_name'] ?? 'top_system');
$vUser = htmlspecialchars($_POST['db_user'] ?? 'root');
echo '<form method="post" action="?step=2" class="card">
<label>Хост базы данных</label>
<input type="text" name="db_host" value="' . $vHost . '" required>
<label>Имя базы данных</label>
<input type="text" name="db_name" value="' . $vName . '" required pattern="[a-zA-Z0-9_]+" title="Только латиница, цифры и _">
<label>Пользователь</label>
<input type="text" name="db_user" value="' . $vUser . '" required>
<label>Пароль</label>
<input type="password" name="db_pass" value="">
<div class="actions">
<a href="?step=1" class="btn btn-secondary">← Назад</a>
<button type="submit" class="btn">Создать таблицы и продолжить</button>
</div>
</form>';
renderFooter();
exit;
}
// =====================================================================
// STEP 3 — сначала POST (создание админа + конфиг + редирект), потом HTML
// =====================================================================
if ($step === 3) {
if (empty($_SESSION['install'])) {
installRedirect(2);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$login = trim($_POST['login'] ?? '');
$email = trim($_POST['email'] ?? '');
$pass = $_POST['password'] ?? '';
$pass2 = $_POST['password2'] ?? '';
$siteTitle = trim($_POST['site_title'] ?? 'Top Rating System');
if (mb_strlen($login) < 3) {
$errors[] = 'Логин должен содержать не менее 3 символов.';
}
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Некорректный email.';
}
if (mb_strlen($pass) < 6) {
$errors[] = 'Пароль должен содержать не менее 6 символов.';
}
if ($pass !== $pass2) {
$errors[] = 'Пароли не совпадают.';
}
if (!$errors) {
try {
$cfg = $_SESSION['install'];
$dsn = "mysql:host={$cfg['db_host']};dbname={$cfg['db_name']};charset=utf8mb4";
$pdo = new PDO($dsn, $cfg['db_user'], $cfg['db_pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$hash = password_hash($pass, PASSWORD_DEFAULT);
$time = time();
$stmt = $pdo->prepare(
'INSERT INTO `user` (`login`, `password`, `email`, `level`, `reg_time`, `last_time`) VALUES (?, ?, ?, 9, ?, ?)'
);
$stmt->execute([$login, $hash, $email, $time, $time]);
$pdo->prepare('UPDATE `config` SET `title` = ? WHERE `id` = 1')->execute([$siteTitle]);
$configDir = INSTALL_ROOT . '/core/config';
if (!is_dir($configDir)) {
mkdir($configDir, 0755, true);
}
$configContent = generateConfigFile($cfg, $siteTitle);
if (file_put_contents($configDir . '/database.php', $configContent) === false) {
throw new RuntimeException('Не удалось записать core/config/database.php. Проверьте права на запись.');
}
@file_put_contents(INSTALL_DIR . '/install.lock', date('c'));
unset($_SESSION['install']);
// Редирект ДО любого HTML
installRedirect(4);
} catch (Throwable $e) {
$errors[] = 'Ошибка: ' . $e->getMessage();
}
}
}
renderHeader('Создание администратора');
echo '<div class="steps">
<div class="step done">1. Окружение</div>
<div class="step done">2. База данных</div>
<div class="step active">3. Администратор</div>
<div class="step">4. Завершение</div>
</div>';
foreach ($errors as $e) {
echo '<div class="error">' . htmlspecialchars($e) . '</div>';
}
$vTitle = htmlspecialchars($_POST['site_title'] ?? 'Top Rating System');
$vLogin = htmlspecialchars($_POST['login'] ?? 'admin');
$vEmail = htmlspecialchars($_POST['email'] ?? '');
echo '<form method="post" action="?step=3" class="card">
<label>Название сайта</label>
<input type="text" name="site_title" value="' . $vTitle . '" required>
<label>Логин администратора</label>
<input type="text" name="login" value="' . $vLogin . '" required autocomplete="username">
<label>Email (необязательно)</label>
<input type="email" name="email" value="' . $vEmail . '">
<label>Пароль</label>
<input type="password" name="password" required autocomplete="new-password">
<label>Повтор пароля</label>
<input type="password" name="password2" required autocomplete="new-password">
<div class="actions">
<a href="?step=2" class="btn btn-secondary">← Назад</a>
<button type="submit" class="btn">Создать и завершить</button>
</div>
</form>';
renderFooter();
exit;
}
// =====================================================================
// STEP 4 — завершение
// =====================================================================
if ($step === 4) {
renderHeader('Установка завершена');
echo '<div class="steps">
<div class="step done">1. Окружение</div>
<div class="step done">2. База данных</div>
<div class="step done">3. Администратор</div>
<div class="step active">4. Завершение</div>
</div>
<div class="success"><strong>Поздравляем!</strong><br>
Система успешно установлена. Файл index.php находится в корне проекта.
Счётчики доступны по адресам <code>/cn/big/{id}</code> и <code>/cn/small/{id}</code>.
</div>
<div class="card">
<p><strong>Рекомендации по безопасности:</strong></p>
<ul style="margin:12px 0 0 20px">
<li>Удалите или переименуйте папку <code>/install</code>.</li>
<li>Файл <code>core/config/database.php</code> не должен быть доступен извне.</li>
<li>Права: папки 755, файлы 644.</li>
</ul>
</div>
<div class="actions">
<a href="../" class="btn">Перейти на сайт</a>
<a href="../?module=admin" class="btn">Админ-панель</a>
</div>';
renderFooter();
exit;
}
// Неизвестный шаг
header('Location: index.php?step=1');
exit;