Require a captcha for visitor submissions and for login.

Without the form field, the check was skipped. A solved captcha lasts
30 minutes in the session.
This commit is contained in:
tim
2026-09-25 21:54:14 +02:00
parent 1698680e24
commit 1468cf7138
8 changed files with 97 additions and 25 deletions
+72
View File
@@ -0,0 +1,72 @@
<?php
/*
* Google reCAPTCHA v2. The keys come from RECAPTCHA_SITE_KEY and
* RECAPTCHA_SECRET in .env. When the secret is missing or Google cannot
* be reached, every check fails (and the reason is logged), so a broken
* configuration never turns the captcha off.
*/
require_once(__DIR__ . '/config.lib.php');
require_once(__DIR__ . '/html.lib.php');
define('CAPTCHA_PASS_SECONDS', 30 * 60);
function captcha_script() {
return '<script src="https://www.google.com/recaptcha/api.js" async defer></script>';
}
function captcha_widget() {
return '<div class="g-recaptcha" data-sitekey="'.form_value(config('RECAPTCHA_SITE_KEY')).'"></div>';
}
// Ask Google whether the captcha in this POST was solved
function captcha_verify_response() {
$response = $_POST['g-recaptcha-response'] ?? '';
if (!is_string($response) || $response === '')
return FALSE;
$secret = (string)config('RECAPTCHA_SECRET');
if ($secret === '') {
error_log('xxx: RECAPTCHA_SECRET is not set, so every captcha check fails');
return FALSE;
}
$context = stream_context_create(array('http' => array(
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'content' => http_build_query(array(
'secret' => $secret,
'response' => $response,
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
)),
'timeout' => 10,
)));
$reply = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', FALSE, $context);
if ($reply === FALSE) {
error_log('xxx: could not reach the reCAPTCHA server');
return FALSE;
}
$result = json_decode($reply);
return is_object($result) && !empty($result->success);
}
/*
* 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']);
}
?>