v1.0.0.1
StocketBase
- <?php
- declare(strict_types=1);
- final class AntiBot
- {
- private const CHARACTERS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
- private const LENGTH = 6;
- private const MAX_AGE = 1800;
- private const MIN_FORM_AGE = 2;
- public static function boot(string $scope): void
- {
- self::validateScope($scope);
- $key = self::key($scope);
- if (!isset($_SESSION[$key]) || !is_array($_SESSION[$key])) {
- self::rotate($scope);
- }
- }
- public static function refresh(string $scope): void
- {
- self::validateScope($scope);
- self::rotate($scope);
- }
- public static function field(string $scope): string
- {
- self::boot($scope);
- $started = self::key($scope) . '_started';
- return '<input type="hidden" name="antibot_started" value="' . (int) $_SESSION[$started] . '">'
- . '<input type="text" name="website" value="" tabindex="-1" autocomplete="off" aria-hidden="true" class="antibot-trap">';
- }
- public static function verify(string $scope, ?string $answer, ?string $started, ?string $honeypot): bool
- {
- self::validateScope($scope);
- $key = self::key($scope);
- $challenge = $_SESSION[$key] ?? null;
- $now = time();
- $valid = is_array($challenge)
- && is_string($answer)
- && is_string($started)
- && is_string($honeypot)
- && $honeypot === ''
- && isset($challenge['token'], $challenge['started'])
- && hash_equals((string) $challenge['token'], strtoupper(trim($answer)))
- && ctype_digit($started)
- && (int) $started === (int) $challenge['started']
- && $now - (int) $challenge['started'] >= self::MIN_FORM_AGE
- && $now - (int) $challenge['created'] <= self::MAX_AGE;
- self::rotate($scope);
- return $valid;
- }
- public static function image(string $scope): string
- {
- self::boot($scope);
- $token = (string) $_SESSION[self::key($scope) . '_token'];
- $width = 200;
- $height = 70;
- $length = strlen($token);
- $imageLabel = class_exists('Language') ? Language::get('antibot_image_label', 'Security code') : 'Security code';
- $svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' . $width . '" height="' . $height . '" viewBox="0 0 ' . $width . ' ' . $height . '" role="img" aria-label="' . htmlspecialchars($imageLabel, ENT_QUOTES) . '">';
- $svg .= '<defs>';
- $svg .= '<filter id="antibot-noise">';
- $svg .= '<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>';
- $svg .= '<feColorMatrix type="saturate" values="0"/>';
- $svg .= '<feBlend in="SourceGraphic" mode="multiply" result="blend"/>';
- $svg .= '<feComposite in="blend" in2="SourceGraphic" operator="in"/>';
- $svg .= '</filter>';
- $svg .= '</defs>';
- $backgrounds = ['#f0f4ff', '#fff4f0', '#f0fff4', '#fdfdf0', '#f4f0ff'];
- $svg .= '<rect width="' . $width . '" height="' . $height . '" fill="' . $backgrounds[array_rand($backgrounds)] . '" rx="6"/>';
- $svg .= '<filter id="antibot-wave">';
- $svg .= '<feTurbulence type="turbulence" baseFrequency="0.02 0.05" numOctaves="2" result="turb"/>';
- $svg .= '<feDisplacementMap in="SourceGraphic" in2="turb" scale="4" xChannelSelector="R" yChannelSelector="G"/>';
- $svg .= '</filter>';
- $lineColors = ['#c0c8e0', '#e0c0c0', '#c0e0c0', '#d0d0c0', '#d0c0e0'];
- for ($i = 0; $i < 6; $i++) {
- $x1 = random_int(0, $width);
- $y1 = random_int(0, $height);
- $x2 = random_int(0, $width);
- $y2 = random_int(0, $height);
- $color = $lineColors[array_rand($lineColors)];
- $strokeWidth = round(random_int(10, 20) / 10, 1);
- $svg .= '<line x1="' . $x1 . '" y1="' . $y1 . '" x2="' . $x2 . '" y2="' . $y2 . '" stroke="' . $color . '" stroke-width="' . $strokeWidth . '" opacity="0.7"/>';
- }
- $dotColors = ['#9090b0', '#b09090', '#90b090'];
- for ($i = 0; $i < 40; $i++) {
- $cx = random_int(0, $width);
- $cy = random_int(0, $height);
- $radius = random_int(1, 3);
- $svg .= '<circle cx="' . $cx . '" cy="' . $cy . '" r="' . $radius . '" fill="' . $dotColors[array_rand($dotColors)] . '" opacity="0.5"/>';
- }
- $characterColors = ['#1a3a8f', '#8f1a1a', '#1a6e1a', '#6e1a6e', '#1a5a6e', '#8f4a00', '#2a2a8f', '#6e1a3a'];
- $slotWidth = ($width - 20) / $length;
- for ($i = 0; $i < $length; $i++) {
- $char = htmlspecialchars($token[$i], ENT_XML1);
- $x = 10 + $slotWidth * $i + $slotWidth / 2;
- $y = random_int(38, 50);
- $rotate = random_int(-18, 18);
- $scaleX = round(random_int(85, 115) / 100, 2);
- $scaleY = round(random_int(90, 110) / 100, 2);
- $fontSize = random_int(26, 34);
- $color = $characterColors[array_rand($characterColors)];
- $svg .= '<text'
- . ' x="' . round($x, 1) . '"'
- . ' y="' . $y . '"'
- . ' font-size="' . $fontSize . '"'
- . ' font-family="Georgia, serif"'
- . ' font-weight="bold"'
- . ' fill="' . $color . '"'
- . ' text-anchor="middle"'
- . ' transform="rotate(' . $rotate . ' ' . round($x, 1) . ' ' . $y . ') scale(' . $scaleX . ' ' . $scaleY . ')"'
- . ' filter="url(#antibot-wave)"'
- . '>' . $char . '</text>';
- }
- $svg .= '<rect width="' . $width . '" height="' . $height . '" fill="url(#antibot-noise)" opacity="0.08" rx="6"/>';
- $svg .= '</svg>';
- return '<img src="data:image/svg+xml;base64,' . base64_encode($svg) . '" width="' . $width . '" height="' . $height . '" alt="Security verification code" draggable="false">';
- }
- private static function rotate(string $scope): void
- {
- $key = self::key($scope);
- $started = time();
- $token = '';
- for ($i = 0; $i < self::LENGTH; $i++) {
- $token .= self::CHARACTERS[random_int(0, strlen(self::CHARACTERS) - 1)];
- }
- $_SESSION[$key] = [
- 'token' => $token,
- 'started' => $started,
- 'created' => $started,
- ];
- $_SESSION[$key . '_token'] = $token;
- $_SESSION[$key . '_started'] = $started;
- }
- private static function key(string $scope): string
- {
- self::validateScope($scope);
- return 'antibot_' . $scope;
- }
- private static function validateScope(string $scope): void
- {
- if (!in_array($scope, ['user', 'team', 'contact'], true)) {
- throw new InvalidArgumentException('Invalid antibot scope.');
- }
- }
- }
-