├── .gitignore ├── CaptchaBundle.php ├── Controller ├── CaptchaHandlerController.php └── SimpleCaptchaHandlerController.php ├── DependencyInjection ├── CaptchaExtension.php └── Configuration.php ├── Form └── Type │ ├── CaptchaType.php │ └── SimpleCaptchaType.php ├── Helpers ├── BotDetectCaptchaHelper.php └── BotDetectSimpleCaptchaHelper.php ├── Integration ├── BotDetectCaptcha.php └── BotDetectSimpleCaptcha.php ├── Provider ├── botdetect.php └── simple-botdetect.php ├── README.md ├── Resources ├── config │ ├── routing.yml │ └── services.yml └── views │ └── captcha.html.twig ├── Routing ├── CaptchaRoutesLoader.php └── Generator │ └── UrlGenerator.php ├── Security └── Core │ └── Exception │ └── InvalidCaptchaException.php ├── Support ├── CaptchaConfigDefaults.php ├── Exception │ ├── ExceptionInterface.php │ └── FileNotFoundException.php ├── LibraryLoader.php ├── Path.php ├── SimpleLibraryLoader.php └── UserCaptchaConfiguration.php ├── Validator └── Constraints │ ├── ValidCaptcha.php │ ├── ValidCaptchaValidator.php │ ├── ValidSimpleCaptcha.php │ └── ValidSimpleCaptchaValidator.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/captcha-com/symfony-captcha-bundle/1648d8d1cea4de9da3005b4eebbd33c5cd29ba35/.gitignore -------------------------------------------------------------------------------- /CaptchaBundle.php: -------------------------------------------------------------------------------- 1 | isGetResourceContentsRequest()) { 25 | // getting contents of css, js, and gif files. 26 | return $this->getResourceContents(); 27 | } else { 28 | 29 | $this->captcha = $this->getBotDetectCaptchaInstance(); 30 | 31 | if (is_null($this->captcha)) { 32 | throw new BadRequestHttpException('captcha'); 33 | } 34 | 35 | $commandString = $this->getUrlParameter('get'); 36 | if (!\BDC_StringHelper::HasValue($commandString)) { 37 | \BDC_HttpHelper::BadRequest('command'); 38 | } 39 | 40 | $commandString = \BDC_StringHelper::Normalize($commandString); 41 | $command = \BDC_CaptchaHttpCommand::FromQuerystring($commandString); 42 | $responseBody = ''; 43 | switch ($command) { 44 | case \BDC_CaptchaHttpCommand::GetImage: 45 | $responseBody = $this->getImage(); 46 | break; 47 | case \BDC_CaptchaHttpCommand::GetSound: 48 | $responseBody = $this->getSound(); 49 | break; 50 | case \BDC_CaptchaHttpCommand::GetValidationResult: 51 | $responseBody = $this->getValidationResult(); 52 | break; 53 | case \BDC_CaptchaHttpCommand::GetScriptInclude: 54 | $responseBody = $this->getScriptInclude(); 55 | break; 56 | case \BDC_CaptchaHttpCommand::GetP: 57 | $responseBody = $this->getP(); 58 | break; 59 | default: 60 | \BDC_HttpHelper::BadRequest('command'); 61 | break; 62 | } 63 | 64 | // disallow audio file search engine indexing 65 | header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); 66 | echo $responseBody; exit; 67 | } 68 | } 69 | 70 | /** 71 | * Get CAPTCHA object instance. 72 | * 73 | * @return object 74 | */ 75 | private function getBotDetectCaptchaInstance() 76 | { 77 | // load BotDetect Library 78 | $libraryLoader = new LibraryLoader($this->container); 79 | $libraryLoader->load(); 80 | 81 | $captchaId = $this->getUrlParameter('c'); 82 | if (is_null($captchaId) || !preg_match('/^(\w+)$/ui', $captchaId)) { 83 | throw new BadRequestHttpException('Invalid captcha id.'); 84 | } 85 | 86 | $captchaInstanceId = $this->getUrlParameter('t'); 87 | if (is_null($captchaInstanceId) || !(32 == strlen($captchaInstanceId) && 88 | (1 === preg_match("/^([a-f0-9]+)$/u", $captchaInstanceId)))) { 89 | throw new BadRequestHttpException('Invalid instance id.'); 90 | } 91 | 92 | return new BotDetectCaptchaHelper($captchaId, $captchaInstanceId); 93 | } 94 | 95 | /** 96 | * Get contents of Captcha resources (js, css, gif files). 97 | * 98 | * @return string 99 | */ 100 | public function getResourceContents() 101 | { 102 | $filename = $this->getUrlParameter('get'); 103 | 104 | if (!preg_match('/^[a-z-]+\.(css|gif|js)$/', $filename)) { 105 | throw new BadRequestHttpException('Invalid file name.'); 106 | } 107 | 108 | $resourcePath = realpath(Path::getPublicDirPathInLibrary($this->container) . $filename); 109 | 110 | if (!is_file($resourcePath)) { 111 | throw new BadRequestHttpException(sprintf('File "%s" could not be found.', $filename)); 112 | } 113 | 114 | $mimesType = array('css' => 'text/css', 'gif' => 'image/gif', 'js' => 'application/x-javascript'); 115 | $fileInfo = pathinfo($resourcePath); 116 | 117 | return new Response( 118 | file_get_contents($resourcePath), 119 | 200, 120 | array('content-type' => $mimesType[$fileInfo['extension']]) 121 | ); 122 | } 123 | 124 | /** 125 | * Generate a Captcha image. 126 | * 127 | * @return image 128 | */ 129 | public function getImage() 130 | { 131 | if (is_null($this->captcha)) { 132 | \BDC_HttpHelper::BadRequest('captcha'); 133 | } 134 | 135 | // identifier of the particular Captcha object instance 136 | $instanceId = $this->getInstanceId(); 137 | if (is_null($instanceId)) { 138 | \BDC_HttpHelper::BadRequest('instance'); 139 | } 140 | 141 | // image generation invalidates sound cache, if any 142 | $this->clearSoundData($instanceId); 143 | 144 | // response headers 145 | \BDC_HttpHelper::DisallowCache(); 146 | 147 | // response MIME type & headers 148 | $mimeType = $this->captcha->CaptchaBase->ImageMimeType; 149 | header("Content-Type: {$mimeType}"); 150 | 151 | // we don't support content chunking, since image files 152 | // are regenerated randomly on each request 153 | header('Accept-Ranges: none'); 154 | 155 | // image generation 156 | $rawImage = $this->captcha->CaptchaBase->GetImage($instanceId); 157 | $this->captcha->CaptchaBase->SaveCodeCollection(); 158 | 159 | $length = strlen($rawImage); 160 | header("Content-Length: {$length}"); 161 | return $rawImage; 162 | } 163 | 164 | /** 165 | * Generate a Captcha sound. 166 | */ 167 | public function getSound() 168 | { 169 | if (is_null($this->captcha)) { 170 | \BDC_HttpHelper::BadRequest('captcha'); 171 | } 172 | 173 | // identifier of the particular Captcha object instance 174 | $instanceId = $this->getInstanceId(); 175 | if (is_null($instanceId)) { 176 | \BDC_HttpHelper::BadRequest('instance'); 177 | } 178 | 179 | $soundBytes = $this->getSoundData($this->captcha, $instanceId); 180 | 181 | if (is_null($soundBytes)) { 182 | \BDC_HttpHelper::BadRequest('Please reload the form page before requesting another Captcha sound'); 183 | exit; 184 | } 185 | 186 | $totalSize = strlen($soundBytes); 187 | 188 | // response headers 189 | \BDC_HttpHelper::SmartDisallowCache(); 190 | 191 | // response MIME type & headers 192 | $mimeType = $this->captcha->CaptchaBase->SoundMimeType; 193 | header("Content-Type: {$mimeType}"); 194 | header('Content-Transfer-Encoding: binary'); 195 | 196 | if (!array_key_exists('d', $_GET)) { // javascript player not used, we send the file directly as a download 197 | $downloadId = \BDC_CryptoHelper::GenerateGuid(); 198 | header("Content-Disposition: attachment; filename=captcha_{$downloadId}.wav"); 199 | } 200 | 201 | if ($this->detectIosRangeRequest()) { // iPhone/iPad sound issues workaround: chunked response for iOS clients 202 | // sound byte subset 203 | $range = $this->getSoundByteRange(); 204 | $rangeStart = $range['start']; 205 | $rangeEnd = $range['end']; 206 | $rangeSize = $rangeEnd - $rangeStart + 1; 207 | 208 | // initial iOS 6.0.1 testing; leaving as fallback since we can't be sure it won't happen again: 209 | // we depend on observed behavior of invalid range requests to detect 210 | // end of sound playback, cleanup and tell AppleCoreMedia to stop requesting 211 | // invalid "bytes=rangeEnd-rangeEnd" ranges in an infinite(?) loop 212 | if ($rangeStart == $rangeEnd || $rangeEnd > $totalSize) { 213 | \BDC_HttpHelper::BadRequest('invalid byte range'); 214 | } 215 | 216 | $rangeBytes = substr($soundBytes, $rangeStart, $rangeSize); 217 | 218 | // partial content response with the requested byte range 219 | header('HTTP/1.1 206 Partial Content'); 220 | header('Accept-Ranges: bytes'); 221 | header("Content-Length: {$rangeSize}"); 222 | header("Content-Range: bytes {$rangeStart}-{$rangeEnd}/{$totalSize}"); 223 | return $rangeBytes; // chrome needs this kind of response to be able to replay Html5 audio 224 | } else if ($this->detectFakeRangeRequest()) { 225 | header('Accept-Ranges: bytes'); 226 | header("Content-Length: {$totalSize}"); 227 | $end = $totalSize - 1; 228 | header("Content-Range: bytes 0-{$end}/{$totalSize}"); 229 | return $soundBytes; 230 | } else { // regular sound request 231 | header('Accept-Ranges: none'); 232 | header("Content-Length: {$totalSize}"); 233 | return $soundBytes; 234 | } 235 | 236 | } 237 | 238 | public function getSoundData($p_Captcha, $p_InstanceId) 239 | { 240 | $shouldCache = ( 241 | ($p_Captcha->SoundRegenerationMode == \SoundRegenerationMode::None) || // no sound regeneration allowed, so we must cache the first and only generated sound 242 | $this->detectIosRangeRequest() // keep the same Captcha sound across all chunked iOS requests 243 | ); 244 | 245 | if ($shouldCache) { 246 | $loaded = $this->loadSoundData($p_InstanceId); 247 | if (!is_null($loaded)) { 248 | return $loaded; 249 | } 250 | } else { 251 | $this->clearSoundData($p_InstanceId); 252 | } 253 | 254 | $soundBytes = $this->generateSoundData($p_Captcha, $p_InstanceId); 255 | if ($shouldCache) { 256 | $this->saveSoundData($p_InstanceId, $soundBytes); 257 | } 258 | return $soundBytes; 259 | } 260 | 261 | private function generateSoundData($p_Captcha, $p_InstanceId) 262 | { 263 | $rawSound = $p_Captcha->CaptchaBase->GetSound($p_InstanceId); 264 | $p_Captcha->CaptchaBase->SaveCodeCollection(); // always record sound generation count 265 | return $rawSound; 266 | } 267 | 268 | private function saveSoundData($p_InstanceId, $p_SoundBytes) 269 | { 270 | SF_Session_Save("BDC_Cached_SoundData_" . $p_InstanceId, $p_SoundBytes); 271 | } 272 | 273 | private function loadSoundData($p_InstanceId) 274 | { 275 | return SF_Session_Load("BDC_Cached_SoundData_" . $p_InstanceId); 276 | } 277 | 278 | private function clearSoundData($p_InstanceId) 279 | { 280 | SF_Session_Clear("BDC_Cached_SoundData_" . $p_InstanceId); 281 | } 282 | 283 | 284 | // Instead of relying on unreliable user agent checks, we detect the iOS sound 285 | // requests by the Http headers they will always contain 286 | private function detectIosRangeRequest() 287 | { 288 | if (array_key_exists('HTTP_RANGE', $_SERVER) && 289 | \BDC_StringHelper::HasValue($_SERVER['HTTP_RANGE'])) { 290 | 291 | // Safari on MacOS and all browsers on <= iOS 10.x 292 | if (array_key_exists('HTTP_X_PLAYBACK_SESSION_ID', $_SERVER) && 293 | \BDC_StringHelper::HasValue($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) { 294 | return true; 295 | } 296 | 297 | $userAgent = array_key_exists('HTTP_USER_AGENT', $_SERVER) ? $_SERVER['HTTP_USER_AGENT'] : null; 298 | 299 | // all browsers on iOS 11.x and later 300 | if(\BDC_StringHelper::HasValue($userAgent)) { 301 | $userAgentLC = \BDC_StringHelper::Lowercase($userAgent); 302 | if (\BDC_StringHelper::Contains($userAgentLC, "like mac os") || \BDC_StringHelper::Contains($userAgentLC, "like macos")) { 303 | return true; 304 | } 305 | } 306 | } 307 | return false; 308 | } 309 | 310 | private function getSoundByteRange() 311 | { 312 | // chunked requests must include the desired byte range 313 | $rangeStr = $_SERVER['HTTP_RANGE']; 314 | if (!\BDC_StringHelper::HasValue($rangeStr)) { 315 | return; 316 | } 317 | 318 | $matches = array(); 319 | preg_match_all('/bytes=([0-9]+)-([0-9]+)/', $rangeStr, $matches); 320 | return array( 321 | 'start' => (int) $matches[1][0], 322 | 'end' => (int) $matches[2][0] 323 | ); 324 | } 325 | 326 | private function detectFakeRangeRequest() 327 | { 328 | $detected = false; 329 | if (array_key_exists('HTTP_RANGE', $_SERVER)) { 330 | $rangeStr = $_SERVER['HTTP_RANGE']; 331 | if (\BDC_StringHelper::HasValue($rangeStr) && 332 | preg_match('/bytes=0-$/', $rangeStr)) { 333 | $detected = true; 334 | } 335 | } 336 | return $detected; 337 | } 338 | 339 | /** 340 | * The client requests the Captcha validation result (used for Ajax Captcha validation). 341 | * 342 | * @return json 343 | */ 344 | public function getValidationResult() 345 | { 346 | if (is_null($this->captcha)) { 347 | \BDC_HttpHelper::BadRequest('captcha'); 348 | } 349 | 350 | // identifier of the particular Captcha object instance 351 | $instanceId = $this->getInstanceId(); 352 | if (is_null($instanceId)) { 353 | \BDC_HttpHelper::BadRequest('instance'); 354 | } 355 | 356 | $mimeType = 'application/json'; 357 | header("Content-Type: {$mimeType}"); 358 | 359 | // code to validate 360 | $userInput = $this->getUserInput(); 361 | 362 | // JSON-encoded validation result 363 | $result = false; 364 | if (isset($userInput) && (isset($instanceId))) { 365 | $result = $this->captcha->AjaxValidate($userInput, $instanceId); 366 | $this->captcha->CaptchaBase->Save(); 367 | } 368 | $resultJson = $this->getJsonValidationResult($result); 369 | 370 | return $resultJson; 371 | } 372 | 373 | public function getScriptInclude() 374 | { 375 | // saved data for the specified Captcha object in the application 376 | if (is_null($this->captcha)) { 377 | \BDC_HttpHelper::BadRequest('captcha'); 378 | } 379 | 380 | // identifier of the particular Captcha object instance 381 | $instanceId = $this->getInstanceId(); 382 | if (is_null($instanceId)) { 383 | \BDC_HttpHelper::BadRequest('instance'); 384 | } 385 | 386 | // response MIME type & headers 387 | header('Content-Type: text/javascript'); 388 | header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); 389 | 390 | // 1. load BotDetect script 391 | $resourcePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-traditional-api-script-include.js'); 392 | 393 | if (!is_file($resourcePath)) { 394 | throw new BadRequestHttpException(sprintf('File "%s" could not be found.', $resourcePath)); 395 | } 396 | 397 | $script = file_get_contents($resourcePath); 398 | 399 | // 2. load BotDetect Init script 400 | $script .= \BDC_CaptchaScriptsHelper::GetInitScriptMarkup($this->captcha, $instanceId); 401 | 402 | // add remote scripts if enabled 403 | if ($this->captcha->RemoteScriptEnabled) { 404 | $script .= "\r\n"; 405 | $script .= \BDC_CaptchaScriptsHelper::GetRemoteScript($this->captcha); 406 | } 407 | 408 | return $script; 409 | } 410 | 411 | /** 412 | * @return string 413 | */ 414 | private function getInstanceId() 415 | { 416 | $instanceId = $this->getUrlParameter('t'); 417 | if (!\BDC_StringHelper::HasValue($instanceId) || 418 | !\BDC_CaptchaBase::IsValidInstanceId($instanceId) 419 | ) { 420 | return; 421 | } 422 | return $instanceId; 423 | } 424 | 425 | /** 426 | * Extract the user input Captcha code string from the Ajax validation request. 427 | * 428 | * @return string 429 | */ 430 | private function getUserInput() 431 | { 432 | // BotDetect built-in Ajax Captcha validation 433 | $input = $this->getUrlParameter('i'); 434 | 435 | if (is_null($input)) { 436 | // jQuery validation support, the input key may be just about anything, 437 | // so we have to loop through fields and take the first unrecognized one 438 | $recognized = array('get', 'c', 't', 'd'); 439 | foreach ($_GET as $key => $value) { 440 | if (!in_array($key, $recognized)) { 441 | $input = $value; 442 | break; 443 | } 444 | } 445 | } 446 | 447 | return $input; 448 | } 449 | 450 | /** 451 | * Encodes the Captcha validation result in a simple JSON wrapper. 452 | * 453 | * @return string 454 | */ 455 | private function getJsonValidationResult($result) 456 | { 457 | $resultStr = ($result ? 'true': 'false'); 458 | return $resultStr; 459 | } 460 | 461 | /** 462 | * @return bool 463 | */ 464 | private function isGetResourceContentsRequest() 465 | { 466 | return array_key_exists('get', $_GET) && !array_key_exists('c', $_GET); 467 | } 468 | 469 | /** 470 | * @param string $param 471 | * @return string|null 472 | */ 473 | private function getUrlParameter($param) 474 | { 475 | return filter_input(INPUT_GET, $param); 476 | } 477 | 478 | public function getP() 479 | { 480 | if (is_null($this->captcha)) { 481 | \BDC_HttpHelper::BadRequest('captcha'); 482 | } 483 | 484 | // identifier of the particular Captcha object instance 485 | $instanceId = $this->getInstanceId(); 486 | if (is_null($instanceId)) { 487 | \BDC_HttpHelper::BadRequest('instance'); 488 | } 489 | 490 | // create new one 491 | $p = $this->captcha->GenPw($instanceId); 492 | $this->captcha->SavePw($this->captcha); 493 | 494 | // response data 495 | $response = "{\"sp\":\"{$p->GetSP()}\",\"hs\":\"{$p->GetHs()}\"}"; 496 | 497 | // response MIME type & headers 498 | header('Content-Type: application/json'); 499 | header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); 500 | \BDC_HttpHelper::SmartDisallowCache(); 501 | 502 | return $response; 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /Controller/SimpleCaptchaHandlerController.php: -------------------------------------------------------------------------------- 1 | captcha = $this->getBotDetectCaptchaInstance(); 25 | 26 | $commandString = $this->getUrlParameter('get'); 27 | if (!\BDC_StringHelper::HasValue($commandString)) { 28 | \BDC_HttpHelper::BadRequest('command'); 29 | } 30 | 31 | $commandString = \BDC_StringHelper::Normalize($commandString); 32 | $command = \BDC_SimpleCaptchaHttpCommand::FromQuerystring($commandString); 33 | $responseBody = ''; 34 | switch ($command) { 35 | case \BDC_SimpleCaptchaHttpCommand::GetImage: 36 | $responseBody = $this->getImage(); 37 | break; 38 | case \BDC_SimpleCaptchaHttpCommand::GetBase64ImageString: 39 | $responseBody = $this->getBase64ImageString(); 40 | break; 41 | case \BDC_SimpleCaptchaHttpCommand::GetSound: 42 | $responseBody = $this->getSound(); 43 | break; 44 | case \BDC_SimpleCaptchaHttpCommand::GetHtml: 45 | $responseBody = $this->getHtml(); 46 | break; 47 | case \BDC_SimpleCaptchaHttpCommand::GetValidationResult: 48 | $responseBody = $this->getValidationResult(); 49 | break; 50 | 51 | // Sound icon 52 | case \BDC_SimpleCaptchaHttpCommand::GetSoundIcon: 53 | $responseBody = $this->getSoundIcon(); 54 | break; 55 | case \BDC_SimpleCaptchaHttpCommand::GetSoundSmallIcon: 56 | $responseBody = $this->getSmallSoundIcon(); 57 | break; 58 | case \BDC_SimpleCaptchaHttpCommand::GetSoundDisabledIcon: 59 | $responseBody = $this->getDisabledSoundIcon(); 60 | break; 61 | case \BDC_SimpleCaptchaHttpCommand::GetSoundSmallDisabledIcon: 62 | $responseBody = $this->getSmallDisabledSoundIcon(); 63 | break; 64 | 65 | // Reload icon 66 | case \BDC_SimpleCaptchaHttpCommand::GetReloadIcon: 67 | $responseBody = $this->getReloadIcon(); 68 | break; 69 | case \BDC_SimpleCaptchaHttpCommand::GetReloadSmallIcon: 70 | $responseBody = $this->getSmallReloadIcon(); 71 | break; 72 | case \BDC_SimpleCaptchaHttpCommand::GetReloadDisabledIcon: 73 | $responseBody = $this->getDisabledReloadIcon(); 74 | break; 75 | case \BDC_SimpleCaptchaHttpCommand::GetReloadSmallDisabledIcon: 76 | $responseBody = $this->getSmallDisabledReloadIcon(); 77 | break; 78 | 79 | // css, js 80 | case \BDC_SimpleCaptchaHttpCommand::GetScriptInclude: 81 | $responseBody = $this->getScriptInclude(); 82 | break; 83 | case \BDC_SimpleCaptchaHttpCommand::GetLayoutStyleSheet: 84 | $responseBody = $this->getLayoutStyleSheet(); 85 | break; 86 | case \BDC_SimpleCaptchaHttpCommand::GetP: 87 | $responseBody = $this->getP(); 88 | break; 89 | default: 90 | \BDC_HttpHelper::BadRequest('command'); 91 | break; 92 | } 93 | 94 | // disallow audio file search engine indexing 95 | header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); 96 | echo $responseBody; exit; 97 | } 98 | 99 | /** 100 | * Get CAPTCHA object instance. 101 | * 102 | * @return object 103 | */ 104 | private function getBotDetectCaptchaInstance() 105 | { 106 | // load BotDetect Library 107 | $libraryLoader = new SimpleLibraryLoader($this->container); 108 | $libraryLoader->load(); 109 | 110 | $captchaStyleName = $this->getUrlParameter('c'); 111 | if (is_null($captchaStyleName) || !preg_match('/^(\w+)$/ui', $captchaStyleName)) { 112 | return null; 113 | } 114 | 115 | $captchaId = $this->getUrlParameter('t'); 116 | if ($captchaId !== null) { 117 | $captchaId = \BDC_StringHelper::Normalize($captchaId); 118 | if (1 !== preg_match(\BDC_SimpleCaptchaBase::VALID_CAPTCHA_ID, $captchaId)) { 119 | return null; 120 | } 121 | } 122 | 123 | return new BotDetectSimpleCaptchaHelper($captchaStyleName, $captchaId); 124 | } 125 | 126 | 127 | /** 128 | * Generate a Captcha image. 129 | * 130 | * @return image 131 | */ 132 | public function getImage() 133 | { 134 | header("Access-Control-Allow-Origin: *"); 135 | 136 | // authenticate client-side request 137 | $corsAuth = new \CorsAuth(); 138 | if (!$corsAuth->IsClientAllowed()) { 139 | \BDC_HttpHelper::BadRequest($corsAuth->GetFrontEnd() . " is not an allowed front-end"); 140 | return null; 141 | } 142 | 143 | if (is_null($this->captcha)) { 144 | \BDC_HttpHelper::BadRequest('captcha'); 145 | } 146 | 147 | // identifier of the particular Captcha object instance 148 | $captchaId = $this->getCaptchaId(); 149 | if (is_null($captchaId)) { 150 | \BDC_HttpHelper::BadRequest('instance'); 151 | } 152 | 153 | // image generation invalidates sound cache, if any 154 | $this->clearSoundData($this->captcha, $captchaId); 155 | 156 | // response headers 157 | \BDC_HttpHelper::DisallowCache(); 158 | 159 | // response MIME type & headers 160 | $imageType = \ImageFormat::GetName($this->captcha->ImageFormat); 161 | $imageType = strtolower($imageType[0]); 162 | $mimeType = "image/" . $imageType; 163 | header("Content-Type: {$mimeType}"); 164 | 165 | // we don't support content chunking, since image files 166 | // are regenerated randomly on each request 167 | header('Accept-Ranges: none'); 168 | 169 | // image generation 170 | $rawImage = $this->getImageData($this->captcha); 171 | 172 | $length = strlen($rawImage); 173 | header("Content-Length: {$length}"); 174 | return $rawImage; 175 | } 176 | 177 | public function getBase64ImageString() 178 | { 179 | header("Access-Control-Allow-Origin: *"); 180 | 181 | // authenticate client-side request 182 | $corsAuth = new \CorsAuth(); 183 | if (!$corsAuth->IsClientAllowed()) { 184 | \BDC_HttpHelper::BadRequest($corsAuth->GetFrontEnd() . " is not an allowed front-end"); 185 | return null; 186 | } 187 | 188 | 189 | // MIME type 190 | $imageType = \ImageFormat::GetName($this->captcha->ImageFormat); 191 | $imageType = strtolower($imageType[0]); 192 | $mimeType = "image/" . $imageType; 193 | 194 | $rawImage = $this->getImageData($this->captcha); 195 | 196 | $base64ImageString = sprintf('data:%s;base64,%s', $mimeType, base64_encode($rawImage)); 197 | return $base64ImageString; 198 | } 199 | 200 | 201 | private function getImageData($p_Captcha) 202 | { 203 | // identifier of the particular Captcha object instance 204 | $captchaId = $this->getCaptchaId(); 205 | if (is_null($captchaId)) { 206 | \BDC_HttpHelper::BadRequest('Captcha Id doesn\'t exist'); 207 | } 208 | 209 | if ($this->isObviousBotRequest($p_Captcha)) { 210 | return; 211 | } 212 | 213 | // image generation invalidates sound cache, if any 214 | $this->clearSoundData($p_Captcha, $captchaId); 215 | 216 | // response headers 217 | \BDC_HttpHelper::DisallowCache(); 218 | 219 | // we don't support content chunking, since image files 220 | // are regenerated randomly on each request 221 | header('Accept-Ranges: none'); 222 | 223 | // disallow audio file search engine indexing 224 | header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); 225 | 226 | // image generation 227 | $rawImage = $p_Captcha->CaptchaBase->GetImage($captchaId); 228 | $p_Captcha->SaveCode($captchaId, $p_Captcha->CaptchaBase->Code); // record generated Captcha code for validation 229 | 230 | return $rawImage; 231 | } 232 | 233 | /** 234 | * Generate a Captcha sound. 235 | */ 236 | public function getSound() 237 | { 238 | header("Access-Control-Allow-Origin: *"); 239 | 240 | // authenticate client-side request 241 | $corsAuth = new \CorsAuth(); 242 | if (!$corsAuth->IsClientAllowed()) { 243 | \BDC_HttpHelper::BadRequest($corsAuth->GetFrontEnd() . " is not an allowed front-end"); 244 | return null; 245 | } 246 | 247 | if (is_null($this->captcha)) { 248 | \BDC_HttpHelper::BadRequest('captcha'); 249 | } 250 | 251 | // identifier of the particular Captcha object instance 252 | $captchaId = $this->getCaptchaId(); 253 | if (is_null($captchaId)) { 254 | \BDC_HttpHelper::BadRequest('Captcha Id doesn\'t exist'); 255 | } 256 | 257 | if ($this->isObviousBotRequest($this->captcha)) { 258 | return; 259 | } 260 | 261 | $soundBytes = $this->getSoundData($this->captcha, $captchaId); 262 | 263 | if (is_null($soundBytes)) { 264 | \BDC_HttpHelper::BadRequest('Please reload the form page before requesting another Captcha sound'); 265 | exit; 266 | } 267 | 268 | $totalSize = strlen($soundBytes); 269 | 270 | // response headers 271 | \BDC_HttpHelper::SmartDisallowCache(); 272 | 273 | // response MIME type & headers 274 | $mimeType = $this->captcha->CaptchaBase->SoundMimeType; 275 | header("Content-Type: {$mimeType}"); 276 | header('Content-Transfer-Encoding: binary'); 277 | 278 | if (!array_key_exists('d', $_GET)) { // javascript player not used, we send the file directly as a download 279 | $downloadId = \BDC_CryptoHelper::GenerateGuid(); 280 | header("Content-Disposition: attachment; filename=captcha_{$downloadId}.wav"); 281 | } 282 | 283 | if ($this->detectIosRangeRequest()) { // iPhone/iPad sound issues workaround: chunked response for iOS clients 284 | // sound byte subset 285 | $range = $this->getSoundByteRange(); 286 | $rangeStart = $range['start']; 287 | $rangeEnd = $range['end']; 288 | $rangeSize = $rangeEnd - $rangeStart + 1; 289 | 290 | // initial iOS 6.0.1 testing; leaving as fallback since we can't be sure it won't happen again: 291 | // we depend on observed behavior of invalid range requests to detect 292 | // end of sound playback, cleanup and tell AppleCoreMedia to stop requesting 293 | // invalid "bytes=rangeEnd-rangeEnd" ranges in an infinite(?) loop 294 | if ($rangeStart == $rangeEnd || $rangeEnd > $totalSize) { 295 | \BDC_HttpHelper::BadRequest('invalid byte range'); 296 | } 297 | 298 | $rangeBytes = substr($soundBytes, $rangeStart, $rangeSize); 299 | 300 | // partial content response with the requested byte range 301 | header('HTTP/1.1 206 Partial Content'); 302 | header('Accept-Ranges: bytes'); 303 | header("Content-Length: {$rangeSize}"); 304 | header("Content-Range: bytes {$rangeStart}-{$rangeEnd}/{$totalSize}"); 305 | return $rangeBytes; // chrome needs this kind of response to be able to replay Html5 audio 306 | } else if ($this->detectFakeRangeRequest()) { 307 | header('Accept-Ranges: bytes'); 308 | header("Content-Length: {$totalSize}"); 309 | $end = $totalSize - 1; 310 | header("Content-Range: bytes 0-{$end}/{$totalSize}"); 311 | return $soundBytes; 312 | } else { // regular sound request 313 | header('Accept-Ranges: none'); 314 | header("Content-Length: {$totalSize}"); 315 | return $soundBytes; 316 | } 317 | } 318 | 319 | public function getSoundData($p_Captcha, $p_CaptchaId) 320 | { 321 | $shouldCache = ( 322 | ($p_Captcha->SoundRegenerationMode == \SoundRegenerationMode::None) || // no sound regeneration allowed, so we must cache the first and only generated sound 323 | $this->detectIosRangeRequest() // keep the same Captcha sound across all chunked iOS requests 324 | ); 325 | 326 | if ($shouldCache) { 327 | $loaded = $this->loadSoundData($p_Captcha, $p_CaptchaId); 328 | if (!is_null($loaded)) { 329 | return $loaded; 330 | } 331 | } else { 332 | $this->clearSoundData($p_Captcha, $p_CaptchaId); 333 | } 334 | 335 | $soundBytes = $this->generateSoundData($p_Captcha, $p_CaptchaId); 336 | if ($shouldCache) { 337 | $this->saveSoundData($p_Captcha, $p_CaptchaId, $soundBytes); 338 | } 339 | return $soundBytes; 340 | } 341 | 342 | private function generateSoundData($p_Captcha, $p_CaptchaId) 343 | { 344 | $rawSound = $p_Captcha->CaptchaBase->GetSound($p_CaptchaId); 345 | $p_Captcha->SaveCode($p_CaptchaId, $p_Captcha->CaptchaBase->Code); // always record sound generation count 346 | return $rawSound; 347 | } 348 | 349 | private function saveSoundData($p_Captcha, $p_CaptchaId, $p_SoundBytes) 350 | { 351 | $p_Captcha->get_CaptchaPersistence()->GetPersistenceProvider()->Save("BDC_Cached_SoundData_" . $p_CaptchaId, $p_SoundBytes); 352 | } 353 | 354 | private function loadSoundData($p_Captcha, $p_CaptchaId) 355 | { 356 | $soundBytes = $p_Captcha->get_CaptchaPersistence()->GetPersistenceProvider()->Load("BDC_Cached_SoundData_" . $p_CaptchaId); 357 | return $soundBytes; 358 | } 359 | 360 | private function clearSoundData($p_Captcha, $p_CaptchaId) 361 | { 362 | $p_Captcha->get_CaptchaPersistence()->GetPersistenceProvider()->Remove("BDC_Cached_SoundData_" . $p_CaptchaId); 363 | } 364 | 365 | 366 | // Instead of relying on unreliable user agent checks, we detect the iOS sound 367 | // requests by the Http headers they will always contain 368 | private function detectIosRangeRequest() 369 | { 370 | if (array_key_exists('HTTP_RANGE', $_SERVER) && 371 | \BDC_StringHelper::HasValue($_SERVER['HTTP_RANGE'])) { 372 | 373 | // Safari on MacOS and all browsers on <= iOS 10.x 374 | if (array_key_exists('HTTP_X_PLAYBACK_SESSION_ID', $_SERVER) && 375 | \BDC_StringHelper::HasValue($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) { 376 | return true; 377 | } 378 | 379 | $userAgent = array_key_exists('HTTP_USER_AGENT', $_SERVER) ? $_SERVER['HTTP_USER_AGENT'] : null; 380 | 381 | // all browsers on iOS 11.x and later 382 | if(\BDC_StringHelper::HasValue($userAgent)) { 383 | $userAgentLC = \BDC_StringHelper::Lowercase($userAgent); 384 | if (\BDC_StringHelper::Contains($userAgentLC, "like mac os") || \BDC_StringHelper::Contains($userAgentLC, "like macos")) { 385 | return true; 386 | } 387 | } 388 | } 389 | return false; 390 | } 391 | 392 | private function getSoundByteRange() 393 | { 394 | // chunked requests must include the desired byte range 395 | $rangeStr = $_SERVER['HTTP_RANGE']; 396 | if (!\BDC_StringHelper::HasValue($rangeStr)) { 397 | return; 398 | } 399 | 400 | $matches = array(); 401 | preg_match_all('/bytes=([0-9]+)-([0-9]+)/', $rangeStr, $matches); 402 | return array( 403 | 'start' => (int) $matches[1][0], 404 | 'end' => (int) $matches[2][0] 405 | ); 406 | } 407 | 408 | private function detectFakeRangeRequest() 409 | { 410 | $detected = false; 411 | if (array_key_exists('HTTP_RANGE', $_SERVER)) { 412 | $rangeStr = $_SERVER['HTTP_RANGE']; 413 | if (\BDC_StringHelper::HasValue($rangeStr) && 414 | preg_match('/bytes=0-$/', $rangeStr)) { 415 | $detected = true; 416 | } 417 | } 418 | return $detected; 419 | } 420 | 421 | public function getHtml() 422 | { 423 | header("Access-Control-Allow-Origin: *"); 424 | 425 | $corsAuth = new \CorsAuth(); 426 | if (!$corsAuth->IsClientAllowed()) { 427 | \BDC_HttpHelper::BadRequest($corsAuth->GetFrontEnd() . " is not an allowed front-end"); 428 | } 429 | 430 | $html = "
" . $this->captcha->Html() . "
"; 431 | return $html; 432 | } 433 | 434 | /** 435 | * The client requests the Captcha validation result (used for Ajax Captcha validation). 436 | * 437 | * @return json 438 | */ 439 | public function getValidationResult() 440 | { 441 | header("Access-Control-Allow-Origin: *"); 442 | 443 | // authenticate client-side request 444 | $corsAuth = new \CorsAuth(); 445 | if (!$corsAuth->IsClientAllowed()) { 446 | \BDC_HttpHelper::BadRequest($corsAuth->GetFrontEnd() . " is not an allowed front-end"); 447 | return null; 448 | } 449 | 450 | if (is_null($this->captcha)) { 451 | \BDC_HttpHelper::BadRequest('captcha'); 452 | } 453 | 454 | // identifier of the particular Captcha object instance 455 | $captchaId = $this->getCaptchaId(); 456 | if (is_null($captchaId)) { 457 | \BDC_HttpHelper::BadRequest('instance'); 458 | } 459 | 460 | $mimeType = 'application/json'; 461 | header("Content-Type: {$mimeType}"); 462 | 463 | // code to validate 464 | $userInput = $this->getUserInput(); 465 | 466 | // JSON-encoded validation result 467 | $result = false; 468 | if (isset($userInput) && (isset($captchaId))) { 469 | $result = $this->captcha->AjaxValidate($userInput, $captchaId); 470 | } 471 | $resultJson = $this->getJsonValidationResult($result); 472 | 473 | return $resultJson; 474 | } 475 | 476 | // Get Reload Icon group 477 | public function getSoundIcon() 478 | { 479 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-sound-icon.gif'); 480 | return $this->getWebResource($filePath, 'image/gif'); 481 | } 482 | 483 | public function getSmallSoundIcon() 484 | { 485 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-sound-small-icon.gif'); 486 | return $this->getWebResource($filePath, 'image/gif'); 487 | } 488 | 489 | public function getDisabledSoundIcon() 490 | { 491 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-sound-disabled-icon.gif'); 492 | return $this->getWebResource($filePath, 'image/gif'); 493 | } 494 | 495 | public function getSmallDisabledSoundIcon() 496 | { 497 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-sound-small-disabled-icon.gif'); 498 | return $this->getWebResource($filePath, 'image/gif'); 499 | } 500 | 501 | // Get Reload Icon group 502 | public function getReloadIcon() 503 | { 504 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-reload-icon.gif'); 505 | return $this->getWebResource($filePath, 'image/gif'); 506 | } 507 | 508 | public function getSmallReloadIcon() 509 | { 510 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-reload-small-icon.gif'); 511 | return $this->getWebResource($filePath, 'image/gif'); 512 | } 513 | 514 | public function getDisabledReloadIcon() 515 | { 516 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-reload-disabled-icon.gif'); 517 | return $this->getWebResource($filePath, 'image/gif'); 518 | } 519 | 520 | public function getSmallDisabledReloadIcon() 521 | { 522 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-reload-small-disabled-icon.gif'); 523 | return $this->getWebResource($filePath, 'image/gif'); 524 | } 525 | 526 | public function getLayoutStyleSheet() 527 | { 528 | $filePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-layout-stylesheet.css'); 529 | return $this->getWebResource($filePath, 'text/css'); 530 | } 531 | 532 | public function getScriptInclude() 533 | { 534 | header("Access-Control-Allow-Origin: *"); 535 | 536 | // saved data for the specified Captcha object in the application 537 | if (is_null($this->captcha)) { 538 | \BDC_HttpHelper::BadRequest('captcha'); 539 | } 540 | 541 | // identifier of the particular Captcha object instance 542 | $captchaId = $this->getCaptchaId(); 543 | if (is_null($captchaId)) { 544 | \BDC_HttpHelper::BadRequest('instance'); 545 | } 546 | 547 | // response MIME type & headers 548 | header('Content-Type: text/javascript'); 549 | header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); 550 | 551 | // 1. load BotDetect script 552 | $resourcePath = realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-simple-api-script-include.js'); 553 | 554 | if (!is_file($resourcePath)) { 555 | throw new BadRequestHttpException(sprintf('File "%s" could not be found.', $resourcePath)); 556 | } 557 | 558 | $script = $this->getWebResource($resourcePath, 'text/javascript', false); 559 | 560 | // 2. load BotDetect Init script 561 | $script .= \BDC_SimpleCaptchaScriptsHelper::GetInitScriptMarkup($this->captcha, $captchaId); 562 | 563 | // add remote scripts if enabled 564 | if ($this->captcha->RemoteScriptEnabled) { 565 | $script .= "\r\n"; 566 | $script .= \BDC_SimpleCaptchaScriptsHelper::GetRemoteScript($this->captcha, $this->getClientSideFramework()); 567 | } 568 | 569 | return $script; 570 | } 571 | 572 | private function getClientSideFramework() 573 | { 574 | $clientSide = $this->getUrlParameter('cs'); 575 | if (\BDC_StringHelper::HasValue($clientSide)) { 576 | $clientSide = \BDC_StringHelper::Normalize($clientSide); 577 | return $clientSide; 578 | } 579 | return null; 580 | } 581 | 582 | private function getWebResource($p_Resource, $p_MimeType, $hasEtag = true) 583 | { 584 | header("Content-Type: $p_MimeType"); 585 | if ($hasEtag) { 586 | \BDC_HttpHelper::AllowEtagCache($p_Resource); 587 | } 588 | 589 | return file_get_contents($p_Resource); 590 | } 591 | 592 | private function isObviousBotRequest($p_Captcha) 593 | { 594 | $captchaRequestValidator = new \SimpleCaptchaRequestValidator($p_Captcha->Configuration); 595 | 596 | 597 | // some basic request checks 598 | $captchaRequestValidator->RecordRequest(); 599 | 600 | if ($captchaRequestValidator->IsObviousBotAttempt()) { 601 | \BDC_HttpHelper::TooManyRequests('IsObviousBotAttempt'); 602 | } 603 | 604 | return false; 605 | } 606 | 607 | /** 608 | * @return string 609 | */ 610 | private function getCaptchaId() 611 | { 612 | $captchaId = $this->getUrlParameter('t'); 613 | 614 | if (!\BDC_StringHelper::HasValue($captchaId)) { 615 | return null; 616 | } 617 | 618 | // captchaId consists of 32 lowercase hexadecimal digits 619 | if (strlen($captchaId) != 32) { 620 | return null; 621 | } 622 | 623 | if (1 !== preg_match(\BDC_SimpleCaptchaBase::VALID_CAPTCHA_ID, $captchaId)) { 624 | return null; 625 | } 626 | 627 | return $captchaId; 628 | } 629 | 630 | /** 631 | * Extract the user input Captcha code string from the Ajax validation request. 632 | * 633 | * @return string 634 | */ 635 | private function getUserInput() 636 | { 637 | // BotDetect built-in Ajax Captcha validation 638 | $input = $this->getUrlParameter('i'); 639 | 640 | if (is_null($input)) { 641 | // jQuery validation support, the input key may be just about anything, 642 | // so we have to loop through fields and take the first unrecognized one 643 | $recognized = array('get', 'c', 't', 'd'); 644 | foreach ($_GET as $key => $value) { 645 | if (!in_array($key, $recognized)) { 646 | $input = $value; 647 | break; 648 | } 649 | } 650 | } 651 | 652 | return $input; 653 | } 654 | 655 | /** 656 | * Encodes the Captcha validation result in a simple JSON wrapper. 657 | * 658 | * @return string 659 | */ 660 | private function getJsonValidationResult($result) 661 | { 662 | $resultStr = ($result ? 'true': 'false'); 663 | return $resultStr; 664 | } 665 | 666 | /** 667 | * @param string $param 668 | * @return string|null 669 | */ 670 | private function getUrlParameter($param) 671 | { 672 | return filter_input(INPUT_GET, $param); 673 | } 674 | 675 | public function getP() 676 | { 677 | header("Access-Control-Allow-Origin: *"); 678 | 679 | // authenticate client-side request 680 | $corsAuth = new \CorsAuth(); 681 | if (!$corsAuth->IsClientAllowed()) { 682 | \BDC_HttpHelper::BadRequest($corsAuth->GetFrontEnd() . " is not an allowed front-end"); 683 | return null; 684 | } 685 | 686 | if (is_null($this->captcha)) { 687 | \BDC_HttpHelper::BadRequest('captcha'); 688 | } 689 | 690 | // identifier of the particular Captcha object instance 691 | $captchaId = $this->getCaptchaId(); 692 | if (is_null($captchaId)) { 693 | \BDC_HttpHelper::BadRequest('instance'); 694 | } 695 | 696 | // create new one 697 | $p = $this->captcha->GenPw($captchaId); 698 | $this->captcha->SavePw($this->captcha, $captchaId); 699 | 700 | // response data 701 | $response = "{\"sp\":\"{$p->GetSP()}\",\"hs\":\"{$p->GetHs()}\"}"; 702 | 703 | // response MIME type & headers 704 | header('Content-Type: application/json'); 705 | header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); 706 | \BDC_HttpHelper::SmartDisallowCache(); 707 | 708 | return $response; 709 | } 710 | } 711 | -------------------------------------------------------------------------------- /DependencyInjection/CaptchaExtension.php: -------------------------------------------------------------------------------- 1 | load('services.yml'); 23 | 24 | // set captcha configuration 25 | $configuration = new Configuration(); 26 | $config = $this->processConfiguration($configuration, $configs); 27 | 28 | $container->setParameter('captcha.config', $config); 29 | $container->setParameter('captcha.config.botdetect_captcha_path', $config['botdetect_captcha_path']); 30 | $container->setParameter('captcha.config.captchaConfig', $config['captchaConfig']); 31 | $container->setParameter('captcha.config.captchaStyleName', $config['captchaStyleName']); 32 | 33 | // set captcha template 34 | $resources = $container->getParameter('twig.form.resources'); 35 | $container->setParameter( 36 | 'twig.form.resources', 37 | array_merge(array('@Captcha/captcha.html.twig'), $resources) 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('captcha'); 20 | 21 | $rootNode 22 | ->children() 23 | ->variableNode('botdetect_captcha_path')->defaultValue($captchaLibPathDefault)->end() 24 | ->variableNode('captchaConfig')->defaultValue(null)->end() 25 | ->variableNode('captchaStyleName')->defaultValue(null)->end() 26 | ->end() 27 | ; 28 | 29 | return $treeBuilder; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Form/Type/CaptchaType.php: -------------------------------------------------------------------------------- 1 | container = $container; 41 | $this->options = $options; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function configureOptions(OptionsResolver $resolver) 48 | { 49 | $resolver->setDefaults($this->options); 50 | } 51 | 52 | // BC for SF < 2.7 53 | public function setDefaultOptions(OptionsResolverInterface $resolver) 54 | { 55 | $this->configureOptions($resolver); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function buildForm(FormBuilderInterface $builder, array $options) 62 | { 63 | if (!isset($options['captchaConfig']) || '' === $options['captchaConfig']) { 64 | $path = Path::getRelativeConfigFilePath('captcha.php'); 65 | $errorMessage = 'The Captcha field type requires you to declare the "captchaConfig" option and assigns a captcha configuration key that defined in ' . $path . ' file. '; 66 | $errorMessage .= 'For example: $builder->add(\'captchaCode\', CaptchaType::class, array(\'captchaConfig\' => \'LoginCaptcha\'))'; 67 | throw new InvalidArgumentException($errorMessage); 68 | } 69 | 70 | $this->captcha = $this->container->get('captcha')->setConfig($options['captchaConfig']); 71 | 72 | if (!$this->captcha instanceof BotDetectCaptchaHelper) { 73 | throw new UnexpectedTypeException($this->captcha, 'Captcha\Bundle\CaptchaBundle\Helpers\BotDetectCaptchaHelper'); 74 | } 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function buildView(FormView $view, FormInterface $form, array $options) 81 | { 82 | $view->vars['captcha_html'] = $this->captcha->Html(); 83 | $view->vars['user_input_id'] = $this->captcha->UserInputID; 84 | } 85 | 86 | // BC for SF < 3.0 87 | public function getName() 88 | { 89 | return 'captcha'; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function getParent() 96 | { 97 | if (version_compare(Kernel::VERSION, '2.8', '<')) { 98 | return 'text'; 99 | } 100 | 101 | return 'Symfony\Component\Form\Extension\Core\Type\TextType'; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Form/Type/SimpleCaptchaType.php: -------------------------------------------------------------------------------- 1 | container = $container; 41 | $this->options = $options; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function configureOptions(OptionsResolver $resolver) 48 | { 49 | $resolver->setDefaults($this->options); 50 | } 51 | 52 | // BC for SF < 2.7 53 | public function setDefaultOptions(OptionsResolverInterface $resolver) 54 | { 55 | $this->configureOptions($resolver); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function buildForm(FormBuilderInterface $builder, array $options) 62 | { 63 | $captchaStyleName = !isset($options['captchaStyleName']) ? '' : $options['captchaStyleName']; 64 | 65 | $this->captcha = $this->container->get('simple_captcha')->getInstance($captchaStyleName); 66 | 67 | if (!$this->captcha instanceof BotDetectSimpleCaptchaHelper) { 68 | throw new UnexpectedTypeException($this->captcha, 'Captcha\Bundle\CaptchaBundle\Helpers\BotDetectSimpleCaptchaHelper'); 69 | } 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function buildView(FormView $view, FormInterface $form, array $options) 76 | { 77 | $view->vars['captcha_html'] = $this->captcha->Html(); 78 | $view->vars['user_input_id'] = $this->captcha->UserInputID; 79 | } 80 | 81 | // BC for SF < 3.0 82 | public function getName() 83 | { 84 | return 'simple_captcha'; 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function getParent() 91 | { 92 | if (version_compare(Kernel::VERSION, '2.8', '<')) { 93 | return 'text'; 94 | } 95 | 96 | return 'Symfony\Component\Form\Extension\Core\Type\TextType'; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Helpers/BotDetectCaptchaHelper.php: -------------------------------------------------------------------------------- 1 | initCaptcha($config, $captchaInstanceId); 45 | } 46 | 47 | /** 48 | * Initialize CAPTCHA object instance. 49 | * 50 | * @param array $config 51 | * 52 | * @return void 53 | */ 54 | public function initCaptcha(array $config, $captchaInstanceId = null) 55 | { 56 | // set captchaId and create an instance of Captcha 57 | $captchaId = (array_key_exists('CaptchaId', $config)) ? $config['CaptchaId'] : 'defaultCaptchaId'; 58 | $this->captcha = new \Captcha($captchaId, $captchaInstanceId); 59 | 60 | // set user's input id 61 | if (array_key_exists('UserInputID', $config)) { 62 | $this->captcha->UserInputID = $config['UserInputID']; 63 | } 64 | } 65 | 66 | /** 67 | * Override Captcha's Html method. 68 | * Add stylesheet css into Captcha html markup. 69 | */ 70 | public function Html() 71 | { 72 | $captchaUrlGenerator = UrlGenerator::getInstance(); 73 | $html = \BDC_HtmlHelper::StylesheetInclude($captchaUrlGenerator->generate('captcha_handler', array(), UrlGeneratorInterface::RELATIVE_PATH) . '?get=bdc-layout-stylesheet.css'); 74 | $html .= $this->captcha->Html(); 75 | return $html; 76 | } 77 | 78 | public function __call($method, $args = array()) 79 | { 80 | if (method_exists($this, $method)) { 81 | return call_user_func_array(array($this, $method), $args); 82 | } 83 | 84 | if (method_exists($this->captcha, $method)) { 85 | return call_user_func_array(array($this->captcha, $method), $args); 86 | } 87 | 88 | if (method_exists($this->captcha->get_CaptchaBase(), $method)) { 89 | return call_user_func_array(array($this->captcha->get_CaptchaBase(), $method), $args); 90 | } 91 | } 92 | 93 | /** 94 | * Auto-magic helpers for civilized property access. 95 | */ 96 | public function __get($name) 97 | { 98 | if (method_exists($this->captcha->get_CaptchaBase(), ($method = 'get_'.$name))) { 99 | return $this->captcha->get_CaptchaBase()->$method(); 100 | } 101 | 102 | if (method_exists($this->captcha, ($method = 'get_'.$name))) { 103 | return $this->captcha->$method(); 104 | } 105 | 106 | if (method_exists($this, ($method = 'get_'.$name))) { 107 | return $this->$method(); 108 | } 109 | } 110 | 111 | public function __isset($name) 112 | { 113 | if (method_exists($this->captcha->get_CaptchaBase(), ($method = 'isset_'.$name))) { 114 | return $this->captcha->get_CaptchaBase()->$method(); 115 | } 116 | 117 | if (method_exists($this->captcha, ($method = 'isset_'.$name))) { 118 | return $this->captcha->$method(); 119 | } 120 | 121 | if (method_exists($this, ($method = 'isset_'.$name))) { 122 | return $this->$method(); 123 | } 124 | } 125 | 126 | public function __set($name, $value) 127 | { 128 | if (method_exists($this->captcha->get_CaptchaBase(), ($method = 'set_'.$name))) { 129 | return $this->captcha->get_CaptchaBase()->$method($value); 130 | } 131 | 132 | if (method_exists($this->captcha, ($method = 'set_'.$name))) { 133 | $this->captcha->$method($value); 134 | } else if (method_exists($this, ($method = 'set_'.$name))) { 135 | $this->$method($value); 136 | } 137 | } 138 | 139 | public function __unset($name) 140 | { 141 | if (method_exists($this->captcha->get_CaptchaBase(), ($method = 'unset_'.$name))) { 142 | return $this->captcha->get_CaptchaBase()->$method(); 143 | } 144 | 145 | if (method_exists($this->captcha, ($method = 'unset_'.$name))) { 146 | $this->captcha->$method(); 147 | } else if (method_exists($this, ($method = 'unset_'.$name))) { 148 | $this->$method(); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Helpers/BotDetectSimpleCaptchaHelper.php: -------------------------------------------------------------------------------- 1 | initCaptcha($captchaStyleName, $captchaId); 27 | } 28 | 29 | /** 30 | * Initialize SimpleCaptcha object instance. 31 | * 32 | * @param string $captchaStyleName 33 | * @param string $captchaId 34 | * 35 | * @return void 36 | */ 37 | public function initCaptcha($captchaStyleName, $captchaId = null) 38 | { 39 | if (($captchaStyleName === null) || ($captchaStyleName === '')) { 40 | $captchaStyleName = 'defaultCaptcha'; 41 | } 42 | $this->captcha = new \SimpleCaptcha($captchaStyleName, $captchaId); 43 | } 44 | 45 | public function __call($method, $args = array()) 46 | { 47 | if (method_exists($this, $method)) { 48 | return call_user_func_array(array($this, $method), $args); 49 | } 50 | 51 | if (method_exists($this->captcha, $method)) { 52 | return call_user_func_array(array($this->captcha, $method), $args); 53 | } 54 | 55 | if (method_exists($this->captcha->get_CaptchaBase(), $method)) { 56 | return call_user_func_array(array($this->captcha->get_CaptchaBase(), $method), $args); 57 | } 58 | } 59 | 60 | /** 61 | * Auto-magic helpers for civilized property access. 62 | */ 63 | public function __get($name) 64 | { 65 | if (method_exists($this->captcha->get_CaptchaBase(), ($method = 'get_'.$name))) { 66 | return $this->captcha->get_CaptchaBase()->$method(); 67 | } 68 | 69 | if (method_exists($this->captcha, ($method = 'get_'.$name))) { 70 | return $this->captcha->$method(); 71 | } 72 | 73 | if (method_exists($this, ($method = 'get_'.$name))) { 74 | return $this->$method(); 75 | } 76 | } 77 | 78 | public function __isset($name) 79 | { 80 | if (method_exists($this->captcha->get_CaptchaBase(), ($method = 'isset_'.$name))) { 81 | return $this->captcha->get_CaptchaBase()->$method(); 82 | } 83 | 84 | if (method_exists($this->captcha, ($method = 'isset_'.$name))) { 85 | return $this->captcha->$method(); 86 | } 87 | 88 | if (method_exists($this, ($method = 'isset_'.$name))) { 89 | return $this->$method(); 90 | } 91 | } 92 | 93 | public function __set($name, $value) 94 | { 95 | if (method_exists($this->captcha->get_CaptchaBase(), ($method = 'set_'.$name))) { 96 | return $this->captcha->get_CaptchaBase()->$method($value); 97 | } 98 | 99 | if (method_exists($this->captcha, ($method = 'set_'.$name))) { 100 | $this->captcha->$method($value); 101 | } else if (method_exists($this, ($method = 'set_'.$name))) { 102 | $this->$method($value); 103 | } 104 | } 105 | 106 | public function __unset($name) 107 | { 108 | if (method_exists($this->captcha->get_CaptchaBase(), ($method = 'unset_'.$name))) { 109 | return $this->captcha->get_CaptchaBase()->$method(); 110 | } 111 | 112 | if (method_exists($this->captcha, ($method = 'unset_'.$name))) { 113 | $this->captcha->$method(); 114 | } else if (method_exists($this, ($method = 'unset_'.$name))) { 115 | $this->$method(); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Integration/BotDetectCaptcha.php: -------------------------------------------------------------------------------- 1 | container = $container; 34 | } 35 | 36 | /** 37 | * Set captcha configuration and create a Captcha object instance. 38 | * 39 | * @param string $configName 40 | */ 41 | public function setConfig($configName) 42 | { 43 | return $this->getInstance($configName); 44 | } 45 | 46 | /** 47 | * @return bool 48 | */ 49 | private function isInstanceCreated() 50 | { 51 | return isset($this->captcha); 52 | } 53 | 54 | /** 55 | * Get an instance of the Captcha class. 56 | * 57 | * @param string $configName 58 | * 59 | * @return object 60 | */ 61 | public function getInstance($configName = '') 62 | { 63 | if (!$this->isInstanceCreated()) { 64 | // load BotDetect Library 65 | $libraryLoader = new LibraryLoader($this->container); 66 | $libraryLoader->load(); 67 | 68 | $this->captcha = new BotDetectCaptchaHelper($configName); 69 | } 70 | 71 | return $this->captcha; 72 | } 73 | 74 | /** 75 | * Get BotDetect Symfony CAPTCHA Bundle information. 76 | * 77 | * @return array 78 | */ 79 | public static function getProductInfo() 80 | { 81 | return self::$productInfo; 82 | } 83 | } 84 | 85 | // static field initialization 86 | BotDetectCaptcha::$productInfo = array( 87 | 'name' => 'BotDetect 4 PHP Captcha generator integration for the Symfony framework', 88 | 'version' => '4.2.12' 89 | ); 90 | -------------------------------------------------------------------------------- /Integration/BotDetectSimpleCaptcha.php: -------------------------------------------------------------------------------- 1 | container = $container; 30 | } 31 | 32 | /** 33 | * @return bool 34 | */ 35 | private function isInstanceCreated() 36 | { 37 | return isset($this->captcha); 38 | } 39 | 40 | /** 41 | * Get an instance of the SimpleCaptcha class. 42 | * 43 | * @param string $captchaStyleName 44 | * 45 | * @return object 46 | */ 47 | public function getInstance($captchaStyleName = '') 48 | { 49 | if (!$this->isInstanceCreated()) { 50 | // load BotDetect Library 51 | $libraryLoader = new SimpleLibraryLoader($this->container); 52 | $libraryLoader->load(); 53 | 54 | $this->captcha = new BotDetectSimpleCaptchaHelper($captchaStyleName); 55 | } 56 | 57 | return $this->captcha; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Provider/botdetect.php: -------------------------------------------------------------------------------- 1 | generate('captcha_handler', array(), \Symfony\Component\Routing\Generator\UrlGeneratorInterface::RELATIVE_PATH) . '?get='; 21 | 22 | // physical path to the folder with the (optional!) config override file 23 | $BDC_Config_Override_Path = __DIR__; 24 | 25 | 26 | // normalize paths 27 | if (is_file(__DIR__ . '/botdetect/CaptchaIncludes.php')) { 28 | // in case a local copy of the library exists, it is always used 29 | $BDC_Include_Path = __DIR__ . '/botdetect/'; 30 | $BDC_Url_Root = 'botdetect/public/'; 31 | } else { 32 | // clean-up path specifications 33 | $BDC_Include_Path = BDC_NormalizePath($BDC_Include_Path); 34 | $BDC_Url_Root = rtrim(BDC_NormalizePath($BDC_Url_Root), '/'); 35 | $BDC_Config_Override_Path = BDC_NormalizePath($BDC_Config_Override_Path); 36 | } 37 | define('BDC_INCLUDE_PATH', $BDC_Include_Path); 38 | define('BDC_URL_ROOT', $BDC_Url_Root); 39 | define('BDC_CONFIG_OVERRIDE_PATH', $BDC_Config_Override_Path); 40 | 41 | 42 | function BDC_NormalizePath($p_Path) { 43 | // replace backslashes with forward slashes 44 | $canonical = str_replace('\\', '/', $p_Path); 45 | // ensure ending slash 46 | $canonical = rtrim($canonical, '/'); 47 | $canonical .= '/'; 48 | return $canonical; 49 | } 50 | 51 | 52 | // 2. include required base class declarations 53 | require_once (BDC_INCLUDE_PATH . 'CaptchaIncludes.php'); 54 | 55 | 56 | // 3. include BotDetect configuration 57 | 58 | // a) mandatory global config, located in lib path 59 | require_once(BDC_INCLUDE_PATH . 'CaptchaConfigDefaults.php'); 60 | 61 | // b) optional config override 62 | function BDC_ApplyUserConfigOverride($CaptchaConfig, $CurrentCaptchaId) { 63 | $BotDetect = clone $CaptchaConfig; 64 | $BDC_ConfigOverridePath = BDC_CONFIG_OVERRIDE_PATH . 'CaptchaConfig.php'; 65 | if (is_file($BDC_ConfigOverridePath)) { 66 | include($BDC_ConfigOverridePath); 67 | CaptchaConfiguration::ProcessGlobalDeclarations($BotDetect); 68 | // 2nd pass correctly takes global declarations such as DisabledImageStyles into account 69 | // even if they're declared after affected values in the CaptchaConfig.php file 70 | // e.g. ImageStyle setting needs to be re-calculated according to DisabledImageStyles value 71 | include($BDC_ConfigOverridePath); 72 | } 73 | return $BotDetect; 74 | } 75 | 76 | 77 | // 4. determine is this file included in a form/class, or requested directly 78 | $BDC_RequestFilename = basename($_SERVER['REQUEST_URI']); 79 | if (BDC_StringHelper::StartsWith($BDC_RequestFilename, 'botdetect.php')) { 80 | // direct access, proceed as Captcha handler (serving images and sounds) 81 | require_once(BDC_INCLUDE_PATH . 'CaptchaHandler.php'); 82 | } else { 83 | // included in another file, proceed as Captcha class (form helper) 84 | require_once(BDC_INCLUDE_PATH . 'CaptchaClass.php'); 85 | } 86 | -------------------------------------------------------------------------------- /Provider/simple-botdetect.php: -------------------------------------------------------------------------------- 1 | generate('simple_captcha_handler', array(), \Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_PATH); 25 | 26 | // BotDetect Url prefix (base Url of the BotDetect public resources) 27 | $BDC_Url_Root = $BDC_Include_Path . 'public/'; 28 | 29 | // normalize paths 30 | if (is_file(__DIR__ . '/botdetect/CaptchaIncludes.php')) { 31 | // in case a local copy of the library exists, it is always used 32 | $BDC_Include_Path = __DIR__ . '/botdetect/'; 33 | } else { 34 | // clean-up path specifications 35 | $BDC_Include_Path = BDC_NormalizePath($BDC_Include_Path); 36 | } 37 | 38 | $BDC_Url_Root = BDC_NormalizePath($BDC_Url_Root); 39 | 40 | $BDC_Configuration_Cache_Path = BDC_NormalizePath($BDC_Configuration_Cache_Path); 41 | 42 | define('BDC_INCLUDE_PATH', $BDC_Include_Path); 43 | define('BDC_URL_ROOT', $BDC_Url_Root); 44 | //define('BDC_HANDLER_PATH', $BDC_Handler_Path); 45 | define('BDC_CONFIG_CONFIGURATION_PATH', $BDC_Configuration_Cache_Path); 46 | define('BDC_CONFIG_FILE_PATH', $BDC_Config_File_Path); 47 | 48 | function BDC_NormalizePath($p_Path) { 49 | // replace backslashes with forward slashes 50 | $canonical = str_replace('\\', '/', $p_Path); 51 | // ensure ending slash 52 | $canonical = rtrim($canonical, '/'); 53 | $canonical .= '/'; 54 | return $canonical; 55 | } 56 | 57 | function BDC_GetHandlerPath() { 58 | $serverRoot = BDC_NormalizePath($_SERVER['DOCUMENT_ROOT']); 59 | 60 | return '/' . substr(dirname(__FILE__), strlen($serverRoot)); 61 | } 62 | 63 | 64 | // 2. include required base class declarations 65 | require_once(BDC_INCLUDE_PATH . 'CaptchaIncludes.php'); 66 | 67 | // 3. set custom handler path 68 | if (class_exists('BDC_SimpleCaptchaDefaults')) { 69 | BDC_SimpleCaptchaDefaults::$HandlerUrl = $BDC_Handler_Path; 70 | } 71 | 72 | // 4. determine is this file included in a form/class, or requested directly 73 | 74 | // included in another file, proceed as Captcha class (form helper) 75 | require_once(BDC_INCLUDE_PATH . 'SimpleCaptchaClass.php'); 76 | 77 | $BDC_RequestFilename = basename($_SERVER['REQUEST_URI']); 78 | if (BDC_StringHelper::StartsWith($BDC_RequestFilename, basename(__FILE__))) { 79 | // direct access, proceed as Captcha handler (serving images and sounds) 80 | require_once(BDC_INCLUDE_PATH . 'SimpleCaptchaHandler.php'); 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BotDetect PHP Captcha generator integration for the Symfony framework 2 | 3 | [![Total Downloads](https://poser.pugx.org/captcha-com/symfony-captcha-bundle/downloads)](https://packagist.org/packages/captcha-com/symfony-captcha-bundle) 4 | [![Latest Stable Version](https://poser.pugx.org/captcha-com/symfony-captcha-bundle/v/stable)](https://packagist.org/packages/captcha-com/symfony-captcha-bundle) 5 | 6 | ![BotDetect PHP CAPTCHA Library](https://captcha.com/images/help/screenshots/captcha-examples.png) 7 | 8 | 9 | ## BotDetect Symfony CAPTCHA integration on captcha.com 10 | 11 | * [Symfony Captcha Integration Quickstart](https://captcha.com/doc/php/symfony-captcha-bundle-quickstart.html) 12 | * [Symfony Application](https://captcha.com/doc/php/howto/symfony-captcha-bundle.html) 13 | * [Symfony Captcha Api Basics Example](https://captcha.com/doc/php/examples/symfony-basic-captcha-bundle-example.html) 14 | * [Symfony Captcha Form Model Validation Example](https://captcha.com/doc/php/examples/symfony-form-validation-captcha-bundle-example.html) 15 | * [Symfony Captcha FOSUserBundle Example](https://captcha.com/doc/php/examples/symfony-fosuserbundle-captcha-example.html) 16 | 17 | 18 | ## Other BotDetect PHP Captcha integrations 19 | 20 | * [Plain PHP Captcha Integration](https://captcha.com/doc/php/php-captcha-quickstart.html) 21 | * [WordPress Captcha Plugin](https://captcha.com/doc/php/wordpress-captcha.html) 22 | * [CakePHP Captcha Integration](https://captcha.com/doc/php/cakephp-captcha-quickstart.html) 23 | * [CodeIgniter Captcha Integration](https://captcha.com/doc/php/codeigniter-captcha-quickstart.html) 24 | * [Laravel Captcha Integration](https://captcha.com/doc/php/laravel-captcha-quickstart.html) 25 | 26 | 27 | ## Questions? 28 | 29 | If you encounter bugs, implementation issues, a usage scenario you would like to discuss, or you have any questions, please contact [BotDetect CAPTCHA Support](http://captcha.com/support). -------------------------------------------------------------------------------- /Resources/config/routing.yml: -------------------------------------------------------------------------------- 1 | captcha_handler: 2 | path: /captcha-handler 3 | defaults: { _controller: CaptchaBundle:CaptchaHandler:index } 4 | methods: [GET] 5 | 6 | simple_captcha_handler: 7 | path: /simple-captcha-handler 8 | defaults: { _controller: CaptchaBundle:SimpleCaptchaHandler:index } 9 | methods: [GET] -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | botdetect_captcha.class: Captcha\Bundle\CaptchaBundle\Integration\BotDetectCaptcha 3 | botdetect_simple_captcha.class: Captcha\Bundle\CaptchaBundle\Integration\BotDetectSimpleCaptcha 4 | captcha_routes_loader.class: Captcha\Bundle\CaptchaBundle\Routing\CaptchaRoutesLoader 5 | captcha_type.class: Captcha\Bundle\CaptchaBundle\Form\Type\CaptchaType 6 | simple_captcha_type.class: Captcha\Bundle\CaptchaBundle\Form\Type\SimpleCaptchaType 7 | valid_captcha_validator.class: Captcha\Bundle\CaptchaBundle\Validator\Constraints\ValidCaptchaValidator 8 | valid_simple_captcha_validator.class: Captcha\Bundle\CaptchaBundle\Validator\Constraints\ValidSimpleCaptchaValidator 9 | 10 | services: 11 | captcha: 12 | class: '%botdetect_captcha.class%' 13 | public: true 14 | arguments: 15 | - '@service_container' 16 | 17 | simple_captcha: 18 | class: '%botdetect_simple_captcha.class%' 19 | public: true 20 | arguments: 21 | - '@service_container' 22 | 23 | captcha.routing_loader: 24 | class: '%captcha_routes_loader.class%' 25 | tags: 26 | - { name: routing.loader } 27 | 28 | captcha.form.type: 29 | class: '%captcha_type.class%' 30 | public: true 31 | arguments: 32 | - '@service_container' 33 | - '%captcha.config%' 34 | tags: 35 | - { name: form.type, alias: captcha } 36 | 37 | simple_captcha.form.type: 38 | class: '%simple_captcha_type.class%' 39 | public: true 40 | arguments: 41 | - '@service_container' 42 | - '%captcha.config%' 43 | tags: 44 | - { name: form.type, alias: captcha } 45 | 46 | captcha.validator: 47 | class: '%valid_captcha_validator.class%' 48 | public: true 49 | arguments: 50 | - '@service_container' 51 | tags: 52 | - { name: validator.constraint_validator, alias: valid_captcha } 53 | 54 | simple_captcha.validator: 55 | class: '%valid_simple_captcha_validator.class%' 56 | public: true 57 | arguments: 58 | - '@service_container' 59 | tags: 60 | - { name: validator.constraint_validator, alias: valid_simple_captcha } 61 | -------------------------------------------------------------------------------- /Resources/views/captcha.html.twig: -------------------------------------------------------------------------------- 1 | {% block simple_captcha_widget %} 2 | {% spaceless %} 3 | {{ captcha_html | raw }} 4 | {{ form_widget(form, { 'id': user_input_id, 'value': '' }) }} 5 | {% endspaceless %} 6 | {% endblock %} 7 | 8 | {% block captcha_widget %} 9 | {% spaceless %} 10 | {{ captcha_html | raw }} 11 | {{ form_widget(form, { 'id': user_input_id, 'value': '' }) }} 12 | {% endspaceless %} 13 | {% endblock %} -------------------------------------------------------------------------------- /Routing/CaptchaRoutesLoader.php: -------------------------------------------------------------------------------- 1 | import($routingResource, $type); 21 | 22 | $collection->addCollection($importedRoutes); 23 | 24 | return $collection; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function supports($resource, $type = null) 31 | { 32 | return 'captcha_routes' === $type; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Routing/Generator/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | load('routing.yml'); 25 | 26 | $requestContext = new RequestContext(); 27 | $requestContext->fromRequest(Request::createFromGlobals()); 28 | 29 | return new SymfonyUrlGenerator($captchaRoutes, $requestContext); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Security/Core/Exception/InvalidCaptchaException.php: -------------------------------------------------------------------------------- 1 | message; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Support/CaptchaConfigDefaults.php: -------------------------------------------------------------------------------- 1 | HandlerUrl = $captchaUrlGenerator->generate('captcha_handler', array(), \Symfony\Component\Routing\Generator\UrlGeneratorInterface::RELATIVE_PATH); 9 | 10 | // use Symfony sessions to store persist Captcha codes and other Captcha data 11 | $BotDetect->SaveFunctionName = 'SF_Session_Save'; 12 | $BotDetect->LoadFunctionName = 'SF_Session_Load'; 13 | $BotDetect->ClearFunctionName = 'SF_Session_Clear'; 14 | 15 | \CaptchaConfiguration::SaveSettings($BotDetect); 16 | 17 | // re-define custom session handler functions 18 | global $session; 19 | $session = $includeData; 20 | 21 | function SF_Session_Save($key, $value) 22 | { 23 | global $session; 24 | // save the given value with the given string key 25 | $session->set($key, serialize($value)); 26 | } 27 | 28 | function SF_Session_Load($key) 29 | { 30 | global $session; 31 | // load persisted value for the given string key 32 | if ($session->has($key)) { 33 | return unserialize($session->get($key)); // NOTE: returns false in case of failure 34 | } 35 | } 36 | 37 | function SF_Session_Clear($key) 38 | { 39 | global $session; 40 | // clear persisted value for the given string key 41 | if ($session->has($key)) { 42 | $session->remove($key); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Support/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | container = $container; 29 | $this->session = $this->container->get('session'); 30 | } 31 | 32 | /** 33 | * Load BotDetect CAPTCHA Library and override Captcha Library settings. 34 | */ 35 | public function load() 36 | { 37 | // load bd php library 38 | self::loadBotDetectLibrary(); 39 | 40 | // load the captcha configuration defaults 41 | self::loadCaptchaConfigDefaults(); 42 | } 43 | 44 | /** 45 | * Load BotDetect CAPTCHA Library. 46 | */ 47 | private function loadBotDetectLibrary() 48 | { 49 | $libPath = Path::getCaptchaLibPath($this->container); 50 | 51 | if (!self::isLibraryFound($libPath)) { 52 | throw new FileNotFoundException(sprintf('The BotDetect Captcha library could not be found in %s.', $libPath)); 53 | } 54 | 55 | self::includeFile(Path::getBotDetectFilePath(), true, $libPath); 56 | } 57 | 58 | /** 59 | * Load the captcha configuration defaults. 60 | * 61 | * @param SessionInterface $session 62 | */ 63 | private function loadCaptchaConfigDefaults() 64 | { 65 | self::includeFile(Path::getCaptchaConfigDefaultsFilePath(), true, $this->session); 66 | } 67 | 68 | /** 69 | * Check if the path to Captcha library is correct or not 70 | */ 71 | private static function isLibraryFound($libPath) 72 | { 73 | return file_exists($libPath . '/botdetect/CaptchaIncludes.php'); 74 | } 75 | 76 | /** 77 | * Include a file. 78 | * 79 | * @param string $filePath 80 | * @param bool $once 81 | * @param string $includeData 82 | */ 83 | private static function includeFile($filePath, $once = false, $includeData = null) 84 | { 85 | if (is_file($filePath)) { 86 | ($once) ? include_once($filePath) : include($filePath); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Support/Path.php: -------------------------------------------------------------------------------- 1 | getParameter('captcha.config.botdetect_captcha_path'); 23 | $libPath = rtrim($libPath, '/'); 24 | return $libPath; 25 | } 26 | 27 | /** 28 | * Physical path of the captcha-com/captcha package. 29 | * 30 | * @return string 31 | */ 32 | public static function getDefaultLibPackageDirPath() 33 | { 34 | $libPath1 = __DIR__ . '/../../captcha/botdetect-captcha-lib'; 35 | $libPath2 = __DIR__ . '/../../captcha/lib'; 36 | 37 | if (is_dir($libPath1)) { 38 | return $libPath1; 39 | } 40 | 41 | if (is_dir($libPath2)) { 42 | return $libPath2; 43 | } 44 | 45 | return null; 46 | } 47 | 48 | /** 49 | * Physical path of public directory which is located inside the captcha package. 50 | * 51 | * @return string 52 | */ 53 | public static function getPublicDirPathInLibrary(ContainerInterface $container) 54 | { 55 | return self::getCaptchaLibPath($container) . '/botdetect/public/'; 56 | } 57 | 58 | /** 59 | * Physical path of botdetect.php file. 60 | * 61 | * @return string 62 | */ 63 | public static function getBotDetectFilePath() 64 | { 65 | return __DIR__ . '/../Provider/botdetect.php'; 66 | } 67 | 68 | /** 69 | * Physical path of simple-botdetect.php file. 70 | * 71 | * @return string 72 | */ 73 | public static function getSimpleBotDetectFilePath() 74 | { 75 | return __DIR__ . '/../Provider/simple-botdetect.php'; 76 | } 77 | 78 | /** 79 | * Physical path of captcha config defaults file. 80 | * 81 | * @return string 82 | */ 83 | public static function getCaptchaConfigDefaultsFilePath() 84 | { 85 | return __DIR__ . '/CaptchaConfigDefaults.php'; 86 | } 87 | 88 | /** 89 | * Relative path of user's captcha config file. 90 | * 91 | * @return string 92 | */ 93 | public static function getRelativeConfigFilePath($file = '') 94 | { 95 | if (version_compare(Kernel::VERSION, '4.0', '>=')) { 96 | $configDirPath = 'config/packages/'; 97 | } else { 98 | $configDirPath = 'app/config/'; 99 | } 100 | return $configDirPath . $file; 101 | } 102 | 103 | /** 104 | * Physical path of the Symfony's config directory. 105 | * 106 | * @param string $path 107 | * 108 | * @return string 109 | */ 110 | public static function getConfigDirPath($file = '') 111 | { 112 | return __DIR__ . '/../../../../'. self::getRelativeConfigFilePath($file); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Support/SimpleLibraryLoader.php: -------------------------------------------------------------------------------- 1 | container = $container; 24 | } 25 | 26 | /** 27 | * Load BotDetect CAPTCHA Library and override Captcha Library settings. 28 | */ 29 | public function load() 30 | { 31 | // load bd php library 32 | self::loadBotDetectLibrary(); 33 | } 34 | 35 | /** 36 | * Load BotDetect CAPTCHA Library. 37 | */ 38 | private function loadBotDetectLibrary() 39 | { 40 | $libPath = Path::getCaptchaLibPath($this->container); 41 | 42 | if (!self::isLibraryFound($libPath)) { 43 | throw new FileNotFoundException(sprintf('The BotDetect Captcha library could not be found in %s.', $libPath)); 44 | } 45 | 46 | self::includeFile(Path::getSimpleBotDetectFilePath(), true, $libPath); 47 | } 48 | 49 | /** 50 | * Check if the path to Captcha library is correct or not 51 | */ 52 | private static function isLibraryFound($libPath) 53 | { 54 | return file_exists($libPath . '/botdetect/CaptchaIncludes.php'); 55 | } 56 | 57 | /** 58 | * Include a file. 59 | * 60 | * @param string $filePath 61 | * @param bool $once 62 | * @param string $includeData 63 | */ 64 | private static function includeFile($filePath, $once = false, $includeData = null) 65 | { 66 | if (is_file($filePath)) { 67 | ($once) ? include_once($filePath) : include($filePath); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Support/UserCaptchaConfiguration.php: -------------------------------------------------------------------------------- 1 | $value) { 80 | $settings->$option = $value; 81 | } 82 | 83 | \CaptchaConfiguration::SaveSettings($settings); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Validator/Constraints/ValidCaptcha.php: -------------------------------------------------------------------------------- 1 | container = $container; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function validate($value, Constraint $constraint) 30 | { 31 | $captcha = $this->container->get('captcha')->getInstance(); 32 | 33 | if (!$captcha instanceof BotDetectCaptchaHelper) { 34 | throw new UnexpectedTypeException($captcha, 'Captcha\Bundle\CaptchaBundle\Helpers\BotDetectCaptchaHelper'); 35 | } 36 | 37 | if (!$captcha->Validate($value)) { 38 | $this->context->addViolation($constraint->message); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Validator/Constraints/ValidSimpleCaptcha.php: -------------------------------------------------------------------------------- 1 | container = $container; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function validate($value, Constraint $constraint) 30 | { 31 | $captcha = $this->container->get('simple_captcha')->getInstance(); 32 | 33 | if (!$captcha instanceof BotDetectSimpleCaptchaHelper) { 34 | throw new UnexpectedTypeException($captcha, 'Captcha\Bundle\CaptchaBundle\Helpers\BotDetectSimpleCaptchaHelper'); 35 | } 36 | 37 | if (!$captcha->Validate($value)) { 38 | $this->context->addViolation($constraint->message); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "captcha-com/symfony-captcha-bundle", 3 | "type": "symfony-bundle", 4 | "description": "Symfony Captcha Bundle -- BotDetect PHP CAPTCHA generator integration for the Symfony framework.", 5 | "keywords": ["symfony captcha bundle", "symfony captcha", "captcha bundle", "captcha", "captcha generator", "botdetect"], 6 | "homepage": "https://captcha.com/doc/php/symfony-captcha-bundle-quickstart.html", 7 | "authors": [ 8 | { 9 | "name": "Symfony Captcha Bundle Support", 10 | "email": "botdetect.support@captcha.com", 11 | "homepage": "https://captcha.com/doc/php/symfony-captcha-bundle-quickstart.html" 12 | } 13 | ], 14 | "support": { 15 | "email": "botdetect.support@captcha.com", 16 | "issues": "https://captcha.com/support", 17 | "forum": "https://captcha.com/support", 18 | "source": "https://captcha.com/doc/php/symfony-captcha-bundle-quickstart.html" 19 | }, 20 | "require": { 21 | "captcha-com/captcha": "4.*", 22 | "php": ">=5.3.9" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Captcha\\Bundle\\CaptchaBundle\\": "/" 27 | } 28 | }, 29 | "extra": { 30 | "branch-alias": { 31 | "dev-master": "4.x-dev" 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------