function Get_iOS_ChunkedSound() { // detect the first request after a new sound icon click, and clear any stored data // to avoid reusing the same sound endlessly within a session // javascript player adds a timestamp querystring param ("&d=..."), so it can be detected by it $isJavaScriptPlayerRequest = array_key_exists('d', $_GET) && LBD_StringHelper::HasValue($_GET['d']); if ($isJavaScriptPlayerRequest) { // when javascript is enabled, we can detect the first request because the timestamp changed $soundClickId = LBD_StringHelper::Normalize($_GET['d']); $prevSoundClickId = LBD_Persistence_Load('prevSoundClickId'); if (0 != strcasecmp($soundClickId, $prevSoundClickId)) { Clear_iOS_SoundData(); LBD_Persistence_Save('prevSoundClickId', $soundClickId); // on first request, save for future checks } } // sound byte subset $range = GetSoundByteRange(); $rangeStart = $range['start']; $rangeEnd = $range['end']; $rangeSize = $rangeEnd - $rangeStart; // full sound bytes $soundBytes = Get_iOS_SoundData(); if (is_null($soundBytes)) { return; } $totalSize = strlen($soundBytes) - 1; // initial iOS 6.0.1 testing; leaving as fallback since we can't be sure it won't happen again: // we depend on observed behavior of invalid range requests to detect // end of sound playback, cleanup and tell AppleCoreMedia to stop requesting // invalid "bytes=rangeEnd-rangeEnd" ranges in an infinite(?) loop if ($rangeStart == $rangeEnd || $rangeEnd > $totalSize) { Clear_iOS_SoundData(); LBD_HttpHelper::BadRequest('invalid byte range'); } while (ob_get_length()) { ob_end_clean(); } ob_start(); try { // partial content response with the requested byte range header('HTTP/1.1 206 Partial Content'); $mimeType = $captcha->SoundMimeType; header("Content-Type: {$mimeType}"); header('Content-Transfer-Encoding: binary'); header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet'); header('Accept-Ranges: bytes'); header("Content-Length: {$rangeSize}"); header("Content-Range: bytes {$rangeStart}-{$rangeEnd}/{$totalSize}"); if (!array_key_exists('d', $_GET)) { // javascript player not used, we send the file directly as a download $downloadId = LBD_CryptoHelper::GenerateGuid(); header("Content-Disposition: attachment; filename=captcha_{$downloadId}.wav"); } LBD_HttpHelper::SmartDisallowCache(); $rangeBytes = substr($soundBytes, $rangeStart, $rangeSize); echo $rangeBytes; } catch (Exception $e) { header('Content-Type: text/plain'); echo $e->getMessage(); } ob_end_flush(); exit; }
function GetSound() { $captcha = GetCaptchaObject(); if (is_null($captcha)) { LBD_HttpHelper::BadRequest('Captcha doesn\'t exist'); } if (!$captcha->SoundEnabled) { // sound requests can be disabled with this config switch / instance property LBD_HttpHelper::BadRequest('Sound disabled'); } $instanceId = GetInstanceId(); if (is_null($instanceId)) { LBD_HttpHelper::BadRequest('Instance doesn\'t exist'); } $soundBytes = GetSoundData($captcha, $instanceId); session_write_close(); if (is_null($soundBytes)) { LBD_HttpHelper::BadRequest('Please reload the form page before requesting another Captcha sound'); exit; } $totalSize = strlen($soundBytes); // response headers LBD_HttpHelper::SmartDisallowCache(); $mimeType = $captcha->SoundMimeType; header("Content-Type: {$mimeType}"); header('Content-Transfer-Encoding: binary'); if (!array_key_exists('d', $_GET)) { // javascript player not used, we send the file directly as a download $downloadId = LBD_CryptoHelper::GenerateGuid(); header("Content-Disposition: attachment; filename=captcha_{$downloadId}.wav"); } if (DetectIosRangeRequest()) { // iPhone/iPad sound issues workaround: chunked response for iOS clients // sound byte subset $range = GetSoundByteRange(); $rangeStart = $range['start']; $rangeEnd = $range['end']; $rangeSize = $rangeEnd - $rangeStart + 1; // initial iOS 6.0.1 testing; leaving as fallback since we can't be sure it won't happen again: // we depend on observed behavior of invalid range requests to detect // end of sound playback, cleanup and tell AppleCoreMedia to stop requesting // invalid "bytes=rangeEnd-rangeEnd" ranges in an infinite(?) loop if ($rangeStart == $rangeEnd || $rangeEnd > $totalSize) { LBD_HttpHelper::BadRequest('invalid byte range'); } $rangeBytes = substr($soundBytes, $rangeStart, $rangeSize); // partial content response with the requested byte range header('HTTP/1.1 206 Partial Content'); header('Accept-Ranges: bytes'); header("Content-Length: {$rangeSize}"); header("Content-Range: bytes {$rangeStart}-{$rangeEnd}/{$totalSize}"); echo $rangeBytes; // chrome needs this kind of response to be able to replay Html5 audio } else { if (DetectFakeRangeRequest()) { header('Accept-Ranges: bytes'); header("Content-Length: {$totalSize}"); $end = $totalSize - 1; header("Content-Range: bytes 0-{$end}/{$totalSize}"); echo $soundBytes; } else { // regular sound request header('Accept-Ranges: none'); header("Content-Length: {$totalSize}"); echo $soundBytes; } } }