c7f1bdd630
register.php was a fully open signup form with no bot defenses — the likely source of the unconfirmed accounts piling up in admin/users.php. - Add an always-on honeypot field + timing trap to register.php: either tripping silently pretends success without creating an account, so a bot doesn't learn it was caught. No configuration needed. - Add optional Google reCAPTCHA v3 support (includes/recaptcha.php, recaptcha_enabled()/verify_recaptcha(), no Composer dependency — a raw file_get_contents() POST like mailer.php's SMTP socket approach). A failed check here shows a real, visible error instead of the silent honeypot path, since a legitimate low-score user deserves a retry. - Configure it through admin/settings.php's new "Bot Protection" section, mirroring the existing SMTP pattern exactly: recaptcha_enabled/ recaptcha_site_key/recaptcha_secret_key in site_settings, secret key masked the same way smtp_pass now is (blank submission keeps it unchanged). install.php seeds sane defaults so the feature stays off until explicitly configured — fully backward compatible. - Add cron/cleanup_unconfirmed.php: deletes accounts still unconfirmed after 3 days. CLI-only (refuses to run over HTTP, and cron/.htaccess denies web access to the directory as a second layer) since it's an unattended, irreversible deletion. Safe by construction — login.php already refuses login to unconfirmed accounts, so these rows can never own a session/novena_group/custom_prayer row. Not wired up automatically; README documents the Hostinger cron job to schedule it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
49 lines
1.6 KiB
PHP
49 lines
1.6 KiB
PHP
<?php
|
|
/**
|
|
* includes/recaptcha.php — Google reCAPTCHA v3 verification.
|
|
* No Composer dependency: uses file_get_contents() with a POST stream context,
|
|
* matching the no-dependency style of includes/mailer.php.
|
|
*/
|
|
|
|
/** True if reCAPTCHA is turned on and a site key is configured. */
|
|
function recaptcha_enabled(): bool {
|
|
return get_setting('recaptcha_enabled', '0') === '1' && get_setting('recaptcha_site_key') !== '';
|
|
}
|
|
|
|
/**
|
|
* Verify a reCAPTCHA v3 token against Google's siteverify endpoint.
|
|
* Returns true only if the request succeeded and the score meets $min_score.
|
|
*/
|
|
function verify_recaptcha(string $token, float $min_score = 0.5): bool {
|
|
$secret = get_setting('recaptcha_secret_key');
|
|
if ($secret === '' || $token === '') return false;
|
|
|
|
$post_data = http_build_query([
|
|
'secret' => $secret,
|
|
'response' => $token,
|
|
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
|
|
]);
|
|
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'POST',
|
|
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
|
|
'content' => $post_data,
|
|
'timeout' => 10,
|
|
],
|
|
]);
|
|
|
|
try {
|
|
$response = @file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context);
|
|
if ($response === false) return false;
|
|
|
|
$result = json_decode($response, true);
|
|
if (!is_array($result)) return false;
|
|
|
|
return !empty($result['success']) && (float)($result['score'] ?? 0) >= $min_score;
|
|
} catch (Throwable $e) {
|
|
error_log('reCAPTCHA verify error: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|