Merge pull request #6737 from marc1706/feature/captcha_v2

[feature/captcha_v2] Refactor captcha classes and implement turnstile captcha
This commit is contained in:
Marc Alexander 2024-10-23 20:25:45 +02:00 committed by GitHub
commit 4985a27dcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 2260 additions and 259 deletions

View file

@ -1,79 +1,85 @@
<!-- INCLUDE overall_header.html -->
{% include 'overall_header.html' %}
<a id="maincontent"></a>
<h1>{L_ACP_VC_SETTINGS}</h1>
<h1>{{ lang('ACP_VC_SETTINGS') }}</h1>
<p>{L_ACP_VC_SETTINGS_EXPLAIN}</p>
<p>{{ lang('ACP_VC_SETTINGS_EXPLAIN') }}</p>
<p>{L_ACP_VC_EXT_GET_MORE}</p>
<p>{{ lang('ACP_VC_EXT_GET_MORE') }}</p>
<!-- IF ERROR_MSG -->
{% if ERRORS %}
<div class="errorbox">
<h3>{L_WARNING}</h3>
<p>{ERROR_MSG}</p>
<h3>{{ lang('WARNING') }}</h3>
<p>{{ ERRORS|join('<br>') }}</p>
</div>
<!-- ENDIF -->
{% endif %}
<form id="acp_captcha" method="post" action="{U_ACTION}">
<form id="acp_captcha" method="post" action="{{ U_ACTION }}">
<fieldset>
<legend>{L_GENERAL_OPTIONS}</legend>
<legend>{{ lang('GENERAL_OPTIONS') }}</legend>
<dl>
<dt><label for="enable_confirm">{L_VISUAL_CONFIRM_REG}{L_COLON}</label><br /><span>{L_VISUAL_CONFIRM_REG_EXPLAIN}</span></dt>
<dd><label><input type="radio" class="radio" id="enable_confirm" name="enable_confirm" value="1"<!-- IF REG_ENABLE --> checked="checked"<!-- ENDIF --> /> {L_ENABLED}</label>
<label><input type="radio" class="radio" name="enable_confirm" value="0"<!-- IF not REG_ENABLE --> checked="checked"<!-- ENDIF --> /> {L_DISABLED}</label></dd>
<dt><label for="enable_confirm">{{ lang('VISUAL_CONFIRM_REG') ~ lang('COLON') }}</label><br /><span>{{ lang('VISUAL_CONFIRM_REG_EXPLAIN') }}</span></dt>
<dd>
<label><input type="radio" class="radio" id="enable_confirm" name="enable_confirm" value="1"{% if REG_ENABLE %} checked="checked"{% endif %}/> {{ lang('ENABLED') }}</label>
<label><input type="radio" class="radio" name="enable_confirm" value="0"{% if not REG_ENABLE %} checked="checked"{% endif %}/> {{ lang('DISABLED') }}</label>
</dd>
</dl>
<dl>
<dt><label for="max_reg_attempts">{L_REG_LIMIT}{L_COLON}</label><br /><span>{L_REG_LIMIT_EXPLAIN}</span></dt>
<dd><input id="max_reg_attempts" type="number" min="0" max="9999" name="max_reg_attempts" value="{REG_LIMIT}" /></dd>
<dt><label for="max_reg_attempts">{{ lang('REG_LIMIT') ~ lang('COLON') }}</label><br /><span>{{ lang('REG_LIMIT_EXPLAIN') }}</span></dt>
<dd><input id="max_reg_attempts" type="number" min="0" max="9999" name="max_reg_attempts" value="{{ REG_LIMIT }}" /></dd>
</dl>
<dl>
<dt><label for="max_login_attempts">{L_MAX_LOGIN_ATTEMPTS}{L_COLON}</label><br /><span>{L_MAX_LOGIN_ATTEMPTS_EXPLAIN}</span></dt>
<dd><input id="max_login_attempts" type="number" min="0" max="9999" name="max_login_attempts" value="{MAX_LOGIN_ATTEMPTS}" /></dd>
<dt><label for="max_login_attempts">{{ lang('MAX_LOGIN_ATTEMPTS') ~ lang('COLON') }}</label><br /><span>{{ lang('MAX_LOGIN_ATTEMPTS_EXPLAIN') }}</span></dt>
<dd><input id="max_login_attempts" type="number" min="0" max="9999" name="max_login_attempts" value="{{ MAX_LOGIN_ATTEMPTS }}" /></dd>
</dl>
<dl>
<dt><label for="enable_post_confirm">{L_VISUAL_CONFIRM_POST}{L_COLON}</label><br /><span>{L_VISUAL_CONFIRM_POST_EXPLAIN}</span></dt>
<dd><label><input type="radio" class="radio" id="enable_post_confirm" name="enable_post_confirm" value="1"<!-- IF POST_ENABLE --> checked="checked"<!-- ENDIF --> /> {L_ENABLED}</label>
<label><input type="radio" class="radio" name="enable_post_confirm" value="0"<!-- IF not POST_ENABLE --> checked="checked"<!-- ENDIF --> /> {L_DISABLED}</label></dd>
<dt><label for="enable_post_confirm">{{ lang('VISUAL_CONFIRM_POST') ~ lang('COLON') }}</label><br /><span>{{ lang('VISUAL_CONFIRM_POST_EXPLAIN') }}</span></dt>
<dd>
<label><input type="radio" class="radio" id="enable_post_confirm" name="enable_post_confirm" value="1"{% if POST_ENABLE %} checked="checked"{% endif %}/> {{ lang('ENABLED') }}</label>
<label><input type="radio" class="radio" name="enable_post_confirm" value="0"{% if not POST_ENABLE %} checked="checked"{% endif %}/> {{ lang('DISABLED') }}</label>
</dd>
</dl>
<dl>
<dt><label for="confirm_refresh">{L_VISUAL_CONFIRM_REFRESH}{L_COLON}</label><br /><span>{L_VISUAL_CONFIRM_REFRESH_EXPLAIN}</span></dt>
<dd><label><input type="radio" class="radio" id="confirm_refresh" name="confirm_refresh" value="1"<!-- IF CONFIRM_REFRESH --> checked="checked"<!-- ENDIF --> /> {L_ENABLED}</label>
<label><input type="radio" class="radio" name="confirm_refresh" value="0"<!-- IF not CONFIRM_REFRESH --> checked="checked"<!-- ENDIF --> /> {L_DISABLED}</label></dd>
<dt><label for="confirm_refresh">{{ lang('VISUAL_CONFIRM_REFRESH') ~ lang('COLON') }}</label><br /><span>{{ lang('VISUAL_CONFIRM_REFRESH_EXPLAIN') }}</span></dt>
<dd>
<label><input type="radio" class="radio" id="confirm_refresh" name="confirm_refresh" value="1"{% if CONFIRM_REFRESH %} checked="checked"{% endif %}/> {{ lang('ENABLED') }}</label>
<label><input type="radio" class="radio" name="confirm_refresh" value="0"{% if not CONFIRM_REFRESH %} checked="checked"{% endif %}/> {{ lang('DISABLED') }}</label>
</dd>
</dl>
</fieldset>
<fieldset>
<legend>{L_AVAILABLE_CAPTCHAS}</legend>
<dl>
<dt><label for="captcha_select">{L_CAPTCHA_SELECT}{L_COLON}</label><br /><span>{L_CAPTCHA_SELECT_EXPLAIN}</span></dt>
<dd><select id="captcha_select" name="select_captcha" onchange="(document.getElementById('acp_captcha')).submit()" >{CAPTCHA_SELECT}</select></dd>
</dl>
<!-- IF S_CAPTCHA_HAS_CONFIG -->
<dl>
<dt><label for="configure">{L_CAPTCHA_CONFIGURE}{L_COLON}</label><br /><span>{L_CAPTCHA_CONFIGURE_EXPLAIN}</span></dt>
<dd><input class="button2" type="submit" id="configure" name="configure" value="{L_CONFIGURE}" /></dd>
</dl>
<!-- ENDIF -->
<legend>{{ lang('AVAILABLE_CAPTCHAS') }}</legend>
<dl>
<dt><label for="captcha_select">{{ lang('CAPTCHA_SELECT') ~ lang('COLON') }}</label><br><span>{{ lang('CAPTCHA_SELECT_EXPLAIN') }}</span></dt>
<dd>{{ FormsSelect(CAPTCHA_SELECT | merge({id: 'captcha_select', onchange: "(document.getElementById('acp_captcha')).submit()"})) }}</dd>
</dl>
{% if S_CAPTCHA_HAS_CONFIG %}
<dl>
<dt><label for="configure">{{ lang('CAPTCHA_CONFIGURE') ~ lang('COLON') }}</label><br /><span>{{ lang('CAPTCHA_CONFIGURE_EXPLAIN') }}</span></dt>
<dd><input class="button2" type="submit" id="configure" name="configure" value="{{ lang('CONFIGURE') }}" /></dd>
</dl>
{% endif %}
</fieldset>
<!-- IF CAPTCHA_PREVIEW_TPL -->
{% if CAPTCHA_PREVIEW_TPL %}
<fieldset>
<legend>{L_PREVIEW}</legend>
<!-- INCLUDE {CAPTCHA_PREVIEW_TPL} -->
<legend>{{ lang('PREVIEW') }}</legend>
{% include CAPTCHA_PREVIEW_TPL %}
</fieldset>
<!-- ENDIF -->
{% endif %}
<fieldset>
<legend>{L_ACP_SUBMIT_CHANGES}</legend>
<legend>{{ lang('ACP_SUBMIT_CHANGES') }}</legend>
<p class="submit-buttons">
<input class="button1" type="submit" id="main_submit" name="main_submit" value="{L_SUBMIT}" />&nbsp;
<input class="button2" type="reset" id="form_reset" name="reset" value="{L_RESET}" />&nbsp;
<input class="button1" type="submit" id="main_submit" name="main_submit" value="{{ lang('SUBMIT') }}" />&nbsp;
<input class="button2" type="reset" id="form_reset" name="reset" value="{{ lang('RESET') }}" />&nbsp;
</p>
{S_FORM_TOKEN}
{{ S_FORM_TOKEN }}
</fieldset>
</form>
<!-- INCLUDE overall_footer.html -->
{% include 'overall_footer.html' %}

View file

@ -18,11 +18,13 @@
<tr>
<th colspan="3">{L_QUESTIONS}</th>
</tr>
{% if questions %}
<tr class="row3">
<td style="text-align: center;">{L_QUESTION_TEXT}</td>
<td style="width: 5%; text-align: center;">{L_QUESTION_LANG}</td>
<td style="vertical-align: top; width: 50px; text-align: center; white-space: nowrap;">{L_ACTION}</td>
</tr>
{% endif %}
</thead>
<tbody>
@ -33,6 +35,10 @@
<td style="text-align: center;">{{ question.QUESTION_LANG }}</td>
<td style="text-align: center;"><a href="{{ question.U_EDIT }}">{{ ICON_EDIT }}</a> <a href="{{ question.U_DELETE }}">{{ ICON_DELETE }}</a></td>
</tr>
{% else %}
<tr>
<td class="row3 centered-text" colspan="3">{{ lang('QA_NO_QUESTIONS') }}</td>
</tr>
{% endfor %}
</tbody>

View file

@ -0,0 +1,64 @@
{% include('overall_header.html') %}
<a id="maincontent"></a>
<h1>{{ lang('ACP_VC_SETTINGS') }}</h1>
<p>{{ lang('ACP_VC_SETTINGS_EXPLAIN') }}</p>
<form id="acp_captcha" method="post" action="{{ U_ACTION }}">
<fieldset>
<legend>{{ lang('GENERAL_OPTIONS') }}</legend>
<dl>
<dt>
<label for="captcha_turnstile_sitekey">{{ lang('CAPTCHA_TURNSTILE_SITEKEY') ~ lang('COLON') }}</label><br>
<span>{{ lang('CAPTCHA_TURNSTILE_SITEKEY_EXPLAIN') }}</span>
</dt>
<dd><input id="captcha_turnstile_sitekey" name="captcha_turnstile_sitekey" value="{{ CAPTCHA_TURNSTILE_SITEKEY }}" size="50" type="text" /></dd>
</dl>
<dl>
<dt>
<label for="captcha_turnstile_secret">{{ lang('CAPTCHA_TURNSTILE_SECRET') ~ lang('COLON') }}</label><br>
<span>{{ lang('CAPTCHA_TURNSTILE_SECRET_EXPLAIN') }}</span>
</dt>
<dd><input id="captcha_turnstile_secret" name="captcha_turnstile_secret" value="{{ CAPTCHA_TURNSTILE_SECRET }}" size="50" type="text" /></dd>
</dl>
<dl>
<dt>
<label>{{ lang('CAPTCHA_TURNSTILE_THEME') ~ lang('COLON') }}</label>
<br><span>{{ lang('CAPTCHA_TURNSTILE_THEME_EXPLAIN') }}</span>
</dt>
<dd>
{% for theme in CAPTCHA_TURNSTILE_THEMES %}
<label>
<input class="radio" name="captcha_turnstile_theme" type="radio" value="{{ theme }}"{{ theme == CAPTCHA_TURNSTILE_THEME ? ' checked' }}>
<span>{{ lang('CAPTCHA_TURNSTILE_THEME_' ~ theme|upper) }}</span>
</label>
{% endfor %}
</dd>
</dl>
</fieldset>
<fieldset>
<legend>{{ lang('PREVIEW') }}</legend>
{% if PREVIEW %}
<div class="successbox">
<h3>{{ lang('WARNING') }}</h3>
<p>{{ lang('CAPTCHA_PREVIEW_MSG') }}</p>
</div>
{% endif %}
{% include(CAPTCHA_PREVIEW) %}
</fieldset>
<fieldset>
<legend>{{ lang('ACP_SUBMIT_CHANGES') }}</legend>
<p class="submit-buttons">
<input class="button1" type="submit" id="submit" name="submit" value="{{ lang('SUBMIT') }}" />&nbsp;
<input class="button2" type="reset" id="reset" name="reset" value="{{ lang('RESET') }}" />&nbsp;
</p>
<input type="hidden" name="select_captcha" value="{{ CAPTCHA_NAME }}" />
<input type="hidden" name="configure" value="1" />
{{ S_FORM_TOKEN }}
</fieldset>
</form>
{% include('overall_footer.html') %}

View file

@ -0,0 +1,23 @@
<dl>
<dt><div id="captcha_turnstile" data-language="{{ lang('TURNSTILE_LANG') }}"{% if TURNSTILE_THEME %} data-theme="{{ TURNSTILE_THEME }}"{% endif %}></div></dt>
</dl>
{% INCLUDEJS U_TURNSTILE_SCRIPT %}
<script>
function domReady(callBack) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callBack);
} else {
callBack();
}
}
domReady(() => {
/* global turnstile */
console.debug('_turnstileCb called');
turnstile.render('#captcha_turnstile', {
sitekey: '1x00000000000000000000AA',
theme: 'light',
});
});
</script>

View file

@ -19,7 +19,11 @@ services:
shared: false
arguments:
- '@config'
- '@dbal.conn'
- '@language'
- '@request'
- '@template'
- '@user'
- '%core.root_path%'
- '%core.php_ext%'
calls:
@ -54,3 +58,19 @@ services:
- ['set_name', ['core.captcha.plugins.recaptcha_v3']]
tags:
- { name: captcha.plugins }
core.captcha.plugins.turnstile:
class: phpbb\captcha\plugins\turnstile
shared: false
arguments:
- '@config'
- '@dbal.conn'
- '@language'
- '@log'
- '@request'
- '@template'
- '@user'
calls:
- ['set_name', ['core.captcha.plugins.turnstile']]
tags:
- { name: captcha.plugins }

View file

@ -95,7 +95,7 @@ class acp_captcha
add_form_key($form_key);
$submit = $request->variable('main_submit', false);
$error = $cfg_array = array();
$errors = $cfg_array = array();
if ($submit)
{
@ -103,13 +103,13 @@ class acp_captcha
{
$cfg_array[$config_var] = $request->variable($config_var, $options['default']);
}
validate_config_vars($config_vars, $cfg_array, $error);
validate_config_vars($config_vars, $cfg_array, $errors);
if (!check_form_key($form_key))
{
$error[] = $user->lang['FORM_INVALID'];
$errors[] = $user->lang['FORM_INVALID'];
}
if ($error)
if ($errors)
{
$submit = false;
}
@ -128,11 +128,9 @@ class acp_captcha
if (isset($captchas['available'][$selected]))
{
$old_captcha = $factory->get_instance($config['captcha_plugin']);
$old_captcha->uninstall();
$old_captcha->garbage_collect();
$config->set('captcha_plugin', $selected);
$new_captcha = $factory->get_instance($config['captcha_plugin']);
$new_captcha->install();
$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_CONFIG_VISUAL');
}
@ -145,17 +143,24 @@ class acp_captcha
}
else
{
$captcha_select = '';
$captcha_options = [];
foreach ($captchas['available'] as $value => $title)
{
$current = ($selected !== false && $value == $selected) ? ' selected="selected"' : '';
$captcha_select .= '<option value="' . $value . '"' . $current . '>' . $user->lang($title) . '</option>';
$captcha_options[] = [
'value' => $value,
'label' => $user->lang($title),
'selected' => $selected !== false && $value == $selected,
];
}
foreach ($captchas['unavailable'] as $value => $title)
{
$current = ($selected !== false && $value == $selected) ? ' selected="selected"' : '';
$captcha_select .= '<option value="' . $value . '"' . $current . ' class="disabled-option">' . $user->lang($title) . '</option>';
$captcha_options[] = [
'value' => $value,
'label' => $user->lang($title),
'selected' => $selected !== false && $value == $selected,
'class' => 'disabled-option',
];
}
$demo_captcha = $factory->get_instance($selected);
@ -168,8 +173,12 @@ class acp_captcha
$template->assign_vars(array(
'CAPTCHA_PREVIEW_TPL' => $demo_captcha->get_demo_template($id),
'S_CAPTCHA_HAS_CONFIG' => $demo_captcha->has_config(),
'CAPTCHA_SELECT' => $captcha_select,
'ERROR_MSG' => implode('<br />', $error),
'CAPTCHA_SELECT' => [
'tag' => 'select',
'name' => 'select_captcha',
'options' => $captcha_options,
],
'ERRORS' => $errors,
'U_ACTION' => $this->u_action,
));

View file

@ -152,9 +152,13 @@ define('FULL_FOLDER_DELETE', -2);
define('FULL_FOLDER_HOLD', -1);
// Confirm types
/** @deprecated 4.0.0-a1 Replaced by \phpbb\captcha\plugins\confirm_type::REGISTRATION, to be removed in 5.0.0-a1 */
define('CONFIRM_REG', 1);
/** @deprecated 4.0.0-a1 Replaced by \phpbb\captcha\plugins\confirm_type::LOGIN, to be removed in 5.0.0-a1 */
define('CONFIRM_LOGIN', 2);
/** @deprecated 4.0.0-a1 Replaced by \phpbb\captcha\plugins\confirm_type::POST, to be removed in 5.0.0-a1 */
define('CONFIRM_POST', 3);
/** @deprecated 4.0.0-a1 Replaced by \phpbb\captcha\plugins\confirm_type::REPORT, to be removed in 5.0.0-a1 */
define('CONFIRM_REPORT', 4);
// Categories - Attachments

View file

@ -203,8 +203,8 @@ function adm_page_footer($copyright_html = true)
*/
function adm_back_link($u_action)
{
global $user;
return '<br /><br /><a href="' . $u_action . '">&laquo; ' . $user->lang['BACK_TO_PREV'] . '</a>';
global $language;
return '<br /><br /><a href="' . $u_action . '">&laquo; ' . $language->lang('BACK_TO_PREV') . '</a>';
}
/**

View file

@ -235,8 +235,10 @@ class ucp_register
// The CAPTCHA kicks in here. We can't help that the information gets lost on language change.
if ($config['enable_confirm'])
{
$captcha = $phpbb_container->get('captcha.factory')->get_instance($config['captcha_plugin']);
$captcha->init(CONFIRM_REG);
/** @var \phpbb\captcha\factory $captcha_factory */
$captcha_factory = $phpbb_container->get('captcha.factory');
$captcha = $captcha_factory->get_instance($config['captcha_plugin']);
$captcha->init(\phpbb\captcha\plugins\confirm_type::REGISTRATION);
}
$timezone = $config['board_timezone'];
@ -291,10 +293,9 @@ class ucp_register
if ($config['enable_confirm'])
{
$vc_response = $captcha->validate($data);
if ($vc_response !== false)
if ($captcha->validate() !== true)
{
$error[] = $vc_response;
$error[] = $captcha->get_error();
}
if ($config['max_reg_attempts'] && $captcha->get_attempt_count() > $config['max_reg_attempts'])
@ -426,7 +427,7 @@ class ucp_register
}
// Okay, captcha, your job is done.
if ($config['enable_confirm'] && isset($captcha))
if ($config['enable_confirm'])
{
$captcha->reset();
}

View file

@ -77,6 +77,9 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('captcha_gd_wave',
INSERT INTO phpbb_config (config_name, config_value) VALUES ('captcha_gd_x_grid', '25');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('captcha_gd_y_grid', '25');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('captcha_plugin', 'core.captcha.plugins.incomplete');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('captcha_turnstile_sitekey', '');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('captcha_turnstile_secret', '');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('captcha_turnstile_theme', 'light');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('check_attachment_content', '1');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('check_dnsbl', '0');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('chg_passforce', '0');

View file

@ -61,4 +61,5 @@ $lang = array_merge($lang, array(
'QA_ERROR_MSG' => 'Please fill in all fields and enter at least one answer.',
'QA_LAST_QUESTION' => 'You cannot delete all questions while the plugin is active.',
'QA_NO_QUESTIONS' => 'There are no questions yet.',
));

View file

@ -0,0 +1,53 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
/**
* DO NOT CHANGE
*/
if (!defined('IN_PHPBB'))
{
exit;
}
if (empty($lang) || !is_array($lang))
{
$lang = [];
}
// DEVELOPERS PLEASE NOTE
//
// All language files should use UTF-8 as their encoding and the files must not contain a BOM.
//
// Placeholders can now contain order information, e.g. instead of
// 'Page %s of %s' you can (and should) write 'Page %1$s of %2$s', this allows
// translators to re-order the output of data while ensuring it remains correct
//
// You do not need this where single placeholders are used, e.g. 'Message %d' is fine
// equally where a string contains only two placeholders which are used to wrap text
// in a url you again do not need to specify an order e.g., 'Click %sHERE%s' is fine
$lang = array_merge($lang, [
'CAPTCHA_TURNSTILE' => 'Turnstile',
'CAPTCHA_TURNSTILE_INCORRECT' => 'The solution you provided was incorrect',
'CAPTCHA_TURNSTILE_NOSCRIPT' => 'Please enable JavaScript in your browser to load the challenge.',
'CAPTCHA_TURNSTILE_NOT_AVAILABLE' => 'In order to use Turnstile you must create a <a href="https://www.cloudflare.com/products/turnstile/">Cloudflare account</a>.',
'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_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_THEME' => 'Widget theme',
'CAPTCHA_TURNSTILE_THEME_EXPLAIN' => 'The theme of the CAPTCHA widget. By default, <samp>light</samp> will be used. Other possibilities are <samp>dark</samp> and <samp>auto</samp>, which respects the users preference.',
'CAPTCHA_TURNSTILE_THEME_AUTO' => 'Auto',
'CAPTCHA_TURNSTILE_THEME_DARK' => 'Dark',
'CAPTCHA_TURNSTILE_THEME_LIGHT' => 'Light',
]);

View file

@ -26,6 +26,7 @@
"direction": "ltr",
"user-lang": "en-gb",
"plural-rule": 1,
"recaptcha-lang": "en-GB"
"recaptcha-lang": "en-GB",
"turnstile-lang": "en"
}
}

View file

@ -176,9 +176,8 @@ class db extends base
// Every auth module is able to define what to do by itself...
if ($show_captcha)
{
$captcha->init(CONFIRM_LOGIN);
$vc_response = $captcha->validate($row);
if ($vc_response)
$captcha->init(\phpbb\captcha\plugins\confirm_type::LOGIN);
if ($captcha->validate() !== true)
{
return array(
'status' => LOGIN_ERROR_ATTEMPTS,

View file

@ -13,6 +13,9 @@
namespace phpbb\captcha;
use phpbb\captcha\plugins\legacy_wrapper;
use phpbb\captcha\plugins\plugin_interface;
class factory
{
/**
@ -41,11 +44,17 @@ class factory
* Return a new instance of a given plugin
*
* @param $name
* @return object|null
* @return plugin_interface
*/
public function get_instance($name)
public function get_instance($name): plugin_interface
{
return $this->container->get($name);
$captcha = $this->container->get($name);
if ($captcha instanceof plugin_interface)
{
return $captcha;
}
return new legacy_wrapper($captcha);
}
/**
@ -56,7 +65,7 @@ class factory
function garbage_collect($name)
{
$captcha = $this->get_instance($name);
$captcha->garbage_collect(0);
$captcha->garbage_collect();
}
/**

View file

@ -0,0 +1,256 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\captcha\plugins;
use phpbb\config\config;
use phpbb\db\driver\driver_interface;
use phpbb\language\language;
use phpbb\request\request_interface;
use phpbb\user;
abstract class base implements plugin_interface
{
/** @var config */
protected config $config;
/** @var driver_interface */
protected driver_interface $db;
/** @var language */
protected language $language;
/** @var request_interface */
protected request_interface $request;
/** @var user */
protected user $user;
/** @var int Attempts at solving the CAPTCHA */
protected int $attempts = 0;
/** @var string Stored random CAPTCHA code */
protected string $code = '';
/** @var bool Resolved state of captcha */
protected bool $solved = false;
/** @var string User supplied confirm code */
protected string $confirm_code = '';
/** @var string Confirm id hash */
protected string $confirm_id = '';
/** @var confirm_type Confirmation type */
protected confirm_type $type = confirm_type::UNDEFINED;
/** @var string Last error message */
protected string $last_error = '';
/**
* Constructor for abstract captcha base class
*
* @param config $config
* @param driver_interface $db
* @param language $language
* @param request_interface $request
* @param user $user
*/
public function __construct(config $config, driver_interface $db, language $language, request_interface $request, user $user)
{
$this->config = $config;
$this->db = $db;
$this->language = $language;
$this->request = $request;
$this->user = $user;
}
/**
* {@inheritDoc}
*/
public function init(confirm_type $type): void
{
$this->confirm_id = $this->request->variable('confirm_id', '');
$this->confirm_code = $this->request->variable('confirm_code', '');
$this->type = $type;
if (empty($this->confirm_id) || !$this->load_confirm_data())
{
// we have no confirm ID, better get ready to display something
$this->generate_confirm_data();
}
}
/**
* {@inheritDoc}
*/
public function validate(): bool
{
if ($this->confirm_id && hash_equals($this->code, $this->confirm_code))
{
$this->solved = true;
return true;
}
$this->increment_attempts();
$this->last_error = $this->language->lang('CONFIRM_CODE_WRONG');
return false;
}
/**
* {@inheritDoc}
*/
public function reset(): void
{
$sql = 'DELETE FROM ' . CONFIRM_TABLE . "
WHERE session_id = '" . $this->db->sql_escape($this->user->session_id) . "'
AND confirm_type = " . $this->type->value;
$this->db->sql_query($sql);
$this->generate_confirm_data();
}
/**
* {@inheritDoc}
*/
public function get_attempt_count(): int
{
return $this->attempts;
}
/**
* Look up attempts from confirm table
*/
protected function load_confirm_data(): bool
{
$sql = 'SELECT code, attempts
FROM ' . CONFIRM_TABLE . "
WHERE confirm_id = '" . $this->db->sql_escape($this->confirm_id) . "'
AND session_id = '" . $this->db->sql_escape($this->user->session_id) . "'
AND confirm_type = " . $this->type->value;
$result = $this->db->sql_query($sql);
$row = $this->db->sql_fetchrow($result);
$this->db->sql_freeresult($result);
if ($row)
{
$this->attempts = $row['attempts'];
$this->code = $row['code'];
return true;
}
return false;
}
/**
* Generate confirm data for tracking attempts
*
* @return void
*/
protected function generate_confirm_data(): void
{
$this->code = gen_rand_string_friendly(CAPTCHA_MAX_CHARS);
$this->confirm_id = md5(unique_id());
$this->attempts = 0;
$sql = 'INSERT INTO ' . CONFIRM_TABLE . ' ' . $this->db->sql_build_array('INSERT', array(
'confirm_id' => $this->confirm_id,
'session_id' => (string) $this->user->session_id,
'confirm_type' => $this->type->value,
'code' => $this->code,
));
$this->db->sql_query($sql);
}
/**
* Increment number of attempts for confirm ID and session
*
* @return void
*/
protected function increment_attempts(): void
{
$sql = 'UPDATE ' . CONFIRM_TABLE . "
SET attempts = attempts + 1
WHERE confirm_id = '{$this->db->sql_escape($this->confirm_id)}'
AND session_id = '{$this->db->sql_escape($this->user->session_id)}'";
$this->db->sql_query($sql);
$this->attempts++;
}
/**
* {@inheritDoc}
*/
public function get_hidden_fields(): array
{
return [
'confirm_id' => $this->confirm_id,
'confirm_code' => $this->solved === true ? $this->confirm_code : '',
];
}
/**
* {@inheritDoc}
*/
public function is_solved(): bool
{
return $this->solved;
}
/**
* {@inheritDoc}
*/
public function get_error(): string
{
return $this->last_error;
}
/**
* @inheritDoc
*/
public function garbage_collect(confirm_type $confirm_type = confirm_type::UNDEFINED): void
{
$sql = 'SELECT DISTINCT c.session_id
FROM ' . CONFIRM_TABLE . ' c
LEFT JOIN ' . SESSIONS_TABLE . ' s ON (c.session_id = s.session_id)
WHERE s.session_id IS NULL' .
((empty($confirm_type)) ? '' : ' AND c.confirm_type = ' . $confirm_type->value);
$result = $this->db->sql_query($sql);
if ($row = $this->db->sql_fetchrow($result))
{
$sql_in = [];
do
{
$sql_in[] = (string) $row['session_id'];
}
while ($row = $this->db->sql_fetchrow($result));
if (count($sql_in))
{
$sql = 'DELETE FROM ' . CONFIRM_TABLE . '
WHERE ' . $this->db->sql_in_set('session_id', $sql_in);
$this->db->sql_query($sql);
}
}
$this->db->sql_freeresult($result);
}
/**
* {@inheritDoc}
*/
public function acp_page(mixed $id, mixed $module): void
{
}
}

View file

@ -179,16 +179,6 @@ abstract class captcha_abstract
$db->sql_freeresult($result);
}
function uninstall()
{
$this->garbage_collect(0);
}
function install()
{
return;
}
function validate()
{
global $user;

View file

@ -0,0 +1,25 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\captcha\plugins;
/**
* Confirmation types for CAPTCHA plugins
*/
enum confirm_type: int {
case UNDEFINED = 0;
case REGISTRATION = 1;
case LOGIN = 2;
case POST = 3;
case REPORT = 4;
}

View file

@ -14,25 +14,34 @@
namespace phpbb\captcha\plugins;
use phpbb\config\config;
use phpbb\exception\runtime_exception;
use phpbb\db\driver\driver_interface;
use phpbb\language\language;
use phpbb\request\request_interface;
use phpbb\template\template;
use phpbb\user;
class incomplete extends captcha_abstract
class incomplete extends base
{
/**
* Constructor for incomplete captcha
*
* @param config $config
* @param driver_interface $db
* @param language $language
* @param request_interface $request
* @param template $template
* @param user $user
* @param string $phpbb_root_path
* @param string $phpEx
*/
public function __construct(protected config $config, protected template $template,
protected string $phpbb_root_path, protected string $phpEx)
{}
public function __construct(config $config, driver_interface $db, language $language, request_interface $request,
protected template $template, user $user, protected string $phpbb_root_path, protected string $phpEx)
{
parent::__construct($config, $db, $language, $request, $user);
}
/**
* @return bool True if captcha is available, false if not
* {@inheritDoc}
*/
public function is_available(): bool
{
@ -40,70 +49,45 @@ class incomplete extends captcha_abstract
}
/**
* Dummy implementation, not supported by this captcha
*
* @throws runtime_exception
* @return void
* {@inheritDoc}
*/
public function get_generator_class(): void
public function has_config(): bool
{
throw new runtime_exception('NO_GENERATOR_CLASS');
return false;
}
/**
* Get CAPTCHA name language variable
*
* @return string Language variable
* {@inheritDoc}
*/
public static function get_name(): string
public function get_name(): string
{
return 'CAPTCHA_INCOMPLETE';
}
/**
* Init CAPTCHA
*
* @param int $type CAPTCHA type
* @return void
* {@inheritDoc}
*/
public function init($type)
public function set_name(string $name): void
{
}
/**
* Execute demo
*
* @return void
* {@inheritDoc}
*/
public function execute_demo()
public function init(confirm_type $type): void
{
}
/**
* Execute CAPTCHA
*
* @return void
* {@inheritDoc}
*/
public function execute()
{
}
/**
* Get template data for demo
*
* @param int|string $id ACP module ID
*
* @return string Demo template file name
*/
public function get_demo_template($id): string
public function get_demo_template(): string
{
return '';
}
/**
* Get template data for CAPTCHA
*
* @return string CAPTCHA template file name
* {@inheritDoc}
*/
public function get_template(): string
{
@ -118,9 +102,7 @@ class incomplete extends captcha_abstract
}
/**
* Validate CAPTCHA
*
* @return false Incomplete CAPTCHA will never validate
* {@inheritDoc}
*/
public function validate(): bool
{
@ -128,12 +110,26 @@ class incomplete extends captcha_abstract
}
/**
* Check whether CAPTCHA is solved
*
* @return false Incomplete CAPTCHA will never be solved
* {@inheritDoc}
*/
public function get_error(): string
{
return '';
}
/**
* {@inheritDoc}
*/
public function is_solved(): bool
{
return false;
}
/**
* {@inheritDoc}
*/
public function get_attempt_count(): int
{
return 0;
}
}

View file

@ -0,0 +1,221 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\captcha\plugins;
class legacy_wrapper implements plugin_interface
{
/** @var object Legacy CAPTCHA instance, should implement functionality as required in phpBB 3.3 */
private $legacy_captcha;
/** @var string Last error */
private string $last_error;
/**
* Constructor for legacy CAPTCHA wrapper
*
* @param object $legacy_captcha
*/
public function __construct(object $legacy_captcha)
{
$this->legacy_captcha = $legacy_captcha;
}
/**
* {@inheritDoc}
*/
public function is_available(): bool
{
if (method_exists($this->legacy_captcha, 'is_available'))
{
return $this->legacy_captcha->is_available();
}
return false;
}
/**
* {@inheritDoc}
*/
public function has_config(): bool
{
if (method_exists($this->legacy_captcha, 'has_config'))
{
return $this->legacy_captcha->has_config();
}
return false;
}
/**
* {@inheritDoc}
*/
public function get_name(): string
{
if (method_exists($this->legacy_captcha, 'get_name'))
{
return $this->legacy_captcha->get_name();
}
return '';
}
/**
* {@inheritDoc}
*/
public function set_name(string $name): void
{
if (method_exists($this->legacy_captcha, 'set_name'))
{
$this->legacy_captcha->set_name($name);
}
}
/**
* {@inheritDoc}
*/
public function init(confirm_type $type): void
{
if (method_exists($this->legacy_captcha, 'init'))
{
$this->legacy_captcha->init($type->value);
}
}
/**
* {@inheritDoc}
*/
public function get_hidden_fields(): array
{
if (method_exists($this->legacy_captcha, 'get_hidden_fields'))
{
return $this->legacy_captcha->get_hidden_fields();
}
return [];
}
/**
* {@inheritDoc}
*/
public function validate(): bool
{
if (method_exists($this->legacy_captcha, 'validate'))
{
$error = $this->legacy_captcha->validate();
if ($error)
{
$this->last_error = $error;
return false;
}
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
public function get_error(): string
{
return $this->last_error;
}
/**
* {@inheritDoc}
*/
public function is_solved(): bool
{
if (method_exists($this->legacy_captcha, 'is_solved'))
{
return $this->legacy_captcha->is_solved();
}
return false;
}
/**
* {@inheritDoc}
*/
public function reset(): void
{
if (method_exists($this->legacy_captcha, 'reset'))
{
$this->legacy_captcha->reset();
}
}
/**
* {@inheritDoc}
*/
public function get_attempt_count(): int
{
if (method_exists($this->legacy_captcha, 'get_attempt_count'))
{
return $this->legacy_captcha->get_attempt_count();
}
// Ensure this is deemed as too many attempts
return PHP_INT_MAX;
}
/**
* {@inheritDoc}
*/
public function get_template(): string
{
if (method_exists($this->legacy_captcha, 'get_template'))
{
return $this->legacy_captcha->get_template();
}
return '';
}
/**
* {@inheritDoc}
*/
public function get_demo_template(): string
{
if (method_exists($this->legacy_captcha, 'get_demo_template'))
{
return $this->legacy_captcha->get_demo_template(0);
}
return '';
}
/**
* {@inheritDoc}
*/
public function garbage_collect(confirm_type $confirm_type = confirm_type::UNDEFINED): void
{
if (method_exists($this->legacy_captcha, 'garbage_collect'))
{
$this->legacy_captcha->garbage_collect($confirm_type->value);
}
}
/**
* {@inheritDoc}
*/
public function acp_page(mixed $id, mixed $module): void
{
if (method_exists($this->legacy_captcha, 'acp_page'))
{
$this->legacy_captcha->acp_page($id, $module);
}
}
}

View file

@ -0,0 +1,126 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\captcha\plugins;
interface plugin_interface
{
/**
* Check if the plugin is available
*
* @return bool True if the plugin is available, false if not
*/
public function is_available(): bool;
/**
* Check if the plugin has a configuration
*
* @return bool True if the plugin has a configuration, false if not
*/
public function has_config(): bool;
/**
* Get the name of the plugin, should be language variable
*
* @return string
*/
public function get_name(): string;
/**
* Set the service name of the plugin
*
* @param string $name
*/
public function set_name(string $name): void;
/**
* Display the captcha for the specified type
*
* @param confirm_type $type Type of captcha, should be one of the CONFIRMATION_* constants
* @return void
*/
public function init(confirm_type $type): void;
/**
* Get hidden form fields for this captcha plugin
*
* @return array Hidden form fields
*/
public function get_hidden_fields(): array;
/**
* Validate the captcha with the given request data
*
* @return bool True if request data was valid captcha reply, false if not
*/
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 bool True if captcha was solved, false if not
*/
public function is_solved(): bool;
/**
* Reset captcha state, e.g. after checking if it's valid
*
* @return void
*/
public function reset(): void;
/**
* Get attempt count for this captcha and user
*
* @return int Number of attempts
*/
public function get_attempt_count(): int;
/**
* Get template filename for captcha
*
* @return string Template file name
*/
public function get_template(): string;
/**
* Get template filename for demo
*
* @return string Demo template file name
*/
public function get_demo_template(): string;
/**
* Garbage collect captcha plugin
*
* @param confirm_type $confirm_type Confirm type to garbage collect, defaults to all (0)
* @return void
*/
public function garbage_collect(confirm_type $confirm_type = confirm_type::UNDEFINED): void;
/**
* Display acp page
*
* @param mixed $id ACP module id
* @param mixed $module ACP module name
* @return void
*/
public function acp_page(mixed $id, mixed $module): void;
}

View file

@ -40,7 +40,7 @@ class qa
protected $service_name;
/** @var int Question ID */
protected $question = -1;
private $question = -1;
/**
* Constructor
@ -323,71 +323,6 @@ class qa
$db->sql_freeresult($result);
}
/**
* API function - we don't drop the tables here, as that would cause the loss of all entered questions.
*/
function uninstall()
{
$this->garbage_collect(0);
}
/**
* API function - set up shop
*/
function install()
{
global $phpbb_container;
$db_tool = $phpbb_container->get('dbal.tools');
$schemas = array(
$this->table_captcha_questions => array (
'COLUMNS' => array(
'question_id' => array('UINT', null, 'auto_increment'),
'strict' => array('BOOL', 0),
'lang_id' => array('UINT', 0),
'lang_iso' => array('VCHAR:30', ''),
'question_text' => array('TEXT_UNI', ''),
),
'PRIMARY_KEY' => 'question_id',
'KEYS' => array(
'lang' => array('INDEX', 'lang_iso'),
),
),
$this->table_captcha_answers => array (
'COLUMNS' => array(
'question_id' => array('UINT', 0),
'answer_text' => array('STEXT_UNI', ''),
),
'KEYS' => array(
'qid' => array('INDEX', 'question_id'),
),
),
$this->table_qa_confirm => array (
'COLUMNS' => array(
'session_id' => array('CHAR:32', ''),
'confirm_id' => array('CHAR:32', ''),
'lang_iso' => array('VCHAR:30', ''),
'question_id' => array('UINT', 0),
'attempts' => array('UINT', 0),
'confirm_type' => array('USINT', 0),
),
'KEYS' => array(
'session_id' => array('INDEX', 'session_id'),
'lookup' => array('INDEX', array('confirm_id', 'session_id', 'lang_iso')),
),
'PRIMARY_KEY' => 'confirm_id',
),
);
foreach ($schemas as $table => $schema)
{
if (!$db_tool->sql_table_exists($table))
{
$db_tool->sql_create_table($table, $schema);
}
}
}
/**
* API function - see what has to be done to validate
*/
@ -647,11 +582,6 @@ class qa
$user->add_lang('acp/board');
$user->add_lang('captcha_qa');
if (!self::is_installed())
{
$this->install();
}
$module->tpl_name = 'captcha_qa_acp';
$module->page_title = 'ACP_VC_SETTINGS';
$form_key = 'acp_captcha';

View file

@ -179,16 +179,6 @@ class recaptcha extends captcha_abstract
return $hidden_fields;
}
function uninstall()
{
$this->garbage_collect(0);
}
function install()
{
return;
}
function validate()
{
if (!parent::validate())

View file

@ -0,0 +1,287 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\captcha\plugins;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use phpbb\config\config;
use phpbb\db\driver\driver_interface;
use phpbb\language\language;
use phpbb\log\log_interface;
use phpbb\request\request_interface;
use phpbb\template\template;
use phpbb\user;
class turnstile extends base
{
/** @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 Client */
protected Client $client;
/** @var language */
protected language $language;
/** @var log_interface */
protected log_interface $log;
/** @var template */
protected template $template;
/** @var string Service name */
protected string $service_name = '';
/** @var array|string[] Supported themes for Turnstile CAPTCHA */
protected static array $supported_themes = [
'light',
'dark',
'auto'
];
/**
* Constructor for turnstile captcha plugin
*
* @param config $config
* @param driver_interface $db
* @param language $language
* @param log_interface $log
* @param request_interface $request
* @param template $template
* @param user $user
*/
public function __construct(config $config, driver_interface $db, language $language, log_interface $log, request_interface $request, template $template, user $user)
{
parent::__construct($config, $db, $language, $request, $user);
$this->language = $language;
$this->log = $log;
$this->template = $template;
}
/**
* {@inheritDoc}
*/
public function is_available(): bool
{
$this->init($this->type);
return !empty($this->config->offsetGet('captcha_turnstile_sitekey'))
&& !empty($this->config->offsetGet('captcha_turnstile_secret'));
}
/**
* {@inheritDoc}
*/
public function has_config(): bool
{
return true;
}
/**
* {@inheritDoc}
*/
public function get_name(): string
{
return 'CAPTCHA_TURNSTILE';
}
/**
* {@inheritDoc}
*/
public function set_name(string $name): void
{
$this->service_name = $name;
}
/**
* {@inheritDoc}
*/
public function init(confirm_type $type): void
{
parent::init($type);
$this->language->add_lang('captcha_turnstile');
}
/**
* {@inheritDoc}
*/
public function validate(): bool
{
if (parent::validate())
{
return true;
}
$turnstile_response = $this->request->variable('cf-turnstile-response', '');
if (!$turnstile_response)
{
// Return without checking against server without a turnstile response
return false;
}
// Retrieve form data for verification
$form_data = [
'secret' => $this->config['captcha_turnstile_secret'],
'response' => $turnstile_response,
'remoteip' => $this->user->ip,
];
// Create guzzle client
$client = $this->get_client();
// Check captcha with turnstile API
try
{
$response = $client->request('POST', self::VERIFY_ENDPOINT, [
'form_params' => $form_data,
]);
}
catch (GuzzleException)
{
// Something went wrong during the request to Cloudflare, assume captcha was bad
$this->solved = false;
return false;
}
// Decode the JSON response
$result = json_decode($response->getBody(), true);
// Check if the response indicates success
if (isset($result['success']) && $result['success'] === true)
{
$this->solved = true;
$this->confirm_code = $this->code;
return true;
}
else
{
$this->last_error = $this->language->lang('CAPTCHA_TURNSTILE_INCORRECT');
return false;
}
}
/**
* Get Guzzle client
*
* @return Client
*/
protected function get_client(): Client
{
if (!isset($this->client))
{
$this->client = new Client();
}
return $this->client;
}
/**
* {@inheritDoc}
*/
public function get_template(): string
{
if ($this->is_solved())
{
return '';
}
$this->template->assign_vars([
'S_TURNSTILE_AVAILABLE' => $this->is_available(),
'TURNSTILE_SITEKEY' => $this->config->offsetGet('captcha_turnstile_sitekey'),
'TURNSTILE_THEME' => $this->config->offsetGet('captcha_turnstile_theme'),
'U_TURNSTILE_SCRIPT' => self::SCRIPT_URL,
'CONFIRM_TYPE_REGISTRATION' => $this->type->value,
]);
return 'captcha_turnstile.html';
}
/**
* {@inheritDoc}
*/
public function get_demo_template(): string
{
$this->template->assign_vars([
'TURNSTILE_THEME' => $this->config->offsetGet('captcha_turnstile_theme'),
'U_TURNSTILE_SCRIPT' => self::SCRIPT_URL,
]);
return 'captcha_turnstile_acp_demo.html';
}
/**
* {@inheritDoc}
*/
public function acp_page(mixed $id, mixed $module): void
{
$captcha_vars = [
'captcha_turnstile_sitekey' => 'CAPTCHA_TURNSTILE_SITEKEY',
'captcha_turnstile_secret' => 'CAPTCHA_TURNSTILE_SECRET',
];
$module->tpl_name = 'captcha_turnstile_acp';
$module->page_title = 'ACP_VC_SETTINGS';
$form_key = 'acp_captcha';
add_form_key($form_key);
$submit = $this->request->is_set_post('submit');
if ($submit && check_form_key($form_key))
{
$captcha_vars = array_keys($captcha_vars);
foreach ($captcha_vars as $captcha_var)
{
$value = $this->request->variable($captcha_var, '');
if ($value)
{
$this->config->set($captcha_var, $value);
}
}
$captcha_theme = $this->request->variable('captcha_turnstile_theme', self::$supported_themes[0]);
if (in_array($captcha_theme, self::$supported_themes))
{
$this->config->set('captcha_turnstile_theme', $captcha_theme);
}
$this->log->add('admin', $this->user->data['user_id'], $this->user->ip, 'LOG_CONFIG_VISUAL');
trigger_error($this->language->lang('CONFIG_UPDATED') . adm_back_link($module->u_action));
}
else if ($submit)
{
trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($module->u_action));
}
else
{
foreach ($captcha_vars as $captcha_var => $template_var)
{
$var = $this->request->is_set($captcha_var) ? $this->request->variable($captcha_var, '') : $this->config->offsetGet($captcha_var);
$this->template->assign_var($template_var, $var);
}
$this->template->assign_vars(array(
'CAPTCHA_PREVIEW' => $this->get_demo_template(),
'CAPTCHA_NAME' => $this->service_name,
'CAPTCHA_TURNSTILE_THEME' => $this->config->offsetGet('captcha_turnstile_theme'),
'CAPTCHA_TURNSTILE_THEMES' => self::$supported_themes,
'U_ACTION' => $module->u_action,
));
}
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\db\migration\data\v400;
use phpbb\db\migration\migration;
class qa_captcha extends migration
{
public function effectively_installed(): bool
{
return $this->db_tools->sql_table_exists($this->tables['captcha_qa_questions'])
&& $this->db_tools->sql_table_exists($this->tables['captcha_qa_answers'])
&& $this->db_tools->sql_table_exists($this->tables['captcha_qa_confirm']);
}
public static function depends_on(): array
{
return [
'\phpbb\db\migration\data\v400\dev',
];
}
public function update_schema(): array
{
return [
'add_tables' => [
$this->tables['captcha_qa_questions'] => [
'COLUMNS' => [
'question_id' => ['UINT', null, 'auto_increment'],
'strict' => ['BOOL', 0],
'lang_id' => ['UINT', 0],
'lang_iso' => ['VCHAR:30', ''],
'question_text' => ['TEXT_UNI', ''],
],
'PRIMARY_KEY' => 'question_id',
'KEYS' => [
'lang' => ['INDEX', 'lang_iso'],
],
],
$this->tables['captcha_qa_answers'] => [
'COLUMNS' => [
'question_id' => ['UINT', 0],
'answer_text' => ['STEXT_UNI', ''],
],
'KEYS' => [
'qid' => ['INDEX', 'question_id'],
],
],
$this->tables['captcha_qa_confirm'] => [
'COLUMNS' => [
'session_id' => ['CHAR:32', ''],
'confirm_id' => ['CHAR:32', ''],
'lang_iso' => ['VCHAR:30', ''],
'question_id' => ['UINT', 0],
'attempts' => ['UINT', 0],
'confirm_type' => ['USINT', 0],
],
'KEYS' => [
'session_id' => ['INDEX', 'session_id'],
'lookup' => ['INDEX', ['confirm_id', 'session_id', 'lang_iso']],
],
'PRIMARY_KEY' => 'confirm_id',
],
],
];
}
public function revert_schema(): array
{
return [
'drop_tables' => [
$this->tables['captcha_qa_questions'],
$this->tables['captcha_qa_answers'],
$this->tables['captcha_qa_confirm']
],
];
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
namespace phpbb\db\migration\data\v400;
use phpbb\db\migration\migration;
class turnstile_captcha extends migration
{
public function effectively_installed(): bool
{
return $this->config->offsetExists('captcha_turnstile_sitekey')
&& $this->config->offsetExists('captcha_turnstile_secret')
&& $this->config->offsetExists('captcha_turnstile_theme');
}
public static function depends_on(): array
{
return [
'\phpbb\db\migration\data\v400\dev',
];
}
public function update_data(): array
{
return [
['config.add', ['captcha_turnstile_sitekey', '']],
['config.add', ['captcha_turnstile_secret', '']],
['config.add', ['captcha_turnstile_theme', 'light']],
];
}
public function revert_data(): array
{
return [
['config.remove', ['captcha_turnstile_sitekey']],
['config.remove', ['captcha_turnstile_secret']],
['config.remove', ['captcha_turnstile_theme']],
];
}
}

View file

@ -403,6 +403,7 @@ class language
$this->lang['USER_LANG'] = $lang_values['user_lang'] ?? 'en-gb';
$this->lang['PLURAL_RULE'] = $lang_values['plural_rule'] ?? 1;
$this->lang['RECAPTCHA_LANG'] = $lang_values['recaptcha_lang'] ?? 'en-GB';
$this->lang['TURNSTILE_LANG'] = $lang_values['turnstile_lang'] ?? 'auto'; // default to auto mode
}
/**

View file

@ -110,16 +110,17 @@ class language_file_helper
}
return [
'iso' => $data['extra']['language-iso'],
'name' => $data['extra']['english-name'],
'local_name' => $data['extra']['local-name'],
'author' => implode(', ', $authors),
'version' => $data['version'],
'phpbb_version' => $data['extra']['phpbb-version'],
'direction' => $data['extra']['direction'],
'user_lang' => $data['extra']['user-lang'],
'plural_rule' => $data['extra']['plural-rule'],
'recaptcha_lang'=> $data['extra']['recaptcha-lang'],
'iso' => $data['extra']['language-iso'],
'name' => $data['extra']['english-name'],
'local_name' => $data['extra']['local-name'],
'author' => implode(', ', $authors),
'version' => $data['version'],
'phpbb_version' => $data['extra']['phpbb-version'],
'direction' => $data['extra']['direction'],
'user_lang' => $data['extra']['user-lang'],
'plural_rule' => $data['extra']['plural-rule'],
'recaptcha_lang' => $data['extra']['recaptcha-lang'],
'turnstile_lang' => $data['extra']['turnstile-lang'] ?? '',
];
}
}

View file

@ -13,6 +13,8 @@
namespace phpbb\report\controller;
use phpbb\captcha\plugins\confirm_type;
use phpbb\captcha\plugins\plugin_interface;
use phpbb\exception\http_exception;
use phpbb\report\report_handler_interface;
use Symfony\Component\HttpFoundation\RedirectResponse;
@ -131,7 +133,7 @@ class report
if ($this->config['enable_post_confirm'] && !$this->user->data['is_registered'])
{
$captcha = $this->captcha_factory->get_instance($this->config['captcha_plugin']);
$captcha->init(CONFIRM_REPORT);
$captcha->init(confirm_type::REPORT);
}
//Has the report been cancelled?
@ -140,7 +142,7 @@ class report
return new RedirectResponse($redirect_url, 302);
}
// Check CAPTCHA, if the form was submited
// Check CAPTCHA, if the form was submitted
if (!empty($submit) && isset($captcha))
{
$captcha_template_array = $this->check_captcha($captcha);
@ -298,18 +300,17 @@ class report
/**
* Check CAPTCHA
*
* @param object $captcha A phpBB CAPTCHA object
* @param plugin_interface $captcha A phpBB CAPTCHA object
* @return array template variables which ensures that CAPTCHA's work correctly
*/
protected function check_captcha($captcha)
protected function check_captcha(plugin_interface $captcha)
{
$error = array();
$captcha_hidden_fields = '';
$visual_confirmation_response = $captcha->validate();
if ($visual_confirmation_response)
if ($captcha->validate() !== true)
{
$error[] = $visual_confirmation_response;
$error[] = $captcha->get_error();
}
if (count($error) === 0)

View file

@ -183,6 +183,7 @@ class forms extends AbstractExtension
'GROUP_ONLY' => (bool) ($form_data['group_only'] ?? false),
'SIZE' => (int) ($form_data['size'] ?? 0),
'MULTIPLE' => (bool) ($form_data['multiple'] ?? false),
'ONCHANGE' => (string) ($form_data['onchange'] ?? ''),
]);
}
catch (\Twig\Error\Error $e)

View file

@ -455,8 +455,10 @@ if (!$is_authed || !empty($error))
if ($config['enable_post_confirm'] && !$user->data['is_registered'])
{
$captcha = $phpbb_container->get('captcha.factory')->get_instance($config['captcha_plugin']);
$captcha->init(CONFIRM_POST);
/** @var \phpbb\captcha\factory $captcha_factory */
$captcha_factory = $phpbb_container->get('captcha.factory');
$captcha = $captcha_factory->get_instance($config['captcha_plugin']);
$captcha->init(\phpbb\captcha\plugins\confirm_type::POST);
}
// Is the user able to post within this forum?
@ -1208,15 +1210,9 @@ if ($submit || $preview || $refresh)
if ($config['enable_post_confirm'] && !$user->data['is_registered'] && in_array($mode, array('quote', 'post', 'reply')))
{
$captcha_data = array(
'message' => $request->variable('message', '', true),
'subject' => $request->variable('subject', '', true),
'username' => $request->variable('username', '', true),
);
$vc_response = $captcha->validate($captcha_data);
if ($vc_response)
if ($captcha->validate() !== true)
{
$error[] = $vc_response;
$error[] = $captcha->get_error();
}
}
@ -1600,7 +1596,7 @@ if ($submit || $preview || $refresh)
);
extract($phpbb_dispatcher->trigger_event('core.posting_modify_submit_post_after', compact($vars)));
if ($config['enable_post_confirm'] && !$user->data['is_registered'] && (isset($captcha) && $captcha->is_solved() === true) && ($mode == 'post' || $mode == 'reply' || $mode == 'quote'))
if ($config['enable_post_confirm'] && !$user->data['is_registered'] && $captcha->is_solved() === true && ($mode == 'post' || $mode == 'reply' || $mode == 'quote'))
{
$captcha->reset();
}

View file

@ -8,6 +8,7 @@
name="{{ NAME }}"
{% if TOGGLEABLE %}data-togglable-settings="true"{% endif %}
{% if MULTIPLE %}multiple="multiple"{% endif %}
{% if ONCHANGE %}onchange="{{ ONCHANGE }}"{% endif %}
{% if SIZE %}size="{{ SIZE }}"{% endif %}>
{% for element in OPTIONS %}
{% if not GROUP_ONLY and element.options %}

View file

@ -0,0 +1,23 @@
{% if CONFIRM_TYPE_REGISTRATION %}
<div class="panel captcha-panel">
<div class="inner">
<h3 class="captcha-title">{{ lang('CONFIRMATION') }}</h3>
<fieldset class="fields2">
{% endif %}
{% 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-language="{{ lang('TURNSTILE_LANG') }}" data-sitekey="{{ TURNSTILE_SITEKEY }}"{% if TURNSTILE_THEME %} data-theme="{{ TURNSTILE_THEME }}"{% endif %}></div>
{% else %}
{{ lang('CAPTCHA_TURNSTILE_NOT_AVAILABLE') }}
{% endif %}
{% if CONFIRM_TYPE_REGISTRATION %}
</fieldset>
</div>
</div>
{% endif %}

View file

@ -11,11 +11,15 @@
*
*/
use phpbb\captcha\plugins\confirm_type;
use phpbb\captcha\plugins\incomplete;
use phpbb\config\config;
use phpbb\language\language;
use phpbb\request\request;
use phpbb\template\template;
use phpbb\user;
class phpbb_captcha_incomplete_test extends phpbb_test_case
class phpbb_captcha_incomplete_test extends phpbb_database_test_case
{
protected config $config;
@ -32,21 +36,34 @@ class phpbb_captcha_incomplete_test extends phpbb_test_case
$this->assigned_vars = array_merge($this->assigned_vars, $vars);
}
public function getDataSet()
{
return $this->createXMLDataSet(__DIR__ . '/../fixtures/empty.xml');
}
protected function setUp(): void
{
global $phpbb_root_path, $phpEx;
$this->config = new config([]);
$this->template = $this->getMockBuilder('\phpbb\template\twig\twig')
->setMethods(['assign_vars'])
->onlyMethods(['assign_vars'])
->disableOriginalConstructor()
->getMock();
$this->template->method('assign_vars')
->willReturnCallback([$this, 'assign_vars']);
$db = $this->new_dbal();
$language = $this->createMock(language::class);
$request = $this->createMock(request::class);
$user = $this->createMock(user::class);
$this->incomplete_captcha = new incomplete(
$this->config,
$db,
$language,
$request,
$this->template,
$user,
$phpbb_root_path,
$phpEx
);
@ -57,29 +74,25 @@ class phpbb_captcha_incomplete_test extends phpbb_test_case
$this->assertTrue($this->incomplete_captcha->is_available());
$this->assertFalse($this->incomplete_captcha->is_solved());
$this->assertFalse($this->incomplete_captcha->validate());
$this->assertSame('CAPTCHA_INCOMPLETE', incomplete::get_name());
$this->incomplete_captcha->init(0);
$this->incomplete_captcha->execute();
$this->incomplete_captcha->execute_demo();
$this->assertFalse($this->incomplete_captcha->has_config());
$this->incomplete_captcha->set_name('foo');
$this->assertSame('CAPTCHA_INCOMPLETE', $this->incomplete_captcha->get_name());
$this->incomplete_captcha->init(confirm_type::UNDEFINED);
$this->assertEmpty($this->assigned_vars);
$this->assertEmpty($this->incomplete_captcha->get_demo_template(0));
}
public function test_get_generator_class(): void
{
$this->expectException(\phpbb\exception\runtime_exception::class);
$this->incomplete_captcha->get_generator_class();
$this->assertEmpty($this->incomplete_captcha->get_error());
$this->assertSame(0, $this->incomplete_captcha->get_attempt_count());
}
public function test_get_tempate(): void
{
$this->incomplete_captcha->init(CONFIRM_REG);
$this->incomplete_captcha->init(confirm_type::REGISTRATION);
$this->assertSame('captcha_incomplete.html', $this->incomplete_captcha->get_template());
$this->assertEquals('CONFIRM_INCOMPLETE', $this->assigned_vars['CONFIRM_LANG']);
$this->assigned_vars = [];
$this->incomplete_captcha->init(CONFIRM_POST);
$this->incomplete_captcha->init(confirm_type::POST);
$this->assertSame('captcha_incomplete.html', $this->incomplete_captcha->get_template());
$this->assertEquals('CONFIRM_INCOMPLETE', $this->assigned_vars['CONFIRM_LANG']);
}

View file

@ -0,0 +1,261 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
use phpbb\captcha\plugins\confirm_type;
use phpbb\captcha\plugins\legacy_wrapper;
class phpbb_captcha_legacy_wrapper_test extends phpbb_test_case
{
private $legacy_captcha;
private $legacy_wrapper;
public function setUp(): void
{
$this->legacy_captcha = $this->createMock(stdClass::class);
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
}
public function test_is_available_with_method_exists(): void
{
// Simulate is_available method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['is_available'])
->getMock();
$this->legacy_captcha->method('is_available')->willReturn(true);
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->assertTrue($this->legacy_wrapper->is_available());
}
public function test_is_available_without_method_exists(): void
{
// Simulate is_available method does not exist in the legacy captcha
$this->assertFalse($this->legacy_wrapper->is_available());
}
public function test_has_config_with_method_exists(): void
{
// Simulate has_config method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['has_config'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('has_config')->willReturn(true);
$this->assertTrue($this->legacy_wrapper->has_config());
}
public function test_has_config_without_method_exists(): void
{
// Simulate has_config method does not exist in the legacy captcha
$this->assertFalse($this->legacy_wrapper->has_config());
}
public function test_get_name_with_method_exists(): void
{
// Simulate get_name method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['get_name'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('get_name')->willReturn('LegacyCaptchaName');
$this->assertSame('LegacyCaptchaName', $this->legacy_wrapper->get_name());
}
public function test_get_name_without_method_exists(): void
{
// Simulate get_name method does not exist in the legacy captcha
$this->assertSame('', $this->legacy_wrapper->get_name());
}
public function test_set_name_with_method_exists(): void
{
// Simulate set_name method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['set_name'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->expects($this->once())->method('set_name')->with('NewName');
$this->legacy_wrapper->set_name('NewName');
}
public function test_init_with_method_exists(): void
{
// Simulate init method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['init'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->expects($this->once())->method('init')->with(confirm_type::REGISTRATION->value);
$this->legacy_wrapper->init(confirm_type::REGISTRATION);
}
public function test_get_hidden_fields_with_method_exists(): void
{
// Simulate get_hidden_fields method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['get_hidden_fields'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('get_hidden_fields')->willReturn(['field1' => 'value1']);
$this->assertSame(['field1' => 'value1'], $this->legacy_wrapper->get_hidden_fields());
}
public function test_get_hidden_fields_without_method_exists(): void
{
// Simulate get_hidden_fields method does not exist in the legacy captcha
$this->assertSame([], $this->legacy_wrapper->get_hidden_fields());
}
public function test_validate_with_error(): void
{
// Simulate validate method returns an error
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['validate'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('validate')->willReturn('Captcha Error');
$this->assertFalse($this->legacy_wrapper->validate());
$this->assertSame('Captcha Error', $this->legacy_wrapper->get_error());
}
public function test_validate_without_method_exists(): void
{
$this->assertFalse($this->legacy_wrapper->validate());
}
public function test_validate_without_error(): void
{
// Simulate validate method does not return an error
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['validate'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('validate')->willReturn(null);
$this->assertTrue($this->legacy_wrapper->validate());
}
public function test_is_solved_with_method_exists(): void
{
// Simulate is_solved method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['is_solved'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('is_solved')->willReturn(true);
$this->assertTrue($this->legacy_wrapper->is_solved());
}
public function test_is_solved_without_method_exists(): void
{
// Simulate is_solved method does not exist in the legacy captcha
$this->assertFalse($this->legacy_wrapper->is_solved());
}
public function test_reset_with_method_exists(): void
{
// Simulate reset method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['reset'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->expects($this->once())->method('reset');
$this->legacy_wrapper->reset();
}
public function test_get_attempt_count_with_method_exists(): void
{
// Simulate get_attempt_count method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['get_attempt_count'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('get_attempt_count')->willReturn(5);
$this->assertSame(5, $this->legacy_wrapper->get_attempt_count());
}
public function test_get_attempt_count_without_method_exists(): void
{
// Simulate get_attempt_count method does not exist in the legacy captcha
$this->assertSame(PHP_INT_MAX, $this->legacy_wrapper->get_attempt_count());
}
public function test_get_template_with_method_exists(): void
{
// Simulate get_template method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['get_template'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('get_template')->willReturn('template_content');
$this->assertSame('template_content', $this->legacy_wrapper->get_template());
}
public function test_get_template_without_method_exists(): void
{
// Simulate get_template method does not exist in the legacy captcha
$this->assertSame('', $this->legacy_wrapper->get_template());
}
public function test_get_demo_template_with_method_exists(): void
{
// Simulate get_demo_template method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['get_demo_template'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->method('get_demo_template')->willReturn('demo_template_content');
$this->assertSame('demo_template_content', $this->legacy_wrapper->get_demo_template());
}
public function test_get_demo_template_without_method_exists(): void
{
// Simulate get_demo_template method does not exist in the legacy captcha
$this->assertSame('', $this->legacy_wrapper->get_demo_template());
}
public function test_garbage_collect_with_method_exists(): void
{
// Simulate garbage_collect method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['garbage_collect'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->expects($this->once())->method('garbage_collect')->with(confirm_type::REGISTRATION->value);
$this->legacy_wrapper->garbage_collect(confirm_type::REGISTRATION);
}
public function test_acp_page_with_method_exists(): void
{
// Simulate acp_page method exists in the legacy captcha
$this->legacy_captcha = $this->getMockBuilder(stdClass::class)
->addMethods(['acp_page'])
->getMock();
$this->legacy_wrapper = new legacy_wrapper($this->legacy_captcha);
$this->legacy_captcha->expects($this->once())->method('acp_page')->with(1, 'module');
$this->legacy_wrapper->acp_page(1, 'module');
}
}

View file

@ -41,10 +41,6 @@ class phpbb_captcha_qa_test extends \phpbb_database_test_case
public function test_is_installed()
{
$this->assertFalse($this->qa->is_installed());
$this->qa->install();
$this->assertTrue($this->qa->is_installed());
}

View file

@ -0,0 +1,547 @@
<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
use phpbb\captcha\plugins\confirm_type;
use phpbb\captcha\plugins\turnstile;
use phpbb\config\config;
use phpbb\db\driver\driver_interface;
use phpbb\form\form_helper;
use phpbb\language\language;
use phpbb\log\log_interface;
use phpbb\request\request;
use phpbb\request\request_interface;
use phpbb\template\template;
use phpbb\user;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
require_once __DIR__ . '/../../phpBB/includes/functions_acp.php';
class phpbb_captcha_turnstile_test extends \phpbb_database_test_case
{
/** @var turnstile */
protected $turnstile;
/** @var PHPUnit\Framework\MockObject\MockObject */
protected $config;
/** @var PHPUnit\Framework\MockObject\MockObject */
protected $db;
/** @var PHPUnit\Framework\MockObject\MockObject */
protected $language;
/** @var PHPUnit\Framework\MockObject\MockObject */
protected $log;
/** @var PHPUnit\Framework\MockObject\MockObject */
protected $request;
/** @var PHPUnit\Framework\MockObject\MockObject */
protected $template;
/** @var PHPUnit\Framework\MockObject\MockObject */
protected $user;
public function getDataSet()
{
return $this->createXMLDataSet(__DIR__ . '/../fixtures/empty.xml');
}
protected function setUp(): void
{
// Mock the dependencies
$this->config = $this->createMock(config::class);
$this->db = $this->new_dbal();
$this->language = $this->createMock(language::class);
$this->log = $this->createMock(log_interface::class);
$this->request = $this->createMock(request::class);
$this->template = $this->createMock(template::class);
$this->user = $this->createMock(user::class);
$this->language->method('lang')->willReturnArgument(0);
// Instantiate the turnstile class with the mocked dependencies
$this->turnstile = new turnstile(
$this->config,
$this->db,
$this->language,
$this->log,
$this->request,
$this->template,
$this->user
);
}
public function test_is_available(): void
{
// Test when both sitekey and secret are present
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', 'sitekey_value'],
['captcha_turnstile_secret', 'secret_value'],
]);
$this->request->method('variable')->willReturnMap([
['confirm_id', '', false, request_interface::REQUEST, 'confirm_id'],
['confirm_code', '', false, request_interface::REQUEST, 'confirm_code']
]);
$this->assertTrue($this->turnstile->is_available());
$this->assertEquals(0, $this->turnstile->get_attempt_count());
}
public function test_attempt_count_increase(): void
{
// Test when both sitekey and secret are present
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', 'sitekey_value'],
['captcha_turnstile_secret', 'secret_value'],
]);
$this->request->method('variable')->willReturnMap([
['confirm_id', '', false, request_interface::REQUEST, 'confirm_id'],
['confirm_code', '', false, request_interface::REQUEST, 'confirm_code']
]);
$this->turnstile->init(confirm_type::REGISTRATION);
$this->assertFalse($this->turnstile->validate());
$confirm_id_reflection = new \ReflectionProperty($this->turnstile, 'confirm_id');
$confirm_id = $confirm_id_reflection->getValue($this->turnstile);
$this->request = $this->createMock(request::class);
$this->request->method('variable')->willReturnMap([
['confirm_id', '', false, request_interface::REQUEST, $confirm_id],
['confirm_code', '', false, request_interface::REQUEST, 'confirm_code']
]);
$this->turnstile = new turnstile(
$this->config,
$this->db,
$this->language,
$this->log,
$this->request,
$this->template,
$this->user
);
$this->turnstile->init(confirm_type::REGISTRATION);
$this->assertEquals(1, $this->turnstile->get_attempt_count());
// Run some garbage collection
$this->turnstile->garbage_collect(confirm_type::REGISTRATION);
// Start again at 0 after garbage collection
$this->turnstile->init(confirm_type::REGISTRATION);
$this->assertEquals(0, $this->turnstile->get_attempt_count());
}
public function test_reset(): void
{
// Test when both sitekey and secret are present
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', 'sitekey_value'],
['captcha_turnstile_secret', 'secret_value'],
]);
$this->request->method('variable')->willReturnMap([
['confirm_id', '', false, request_interface::REQUEST, 'confirm_id'],
['confirm_code', '', false, request_interface::REQUEST, 'confirm_code']
]);
$this->turnstile->init(confirm_type::REGISTRATION);
$this->assertFalse($this->turnstile->validate());
$this->turnstile->reset();
$confirm_id_reflection = new \ReflectionProperty($this->turnstile, 'confirm_id');
$confirm_id = $confirm_id_reflection->getValue($this->turnstile);
$this->request = $this->createMock(request::class);
$this->request->method('variable')->willReturnMap([
['confirm_id', '', false, request_interface::REQUEST, $confirm_id],
['confirm_code', '', false, request_interface::REQUEST, 'confirm_code']
]);
$this->turnstile = new turnstile(
$this->config,
$this->db,
$this->language,
$this->log,
$this->request,
$this->template,
$this->user
);
$this->turnstile->init(confirm_type::REGISTRATION);
// Should be zero attempts since we reset the captcha
$this->assertEquals(0, $this->turnstile->get_attempt_count());
}
public function test_get_hidden_fields(): void
{
// Test when both sitekey and secret are present
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', 'sitekey_value'],
['captcha_turnstile_secret', 'secret_value'],
]);
$this->request->method('variable')->willReturnMap([
['confirm_id', '', false, request_interface::REQUEST, 'confirm_id'],
['confirm_code', '', false, request_interface::REQUEST, 'confirm_code']
]);
$this->turnstile->init(confirm_type::REGISTRATION);
$this->assertFalse($this->turnstile->validate());
$this->turnstile->reset();
$confirm_id_reflection = new \ReflectionProperty($this->turnstile, 'confirm_id');
$confirm_id = $confirm_id_reflection->getValue($this->turnstile);
$this->assertEquals(
[
'confirm_id' => $confirm_id,
'confirm_code' => '',
],
$this->turnstile->get_hidden_fields(),
);
$this->assertEquals('CONFIRM_CODE_WRONG', $this->turnstile->get_error());
}
public function test_not_available(): void
{
$this->request->method('variable')->willReturnMap([
['confirm_id', '', false, request_interface::REQUEST, 'confirm_id'],
['confirm_code', '', false, request_interface::REQUEST, 'confirm_code']
]);
// Test when sitekey or secret is missing
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', ''],
['captcha_turnstile_secret', 'secret_value'],
]);
$this->assertFalse($this->turnstile->is_available());
}
public function test_get_name(): void
{
$this->assertEquals('CAPTCHA_TURNSTILE', $this->turnstile->get_name());
}
public function test_set_Name(): void
{
$this->turnstile->set_name('custom_service');
$service_name_property = new \ReflectionProperty($this->turnstile, 'service_name');
$service_name_property->setAccessible(true);
$this->assertEquals('custom_service', $service_name_property->getValue($this->turnstile));
}
public function test_validate_without_response(): void
{
// Test when there is no Turnstile response
$this->request->method('variable')->with('cf-turnstile-response')->willReturn('');
$this->assertFalse($this->turnstile->validate());
}
public function test_validate_with_response_success(): void
{
// Mock the request and response from the Turnstile API
$this->request->method('variable')->with('cf-turnstile-response')->willReturn('valid_response');
$this->request->method('header')->with('CF-Connecting-IP')->willReturn('127.0.0.1');
// Mock the GuzzleHttp client and response
$client_mock = $this->createMock(Client::class);
$response_mock = $this->createMock(Response::class);
$client_mock->method('request')->willReturn($response_mock);
$response_mock->method('getBody')->willReturn(json_encode(['success' => true]));
// Mock config values for secret
$this->config->method('offsetGet')->willReturn('secret_value');
// Use reflection to inject the mocked client into the turnstile class
$reflection = new \ReflectionClass($this->turnstile);
$client_property = $reflection->getProperty('client');
$client_property->setAccessible(true);
$client_property->setValue($this->turnstile, $client_mock);
// Validate that the CAPTCHA was solved successfully
$this->assertTrue($this->turnstile->validate());
}
public function test_validate_with_guzzle_exception(): void
{
// Mock the request and response from the Turnstile API
$this->request->method('variable')->with('cf-turnstile-response')->willReturn('valid_response');
$this->request->method('header')->with('CF-Connecting-IP')->willReturn('127.0.0.1');
// Mock the GuzzleHttp client and response
$client_mock = $this->createMock(Client::class);
$request_mock = $this->createMock(\GuzzleHttp\Psr7\Request::class);
$exception = new \GuzzleHttp\Exception\ConnectException('Failed at connecting', $request_mock);
$client_mock->method('request')->willThrowException($exception);
// Mock config values for secret
$this->config->method('offsetGet')->willReturn('secret_value');
// Use reflection to inject the mocked client into the turnstile class
$reflection = new \ReflectionClass($this->turnstile);
$client_property = $reflection->getProperty('client');
$client_property->setAccessible(true);
$client_property->setValue($this->turnstile, $client_mock);
// Validatation fails due to guzzle exception
$this->assertFalse($this->turnstile->validate());
}
public function test_validate_previous_solve(): void
{
// Use reflection to inject the mocked client into the turnstile class
$reflection = new \ReflectionClass($this->turnstile);
$confirm_id = $reflection->getProperty('confirm_id');
$confirm_id->setValue($this->turnstile, 'confirm_id');
$code_property = $reflection->getProperty('code');
$code_property->setValue($this->turnstile, 'test_code');
$confirm_code_property = $reflection->getProperty('confirm_code');
$confirm_code_property->setValue($this->turnstile, 'test_code');
// Validate that the CAPTCHA was solved successfully
$this->assertTrue($this->turnstile->validate());
$this->assertTrue($this->turnstile->is_solved());
}
public function test_has_config(): void
{
$this->assertTrue($this->turnstile->has_config());
}
public function test_get_client(): void
{
$turnstile_reflection = new \ReflectionClass($this->turnstile);
$get_client_method = $turnstile_reflection->getMethod('get_client');
$get_client_method->setAccessible(true);
$client_property = $turnstile_reflection->getProperty('client');
$client_property->setAccessible(true);
$this->assertFalse($client_property->isInitialized($this->turnstile));
$client = $get_client_method->invoke($this->turnstile);
$this->assertNotNull($client);
$this->assertInstanceOf(\GuzzleHttp\Client::class, $client);
$this->assertTrue($client === $get_client_method->invoke($this->turnstile));
}
public function test_validate_with_response_failure(): void
{
// Mock the request and response from the Turnstile API
$this->request->method('variable')->with('cf-turnstile-response')->willReturn('valid_response');
$this->request->method('header')->with('CF-Connecting-IP')->willReturn('127.0.0.1');
// Mock the GuzzleHttp client and response
$client_mock = $this->createMock(Client::class);
$response_mock = $this->createMock(Response::class);
$client_mock->method('request')->willReturn($response_mock);
$response_mock->method('getBody')->willReturn(json_encode(['success' => false]));
// Mock config values for secret
$this->config->method('offsetGet')->willReturn('secret_value');
// Use reflection to inject the mocked client into the turnstile class
$reflection = new \ReflectionClass($this->turnstile);
$client_property = $reflection->getProperty('client');
$client_property->setAccessible(true);
$client_property->setValue($this->turnstile, $client_mock);
// Validate that the CAPTCHA was not solved
$this->assertFalse($this->turnstile->validate());
}
public function test_get_template(): void
{
// Mock is_solved to return false
$is_solved_property = new \ReflectionProperty($this->turnstile, 'solved');
$is_solved_property->setAccessible(true);
$is_solved_property->setValue($this->turnstile, false);
// Mock the template assignments
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', 'sitekey_value'],
['captcha_turnstile_theme', 'light'],
]);
$this->request->method('variable')->willReturnMap([
['confirm_id', '', false, request_interface::REQUEST, 'confirm_id'],
['confirm_code', '', false, request_interface::REQUEST, 'confirm_code']
]);
$this->template->expects($this->once())->method('assign_vars')->with([
'S_TURNSTILE_AVAILABLE' => $this->turnstile->is_available(),
'TURNSTILE_SITEKEY' => 'sitekey_value',
'TURNSTILE_THEME' => 'light',
'U_TURNSTILE_SCRIPT' => 'https://challenges.cloudflare.com/turnstile/v0/api.js',
'CONFIRM_TYPE_REGISTRATION' => confirm_type::UNDEFINED->value,
]);
$this->assertEquals('captcha_turnstile.html', $this->turnstile->get_template());
$is_solved_property->setValue($this->turnstile, true);
$this->assertEquals('', $this->turnstile->get_template());
}
public function test_get_demo_template(): void
{
// Mock the config assignments
$this->config->method('offsetGet')->willReturn('light');
$this->template->expects($this->once())->method('assign_vars')->with([
'TURNSTILE_THEME' => 'light',
'U_TURNSTILE_SCRIPT' => 'https://challenges.cloudflare.com/turnstile/v0/api.js',
]);
$this->assertEquals('captcha_turnstile_acp_demo.html', $this->turnstile->get_demo_template());
}
public function test_acp_page_display(): void
{
global $phpbb_container, $phpbb_dispatcher, $template;
$phpbb_container = new phpbb_mock_container_builder();
$form_helper = new form_helper($this->config, $this->request, $this->user);
$phpbb_container->set('form_helper', $form_helper);
$this->user->data['user_id'] = ANONYMOUS;
$this->user->data['user_form_salt'] = 'foobar';
$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
$template = $this->template;
// Mock the template assignments
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', 'sitekey_value'],
['captcha_turnstile_theme', 'light'],
]);
$this->request->method('variable')->willReturn('');
$expected = [
1 => [
'TURNSTILE_THEME' => 'light',
'U_TURNSTILE_SCRIPT' => 'https://challenges.cloudflare.com/turnstile/v0/api.js',
],
2 => [
'CAPTCHA_PREVIEW' => 'captcha_turnstile_acp_demo.html',
'CAPTCHA_NAME' => '',
'CAPTCHA_TURNSTILE_THEME' => 'light',
'CAPTCHA_TURNSTILE_THEMES' => ['light', 'dark', 'auto'],
'U_ACTION' => 'test_u_action',
],
];
$matcher = $this->exactly(count($expected));
$this->template
->expects($matcher)
->method('assign_vars')
->willReturnCallback(function ($template_data) use ($matcher, $expected) {
$callNr = $matcher->getInvocationCount();
$this->assertEquals($expected[$callNr], $template_data);
});
$module_mock = new ModuleMock();
$this->turnstile->acp_page('', $module_mock);
}
public function test_acp_page_submit_without_form(): void
{
global $language, $phpbb_container, $phpbb_dispatcher, $template;
$language = $this->language;
$phpbb_container = new phpbb_mock_container_builder();
$form_helper = new form_helper($this->config, $this->request, $this->user);
$phpbb_container->set('form_helper', $form_helper);
$this->user->data['user_id'] = ANONYMOUS;
$this->user->data['user_form_salt'] = 'foobar';
$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
$template = $this->template;
// Mock the template assignments
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', 'sitekey_value'],
['captcha_turnstile_theme', 'light'],
]);
$this->request->method('is_set_post')->willReturnMap([
['creation_time', ''],
['submit', true]
]);
$this->setExpectedTriggerError(E_USER_NOTICE, 'FORM_INVALID');
$module_mock = new ModuleMock();
$this->turnstile->acp_page('', $module_mock);
}
public function test_acp_page_submit_valid(): void
{
global $language, $phpbb_container, $phpbb_dispatcher, $template;
$language = $this->language;
$phpbb_container = new phpbb_mock_container_builder();
$form_helper = new form_helper($this->config, $this->request, $this->user);
$phpbb_container->set('form_helper', $form_helper);
$this->user->data['user_id'] = ANONYMOUS;
$this->user->data['user_form_salt'] = 'foobar';
$phpbb_dispatcher = new phpbb_mock_event_dispatcher();
$template = $this->template;
$form_tokens = $form_helper->get_form_tokens('acp_captcha');
// Mock the template assignments
$this->config->method('offsetGet')->willReturnMap([
['captcha_turnstile_sitekey', 'sitekey_value'],
['captcha_turnstile_theme', 'light'],
]);
$this->config['form_token_lifetime'] = 3600;
$this->request->method('is_set_post')->willReturnMap([
['creation_time', true],
['form_token', true],
['submit', true]
]);
$this->request->method('variable')->willReturnMap([
['creation_time', 0, false, request_interface::REQUEST, $form_tokens['creation_time']],
['form_token', '', false, request_interface::REQUEST, $form_tokens['form_token']],
['captcha_turnstile_sitekey', '', false, request_interface::REQUEST, 'newsitekey'],
['captcha_turnstile_theme', 'light', false, request_interface::REQUEST, 'auto'],
]);
$this->setExpectedTriggerError(E_USER_NOTICE, 'CONFIG_UPDATED');
$module_mock = new ModuleMock();
sleep(1); // sleep for a second to ensure form token validation succeeds
$this->turnstile->acp_page('', $module_mock);
}
}
class ModuleMock
{
public string $tpl_name = '';
public string $page_title = '';
public string $u_action = 'test_u_action';
}