115 lines
3.9 KiB
PHP
115 lines
3.9 KiB
PHP
<?php
|
|
/*
|
|
* Google reCAPTCHA Enterprise, with a checkbox key. Configuration comes
|
|
* from RECAPTCHA_SITE_KEY, RECAPTCHA_PROJECT_ID and RECAPTCHA_API_KEY.
|
|
* SITE_URL supplies the hostname that a response must match.
|
|
*
|
|
* Without RECAPTCHA_SITE_KEY the captcha is off. With a site key, missing
|
|
* configuration or a failed assessment blocks the request.
|
|
*/
|
|
require_once(__DIR__ . '/config.lib.php');
|
|
require_once(__DIR__ . '/html.lib.php');
|
|
|
|
define('CAPTCHA_PASS_SECONDS', 30 * 60);
|
|
|
|
function captcha_enabled() {
|
|
return (string)config('RECAPTCHA_SITE_KEY') !== '';
|
|
}
|
|
|
|
function captcha_script() {
|
|
if (!captcha_enabled())
|
|
return '';
|
|
return '<script src="https://www.google.com/recaptcha/enterprise.js" async defer></script>';
|
|
}
|
|
|
|
function captcha_widget($action = 'submit') {
|
|
if (!captcha_enabled())
|
|
return '';
|
|
return '<div class="g-recaptcha" data-sitekey="'.form_value(config('RECAPTCHA_SITE_KEY')).'" data-action="'.form_value($action).'"></div>';
|
|
}
|
|
|
|
// 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']);
|
|
}
|
|
?>
|