This commit is contained in:
DIGIDEV\corer committed 2026-10-20 13:16:38 +09:00
commit 3e4a056b14
67 files changed
+13926

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* Self-hosted CAPTCHA for the admin login: the image is drawn by the server
* (GD), so no third-party service is called and the Content-Security-Policy
* stays strict. The answer lives only in the session, as a hash, and is valid
* once, for CAPTCHA_TTL seconds.
*/
const CAPTCHA_TTL = 300;
const CAPTCHA_LENGTH = 5;
const CAPTCHA_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; // no 0/O, 1/I/L
function captcha_available(): bool
{
return function_exists('imagecreatetruecolor') && function_exists('imagepng');
}
/** Starts a new challenge (called when the login form is rendered). */
function captcha_new(): void
{
start_session();
if (!captcha_available()) {
// Without GD: a text question, weaker than the image, so login never becomes impossible.
error_log('[serubi] Ekstensi GD tidak tersedia: CAPTCHA memakai soal teks.');
$a = random_int(2, 9);
$b = random_int(2, 9);
$_SESSION['captcha'] = ['code' => (string)($a + $b), 'question' => "Berapa $a + $b?", 'exp' => time() + CAPTCHA_TTL];
return;
}
$code = '';
for ($i = 0; $i < CAPTCHA_LENGTH; $i++) {
$code .= CAPTCHA_ALPHABET[random_int(0, strlen(CAPTCHA_ALPHABET) - 1)];
}
$_SESSION['captcha'] = ['code' => $code, 'exp' => time() + CAPTCHA_TTL];
}
/** Text question when the image cannot be drawn, else null. */
function captcha_question(): ?string
{
return $_SESSION['captcha']['question'] ?? null;
}
/** Checks the answer and always consumes the challenge (one attempt per image). */
function captcha_check(string $answer): bool
{
start_session();
$c = $_SESSION['captcha'] ?? null;
unset($_SESSION['captcha']);
if (!$c || time() > $c['exp']) {
return false;
}
$answer = strtoupper(preg_replace('/\s+/', '', $answer));
return $answer !== '' && hash_equals($c['code'], $answer);
}
/** Outputs the PNG for the current challenge. */
function captcha_render(): void
{
start_session();
$code = (string)($_SESSION['captcha']['code'] ?? '');
$w = 200;
$h = 64;
$img = imagecreatetruecolor($w, $h);
imagefilledrectangle($img, 0, 0, $w, $h, imagecolorallocate($img, 244, 246, 250));
// background noise
for ($i = 0; $i < 9; $i++) {
$c = imagecolorallocate($img, random_int(140, 200), random_int(150, 205), random_int(170, 215));
imageline($img, random_int(0, $w), random_int(0, $h), random_int(0, $w), random_int(0, $h), $c);
}
// each glyph is drawn small (built-in font), then scaled and jittered
$x = 14;
foreach (str_split($code) as $ch) {
$g = imagecreatetruecolor(10, 16);
$bg = imagecolorallocate($g, 244, 246, 250);
imagefill($g, 0, 0, $bg);
imagecolortransparent($g, $bg);
imagestring($g, 5, 1, 0, $ch, imagecolorallocate($g, random_int(10, 60), random_int(30, 70), random_int(90, 140)));
$size = random_int(30, 38);
imagecopyresized($img, $g, $x, random_int(4, $h - $size - 10) + 6, 0, 0, (int)($size * 0.66), $size, 10, 16);
$x += random_int(31, 36);
}
// foreground noise over the text
for ($i = 0; $i < 3; $i++) {
imagesetthickness($img, 2);
imageline($img, 0, random_int(10, $h - 10), $w, random_int(10, $h - 10), imagecolorallocate($img, 60, 80, 130));
}
for ($i = 0; $i < 220; $i++) {
imagesetpixel($img, random_int(0, $w - 1), random_int(0, $h - 1), imagecolorallocate($img, random_int(60, 160), random_int(60, 160), random_int(60, 160)));
}
header('Content-Type: image/png');
header('Cache-Control: no-store, max-age=0');
imagepng($img);
}