vendor/captcha-com/symfony-captcha-bundle/Controller/CaptchaHandlerController.php line 22

Open in your IDE?
  1. <?php
  2. namespace Captcha\Bundle\CaptchaBundle\Controller;
  3. use Captcha\Bundle\CaptchaBundle\Support\Path;
  4. use Captcha\Bundle\CaptchaBundle\Support\LibraryLoader;
  5. use Captcha\Bundle\CaptchaBundle\Helpers\BotDetectCaptchaHelper;
  6. use Symfony\Component\HttpFoundation\Response;
  7. use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  8. use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
  9. class CaptchaHandlerController extends Controller
  10. {
  11.     /**
  12.      * @var object
  13.      */
  14.     private $captcha;
  15.     /**
  16.      * Handle request from querystring such as getting image, getting sound, etc.
  17.      */
  18.     public function indexAction()
  19.     {
  20.         if ($this->isGetResourceContentsRequest()) {
  21.             // getting contents of css, js, and gif files.
  22.             return $this->getResourceContents();
  23.         } else {
  24.             
  25.             $this->captcha $this->getBotDetectCaptchaInstance();
  26.             if (is_null($this->captcha)) {
  27.                 throw new BadRequestHttpException('captcha');
  28.             }
  29.             $commandString $this->getUrlParameter('get');
  30.             if (!\BDC_StringHelper::HasValue($commandString)) {
  31.                 \BDC_HttpHelper::BadRequest('command');
  32.             }
  33.             $commandString = \BDC_StringHelper::Normalize($commandString);
  34.             $command = \BDC_CaptchaHttpCommand::FromQuerystring($commandString);
  35.             $responseBody '';
  36.             switch ($command) {
  37.                 case \BDC_CaptchaHttpCommand::GetImage:
  38.                     $responseBody $this->getImage();
  39.                     break;
  40.                 case \BDC_CaptchaHttpCommand::GetSound:
  41.                     $responseBody $this->getSound();
  42.                     break;
  43.                 case \BDC_CaptchaHttpCommand::GetValidationResult:
  44.                     $responseBody $this->getValidationResult();
  45.                     break;
  46.                 case \BDC_CaptchaHttpCommand::GetScriptInclude:
  47.                     $responseBody $this->getScriptInclude();
  48.                     break;
  49.                 case \BDC_CaptchaHttpCommand::GetP:
  50.                     $responseBody $this->getP();
  51.                     break;
  52.                 default:
  53.                     \BDC_HttpHelper::BadRequest('command');
  54.                     break;
  55.             }
  56.             // disallow audio file search engine indexing
  57.             header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
  58.             echo $responseBody; exit;
  59.         }
  60.     }
  61.     /**
  62.      * Get CAPTCHA object instance.
  63.      *
  64.      * @return object
  65.      */
  66.     private function getBotDetectCaptchaInstance()
  67.     {
  68.         // load BotDetect Library
  69.         $libraryLoader = new LibraryLoader($this->container);
  70.         $libraryLoader->load();
  71.         $captchaId $this->getUrlParameter('c');
  72.         if (is_null($captchaId) || !preg_match('/^(\w+)$/ui'$captchaId)) {
  73.             throw new BadRequestHttpException('Invalid captcha id.');
  74.         }
  75.         $captchaInstanceId $this->getUrlParameter('t');
  76.         if (is_null($captchaInstanceId) || !(32 == strlen($captchaInstanceId) &&
  77.             (=== preg_match("/^([a-f0-9]+)$/u"$captchaInstanceId)))) {
  78.             throw new BadRequestHttpException('Invalid instance id.');
  79.         }
  80.         return new BotDetectCaptchaHelper($captchaId$captchaInstanceId);
  81.     }
  82.     /**
  83.      * Get contents of Captcha resources (js, css, gif files).
  84.      *
  85.      * @return string
  86.      */
  87.     public function getResourceContents()
  88.     {
  89.         $filename $this->getUrlParameter('get');
  90.         if (!preg_match('/^[a-z-]+\.(css|gif|js)$/'$filename)) {
  91.             throw new BadRequestHttpException('Invalid file name.');
  92.         }
  93.         $resourcePath realpath(Path::getPublicDirPathInLibrary($this->container) . $filename);
  94.         if (!is_file($resourcePath)) {
  95.             throw new BadRequestHttpException(sprintf('File "%s" could not be found.'$filename));
  96.         }
  97.         $mimesType = array('css' => 'text/css''gif' => 'image/gif''js'  => 'application/x-javascript');
  98.         $fileInfo pathinfo($resourcePath);
  99.         return new Response(
  100.             file_get_contents($resourcePath),
  101.             200,
  102.             array('content-type' => $mimesType[$fileInfo['extension']])
  103.         );
  104.     }
  105.     /**
  106.      * Generate a Captcha image.
  107.      *
  108.      * @return image
  109.      */
  110.     public function getImage()
  111.     {
  112.         if (is_null($this->captcha)) {
  113.             \BDC_HttpHelper::BadRequest('captcha');
  114.         }
  115.         // identifier of the particular Captcha object instance
  116.         $instanceId $this->getInstanceId();
  117.         if (is_null($instanceId)) {
  118.             \BDC_HttpHelper::BadRequest('instance');
  119.         }
  120.         // image generation invalidates sound cache, if any
  121.         $this->clearSoundData($instanceId);
  122.         // response headers
  123.         \BDC_HttpHelper::DisallowCache();
  124.         // response MIME type & headers
  125.         $mimeType $this->captcha->CaptchaBase->ImageMimeType;
  126.         header("Content-Type: {$mimeType}");
  127.         // we don't support content chunking, since image files
  128.         // are regenerated randomly on each request
  129.         header('Accept-Ranges: none');
  130.         // image generation
  131.         $rawImage $this->captcha->CaptchaBase->GetImage($instanceId);
  132.         $this->captcha->CaptchaBase->SaveCodeCollection();
  133.         $length strlen($rawImage);
  134.         header("Content-Length: {$length}");
  135.         return $rawImage;
  136.     }
  137.     /**
  138.      * Generate a Captcha sound.
  139.      */
  140.     public function getSound()
  141.     {
  142.         if (is_null($this->captcha)) {
  143.             \BDC_HttpHelper::BadRequest('captcha');
  144.         }
  145.         // identifier of the particular Captcha object instance
  146.         $instanceId $this->getInstanceId();
  147.         if (is_null($instanceId)) {
  148.             \BDC_HttpHelper::BadRequest('instance');
  149.         }
  150.         $soundBytes $this->getSoundData($this->captcha$instanceId);
  151.         if (is_null($soundBytes)) {
  152.             \BDC_HttpHelper::BadRequest('Please reload the form page before requesting another Captcha sound');
  153.             exit;
  154.         }
  155.         $totalSize strlen($soundBytes);
  156.         // response headers
  157.         \BDC_HttpHelper::SmartDisallowCache();
  158.         // response MIME type & headers
  159.         $mimeType $this->captcha->CaptchaBase->SoundMimeType;
  160.         header("Content-Type: {$mimeType}");
  161.         header('Content-Transfer-Encoding: binary');
  162.         if (!array_key_exists('d'$_GET)) { // javascript player not used, we send the file directly as a download
  163.             $downloadId = \BDC_CryptoHelper::GenerateGuid();
  164.             header("Content-Disposition: attachment; filename=captcha_{$downloadId}.wav");
  165.         }
  166.         if ($this->detectIosRangeRequest()) { // iPhone/iPad sound issues workaround: chunked response for iOS clients
  167.             // sound byte subset
  168.             $range $this->getSoundByteRange();
  169.             $rangeStart $range['start'];
  170.             $rangeEnd $range['end'];
  171.             $rangeSize $rangeEnd $rangeStart 1;
  172.             // initial iOS 6.0.1 testing; leaving as fallback since we can't be sure it won't happen again:
  173.             // we depend on observed behavior of invalid range requests to detect
  174.             // end of sound playback, cleanup and tell AppleCoreMedia to stop requesting
  175.             // invalid "bytes=rangeEnd-rangeEnd" ranges in an infinite(?) loop
  176.             if ($rangeStart == $rangeEnd || $rangeEnd $totalSize) {
  177.                 \BDC_HttpHelper::BadRequest('invalid byte range');
  178.             }
  179.             $rangeBytes substr($soundBytes$rangeStart$rangeSize);
  180.             // partial content response with the requested byte range
  181.             header('HTTP/1.1 206 Partial Content');
  182.             header('Accept-Ranges: bytes');
  183.             header("Content-Length: {$rangeSize}");
  184.             header("Content-Range: bytes {$rangeStart}-{$rangeEnd}/{$totalSize}");
  185.             return $rangeBytes// chrome needs this kind of response to be able to replay Html5 audio
  186.         } else if ($this->detectFakeRangeRequest()) {
  187.             header('Accept-Ranges: bytes');
  188.             header("Content-Length: {$totalSize}");
  189.             $end $totalSize 1;
  190.             header("Content-Range: bytes 0-{$end}/{$totalSize}");
  191.             return $soundBytes;
  192.         } else { // regular sound request
  193.             header('Accept-Ranges: none');
  194.             header("Content-Length: {$totalSize}");
  195.             return $soundBytes;
  196.         }
  197.     }
  198.     public function getSoundData($p_Captcha$p_InstanceId)
  199.     {
  200.         $shouldCache = (
  201.             ($p_Captcha->SoundRegenerationMode == \SoundRegenerationMode::None) || // no sound regeneration allowed, so we must cache the first and only generated sound
  202.             $this->detectIosRangeRequest() // keep the same Captcha sound across all chunked iOS requests
  203.         );
  204.         if ($shouldCache) {
  205.             $loaded $this->loadSoundData($p_InstanceId);
  206.             if (!is_null($loaded)) {
  207.                 return $loaded;
  208.             }
  209.         } else {
  210.             $this->clearSoundData($p_InstanceId);
  211.         }
  212.         $soundBytes $this->generateSoundData($p_Captcha$p_InstanceId);
  213.         if ($shouldCache) {
  214.             $this->saveSoundData($p_InstanceId$soundBytes);
  215.         }
  216.         return $soundBytes;
  217.     }
  218.     private function generateSoundData($p_Captcha$p_InstanceId)
  219.     {
  220.         $rawSound $p_Captcha->CaptchaBase->GetSound($p_InstanceId);
  221.         $p_Captcha->CaptchaBase->SaveCodeCollection(); // always record sound generation count
  222.         return $rawSound;
  223.     }
  224.     private function saveSoundData($p_InstanceId$p_SoundBytes)
  225.     {
  226.         SF_Session_Save("BDC_Cached_SoundData_" $p_InstanceId$p_SoundBytes);
  227.     }
  228.     private function loadSoundData($p_InstanceId)
  229.     {
  230.         return SF_Session_Load("BDC_Cached_SoundData_" $p_InstanceId);
  231.     }
  232.     private function clearSoundData($p_InstanceId)
  233.     {
  234.         SF_Session_Clear("BDC_Cached_SoundData_" $p_InstanceId);
  235.     }
  236.     // Instead of relying on unreliable user agent checks, we detect the iOS sound
  237.     // requests by the Http headers they will always contain
  238.     private function detectIosRangeRequest()
  239.     {
  240.         if (array_key_exists('HTTP_RANGE'$_SERVER) &&
  241.             \BDC_StringHelper::HasValue($_SERVER['HTTP_RANGE'])) {
  242.             // Safari on MacOS and all browsers on <= iOS 10.x
  243.             if (array_key_exists('HTTP_X_PLAYBACK_SESSION_ID'$_SERVER) &&
  244.                 \BDC_StringHelper::HasValue($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) {
  245.                 return true;
  246.             }
  247.             $userAgent array_key_exists('HTTP_USER_AGENT'$_SERVER) ? $_SERVER['HTTP_USER_AGENT'] : null;
  248.             // all browsers on iOS 11.x and later
  249.             if(\BDC_StringHelper::HasValue($userAgent)) {
  250.                 $userAgentLC = \BDC_StringHelper::Lowercase($userAgent);
  251.                 if (\BDC_StringHelper::Contains($userAgentLC"like mac os") || \BDC_StringHelper::Contains($userAgentLC"like macos")) {
  252.                     return true;
  253.                 }
  254.             }
  255.         }
  256.         return false;
  257.     }
  258.     private function getSoundByteRange()
  259.     {
  260.         // chunked requests must include the desired byte range
  261.         $rangeStr $_SERVER['HTTP_RANGE'];
  262.         if (!\BDC_StringHelper::HasValue($rangeStr)) {
  263.             return;
  264.         }
  265.         $matches = array();
  266.         preg_match_all('/bytes=([0-9]+)-([0-9]+)/'$rangeStr$matches);
  267.         return array(
  268.             'start' => (int) $matches[1][0],
  269.             'end'   => (int) $matches[2][0]
  270.         );
  271.     }
  272.     private function detectFakeRangeRequest()
  273.     {
  274.         $detected false;
  275.         if (array_key_exists('HTTP_RANGE'$_SERVER)) {
  276.             $rangeStr $_SERVER['HTTP_RANGE'];
  277.             if (\BDC_StringHelper::HasValue($rangeStr) &&
  278.                 preg_match('/bytes=0-$/'$rangeStr)) {
  279.                 $detected true;
  280.             }
  281.         }
  282.         return $detected;
  283.     }
  284.     /**
  285.      * The client requests the Captcha validation result (used for Ajax Captcha validation).
  286.      *
  287.      * @return json
  288.      */
  289.     public function getValidationResult()
  290.     {
  291.         if (is_null($this->captcha)) {
  292.             \BDC_HttpHelper::BadRequest('captcha');
  293.         }
  294.         // identifier of the particular Captcha object instance
  295.         $instanceId $this->getInstanceId();
  296.         if (is_null($instanceId)) {
  297.             \BDC_HttpHelper::BadRequest('instance');
  298.         }
  299.         $mimeType 'application/json';
  300.         header("Content-Type: {$mimeType}");
  301.         // code to validate
  302.         $userInput $this->getUserInput();
  303.         // JSON-encoded validation result
  304.         $result false;
  305.         if (isset($userInput) && (isset($instanceId))) {
  306.             $result $this->captcha->AjaxValidate($userInput$instanceId);
  307.             $this->captcha->CaptchaBase->Save();
  308.         }
  309.         $resultJson $this->getJsonValidationResult($result);
  310.         return $resultJson;
  311.     }
  312.     public function getScriptInclude()
  313.     {
  314.         // saved data for the specified Captcha object in the application
  315.         if (is_null($this->captcha)) {
  316.             \BDC_HttpHelper::BadRequest('captcha');
  317.         }
  318.         // identifier of the particular Captcha object instance
  319.         $instanceId $this->getInstanceId();
  320.         if (is_null($instanceId)) {
  321.             \BDC_HttpHelper::BadRequest('instance');
  322.         }
  323.         // response MIME type & headers
  324.         header('Content-Type: text/javascript');
  325.         header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
  326.         // 1. load BotDetect script
  327.         $resourcePath realpath(Path::getPublicDirPathInLibrary($this->container) . 'bdc-traditional-api-script-include.js');
  328.         if (!is_file($resourcePath)) {
  329.             throw new BadRequestHttpException(sprintf('File "%s" could not be found.'$resourcePath));
  330.         }
  331.         $script file_get_contents($resourcePath);
  332.         // 2. load BotDetect Init script
  333.         $script .= \BDC_CaptchaScriptsHelper::GetInitScriptMarkup($this->captcha$instanceId);
  334.         // add remote scripts if enabled
  335.         if ($this->captcha->RemoteScriptEnabled) {
  336.             $script .= "\r\n";
  337.             $script .= \BDC_CaptchaScriptsHelper::GetRemoteScript($this->captcha);
  338.         }
  339.         return $script;
  340.     }
  341.     /**
  342.      * @return string
  343.      */
  344.     private function getInstanceId()
  345.     {
  346.         $instanceId $this->getUrlParameter('t');
  347.         if (!\BDC_StringHelper::HasValue($instanceId) ||
  348.             !\BDC_CaptchaBase::IsValidInstanceId($instanceId)
  349.         ) {
  350.             return;
  351.         }
  352.         return $instanceId;
  353.     }
  354.     /**
  355.      * Extract the user input Captcha code string from the Ajax validation request.
  356.      *
  357.      * @return string
  358.      */
  359.     private function getUserInput()
  360.     {
  361.         // BotDetect built-in Ajax Captcha validation
  362.         $input $this->getUrlParameter('i');
  363.         if (is_null($input)) {
  364.             // jQuery validation support, the input key may be just about anything,
  365.             // so we have to loop through fields and take the first unrecognized one
  366.             $recognized = array('get''c''t''d');
  367.             foreach ($_GET as $key => $value) {
  368.                 if (!in_array($key$recognized)) {
  369.                     $input $value;
  370.                     break;
  371.                 }
  372.             }
  373.         }
  374.         return $input;
  375.     }
  376.     /**
  377.      * Encodes the Captcha validation result in a simple JSON wrapper.
  378.      *
  379.      * @return string
  380.      */
  381.     private function getJsonValidationResult($result)
  382.     {
  383.         $resultStr = ($result 'true''false');
  384.         return $resultStr;
  385.     }
  386.     /**
  387.      * @return bool
  388.      */
  389.     private function isGetResourceContentsRequest()
  390.     {
  391.         return array_key_exists('get'$_GET) && !array_key_exists('c'$_GET);
  392.     }
  393.     /**
  394.      * @param  string  $param
  395.      * @return string|null
  396.      */
  397.     private function getUrlParameter($param)
  398.     {
  399.         return filter_input(INPUT_GET$param);
  400.     }
  401.     public function getP()
  402.     {
  403.         if (is_null($this->captcha)) {
  404.             \BDC_HttpHelper::BadRequest('captcha');
  405.         }
  406.         // identifier of the particular Captcha object instance
  407.         $instanceId $this->getInstanceId();
  408.         if (is_null($instanceId)) {
  409.             \BDC_HttpHelper::BadRequest('instance');
  410.         }
  411.         // create new one
  412.         $p $this->captcha->GenPw($instanceId);
  413.         $this->captcha->SavePw($this->captcha);
  414.         // response data
  415.         $response "{\"sp\":\"{$p->GetSP()}\",\"hs\":\"{$p->GetHs()}\"}";
  416.         // response MIME type & headers
  417.         header('Content-Type: application/json');
  418.         header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
  419.         \BDC_HttpHelper::SmartDisallowCache();
  420.         return $response;
  421.     }
  422. }