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:
@@ -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']);
|
||||
}
|
||||
?>
|
||||
+1
-1
@@ -19,7 +19,7 @@ require_once("../session.lib.php");
|
||||
<link rel="alternate" type="application/rss+xml" title="xxx.org.za RSS feed" href="/feed" />
|
||||
<?php include_once('tinymce_init.inc.php'); ?>
|
||||
<?php if (!$is_admin ) { ?>
|
||||
<script src="https://www.google.com/recaptcha/api.js"></script>
|
||||
<?= captcha_script() ?>
|
||||
<?php } ?>
|
||||
</head>
|
||||
<?php
|
||||
|
||||
@@ -6,6 +6,7 @@ if ( ! isset($_SERVER['HTTPS'])) {
|
||||
require_once("../session.lib.php");
|
||||
$denied = NULL;
|
||||
$wait_minutes = 0;
|
||||
$captcha_failed = FALSE;
|
||||
$is_admin = NULL;
|
||||
if (isset($_SESSION['editor'])) {
|
||||
session_write_close();
|
||||
@@ -16,6 +17,9 @@ if (isset($_SESSION['editor'])) {
|
||||
} else if (isset($_POST['login']) && ($wait = login_blocked_for((string)($_POST['username'] ?? ''))) > 0) {
|
||||
$denied = TRUE;
|
||||
$wait_minutes = (int)ceil($wait / 60);
|
||||
} else if (isset($_POST['login']) && !captcha_verify_response()) {
|
||||
$denied = TRUE;
|
||||
$captcha_failed = TRUE;
|
||||
} else if (isset($_POST['login'])) {
|
||||
include_once('../open_db.lib.php');
|
||||
$table = "editors";
|
||||
@@ -76,6 +80,7 @@ if (isset($_SESSION['editor'])) {
|
||||
<link rel='stylesheet' type='text/css' href='../publish/style.css' />
|
||||
<script type="text/javascript" src="../utilities.js"></script>
|
||||
<link rel="alternate" type="application/rss+xml" title="xxx.org.za RSS feed" href="/feed" />
|
||||
<?= captcha_script() ?>
|
||||
</head>
|
||||
<?php flush(); ?>
|
||||
<body>
|
||||
@@ -89,6 +94,8 @@ if (isset($_SESSION['editor'])) {
|
||||
<?php
|
||||
if ($wait_minutes)
|
||||
echo "<p><strong>Too many failed logins. Please try again in $wait_minutes minute".($wait_minutes == 1 ? "" : "s").".</strong></p>\n";
|
||||
else if ($captcha_failed)
|
||||
echo "<p><strong>Please complete the captcha.</strong></p>\n";
|
||||
else if ($denied)
|
||||
echo "<p><strong>Access denied. Please try again.</strong></p>\n";
|
||||
?>
|
||||
@@ -112,6 +119,8 @@ else if ($denied)
|
||||
<input type="checkbox" id="remember" name="remember" checked />
|
||||
</div>
|
||||
|
||||
<?= captcha_widget() ?>
|
||||
|
||||
<div class="leftcolumn"></div>
|
||||
<div class="submission">
|
||||
<button type="submit" id="login" name="login" value="1">Login</button>
|
||||
|
||||
@@ -115,6 +115,10 @@ function save_data($id = NULL) {
|
||||
if (!$is_admin && $action == 'submit')
|
||||
$_SESSION['allow_queue_image'] = $id;
|
||||
|
||||
// A visitor's next story needs a new captcha
|
||||
if (!$is_admin)
|
||||
captcha_used();
|
||||
|
||||
// Prune filesystem
|
||||
if ($image_file)
|
||||
prune_images($image_file);
|
||||
@@ -189,7 +193,7 @@ function prune_images($image_file) {
|
||||
*/
|
||||
$validate = array();
|
||||
function validate_data() {
|
||||
global $validate;
|
||||
global $validate, $is_admin;
|
||||
// extract POST variables
|
||||
$result = get_post_data(TRUE);
|
||||
// assign their keys and values to the symbol table
|
||||
@@ -197,6 +201,10 @@ function validate_data() {
|
||||
$upload_error = $_FILES['upload']['error'] ?? UPLOAD_ERR_NO_FILE;
|
||||
$upload_tmp = $_FILES['upload']['tmp_name'] ?? '';
|
||||
|
||||
// Visitors must solve the captcha before a preview or a save
|
||||
if (!$is_admin && !captcha_check()) {
|
||||
$validate['captcha'] = 'Please complete the captcha.';
|
||||
}
|
||||
|
||||
if (!$author) {
|
||||
$validate['author'] = "You have not submitted an author.";
|
||||
|
||||
@@ -2,25 +2,6 @@
|
||||
$result = get_post_data(TRUE);
|
||||
extract($result[0], EXTR_OVERWRITE);
|
||||
|
||||
if (isset($_POST['g-recaptcha-response'])) {
|
||||
$captcha = $_POST['g-recaptcha-response'];
|
||||
if (! $captcha) {
|
||||
$validate['captcha'] = 'Invalid captcha. Please try again.';
|
||||
include('./write.inc.php');
|
||||
return;
|
||||
}
|
||||
$captcha_resp = file_get_contents("https://www.google.com/recaptcha/api/siteverify?secret=".urlencode(config('RECAPTCHA_SECRET'))."&response=".
|
||||
$captcha."&remoteip=".$_SERVER['REMOTE_ADDR']);
|
||||
var_dump($captcha_resp);
|
||||
// PHP 8 treats the bare word "success" as an undefined constant, so decode the JSON reply
|
||||
$captcha_json = json_decode((string)$captcha_resp);
|
||||
if (empty($captcha_json->success)) {
|
||||
$validate['captcha'] = 'The words you typed in the captcha were not correct. Please try again.';
|
||||
include('./write.inc.php');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
echo '<p>Please review your submission. Once you\'ve finished, please click "Submit" at the bottom of the page, or click "Preview" to check it again.</p>';
|
||||
|
||||
// Keep a new image outside the web root until the story is saved;
|
||||
|
||||
+2
-2
@@ -148,8 +148,8 @@ ul.error-box li {
|
||||
}
|
||||
|
||||
#main .grid div.g-recaptcha {
|
||||
overflow: inherit;
|
||||
margin-left: 120px;
|
||||
grid-column: 2;
|
||||
margin: 5px 0;
|
||||
}
|
||||
#captcha #recaptcha_image {
|
||||
margin-left: 120px;
|
||||
|
||||
@@ -103,8 +103,9 @@ if (isset($file_src) || isset($img)) {
|
||||
</div>
|
||||
|
||||
<?php
|
||||
if (!$is_admin && (($action == "") || isset($validate['captcha']))) {
|
||||
echo '<div class="g-recaptcha" data-sitekey="'.htmlspecialchars(config('RECAPTCHA_SITE_KEY')).'"></div>';
|
||||
// Visitors solve the captcha once; the session remembers it until the story is saved
|
||||
if (!$is_admin && !captcha_passed()) {
|
||||
echo captcha_widget();
|
||||
}
|
||||
|
||||
if ($is_admin) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
require_once(realpath(dirname(__FILE__) . '/open_db.lib.php'));
|
||||
require_once(__DIR__ . '/html.lib.php');
|
||||
require_once(__DIR__ . '/captcha.lib.php');
|
||||
|
||||
$is_admin = FALSE;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user