(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); }