CodeIgniter 4 website:
- Catalog 999 SGK (lớp 1-12) từ API, lọc theo lớp/môn/NXB, tìm kiếm
- Trang chủ grouped browse (bậc học -> lớp -> môn, ưu tiên môn quan trọng)
- Chi tiết sách: metadata đầy đủ, cover nghệ thuật /thumb/{W}x{H}/
- Đọc online qua gate captcha + countdown, PDF viewer pdf.js
- Download: captcha + token một lần + auto cache-bust version
- Tự đồng bộ catalog/cover khi API thay đổi (refresh:catalog)
- Legal: DMCA, miễn trừ trách nhiệm, điều khoản, bảo mật, liên hệ
75 lines
2.5 KiB
PHP
75 lines
2.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Libraries;
|
|
|
|
/**
|
|
* Simple self-hosted image captcha (GD), answer stored in session.
|
|
*/
|
|
class Captcha
|
|
{
|
|
public static function issue(): string
|
|
{
|
|
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
$code = '';
|
|
for ($i = 0; $i < 5; $i++) {
|
|
$code .= $chars[random_int(0, strlen($chars) - 1)];
|
|
}
|
|
session()->set('captcha_code', $code);
|
|
session()->set('captcha_expires', time() + 600);
|
|
return $code;
|
|
}
|
|
|
|
public static function verify(?string $input): bool
|
|
{
|
|
$code = session()->get('captcha_code');
|
|
$exp = session()->get('captcha_expires');
|
|
session()->remove('captcha_code');
|
|
session()->remove('captcha_expires');
|
|
|
|
if (! $code || ! $exp || time() > $exp || ! $input) {
|
|
return false;
|
|
}
|
|
return hash_equals(strtoupper(trim($code)), strtoupper(trim($input)));
|
|
}
|
|
|
|
public static function render(string $code): void
|
|
{
|
|
$w = 180;
|
|
$h = 60;
|
|
$img = imagecreatetruecolor($w, $h);
|
|
|
|
$bg = imagecolorallocate($img, 243, 246, 252);
|
|
imagefilledrectangle($img, 0, 0, $w, $h, $bg);
|
|
|
|
// noise
|
|
for ($i = 0; $i < 6; $i++) {
|
|
$c = imagecolorallocate($img, random_int(190, 230), random_int(190, 230), random_int(220, 245));
|
|
imageline($img, random_int(0, $w), random_int(0, $h), random_int(0, $w), random_int(0, $h), $c);
|
|
}
|
|
for ($i = 0; $i < 220; $i++) {
|
|
$c = imagecolorallocate($img, random_int(150, 220), random_int(150, 220), random_int(150, 220));
|
|
imagesetpixel($img, random_int(0, $w - 1), random_int(0, $h - 1), $c);
|
|
}
|
|
|
|
$fonts = glob(SYSTEMPATH . 'fonts/*.{ttf,TTF}', GLOB_BRACE) ?: [];
|
|
for ($i = 0; $i < strlen($code); $i++) {
|
|
$color = imagecolorallocate($img, random_int(10, 90), random_int(30, 100), random_int(120, 200));
|
|
$x = 15 + $i * 32 + random_int(-4, 4);
|
|
$y = random_int(36, 48);
|
|
if ($fonts) {
|
|
imagettftext($img, random_int(22, 26), random_int(-12, 12), $x, $y, $color, $fonts[array_rand($fonts)], $code[$i]);
|
|
} else {
|
|
imagestring($img, 5, $x, $y - 16, $code[$i], $color);
|
|
}
|
|
}
|
|
|
|
header('Content-Type: image/png');
|
|
header('Cache-Control: no-store, no-cache, must-revalidate');
|
|
imagepng($img);
|
|
imagedestroy($img);
|
|
exit;
|
|
}
|
|
}
|