chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
1c85091186
184 changed files with 33303 additions and 0 deletions
89
server/api/captcha.php
Normal file
89
server/api/captcha.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?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 —
|
||||
* случайный поворот/сдвиг/размер, 3–5 шумовых кривых. В 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"/>';
|
||||
|
||||
// 3–5 шумовых кривых (кубические Безье через всё поле).
|
||||
$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');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue