WebOrbiton
v1.0.0.1

StocketBase

163 lines · 6.8 KB
  1. <?php
  2. declare(strict_types=1);
  3. final class AntiBot
  4. {
  5. private const CHARACTERS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
  6. private const LENGTH = 6;
  7. private const MAX_AGE = 1800;
  8. private const MIN_FORM_AGE = 2;
  9. public static function boot(string $scope): void
  10. {
  11. self::validateScope($scope);
  12. $key = self::key($scope);
  13. if (!isset($_SESSION[$key]) || !is_array($_SESSION[$key])) {
  14. self::rotate($scope);
  15. }
  16. }
  17. public static function refresh(string $scope): void
  18. {
  19. self::validateScope($scope);
  20. self::rotate($scope);
  21. }
  22. public static function field(string $scope): string
  23. {
  24. self::boot($scope);
  25. $started = self::key($scope) . '_started';
  26. return '<input type="hidden" name="antibot_started" value="' . (int) $_SESSION[$started] . '">'
  27. . '<input type="text" name="website" value="" tabindex="-1" autocomplete="off" aria-hidden="true" class="antibot-trap">';
  28. }
  29. public static function verify(string $scope, ?string $answer, ?string $started, ?string $honeypot): bool
  30. {
  31. self::validateScope($scope);
  32. $key = self::key($scope);
  33. $challenge = $_SESSION[$key] ?? null;
  34. $now = time();
  35. $valid = is_array($challenge)
  36. && is_string($answer)
  37. && is_string($started)
  38. && is_string($honeypot)
  39. && $honeypot === ''
  40. && isset($challenge['token'], $challenge['started'])
  41. && hash_equals((string) $challenge['token'], strtoupper(trim($answer)))
  42. && ctype_digit($started)
  43. && (int) $started === (int) $challenge['started']
  44. && $now - (int) $challenge['started'] >= self::MIN_FORM_AGE
  45. && $now - (int) $challenge['created'] <= self::MAX_AGE;
  46. self::rotate($scope);
  47. return $valid;
  48. }
  49. public static function image(string $scope): string
  50. {
  51. self::boot($scope);
  52. $token = (string) $_SESSION[self::key($scope) . '_token'];
  53. $width = 200;
  54. $height = 70;
  55. $length = strlen($token);
  56. $imageLabel = class_exists('Language') ? Language::get('antibot_image_label', 'Security code') : 'Security code';
  57. $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) . '">';
  58. $svg .= '<defs>';
  59. $svg .= '<filter id="antibot-noise">';
  60. $svg .= '<feTurbulence type="fractalNoise" baseFrequency="0.65" numOctaves="3" stitchTiles="stitch"/>';
  61. $svg .= '<feColorMatrix type="saturate" values="0"/>';
  62. $svg .= '<feBlend in="SourceGraphic" mode="multiply" result="blend"/>';
  63. $svg .= '<feComposite in="blend" in2="SourceGraphic" operator="in"/>';
  64. $svg .= '</filter>';
  65. $svg .= '</defs>';
  66. $backgrounds = ['#f0f4ff', '#fff4f0', '#f0fff4', '#fdfdf0', '#f4f0ff'];
  67. $svg .= '<rect width="' . $width . '" height="' . $height . '" fill="' . $backgrounds[array_rand($backgrounds)] . '" rx="6"/>';
  68. $svg .= '<filter id="antibot-wave">';
  69. $svg .= '<feTurbulence type="turbulence" baseFrequency="0.02 0.05" numOctaves="2" result="turb"/>';
  70. $svg .= '<feDisplacementMap in="SourceGraphic" in2="turb" scale="4" xChannelSelector="R" yChannelSelector="G"/>';
  71. $svg .= '</filter>';
  72. $lineColors = ['#c0c8e0', '#e0c0c0', '#c0e0c0', '#d0d0c0', '#d0c0e0'];
  73. for ($i = 0; $i < 6; $i++) {
  74. $x1 = random_int(0, $width);
  75. $y1 = random_int(0, $height);
  76. $x2 = random_int(0, $width);
  77. $y2 = random_int(0, $height);
  78. $color = $lineColors[array_rand($lineColors)];
  79. $strokeWidth = round(random_int(10, 20) / 10, 1);
  80. $svg .= '<line x1="' . $x1 . '" y1="' . $y1 . '" x2="' . $x2 . '" y2="' . $y2 . '" stroke="' . $color . '" stroke-width="' . $strokeWidth . '" opacity="0.7"/>';
  81. }
  82. $dotColors = ['#9090b0', '#b09090', '#90b090'];
  83. for ($i = 0; $i < 40; $i++) {
  84. $cx = random_int(0, $width);
  85. $cy = random_int(0, $height);
  86. $radius = random_int(1, 3);
  87. $svg .= '<circle cx="' . $cx . '" cy="' . $cy . '" r="' . $radius . '" fill="' . $dotColors[array_rand($dotColors)] . '" opacity="0.5"/>';
  88. }
  89. $characterColors = ['#1a3a8f', '#8f1a1a', '#1a6e1a', '#6e1a6e', '#1a5a6e', '#8f4a00', '#2a2a8f', '#6e1a3a'];
  90. $slotWidth = ($width - 20) / $length;
  91. for ($i = 0; $i < $length; $i++) {
  92. $char = htmlspecialchars($token[$i], ENT_XML1);
  93. $x = 10 + $slotWidth * $i + $slotWidth / 2;
  94. $y = random_int(38, 50);
  95. $rotate = random_int(-18, 18);
  96. $scaleX = round(random_int(85, 115) / 100, 2);
  97. $scaleY = round(random_int(90, 110) / 100, 2);
  98. $fontSize = random_int(26, 34);
  99. $color = $characterColors[array_rand($characterColors)];
  100. $svg .= '<text'
  101. . ' x="' . round($x, 1) . '"'
  102. . ' y="' . $y . '"'
  103. . ' font-size="' . $fontSize . '"'
  104. . ' font-family="Georgia, serif"'
  105. . ' font-weight="bold"'
  106. . ' fill="' . $color . '"'
  107. . ' text-anchor="middle"'
  108. . ' transform="rotate(' . $rotate . ' ' . round($x, 1) . ' ' . $y . ') scale(' . $scaleX . ' ' . $scaleY . ')"'
  109. . ' filter="url(#antibot-wave)"'
  110. . '>' . $char . '</text>';
  111. }
  112. $svg .= '<rect width="' . $width . '" height="' . $height . '" fill="url(#antibot-noise)" opacity="0.08" rx="6"/>';
  113. $svg .= '</svg>';
  114. return '<img src="data:image/svg+xml;base64,' . base64_encode($svg) . '" width="' . $width . '" height="' . $height . '" alt="Security verification code" draggable="false">';
  115. }
  116. private static function rotate(string $scope): void
  117. {
  118. $key = self::key($scope);
  119. $started = time();
  120. $token = '';
  121. for ($i = 0; $i < self::LENGTH; $i++) {
  122. $token .= self::CHARACTERS[random_int(0, strlen(self::CHARACTERS) - 1)];
  123. }
  124. $_SESSION[$key] = [
  125. 'token' => $token,
  126. 'started' => $started,
  127. 'created' => $started,
  128. ];
  129. $_SESSION[$key . '_token'] = $token;
  130. $_SESSION[$key . '_started'] = $started;
  131. }
  132. private static function key(string $scope): string
  133. {
  134. self::validateScope($scope);
  135. return 'antibot_' . $scope;
  136. }
  137. private static function validateScope(string $scope): void
  138. {
  139. if (!in_array($scope, ['user', 'team', 'contact'], true)) {
  140. throw new InvalidArgumentException('Invalid antibot scope.');
  141. }
  142. }
  143. }
  144. ​