Вход Регистрация
Файл: 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_VERSIONMIN_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'0755true),
        
'value' => is_writable(INSTALL_ROOT '/core') ? 'доступна' 'нет прав',
    ];
    
$checks['writable_cache'] = [
        
'name'  => 'Запись в /core/cache/',
        
'ok'    => is_dir(INSTALL_ROOT '/core/cache') || @mkdir(INSTALL_ROOT '/core/cache'0755true),
        
'value' => (is_dir(INSTALL_ROOT '/core/cache') && is_writable(INSTALL_ROOT '/core/cache')) ? 'доступна' 'нет прав',
    ];
    return 
$checks;
}

/**
 * Выполнить SQL-схему по отдельным запросам (PDO часто не поддерживает multi-query).
 */
function execSchema(PDO $pdostring $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 $cfgstring $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 &copy; ' 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($emailFILTER_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($passPASSWORD_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($configDir0755true);
                }
                
$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;
Онлайн: 2
Реклама