PageRenderTime 48ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/library/captcha/lib/botdetect/CaptchaHandler.php

https://bitbucket.org/Pons-bdreamz/anubavam_task
PHP | 434 lines | 296 code | 90 blank | 48 comment | 50 complexity | a6f3628661d5eaa2adae0c4354413a86 MD5 | raw file
  1. <?php
  2. if (is_callable('session_start')) { session_start(); }
  3. while (ob_get_length()) {
  4. ob_end_clean();
  5. }
  6. ob_start();
  7. try {
  8. BDC_HttpHelper::FixEscapedQuerystrings();
  9. BDC_HttpHelper::CheckForIgnoredRequests();
  10. // There are several Captcha commands accessible through the Http interface;
  11. // first we detect which of the valid commands is the current Http request for.
  12. if (!array_key_exists('get', $_GET) || !BDC_StringHelper::HasValue($_GET['get'])) {
  13. BDC_HttpHelper::BadRequest('command');
  14. }
  15. $commandString = BDC_StringHelper::Normalize($_GET['get']);
  16. $command = BDC_CaptchaHttpCommand::FromQuerystring($commandString);
  17. switch ($command) {
  18. case BDC_CaptchaHttpCommand::GetImage:
  19. GetImage();
  20. break;
  21. case BDC_CaptchaHttpCommand::GetSound:
  22. GetSound();
  23. break;
  24. case BDC_CaptchaHttpCommand::GetValidationResult:
  25. GetValidationResult();
  26. break;
  27. case BDC_CaptchaHttpCommand::GetInitScriptInclude:
  28. GetInitScriptInclude();
  29. break;
  30. case BDC_CaptchaHttpCommand::GetP:
  31. GetP();
  32. break;
  33. default:
  34. BDC_HttpHelper::BadRequest('command');
  35. break;
  36. }
  37. } catch (Exception $e) {
  38. header('Content-Type: text/plain');
  39. echo $e->getMessage();
  40. }
  41. ob_end_flush();
  42. exit;
  43. // Returns the Captcha image binary data
  44. function GetImage() {
  45. // saved data for the specified Captcha object in the application
  46. $captcha = GetCaptchaObject();
  47. if (is_null($captcha)) {
  48. BDC_HttpHelper::BadRequest('Captcha doesn\'t exist');
  49. }
  50. // identifier of the particular Captcha object instance
  51. $instanceId = GetInstanceId();
  52. if (is_null($instanceId)) {
  53. BDC_HttpHelper::BadRequest('Instance doesn\'t exist');
  54. }
  55. if(!$captcha->CaptchaBase->IsInstanceIdExisted($instanceId)) {
  56. BDC_HttpHelper::BadRequest('Instance doesn\'t exist in session');
  57. }
  58. // image generation invalidates sound cache, if any
  59. ClearSoundData($instanceId);
  60. // response headers
  61. BDC_HttpHelper::DisallowCache();
  62. // MIME type
  63. $mimeType = $captcha->ImageMimeType;
  64. header("Content-Type: {$mimeType}");
  65. // we don't support content chunking, since image files
  66. // are regenerated randomly on each request
  67. header('Accept-Ranges: none');
  68. // disallow audio file search engine indexing
  69. header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
  70. // image generation
  71. $rawImage = $captcha->CaptchaBase->GetImage($instanceId);
  72. $captcha->CaptchaBase->SaveCodeCollection(); // record generated Captcha code for validation
  73. session_write_close();
  74. // output image bytes
  75. $length = strlen($rawImage);
  76. header("Content-Length: {$length}");
  77. echo $rawImage;
  78. }
  79. function GetSound() {
  80. $captcha = GetCaptchaObject();
  81. if (is_null($captcha)) {
  82. BDC_HttpHelper::BadRequest('Captcha doesn\'t exist');
  83. }
  84. if (!$captcha->SoundEnabled) { // sound requests can be disabled with this config switch / instance property
  85. BDC_HttpHelper::BadRequest('Sound disabled');
  86. }
  87. $instanceId = GetInstanceId();
  88. if (is_null($instanceId)) {
  89. BDC_HttpHelper::BadRequest('Instance doesn\'t exist');
  90. }
  91. if(!$captcha->CaptchaBase->IsInstanceIdExisted($instanceId)) {
  92. BDC_HttpHelper::BadRequest('Instance doesn\'t exist in session');
  93. }
  94. $soundBytes = GetSoundData($captcha, $instanceId);
  95. session_write_close();
  96. if (is_null($soundBytes)) {
  97. BDC_HttpHelper::BadRequest('Please reload the form page before requesting another Captcha sound');
  98. exit;
  99. }
  100. $totalSize = strlen($soundBytes);
  101. // response headers
  102. BDC_HttpHelper::SmartDisallowCache();
  103. $mimeType = $captcha->SoundMimeType;
  104. header("Content-Type: {$mimeType}");
  105. header('Content-Transfer-Encoding: binary');
  106. if (!array_key_exists('d', $_GET)) { // javascript player not used, we send the file directly as a download
  107. $downloadId = BDC_CryptoHelper::GenerateGuid();
  108. header("Content-Disposition: attachment; filename=captcha_{$downloadId}.wav");
  109. }
  110. header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); // disallow audio file search engine indexing
  111. if (DetectIosRangeRequest()) { // iPhone/iPad sound issues workaround: chunked response for iOS clients
  112. // sound byte subset
  113. $range = GetSoundByteRange();
  114. $rangeStart = $range['start'];
  115. $rangeEnd = $range['end'];
  116. $rangeSize = $rangeEnd - $rangeStart + 1;
  117. // initial iOS 6.0.1 testing; leaving as fallback since we can't be sure it won't happen again:
  118. // we depend on observed behavior of invalid range requests to detect
  119. // end of sound playback, cleanup and tell AppleCoreMedia to stop requesting
  120. // invalid "bytes=rangeEnd-rangeEnd" ranges in an infinite(?) loop
  121. if ($rangeStart == $rangeEnd || $rangeEnd > $totalSize) {
  122. BDC_HttpHelper::BadRequest('invalid byte range');
  123. }
  124. $rangeBytes = substr($soundBytes, $rangeStart, $rangeSize);
  125. // partial content response with the requested byte range
  126. header('HTTP/1.1 206 Partial Content');
  127. header('Accept-Ranges: bytes');
  128. header("Content-Length: {$rangeSize}");
  129. header("Content-Range: bytes {$rangeStart}-{$rangeEnd}/{$totalSize}");
  130. echo $rangeBytes; // chrome needs this kind of response to be able to replay Html5 audio
  131. } else if (DetectFakeRangeRequest()) {
  132. header('Accept-Ranges: bytes');
  133. header("Content-Length: {$totalSize}");
  134. $end = $totalSize - 1;
  135. header("Content-Range: bytes 0-{$end}/{$totalSize}");
  136. echo $soundBytes;
  137. } else { // regular sound request
  138. header('Accept-Ranges: none');
  139. header("Content-Length: {$totalSize}");
  140. echo $soundBytes;
  141. }
  142. }
  143. function GetSoundData($p_Captcha, $p_InstanceId) {
  144. $shouldCache = (
  145. ($p_Captcha->SoundRegenerationMode == SoundRegenerationMode::None) || // no sound regeneration allowed, so we must cache the first and only generated sound
  146. DetectIosRangeRequest() // keep the same Captcha sound across all chunked iOS requests
  147. );
  148. if ($shouldCache) {
  149. $loaded = LoadSoundData($p_InstanceId);
  150. if (!is_null($loaded)) {
  151. return $loaded;
  152. }
  153. } else {
  154. ClearSoundData($p_InstanceId);
  155. }
  156. $soundBytes = GenerateSoundData($p_Captcha, $p_InstanceId);
  157. if ($shouldCache) {
  158. SaveSoundData($p_InstanceId, $soundBytes);
  159. }
  160. return $soundBytes;
  161. }
  162. function GenerateSoundData($p_Captcha, $p_InstanceId) {
  163. $rawSound = $p_Captcha->CaptchaBase->GetSound($p_InstanceId);
  164. $p_Captcha->CaptchaBase->SaveCodeCollection(); // always record sound generation count
  165. return $rawSound;
  166. }
  167. function SaveSoundData($p_InstanceId, $p_SoundBytes) {
  168. BDC_Persistence_Save("BDC_Cached_SoundData_" . $p_InstanceId, $p_SoundBytes);
  169. }
  170. function LoadSoundData($p_InstanceId) {
  171. return BDC_Persistence_Load("BDC_Cached_SoundData_" . $p_InstanceId);
  172. }
  173. function ClearSoundData($p_InstanceId) {
  174. BDC_Persistence_Clear("BDC_Cached_SoundData_" . $p_InstanceId);
  175. }
  176. // Instead of relying on unreliable user agent checks, we detect the iOS sound
  177. // requests by the Http headers they will always contain
  178. function DetectIosRangeRequest() {
  179. $detected = false;
  180. if(array_key_exists('HTTP_RANGE', $_SERVER) &&
  181. BDC_StringHelper::HasValue($_SERVER['HTTP_RANGE'])) {
  182. // Safari on MacOS and all browsers on <= iOS 10.x
  183. if(array_key_exists('HTTP_X_PLAYBACK_SESSION_ID', $_SERVER) &&
  184. BDC_StringHelper::HasValue($_SERVER['HTTP_X_PLAYBACK_SESSION_ID'])) {
  185. $detected = true;
  186. }
  187. // all browsers on iOS 11.x and later
  188. if(array_key_exists('User-Agent', $_SERVER) &&
  189. BDC_StringHelper::HasValue($_SERVER['User-Agent'])) {
  190. $userAgent = $_SERVER['User-Agent'];
  191. if(strpos($userAgent, "iPhone OS") !== false || strpos($userAgent, "iPad") !== false) { // is iPhone or iPad
  192. $detected = true;
  193. }
  194. }
  195. }
  196. return $detected;
  197. }
  198. function GetSoundByteRange() {
  199. // chunked requests must include the desired byte range
  200. $rangeStr = $_SERVER['HTTP_RANGE'];
  201. if (!BDC_StringHelper::HasValue($rangeStr)) {
  202. return;
  203. }
  204. $matches = array();
  205. preg_match_all('/bytes=([0-9]+)-([0-9]+)/', $rangeStr, $matches);
  206. return array(
  207. 'start' => (int) $matches[1][0],
  208. 'end' => (int) $matches[2][0]
  209. );
  210. }
  211. function DetectFakeRangeRequest() {
  212. $detected = false;
  213. if (array_key_exists('HTTP_RANGE', $_SERVER)) {
  214. $rangeStr = $_SERVER['HTTP_RANGE'];
  215. if (BDC_StringHelper::HasValue($rangeStr) &&
  216. preg_match('/bytes=0-$/', $rangeStr)) {
  217. $detected = true;
  218. }
  219. }
  220. return $detected;
  221. }
  222. // Used for client-side validation, returns Captcha validation result as JSON
  223. function GetValidationResult() {
  224. // saved data for the specified Captcha object in the application
  225. $captcha = GetCaptchaObject();
  226. if (is_null($captcha)) {
  227. BDC_HttpHelper::BadRequest('captcha');
  228. }
  229. // identifier of the particular Captcha object instance
  230. $instanceId = GetInstanceId();
  231. if (is_null($instanceId)) {
  232. BDC_HttpHelper::BadRequest('instance');
  233. }
  234. // code to validate
  235. $userInput = GetUserInput();
  236. // response MIME type & headers
  237. header('Content-Type: text/javascript');
  238. header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
  239. // JSON-encoded validation result
  240. $result = false;
  241. if (isset($userInput) && (isset($instanceId))) {
  242. $result = $captcha->AjaxValidate($userInput, $instanceId);
  243. $captcha->CaptchaBase->SaveCodeCollection();
  244. }
  245. session_write_close();
  246. $resultJson = GetJsonValidationResult($result);
  247. echo $resultJson;
  248. }
  249. function GetInitScriptInclude() {
  250. // saved data for the specified Captcha object in the application
  251. $captcha = GetCaptchaObject();
  252. if (is_null($captcha)) {
  253. BDC_HttpHelper::BadRequest('captcha');
  254. }
  255. // identifier of the particular Captcha object instance
  256. $instanceId = GetInstanceId();
  257. if (is_null($instanceId)) {
  258. BDC_HttpHelper::BadRequest('instance');
  259. }
  260. // response MIME type & headers
  261. header('Content-Type: text/javascript');
  262. header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
  263. echo "(function() {\r\n";
  264. // add init script
  265. echo BDC_CaptchaScriptsHelper::GetInitScriptMarkup($captcha, $instanceId);
  266. // add remote scripts if enabled
  267. if ($captcha->RemoteScriptEnabled) {
  268. echo "\r\n";
  269. echo BDC_CaptchaScriptsHelper::GetRemoteScript($captcha);
  270. }
  271. // close a self-invoking functions
  272. echo "\r\n})();";
  273. }
  274. // gets Captcha instance according to the CaptchaId passed in querystring
  275. function GetCaptchaObject() {
  276. $captchaId = BDC_StringHelper::Normalize($_GET['c']);
  277. if (!BDC_StringHelper::HasValue($captchaId) ||
  278. !BDC_CaptchaBase::IsValidCaptchaId($captchaId)) {
  279. return;
  280. }
  281. $captchaInstanceId = BDC_StringHelper::Normalize($_GET['t']);
  282. if (!BDC_StringHelper::HasValue($captchaInstanceId) ||
  283. !BDC_CaptchaBase::IsValidInstanceId($captchaInstanceId)) {
  284. return;
  285. }
  286. $captcha = new Captcha($captchaId, $captchaInstanceId);
  287. return $captcha;
  288. }
  289. // extract the exact Captcha code instance referenced by the request
  290. function GetInstanceId() {
  291. $instanceId = BDC_StringHelper::Normalize($_GET['t']);
  292. if (!BDC_StringHelper::HasValue($instanceId) ||
  293. !BDC_CaptchaBase::IsValidInstanceId($instanceId)) {
  294. return;
  295. }
  296. return $instanceId;
  297. }
  298. // extract the user input Captcha code string from the Ajax validation request
  299. function GetUserInput() {
  300. $input = null;
  301. if (isset($_GET['i'])) {
  302. // BotDetect built-in Ajax Captcha validation
  303. $input = BDC_StringHelper::Normalize($_GET['i']);
  304. } else {
  305. // jQuery validation support, the input key may be just about anything,
  306. // so we have to loop through fields and take the first unrecognized one
  307. $recognized = array('get', 'c', 't', 'd');
  308. foreach($_GET as $key => $value) {
  309. if (!in_array($key, $recognized)) {
  310. $input = $value;
  311. break;
  312. }
  313. }
  314. }
  315. return $input;
  316. }
  317. // encodes the Captcha validation result in a simple JSON wrapper
  318. function GetJsonValidationResult($p_Result) {
  319. $resultStr = ($p_Result ? 'true': 'false');
  320. return $resultStr;
  321. }
  322. function GetP() {
  323. $captcha = GetCaptchaObject();
  324. if (is_null($captcha)) {
  325. BDC_HttpHelper::BadRequest('captcha');
  326. }
  327. $instanceId = GetInstanceId();
  328. if (is_null($instanceId)) {
  329. BDC_HttpHelper::BadRequest('instance');
  330. }
  331. // create new one
  332. $p = new P($instanceId);
  333. // save
  334. BDC_Persistence_Clear($captcha->get_CaptchaBase()->getPPersistenceKey($instanceId));
  335. BDC_Persistence_Save($captcha->get_CaptchaBase()->getPPersistenceKey($instanceId), $p);
  336. // response data
  337. $response = "{\"sp\":\"{$p->GSP()}\",\"hs\":\"{$p->GHs()}\"}";
  338. // response MIME type & headers
  339. header('Content-Type: application/json');
  340. header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
  341. BDC_HttpHelper::SmartDisallowCache();
  342. echo $response;
  343. }
  344. ?>