go-learn/server/api/captcha.php
sab.code.lab 1c85091186 chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 09:34:02 +02:00

89 lines
3.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/**
* GET /api/captcha.php → 200 image/svg+xml
*
* Автономная SVG-капча (без GD и внешних сервисов), контракт:
* docs/INTERFACES.md, раздел «Кабинет 2.0 … (этап 8.1)».
* 5 символов (алфавит без неоднозначных 0/O, 1/I/l), каждый glyph —
* случайный поворот/сдвиг/размер, 35 шумовых кривых. В PHP-сессию
* пишется sha256(strtolower(code)) + expires (10 минут); проверка —
* одноразовая, в register.php.
*
* Тестовый режим GOLEARN_TEST=1: дополнительно отдаётся заголовок
* X-Captcha-Debug с кодом (используется smoke-тестом). В проде env
* отсутствует — заголовок не выставляется.
*/
require __DIR__ . '/../lib/http.php';
require __DIR__ . '/../lib/auth.php';
http_security_headers();
try {
method_must('GET');
// Алфавит без неоднозначных символов (0/O, 1/I/l исключены).
$alphabet = '23456789ABCDEFGHJKMNPQRSTUVWXYZ';
$max = strlen($alphabet) - 1;
$code = '';
for ($i = 0; $i < 5; $i++) {
$code .= $alphabet[random_int(0, $max)];
}
session_start_secure();
$_SESSION['captcha_hash'] = hash('sha256', strtolower($code));
$_SESSION['captcha_expires'] = time() + 600; // 10 минут
// --- SVG: 200x60, glyph'ы + шумовые кривые ---
$palette = ['#1a5276', '#7b241c', '#1e8449', '#6c3483', '#9a7d0a'];
$svg = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60"'
. ' viewBox="0 0 200 60">';
$svg .= '<rect width="200" height="60" fill="#f7f5f0"/>';
// 35 шумовых кривых (кубические Безье через всё поле).
$curves = random_int(3, 5);
for ($i = 0; $i < $curves; $i++) {
$x1 = random_int(-10, 30);
$y1 = random_int(0, 60);
$x2 = random_int(60, 140);
$y2 = random_int(0, 60);
$x3 = random_int(170, 210);
$y3 = random_int(0, 60);
$color = $palette[random_int(0, count($palette) - 1)];
$width = random_int(1, 2);
$svg .= '<path d="M' . $x1 . ' ' . $y1
. ' C' . random_int(20, 80) . ' ' . random_int(0, 60)
. ',' . $x2 . ' ' . $y2
. ',' . $x3 . ' ' . $y3 . '"'
. ' stroke="' . $color . '" stroke-width="' . $width . '"'
. ' fill="none" opacity="0.6"/>';
}
// Glyph'ы: случайные поворот, сдвиг и размер каждого символа.
for ($i = 0; $i < 5; $i++) {
$x = 22 + $i * 36 + random_int(-4, 4);
$y = random_int(34, 44);
$angle = random_int(-25, 25);
$size = random_int(26, 34);
$color = $palette[random_int(0, count($palette) - 1)];
$svg .= '<text x="' . $x . '" y="' . $y . '"'
. ' font-family="monospace" font-size="' . $size . '"'
. ' font-weight="bold" fill="' . $color . '"'
. ' transform="rotate(' . $angle . ' ' . $x . ' ' . $y . ')">'
. $code[$i] . '</text>';
}
$svg .= '</svg>';
header('Content-Type: image/svg+xml');
if (getenv('GOLEARN_TEST') === '1') {
// Только тестовый режим: отладочная выдача кода smoke-тесту.
header('X-Captcha-Debug: ' . $code);
}
echo $svg;
} catch (Throwable $e) {
error_log('captcha: ' . $e->getMessage());
error_out(500, 'internal');
}