Интеграция форума PunBB с Яндекс ID

У меня форум на forum.rcl-radio.ru, движок PunBB, база через PDO, PHP 7.4 — в общем, классика, которая вроде бы работает, но как только пытаешься что-то добавить сбоку, сразу начинаются танцы с бубном.

Идея была простая: пусть люди заходят не через форму логина, а кнопкой «Войти с Яндексом». Я думал, что самое сложное — это разобраться с OAuth у Яндекса. Оказалось, самое сложное — это не Яндекс, а сам PunBB.

PunBB не умеет «просто пускать» по токену. Ему нужна его родная кука — строка в определённом формате, которую он сам и проверяет. И если хоть один символ в этой строке не совпадёт, форум молча выдаст 500 или просто перекинет на страницу логина.

Главный камень преткновения: формула куки

Я долго не мог понять, почему кука (cookie), которую я ставлю, не работает. Казалось бы, бери ID, пароль, соль (salt), склеивай, кодируй — и всё. Но PunBB очень капризный.

Оказалось, что внутри PunBB есть функция forum_hash, и в ней — две строчки, которые всех сбивают с толку:

php
$return = ($hook = get_hook('fn_forum_hash_start')) ? eval($hook) : null;
if ($return !== null) return $return;

Сначала я пытался их вставить в свой код. И тут же получал ошибку 500, потому что в моём скрипте нет ни get_hook, ни системы плагинов PunBB.

Как я потом понял: эти строки — это просто «дверь» для плагинов. Они говорят: «Если какой-то плагин хочет поменять формулу хеша — пусть считает сам. Если нет — считаем по стандарту». А стандарт такой:

SHA‑1(соль + SHA‑1(время))

И финальный хеш для куки:

SHA‑1(соль + пароль_SHA1 + временной_хеш)

То есть в своём скрипте я не копирую эти две строки с хуками. Я сразу беру ту ветку, которая срабатывает, если плагинов нет. Это и есть правильный путь.

Как это выглядит в реальном коде

Вот тот самый кусок, который у меня в итоге заработал. Он маленький, без лишнего, и его можно вставлять частями:

php
// Ищем пользователя по email, который прислал Яндекс
$stmt = $pdo->prepare("SELECT id, salt, password FROM {$db_prefix}users WHERE email = ? LIMIT 1");
$stmt->execute([$yandex_email]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
 
if (!$row) {
    // Если пользователя нет — можно создать нового или просто отказать
    return false;
}
 
$user_id = (int)$row['id'];
$salt = $row['salt'];
$password_field = $row['password']; // В базе уже лежит SHA‑1, не трогаем
 
// Время жизни куки — 14 дней
$now = time();
$expire = $now + 1209600;
 
// Считаем хеши ровно так, как PunBB делает по умолчанию
$inner = sha1((string)$expire);
$time_hash = sha1($salt . $inner);
$final_hash = sha1($salt . $password_field . $time_hash);
 
// Собираем строку и кодируем в Base64
$raw = $user_id . '|' . $password_field . '|' . $expire . '|' . $final_hash;
$encoded = base64_encode($raw);
 
// Ставим куку с правильными флагами
setcookie(
    'punbb_cookie',
    $encoded,
    $expire,
    '/',
    '.forum.rcl-radio.ru',
    true,  // secure — только по HTTPS
    true   // httponly — недоступна из JS
);
 
header('Location: https://forum.rcl-radio.ru/');
exit;

Где я ошибался чаще всего

Если ты сейчас делаешь то же самое, проверь эти моменты — именно на них я терял больше всего времени:

  • Пытался вставить хуки. В отдельном скрипте они не работают и ломают всё.
  • Перехешировал пароль. В базе PunBB уже лежит SHA‑1. Если сверху сделать ещё один sha1 или password_hash, хеш станет другим, и кука не подойдёт.
  • Не приводил $expire к строке. PunBB явно делает (string)$expire. Если этого не сделать, хеш не совпадёт.
  • Порядок склейки. PunBB чувствителен к порядку: соль + пароль + временной_хеш. Поменяешь местами — кука невалидна.
  • Домен куки. PunBB ожидает домен с точкой: .forum.rcl-radio.ru. Без точки кука может не подхватиться.

Как быстро проверить, что всё считается правильно

Самый надёжный способ — взять одну рабочую куку прямо с форума (ту, с которой вход реально работает) и проверить формулу вручную.

  1. Декодируй куку: $parts = explode('|', base64_decode($cookie_value));
  2. Возьми соль этого пользователя из базы.
  3. Посчитай:
    php
    $inner = sha1((string)$parts[2]);
    $time_hash = sha1($salt . $inner);
    $my_final = sha1($salt . $parts[1] . $time_hash);
    
  4. Сравни $my_final с $parts[3]. Если совпало — формула правильная.

Сейчас у меня эта схема работает стабильно: человек нажимает «Войти с Яндексом», скрипт считает куку по этой формуле, ставит её и перекидывает на форум. PunBB видит «родную» куку и сразу пускает пользователя.

Что на самом деле лежит внутри «куки» PunBB

PunBB не хранит в куке ничего лишнего. Внутри одной строки (после Base64) ровно четыре части, разделённые вертикальной чертой |:

ID_пользователя | пароль_SHA1 | время_истечения | финальный_хеш

Пример (условно):

5010|4b8123402efb5de823829985b346b106888caa42|1783920581|b717ca2b42abf8686fbdc3919fb2918b7ba270be
  • ID просто число, кто это.
  • Пароль_SHA1 в базе PunBB он уже захеширован как SHA‑1. Его не надо трогать, не надо делать ещё один sha1() или password_hash().
  • Время Unix‑время, когда кука должна «умереть». PunBB ставит 14 дней вперёд.
  • Финальный хеш вот это и есть самая хитрая часть: именно она доказывает PunBB, что строку собрал «свой», а не кто попало.

Формула финального хеша — по шагам

PunBB считает финальный хеш так (это дефолтная ветка, когда нет плагинов):

  1. Берём время (expire) и делаем из него внутренний хеш:
    php
    $inner = sha1((string)$expire);

    Важно: приводим к строке (string). Если этого не сделать, в PHP может быть разное поведение, и хеш «уедет».

  2. Считаем «временной хеш» с солью:
    php
    $time_hash = sha1($salt . $inner);

    Это аналог той самой функции forum_hash($expire, $salt).

  3. Считаем финальный хеш куки:
    php
    $final_hash = sha1($salt . $password_field . $time_hash);

Обрати внимание на порядок: соль + пароль + временной_хеш. PunBB очень строгий: поменяешь местами — хеш будет другой, и форум отвергнет куку.


Где тут прячется та самая «соль»

Соль (salt) — это уникальная строка из базы для конкретного пользователя. В PunBB она хранится в поле salt таблицы users.

Её роль простая, но критичная: даже если у двух пользователей одинаковый пароль, их хеши будут разными, потому что соль разная. А для PunBB это ещё и часть формулы: без правильной соли финальный хеш не совпадёт, и авторизация не пройдёт.


Регистрация приложения в Яндексе

Заходишь в консоль разработчика Яндекса, создаёшь приложение типа «Веб‑сервис». Тебе дадут:

  • client_id — это просто строка, её можно хранить в конфиге.
  • client_secret — это секрет, его держишь только на сервере, в PHP.

Там же указываешь Redirect URI — адрес, куда Яндекс вернёт пользователя после входа. Для PunBB это отдельный PHP‑файл, например: https://forum.rcl-radio.ru/login_yandex_callback.php. Он должен совпадать буква в букву (протокол, домен, путь, слэш в конце — всё важно).

И выбираешь scope — какие данные просишь:

  • login:email — чтобы получить email (для связки с пользователем PunBB).
  • login:info — имя и фамилию.
  • login:avatar — аватар.

Для форума чаще всего хватает login:email login:info.


Кнопка «Войти через Яндекс»

В меню PunBB ставишь обычную ссылку, которая перенаправляет пользователя на страницу авторизации Яндекса ID.

login_yandex.php:

<?php
// login_yandex.php — ТОЛЬКО РЕДИРЕКТ В ЯНДЕКС
 
// ВСТАВЬ СЮДА НОВЫЙ client_id ИЗ КАБИНЕТА ЯНДЕКСА
$client_id    = 'xxxxxxxxxxxxxxxxxxxxxxxxxxx'; 
$redirect_uri = 'https://forum.rcl-radio.ru/login_yandex_callback.php';
$scope = 'login:info';
 
$auth_url = 'https://oauth.yandex.ru/authorize?' . http_build_query([
    'client_id'     => $client_id,
    'response_type' => 'code',
    'redirect_uri'  => $redirect_uri,
    'scope'         => $scope
]);
 
header('Location: ' . $auth_url);
exit;
?>

$client_id — это одноразовый код, который Яндекс отдаёт только один раз и только на твой Redirect URI.


Получение Яндекс токена

Когда пользователь нажал «Войти (Яндекс ID)» Яндекс делает редирект на твой login_yandex_callback.php с параметром client_id. В этом файле ты делаешь POST‑запрос к Яндексу, чтобы обменять код на токен.

// --- OAUTH TOKEN ---
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://oauth.yandex.ru/token',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'grant_type'    => 'authorization_code',
        'code'          => $code,
        'client_id'     => $client_id,
        'client_secret' => $client_secret,
        'redirect_uri'  => $redirect_uri
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => true
]);
$token_resp = curl_exec($ch);
$curl_err = curl_error($ch);
curl_close($ch);
 
if ($curl_err) { 
    log_msg('CURL token error: ' . $curl_err); 
    header('Location: /index.php?p=login&error=oauth_token');
    exit; 
}
 
$token_data = json_decode($token_resp, true);
if (!$token_data || isset($token_data['error'])) { 
    log_msg('Token error data: ' . json_encode($token_data)); 
    header('Location: /index.php?p=login&error=token_invalid');
    exit; 
}
$access_token = $token_data['access_token'];

Получаем данные пользователя

С этим access_token делаешь GET‑запрос к API Яндекс ID:

// --- USER INFO ---
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://login.yandex.ru/info?format=json',
    CURLOPT_HTTPHEADER => ["Authorization: OAuth {$access_token}"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => true
]);
$user_info = curl_exec($ch);
$curl_err2 = curl_error($ch);
curl_close($ch);
 
if ($curl_err2) {
    log_msg('CURL profile error: ' . $curl_err2);
    header('Location: /index.php?p=login&error=profile_fetch');
    exit;
}
 
$data = json_decode($user_info, true);
if (!$data || !isset($data['id'])) {
    log_msg('Failed to get profile: ' . $user_info);
    header('Location: /index.php?p=login&error=profile_invalid');
    exit;
}
log_msg('Profile received for Yandex ID: ' . $data['id']);
 
$yandex_id = $data['id'];
$email = $data['default_email'] ?? '';
$login_raw = $data['login'] ?? 'yandex_' . $yandex_id;
$login = preg_replace('/[^a-zA-Z0-9_]/', '_', $login_raw);
if (strlen($login) > 200) $login = substr($login, 0, 200);

Этот код запрашивает у Яндекса данные профиля пользователя по access_token и подготавливает их для поиска/создания аккаунта в PunBB.


Поиск существующего или создание нового пользователя в PunBB по данным Яндекса

Вот код:

// --- FIND OR CREATE USER ---
$stmt = $pdo->prepare("SELECT id, username, group_id, salt, password, language FROM {$db_prefix}users WHERE username = ?");
$stmt->execute([$login]);
$row = $stmt->fetch();
 
if ($row) {
    $user_id = (int)$row['id'];
    $group_id = (int)$row['group_id'];
    log_msg("Existing user found: ID={$user_id}, group={$group_id}");
} else {
    // 1. Соль ровно 12 hex-символов
    $salt = bin2hex(random_bytes(6)); //  12 символов
 
    // 2. Генерируем случайный "пароль" (не показываем пользователю, нужен только для хеша PunBB)
    $plain_password = bin2hex(random_bytes(8)); // случайная строка
 
    // 3. Хеш по формуле PunBB: sha1(salt + plain_password)
    $password_hash = sha1($salt . $plain_password); //  валидный хеш
 
    $stmt = $pdo->prepare("INSERT INTO {$db_prefix}users (username, group_id, email, salt, password, language) VALUES (?, 3, ?, ?, ?, ?)");
    try {
        $stmt->execute([$login, $email, $salt, $password_hash, 'Russian']);
        $user_id = (int)$pdo->lastInsertId();
        $group_id = 3;
        log_msg("New user created: ID={$user_id}, username={$login}");
    } catch (PDOException $e) {
        log_msg('DB insert error: ' . $e->getMessage());
        header('Location: /index.php?p=login&error=user_create');
        exit;
    }
}

Коротко, что делает код:

  1. Подключается к БД через PDO с обработкой ошибок: если база недоступна — пишет в лог и редиректит на страницу входа с ошибкой.
  2. Ищет пользователя в PunBB по имени (username), полученному от Яндекса.
  3. Если пользователь найден: берёт его id и group_id, пишет в лог. Дальше можно сразу логинить.
  4. Если не найден: создаёт нового пользователя:
    • генерирует соль (12 hex‑символов) и случайный пароль;
    • считает хеш по формуле PunBB: sha1(salt . plain_password);
    • вставляет запись в таблицу users (группа 3, язык Russian);
    • запоминает user_id нового юзера.
  5. При ошибке вставки пишет в лог и редиректит с ошибкой.

Формирование и установка куки авторизации PunBB для пользователя из Яндекса

// ДЕЛАЕМ КУКИ
// Проверка: пользователь должен существовать
$stmt = $pdo->prepare("SELECT salt, password FROM {$db_prefix}users WHERE id = ? LIMIT 1");
$stmt->execute([$user_id]);
$userData = $stmt->fetch(PDO::FETCH_ASSOC);
 
if (!$userData || empty($userData['salt'])) {
    log_msg('CRITICAL: user not found or missing salt for ID=' . $user_id);
    // Тут твоя логика ошибки (редирект на страницу ошибки или выход)
    exit;
}
 
$salt = trim($userData['salt']);                  // trim обязателен: PunBB чувствителен к пробелам
$form_password_hash = $userData['password'];      // хеш из базы
 
// Expire считаем ОДИН раз
$expire = time() + 1209600; // 14 дней
 
// Считаем timeHash по ТВОЕЙ формуле forum_hash ---
$inner_hash = sha1((string)$expire);
$time_hash = sha1($salt . $inner_hash);
 
$final_hash = sha1($salt . $form_password_hash . $time_hash);
 
//  Собираем и кодируем
$raw_cookie = $user_id . '|' . $form_password_hash . '|' . $expire . '|' . $final_hash;
$encoded_cookie = base64_encode($raw_cookie);
 
//  Ставим куку
$cookie_name = 'forum_cookie_ххххх';// смотри что у тебя в config.php и ставь свое $domain = '.forum.rcl-radio.ru';   // Ставим новую куку setcookie($cookie_name, $encoded_cookie, $expire, '/', $domain, false, true); header('Location: https://forum.rcl-radio.ru/index.php'); exit;

Коротко, что делает код:

  1. Проверяет, что пользователь существует и у него есть соль. Делает SELECT по $user_id, если соли нет или юзер не найден — пишет критическую ошибку в лог и останавливает скрипт.
  2. Берёт соль и хеш пароля из БД. Соль обязательно чистит через trim() — PunBB ломается, если в соли есть лишние пробелы.
  3. Считает время жизни куки: $expire = time() + 1209600 (14 дней).
  4. Считает хеши строго по формуле PunBB:
    • $inner_hash = sha1((string)$expire)
    • $time_hash = sha1($salt . $inner_hash)
    • $final_hash = sha1($salt . $form_password_hash . $time_hash)
  5. Собирает строку куки: id|password_hash|expire|final_hash и кодирует её в base64.
  6. Ставит куку браузера: имя forum_cookie_ххххх, путь /, домен .forum.rcl-radio.ru, флаги httponly (true) и без secure (false).
  7. Редиректит пользователя на главную форума.

Что делает файл (кнопка Выйти) logout.php

Это полностью автономный скрипт выхода — он не требует подключения ядра PunBB (common.php и т.п.) и не выводит никакого HTML/текста в браузер. Его единственная задача — сбросить куки авторизации PunBB, и твой маркер Яндекса), записать отладочные данные в лог и перенаправить пользователя на главную.

<?php
// logout.php — ПОЛНОСТЬЮ АВТОНОМНЫЙ, без require, без echo
 
$cookie_name = 'forum_cookie_ххххх'; $ya_marker = 'ya_marker'; $path = '/'; // <-- поставь сюда реальный путь из колонки Path в браузере   // Лог только в файл, НИКАКОГО echo! $log_file = '/var/www/forum.rcl-radio.ru/logout_debug.log'; file_put_contents($log_file, date('Y-m-d H:i:s') . ' - Start logout attempt' . PHP_EOL, FILE_APPEND | LOCK_EX);   setcookie($cookie_name, '', time() - 3600, $path, '.forum.rcl-radio.ru', false, true); setcookie($cookie_name, '', time() - 3600, $path, 'forum.rcl-radio.ru', false, true); setcookie($cookie_name, '', time() - 3600, $path, '', false, true); file_put_contents($log_file, date('Y-m-d H:i:s') . ' - PunBB cookie set (attempt)' . PHP_EOL, FILE_APPEND | LOCK_EX);   setcookie($ya_marker, '', time() - 3600, $path, '.forum.rcl-radio.ru', false, true); setcookie($ya_marker, '', time() - 3600, $path, 'forum.rcl-radio.ru', false, true); setcookie($ya_marker, '', time() - 3600, $path, '', false, true); file_put_contents($log_file, date('Y-m-d H:i:s') . ' - Ya marker set (attempt)' . PHP_EOL, FILE_APPEND | LOCK_EX);   file_put_contents($log_file, date('Y-m-d H:i:s') . ' - Redirecting...' . PHP_EOL, FILE_APPEND | LOCK_EX); header('Location: https://forum.rcl-radio.ru/index.php'); file_put_contents($log_file, date('Y-m-d H:i:s') . ' - Header sent' . PHP_EOL, FILE_APPEND | LOCK_EX); exit;
Коротко, что делает код:
  1. Задаёт имена кук и путь:
    • forum_cookie_ххххх — кука PunBB, по которой движок понимает, кто залогинен.
    • ya_marker — твоя собственная кука, чтобы помечать вход через Яндекс (для кнопки «Выйти», UI и т.д.).
    • $path = '/' — путь, на котором действуют куки (должен совпадать с тем, как ты их ставил).
  2. Пишет в лог начало операции через file_put_contents с флагом LOCK_EX (чтобы не было конфликтов, если два пользователя выйдут одновременно). Никаких echo — иначе header не сработает и будет ошибка 500.

login_yandex_callback.php целиком:

<?php
// === ЛОГГЕР (самый первый, до всего остального) ===
$log_file = '/var/www/forum.rcl-radio.ru/login_yandex_callback_error.log';
 
function log_msg($msg) {
    global $log_file;
    file_put_contents($log_file, date('Y-m-d H:i:s') . ' - ' . $msg . PHP_EOL, FILE_APPEND | LOCK_EX);
}
 
// ================================================
 
// === НАСТРОЙКИ ===
$client_id     = 'd4c002ххххххххххххххххххх103f0cf';
$client_secret = '47223cххххххххххххххххх802fc2331';
$redirect_uri  = 'https://forum.rcl-radio.ru/login_yandex_callback.php';
 
$db_host       = 'localhost';
$db_name       = 'ххххххххх';
$db_user       = 'ххххххххх';
$db_pass       = 'хххххххххх'; 
$db_prefix     = 'pp_';
// =============
 
 
if (!isset($_GET['code'])) {
    log_msg('No code param');
    header('Location: /index.php?p=login');
    exit;
}
$code = $_GET['code'];
log_msg('Code received: ' . substr($code, 0, 10) . '...');
 
// --- OAUTH TOKEN ---
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://oauth.yandex.ru/token',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'grant_type'    => 'authorization_code',
        'code'          => $code,
        'client_id'     => $client_id,
        'client_secret' => $client_secret,
        'redirect_uri'  => $redirect_uri
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => true
]);
$token_resp = curl_exec($ch);
$curl_err = curl_error($ch);
curl_close($ch);
 
if ($curl_err) { 
    log_msg('CURL token error: ' . $curl_err); 
    header('Location: /index.php?p=login&error=oauth_token');
    exit; 
}
 
$token_data = json_decode($token_resp, true);
if (!$token_data || isset($token_data['error'])) { 
    log_msg('Token error data: ' . json_encode($token_data)); 
    header('Location: /index.php?p=login&error=token_invalid');
    exit; 
}
$access_token = $token_data['access_token'];
log_msg('Access token received');
 
// --- USER INFO ---
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://login.yandex.ru/info?format=json',
    CURLOPT_HTTPHEADER => ["Authorization: OAuth {$access_token}"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_SSL_VERIFYPEER => true
]);
$user_info = curl_exec($ch);
$curl_err2 = curl_error($ch);
curl_close($ch);
 
if ($curl_err2) {
    log_msg('CURL profile error: ' . $curl_err2);
    header('Location: /index.php?p=login&error=profile_fetch');
    exit;
}
 
$data = json_decode($user_info, true);
if (!$data || !isset($data['id'])) {
    log_msg('Failed to get profile: ' . $user_info);
    header('Location: /index.php?p=login&error=profile_invalid');
    exit;
}
log_msg('Profile received for Yandex ID: ' . $data['id']);
 
$yandex_id = $data['id'];
$email = $data['default_email'] ?? '';
$login_raw = $data['login'] ?? 'yandex_' . $yandex_id;
$login = preg_replace('/[^a-zA-Z0-9_]/', '_', $login_raw);
if (strlen($login) > 200) $login = substr($login, 0, 200);
 
// --- DB CONNECTION ---
try {
    $dsn = "mysql:host=$db_host;dbname=$db_name;charset=utf8mb4";
    $pdo = new PDO($dsn, $db_user, $db_pass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]);
} catch (PDOException $e) {
    log_msg('DB connection error: ' . $e->getMessage());
    header('Location: /index.php?p=login&error=db_connection');
    exit;
}
 
// --- FIND OR CREATE USER ---
$stmt = $pdo->prepare("SELECT id, username, group_id, salt, password, language FROM {$db_prefix}users WHERE username = ?");
$stmt->execute([$login]);
$row = $stmt->fetch();
 
if ($row) {
    $user_id = (int)$row['id'];
    $group_id = (int)$row['group_id'];
    log_msg("Existing user found: ID={$user_id}, group={$group_id}");
} else {
    // 1. Соль ровно 12 hex-символов
    $salt = bin2hex(random_bytes(6)); //  12 символов
 
    // 2. Генерируем случайный "пароль" (не показываем пользователю, нужен только для хеша PunBB)
    $plain_password = bin2hex(random_bytes(8)); // случайная строка
 
    // 3. Хеш по формуле PunBB: sha1(salt + plain_password)
    $password_hash = sha1($salt . $plain_password); //  валидный хеш
 
    $stmt = $pdo->prepare("INSERT INTO {$db_prefix}users (username, group_id, email, salt, password, language) VALUES (?, 3, ?, ?, ?, ?)");
    try {
        $stmt->execute([$login, $email, $salt, $password_hash, 'Russian']);
        $user_id = (int)$pdo->lastInsertId();
        $group_id = 3;
        log_msg("New user created: ID={$user_id}, username={$login}");
    } catch (PDOException $e) {
        log_msg('DB insert error: ' . $e->getMessage());
        header('Location: /index.php?p=login&error=user_create');
        exit;
    }
}
 
 
// ДЕЛАЕМ КУКИ
// Проверка: пользователь должен существовать
$stmt = $pdo->prepare("SELECT salt, password FROM {$db_prefix}users WHERE id = ? LIMIT 1");
$stmt->execute([$user_id]);
$userData = $stmt->fetch(PDO::FETCH_ASSOC);
 
if (!$userData || empty($userData['salt'])) {
    log_msg('CRITICAL: user not found or missing salt for ID=' . $user_id);
    // Тут твоя логика ошибки (редирект на страницу ошибки или выход)
    exit;
}
 
$salt = trim($userData['salt']);                  // trim обязателен: PunBB чувствителен к пробелам
$form_password_hash = $userData['password'];      // хеш из базы
 
// Expire считаем ОДИН раз
$expire = time() + 1209600; // 14 дней
 
// Считаем timeHash по ТВОЕЙ формуле forum_hash ---
$inner_hash = sha1((string)$expire);
$time_hash = sha1($salt . $inner_hash);
 
$final_hash = sha1($salt . $form_password_hash . $time_hash);
 
//  Собираем и кодируем
$raw_cookie = $user_id . '|' . $form_password_hash . '|' . $expire . '|' . $final_hash;
$encoded_cookie = base64_encode($raw_cookie);
 
//  Ставим куку
$cookie_name = 'forum_cookie_ххххх'; $domain = '.forum.rcl-radio.ru';   // Ставим новую куку setcookie($cookie_name, $encoded_cookie, $expire, '/', $domain, false, true); $_SESSION['yandex_user'] = 1; header('Location: https://forum.rcl-radio.ru/index.php'); exit;

Добавить комментарий