[ticket/17413] Make turnstile captcha work on registration page

PHPBB-17413
This commit is contained in:
Marc Alexander 2024-10-13 11:04:59 +02:00
parent 01dd0b168a
commit 8290cdb7e7
No known key found for this signature in database
GPG key ID: 50E0D2423696F995
7 changed files with 127 additions and 57 deletions

View file

@ -1,7 +1,7 @@
<dl> <dl>
<dt><div id="captcha_turnstile"></div></dt> <dt><div id="captcha_turnstile"></div></dt>
</dl> </dl>
{% INCLUDEJS 'https://challenges.cloudflare.com/turnstile/v0/api.js' %} {% INCLUDEJS U_TURNSTILE_SCRIPT %}
<script> <script>
function domReady(callBack) { function domReady(callBack) {
if (document.readyState === 'loading') { if (document.readyState === 'loading') {

View file

@ -37,9 +37,12 @@ if (empty($lang) || !is_array($lang))
// in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine // in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine
$lang = array_merge($lang, [ $lang = array_merge($lang, [
'CAPTCHA_TURNSTILE' => 'Turnstile', 'CAPTCHA_TURNSTILE' => 'Turnstile',
'CAPTCHA_TURNSTILE_SITEKEY' => 'Sitekey', 'CAPTCHA_TURNSTILE_INCORRECT' => 'The solution you provided was incorrect',
'CAPTCHA_TURNSTILE_SITEKEY_EXPLAIN' => 'Your Turnstile sitekey. The sitekey can be retrieved from your <a href="https://dash.cloudflare.com/?to=/:account/turnstile">Cloudflare dashboard</a>.', 'CAPTCHA_TURNSTILE_NOSCRIPT' => 'Please enable JavaScript in your browser to load the challenge.',
'CAPTCHA_TURNSTILE_SECRET' => 'Secret key', 'CAPTCHA_TURNSTILE_SECRET' => 'Secret key',
'CAPTCHA_TURNSTILE_SECRET_EXPLAIN' => 'Your Turnstile secret key. The secret key can be retrieved from your <a href="https://dash.cloudflare.com/?to=/:account/turnstile">Cloudflare dashboard</a>.', 'CAPTCHA_TURNSTILE_SECRET_EXPLAIN' => 'Your Turnstile secret key. The secret key can be retrieved from your <a href="https://dash.cloudflare.com/?to=/:account/turnstile">Cloudflare dashboard</a>.',
'CAPTCHA_TURNSTILE_SITEKEY' => 'Sitekey',
'CAPTCHA_TURNSTILE_SITEKEY_EXPLAIN' => 'Your Turnstile sitekey. The sitekey can be retrieved from your <a href="https://dash.cloudflare.com/?to=/:account/turnstile">Cloudflare dashboard</a>.',
'CAPTCHA_TURNSTILE_NOT_AVAILABLE' => 'In order to use Turnstile you must create a <a href="https://www.cloudflare.com/products/turnstile/">Cloudflare account</a>.',
]); ]);

View file

@ -19,6 +19,9 @@ abstract class base implements plugin_interface
/** @var string Confirm id hash */ /** @var string Confirm id hash */
protected string $confirm_id = ''; protected string $confirm_id = '';
/** @var string Last error message */
protected string $last_error = '';
/** /**
* Constructor for abstract captcha base class * Constructor for abstract captcha base class
* *
@ -29,6 +32,22 @@ abstract class base implements plugin_interface
$this->db = $db; $this->db = $db;
} }
/**
* {@inheritDoc}
*/
public function is_solved(): bool
{
return $this->solved;
}
/**
* {@inheritDoc}
*/
public function get_error(): string
{
return $this->last_error;
}
/** /**
* @inheritDoc * @inheritDoc
*/ */

View file

@ -118,6 +118,14 @@ class legacy_wrapper implements plugin_interface
return false; return false;
} }
/**
* {@inheritDoc}
*/
public function get_error(): string
{
return $this->last_error;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */

View file

@ -73,6 +73,13 @@ interface plugin_interface
*/ */
public function validate(): bool; public function validate(): bool;
/**
* Get error string from captcha
*
* @return string Error string, empty string if there is no error
*/
public function get_error(): string;
/** /**
* Return whether captcha was solved * Return whether captcha was solved
* *

View file

@ -13,6 +13,8 @@
namespace phpbb\captcha\plugins; namespace phpbb\captcha\plugins;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use phpbb\config\config; use phpbb\config\config;
use phpbb\db\driver\driver; use phpbb\db\driver\driver;
use phpbb\db\driver\driver_interface; use phpbb\db\driver\driver_interface;
@ -24,7 +26,11 @@ use phpbb\user;
class turnstile extends base class turnstile extends base
{ {
private const API_ENDPOINT = 'https://api.cloudflare.com/client/v4/captcha/validate'; /** @var string URL to cloudflare turnstile API javascript */
private const SCRIPT_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
/** @var string API endpoint for turnstile verification */
private const VERIFY_ENDPOINT = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
/** @var config */ /** @var config */
protected config $config; protected config $config;
@ -70,19 +76,28 @@ class turnstile extends base
$this->user = $user; $this->user = $user;
} }
/**
* {@inheritDoc}
*/
public function is_available(): bool public function is_available(): bool
{ {
$this->language->add_lang('captcha_turnstile'); $this->init(0);
return !empty($this->config->offsetGet('captcha_turnstile_sitekey')) return !empty($this->config->offsetGet('captcha_turnstile_sitekey'))
&& !empty($this->config->offsetGet('captcha_turnstile_secret')); && !empty($this->config->offsetGet('captcha_turnstile_secret'));
} }
/**
* {@inheritDoc}
*/
public function has_config(): bool public function has_config(): bool
{ {
return true; return true;
} }
/**
* {@inheritDoc}
*/
public function get_name(): string public function get_name(): string
{ {
return 'CAPTCHA_TURNSTILE'; return 'CAPTCHA_TURNSTILE';
@ -96,11 +111,17 @@ class turnstile extends base
$this->service_name = $name; $this->service_name = $name;
} }
/**
* {@inheritDoc}
*/
public function init(int $type): void public function init(int $type): void
{ {
$this->language->add_lang('captcha_turnstile'); $this->language->add_lang('captcha_turnstile');
} }
/**
* {@inheritDoc}
*/
public function get_hidden_fields(): array public function get_hidden_fields(): array
{ {
$hidden_fields = []; $hidden_fields = [];
@ -114,70 +135,55 @@ class turnstile extends base
return $hidden_fields; return $hidden_fields;
} }
/**
* {@inheritDoc}
*/
public function validate(): bool public function validate(): bool
{ {
// Implement server-side validation logic here // Retrieve form data for verification
// Example: Validate the submitted CAPTCHA value using Cloudflare API $form_data = [
'secret' => $this->config['captcha_turnstile_secret'],
// Your Cloudflare API credentials 'response' => $this->request->variable('cf-turnstile-response', ''),
$api_email = 'your_email@example.com'; 'remoteip' => $this->request->header('CF-Connecting-IP'),
$api_key = 'your_api_key'; //'idempotency_key' => $this->confirm_id, // check if we need this
// Cloudflare API endpoint for CAPTCHA verification
$endpoint = 'https://api.cloudflare.com/client/v4/captcha/validate';
// CAPTCHA data to be sent in the request
$data = [
'email' => $api_email,
'key' => $api_key,
'response' => $this->confirm_code
]; ];
// Initialize cURL session // Create guzzle client
$ch = curl_init(); $client = new Client();
// Set cURL options // Check captcha with turnstile API
curl_setopt_array($ch, [ try
CURLOPT_URL => $endpoint, {
CURLOPT_POST => true, $response = $client->request('POST', self::VERIFY_ENDPOINT, [
CURLOPT_POSTFIELDS => json_encode($data), 'form_params' => $form_data,
CURLOPT_HTTPHEADER => [ ]);
'Content-Type: application/json', }
'Accept: application/json' catch (GuzzleException)
], {
CURLOPT_RETURNTRANSFER => true // Something went wrong during the request to Cloudflare, assume captcha was bad
]); $this->solved = false;
// Execute the cURL request
$response = curl_exec($ch);
// Check for errors
if ($response === false) {
// Handle cURL error
curl_close($ch);
return false; return false;
} }
// Decode the JSON response // Decode the JSON response
$result = json_decode($response, true); $result = json_decode($response->getBody(), true);
// Check if the response indicates success // Check if the response indicates success
if (isset($result['success']) && $result['success'] === true) { if (isset($result['success']) && $result['success'] === true)
// CAPTCHA validation passed {
curl_close($ch); $this->solved = true;
return true; return true;
} else { }
// CAPTCHA validation failed else
curl_close($ch); {
$this->last_error = $this->language->lang('CAPTCHA_TURNSTILE_INCORRECT');
return false; return false;
} }
} }
public function is_solved(): bool /**
{ * {@inheritDoc}
return false; */
}
public function reset(): void public function reset(): void
{ {
// TODO: Implement reset() method. // TODO: Implement reset() method.
@ -191,11 +197,26 @@ class turnstile extends base
public function get_template(): string public function get_template(): string
{ {
return 'custom_captcha.html'; // Template file for displaying the CAPTCHA if ($this->is_solved())
{
return '';
}
$this->template->assign_vars([
'S_TURNSTILE_AVAILABLE' => $this->is_available(),
'TURNSTILE_SITEKEY' => $this->config->offsetGet('captcha_turnstile_sitekey'),
'U_TURNSTILE_SCRIPT' => self::SCRIPT_URL,
]);
return 'captcha_turnstile.html';
} }
public function get_demo_template(): string public function get_demo_template(): string
{ {
$this->template->assign_vars([
'U_TURNSTILE_SCRIPT' => self::SCRIPT_URL,
]);
return 'captcha_turnstile_acp_demo.html'; return 'captcha_turnstile_acp_demo.html';
} }

View file

@ -0,0 +1,12 @@
{% if S_TURNSTILE_AVAILABLE %}
<noscript>
<div>{{ lang('CAPTCHA_TURNSTILE_NOSCRIPT') }}</div>
</noscript>
<script src="{{ U_TURNSTILE_SCRIPT }}" async defer></script>
{# The cf-turnstile class is used in JavaScript #}
<div class="cf-turnstile" data-sitekey="{{ TURNSTILE_SITEKEY }}"></div>
{% else %}
{{ lang('CAPTCHA_TURNSTILE_NOT_AVAILABLE') }}
{% endif %}