├── .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 | [](https://packagist.org/packages/captcha-com/symfony-captcha-bundle)
4 | [](https://packagist.org/packages/captcha-com/symfony-captcha-bundle)
5 |
6 | 
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 |
--------------------------------------------------------------------------------