'; } function captcha_widget($action = 'submit') { if (!captcha_enabled()) return ''; return '
'; } // The expected action comes from the page, never from the request function captcha_verify_response($action = 'submit') { if (!captcha_enabled()) return TRUE; $response = $_POST['g-recaptcha-response'] ?? ''; if (!is_string($response) || $response === '') return FALSE; $project = trim((string)config('RECAPTCHA_PROJECT_ID')); $api_key = trim((string)config('RECAPTCHA_API_KEY')); $host = strtolower(site_host()); if ($project === '' || $api_key === '' || $host === '') { error_log('xxx: reCAPTCHA needs RECAPTCHA_PROJECT_ID, RECAPTCHA_API_KEY and a SITE_URL with a host'); return FALSE; } if (preg_match('/[\r\n]/', $api_key)) { error_log('xxx: RECAPTCHA_API_KEY contains a line break'); return FALSE; } $body = json_encode(array('event' => array( 'token' => $response, 'siteKey' => (string)config('RECAPTCHA_SITE_KEY'), 'expectedAction' => $action, 'userIpAddress' => $_SERVER['REMOTE_ADDR'] ?? '', ))); if ($body === FALSE) return FALSE; // Keep the API key out of URLs and logs, and do not forward it on redirects $context = stream_context_create(array('http' => array( 'method' => 'POST', 'header' => "Content-Type: application/json\r\nX-Goog-Api-Key: $api_key\r\n", 'content' => $body, 'timeout' => 10, 'follow_location' => 0, 'ignore_errors' => FALSE, ))); $url = 'https://recaptchaenterprise.googleapis.com/v1/projects/'.rawurlencode($project).'/assessments'; $reply = @file_get_contents($url, FALSE, $context); if ($reply === FALSE) { error_log('xxx: reCAPTCHA assessment request failed'); return FALSE; } $result = json_decode($reply); if (!is_object($result) || isset($result->error) || !is_object($result->tokenProperties ?? NULL)) { error_log('xxx: reCAPTCHA returned an invalid assessment or an API error'); return FALSE; } $token = $result->tokenProperties; if (($token->valid ?? FALSE) !== TRUE) return FALSE; if (!is_string($token->hostname ?? NULL) || strtolower($token->hostname) !== $host) { error_log('xxx: reCAPTCHA hostname did not match SITE_URL'); return FALSE; } if (($token->action ?? NULL) !== $action) { error_log('xxx: reCAPTCHA action did not match the page'); return FALSE; } // Keep checkbox behavior; do not reject visitors by a risk score return TRUE; } /* * For story submissions: a solved captcha counts for CAPTCHA_PASS_SECONDS * in this session, so the preview and the submit need only one captcha. * captcha_used() ends the pass once a story is saved. */ function captcha_passed() { return !empty($_SESSION['captcha_ok']) && $_SESSION['captcha_ok'] > time() - CAPTCHA_PASS_SECONDS; } function captcha_check() { if (captcha_passed()) return TRUE; if (captcha_verify_response()) { $_SESSION['captcha_ok'] = time(); return TRUE; } return FALSE; } function captcha_used() { unset($_SESSION['captcha_ok']); } ?>