diff --git a/phpBB/adm/style/acp_captcha.html b/phpBB/adm/style/acp_captcha.html
index 4353becd2f..0c9d3bf0af 100644
--- a/phpBB/adm/style/acp_captcha.html
+++ b/phpBB/adm/style/acp_captcha.html
@@ -1,79 +1,85 @@
-
+{% include 'overall_header.html' %}
-
{L_ACP_VC_SETTINGS}
+{{ lang('ACP_VC_SETTINGS') }}
-{L_ACP_VC_SETTINGS_EXPLAIN}
+{{ lang('ACP_VC_SETTINGS_EXPLAIN') }}
-{L_ACP_VC_EXT_GET_MORE}
+{{ lang('ACP_VC_EXT_GET_MORE') }}
-
+{% if ERRORS %}
-
{L_WARNING}
-
{ERROR_MSG}
+
{{ lang('WARNING') }}
+
{{ ERRORS|join('
') }}
-
+{% endif %}
-
-
+{% include 'overall_footer.html' %}
diff --git a/phpBB/adm/style/captcha_qa_acp.html b/phpBB/adm/style/captcha_qa_acp.html
index 26598c7c2b..1118a1f32e 100644
--- a/phpBB/adm/style/captcha_qa_acp.html
+++ b/phpBB/adm/style/captcha_qa_acp.html
@@ -18,11 +18,13 @@
{L_QUESTIONS} |
+ {% if questions %}
{L_QUESTION_TEXT} |
{L_QUESTION_LANG} |
{L_ACTION} |
+ {% endif %}
@@ -33,6 +35,10 @@
{{ question.QUESTION_LANG }} |
{{ ICON_EDIT }} {{ ICON_DELETE }} |
+ {% else %}
+
+ {{ lang('QA_NO_QUESTIONS') }} |
+
{% endfor %}
diff --git a/phpBB/adm/style/captcha_turnstile_acp.html b/phpBB/adm/style/captcha_turnstile_acp.html
new file mode 100644
index 0000000000..a187e1e41b
--- /dev/null
+++ b/phpBB/adm/style/captcha_turnstile_acp.html
@@ -0,0 +1,64 @@
+{% include('overall_header.html') %}
+
+
+
+{{ lang('ACP_VC_SETTINGS') }}
+
+{{ lang('ACP_VC_SETTINGS_EXPLAIN') }}
+
+
+
+{% include('overall_footer.html') %}
diff --git a/phpBB/adm/style/captcha_turnstile_acp_demo.html b/phpBB/adm/style/captcha_turnstile_acp_demo.html
new file mode 100644
index 0000000000..757d0a4bcb
--- /dev/null
+++ b/phpBB/adm/style/captcha_turnstile_acp_demo.html
@@ -0,0 +1,23 @@
+
+
+
+{% INCLUDEJS U_TURNSTILE_SCRIPT %}
+
diff --git a/phpBB/config/default/container/services_captcha.yml b/phpBB/config/default/container/services_captcha.yml
index 8e9d829b47..ea41fefe80 100644
--- a/phpBB/config/default/container/services_captcha.yml
+++ b/phpBB/config/default/container/services_captcha.yml
@@ -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 }
diff --git a/phpBB/includes/acp/acp_captcha.php b/phpBB/includes/acp/acp_captcha.php
index ff9f515d32..e03d2a3901 100644
--- a/phpBB/includes/acp/acp_captcha.php
+++ b/phpBB/includes/acp/acp_captcha.php
@@ -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 .= '';
+ $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 .= '';
+ $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('
', $error),
+ 'CAPTCHA_SELECT' => [
+ 'tag' => 'select',
+ 'name' => 'select_captcha',
+ 'options' => $captcha_options,
+ ],
+ 'ERRORS' => $errors,
'U_ACTION' => $this->u_action,
));
diff --git a/phpBB/includes/constants.php b/phpBB/includes/constants.php
index 3304194cf4..825a328d39 100644
--- a/phpBB/includes/constants.php
+++ b/phpBB/includes/constants.php
@@ -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
diff --git a/phpBB/includes/functions_acp.php b/phpBB/includes/functions_acp.php
index 646d2f34d5..a3bae23ab5 100644
--- a/phpBB/includes/functions_acp.php
+++ b/phpBB/includes/functions_acp.php
@@ -203,8 +203,8 @@ function adm_page_footer($copyright_html = true)
*/
function adm_back_link($u_action)
{
- global $user;
- return '
« ' . $user->lang['BACK_TO_PREV'] . '';
+ global $language;
+ return '
« ' . $language->lang('BACK_TO_PREV') . '';
}
/**
diff --git a/phpBB/includes/ucp/ucp_register.php b/phpBB/includes/ucp/ucp_register.php
index 40b14a9b54..b8d0d78b40 100644
--- a/phpBB/includes/ucp/ucp_register.php
+++ b/phpBB/includes/ucp/ucp_register.php
@@ -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();
}
diff --git a/phpBB/install/schemas/schema_data.sql b/phpBB/install/schemas/schema_data.sql
index a160057fd2..b0e553960f 100644
--- a/phpBB/install/schemas/schema_data.sql
+++ b/phpBB/install/schemas/schema_data.sql
@@ -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');
diff --git a/phpBB/language/en/captcha_qa.php b/phpBB/language/en/captcha_qa.php
index 637c4e035e..b883166159 100644
--- a/phpBB/language/en/captcha_qa.php
+++ b/phpBB/language/en/captcha_qa.php
@@ -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.',
));
diff --git a/phpBB/language/en/captcha_turnstile.php b/phpBB/language/en/captcha_turnstile.php
new file mode 100644
index 0000000000..687ba4499d
--- /dev/null
+++ b/phpBB/language/en/captcha_turnstile.php
@@ -0,0 +1,53 @@
+
+* @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 Cloudflare account.',
+ 'CAPTCHA_TURNSTILE_SECRET' => 'Secret key',
+ 'CAPTCHA_TURNSTILE_SECRET_EXPLAIN' => 'Your Turnstile secret key. The secret key can be retrieved from your Cloudflare dashboard.',
+ 'CAPTCHA_TURNSTILE_SITEKEY' => 'Sitekey',
+ 'CAPTCHA_TURNSTILE_SITEKEY_EXPLAIN' => 'Your Turnstile sitekey. The sitekey can be retrieved from your Cloudflare dashboard.',
+ 'CAPTCHA_TURNSTILE_THEME' => 'Widget theme',
+ 'CAPTCHA_TURNSTILE_THEME_EXPLAIN' => 'The theme of the CAPTCHA widget. By default, light will be used. Other possibilities are dark and auto, which respects the user’s preference.',
+ 'CAPTCHA_TURNSTILE_THEME_AUTO' => 'Auto',
+ 'CAPTCHA_TURNSTILE_THEME_DARK' => 'Dark',
+ 'CAPTCHA_TURNSTILE_THEME_LIGHT' => 'Light',
+]);
diff --git a/phpBB/language/en/composer.json b/phpBB/language/en/composer.json
index 7402e5efa5..cd6bcdb581 100644
--- a/phpBB/language/en/composer.json
+++ b/phpBB/language/en/composer.json
@@ -26,6 +26,7 @@
"direction": "ltr",
"user-lang": "en-gb",
"plural-rule": 1,
- "recaptcha-lang": "en-GB"
+ "recaptcha-lang": "en-GB",
+ "turnstile-lang": "en"
}
}
diff --git a/phpBB/phpbb/auth/provider/db.php b/phpBB/phpbb/auth/provider/db.php
index ae9d968076..5f8ebd6ac3 100644
--- a/phpBB/phpbb/auth/provider/db.php
+++ b/phpBB/phpbb/auth/provider/db.php
@@ -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,
diff --git a/phpBB/phpbb/captcha/factory.php b/phpBB/phpbb/captcha/factory.php
index 2e75ce8667..fc974569bb 100644
--- a/phpBB/phpbb/captcha/factory.php
+++ b/phpBB/phpbb/captcha/factory.php
@@ -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();
}
/**
diff --git a/phpBB/phpbb/captcha/plugins/base.php b/phpBB/phpbb/captcha/plugins/base.php
new file mode 100644
index 0000000000..a30e679731
--- /dev/null
+++ b/phpBB/phpbb/captcha/plugins/base.php
@@ -0,0 +1,256 @@
+
+ * @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
+ {
+ }
+}
diff --git a/phpBB/phpbb/captcha/plugins/captcha_abstract.php b/phpBB/phpbb/captcha/plugins/captcha_abstract.php
index 012e28c987..c6978a1bf1 100644
--- a/phpBB/phpbb/captcha/plugins/captcha_abstract.php
+++ b/phpBB/phpbb/captcha/plugins/captcha_abstract.php
@@ -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;
diff --git a/phpBB/phpbb/captcha/plugins/confirm_type.php b/phpBB/phpbb/captcha/plugins/confirm_type.php
new file mode 100644
index 0000000000..e3e11edf77
--- /dev/null
+++ b/phpBB/phpbb/captcha/plugins/confirm_type.php
@@ -0,0 +1,25 @@
+
+ * @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;
+}
diff --git a/phpBB/phpbb/captcha/plugins/incomplete.php b/phpBB/phpbb/captcha/plugins/incomplete.php
index ec3376c0a5..07998a7125 100644
--- a/phpBB/phpbb/captcha/plugins/incomplete.php
+++ b/phpBB/phpbb/captcha/plugins/incomplete.php
@@ -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;
+ }
}
diff --git a/phpBB/phpbb/captcha/plugins/legacy_wrapper.php b/phpBB/phpbb/captcha/plugins/legacy_wrapper.php
new file mode 100644
index 0000000000..aaabddb48b
--- /dev/null
+++ b/phpBB/phpbb/captcha/plugins/legacy_wrapper.php
@@ -0,0 +1,221 @@
+
+ * @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);
+ }
+ }
+}
diff --git a/phpBB/phpbb/captcha/plugins/plugin_interface.php b/phpBB/phpbb/captcha/plugins/plugin_interface.php
new file mode 100644
index 0000000000..41483e45a5
--- /dev/null
+++ b/phpBB/phpbb/captcha/plugins/plugin_interface.php
@@ -0,0 +1,126 @@
+
+ * @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;
+}
diff --git a/phpBB/phpbb/captcha/plugins/qa.php b/phpBB/phpbb/captcha/plugins/qa.php
index b9bfac33f1..6e09aaaf13 100644
--- a/phpBB/phpbb/captcha/plugins/qa.php
+++ b/phpBB/phpbb/captcha/plugins/qa.php
@@ -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';
diff --git a/phpBB/phpbb/captcha/plugins/recaptcha.php b/phpBB/phpbb/captcha/plugins/recaptcha.php
index ef30baaa9b..1c080ba67b 100644
--- a/phpBB/phpbb/captcha/plugins/recaptcha.php
+++ b/phpBB/phpbb/captcha/plugins/recaptcha.php
@@ -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())
diff --git a/phpBB/phpbb/captcha/plugins/turnstile.php b/phpBB/phpbb/captcha/plugins/turnstile.php
new file mode 100644
index 0000000000..9a235edc3b
--- /dev/null
+++ b/phpBB/phpbb/captcha/plugins/turnstile.php
@@ -0,0 +1,287 @@
+
+ * @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,
+ ));
+ }
+ }
+}
diff --git a/phpBB/phpbb/db/migration/data/v400/qa_captcha.php b/phpBB/phpbb/db/migration/data/v400/qa_captcha.php
new file mode 100644
index 0000000000..69cca2dc15
--- /dev/null
+++ b/phpBB/phpbb/db/migration/data/v400/qa_captcha.php
@@ -0,0 +1,89 @@
+
+ * @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']
+ ],
+ ];
+ }
+}
diff --git a/phpBB/phpbb/db/migration/data/v400/turnstile_captcha.php b/phpBB/phpbb/db/migration/data/v400/turnstile_captcha.php
new file mode 100644
index 0000000000..77e921c090
--- /dev/null
+++ b/phpBB/phpbb/db/migration/data/v400/turnstile_captcha.php
@@ -0,0 +1,51 @@
+
+ * @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']],
+ ];
+ }
+}
diff --git a/phpBB/phpbb/language/language.php b/phpBB/phpbb/language/language.php
index dee8b58486..1c51049de5 100644
--- a/phpBB/phpbb/language/language.php
+++ b/phpBB/phpbb/language/language.php
@@ -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
}
/**
diff --git a/phpBB/phpbb/language/language_file_helper.php b/phpBB/phpbb/language/language_file_helper.php
index 995c7b4712..83244d674f 100644
--- a/phpBB/phpbb/language/language_file_helper.php
+++ b/phpBB/phpbb/language/language_file_helper.php
@@ -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'] ?? '',
];
}
}
diff --git a/phpBB/phpbb/report/controller/report.php b/phpBB/phpbb/report/controller/report.php
index b8e2b04583..97dbdbd9bb 100644
--- a/phpBB/phpbb/report/controller/report.php
+++ b/phpBB/phpbb/report/controller/report.php
@@ -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)
diff --git a/phpBB/phpbb/template/twig/extension/forms.php b/phpBB/phpbb/template/twig/extension/forms.php
index 5a3d9420db..b146aacfb9 100644
--- a/phpBB/phpbb/template/twig/extension/forms.php
+++ b/phpBB/phpbb/template/twig/extension/forms.php
@@ -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)
diff --git a/phpBB/posting.php b/phpBB/posting.php
index 63ca4fef97..9f8cce0ead 100644
--- a/phpBB/posting.php
+++ b/phpBB/posting.php
@@ -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();
}
diff --git a/phpBB/styles/all/template/macros/forms/select.twig b/phpBB/styles/all/template/macros/forms/select.twig
index 827ccb1121..1ada063e57 100644
--- a/phpBB/styles/all/template/macros/forms/select.twig
+++ b/phpBB/styles/all/template/macros/forms/select.twig
@@ -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 %}
diff --git a/phpBB/styles/prosilver/template/captcha_turnstile.html b/phpBB/styles/prosilver/template/captcha_turnstile.html
new file mode 100644
index 0000000000..1cf4f4c792
--- /dev/null
+++ b/phpBB/styles/prosilver/template/captcha_turnstile.html
@@ -0,0 +1,23 @@
+{% if CONFIRM_TYPE_REGISTRATION %}
+
+
+
{{ lang('CONFIRMATION') }}
+
+
+
+{% endif %}
diff --git a/tests/captcha/incomplete_test.php b/tests/captcha/incomplete_test.php
index c1da2b6c48..4c5e7c6b27 100644
--- a/tests/captcha/incomplete_test.php
+++ b/tests/captcha/incomplete_test.php
@@ -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']);
}
diff --git a/tests/captcha/legacy_wrapper_test.php b/tests/captcha/legacy_wrapper_test.php
new file mode 100644
index 0000000000..873012bd6a
--- /dev/null
+++ b/tests/captcha/legacy_wrapper_test.php
@@ -0,0 +1,261 @@
+
+ * @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');
+ }
+}
diff --git a/tests/captcha/qa_test.php b/tests/captcha/qa_test.php
index d429336104..1c61a7d10c 100644
--- a/tests/captcha/qa_test.php
+++ b/tests/captcha/qa_test.php
@@ -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());
}
diff --git a/tests/captcha/turnstile_test.php b/tests/captcha/turnstile_test.php
new file mode 100644
index 0000000000..f78c4d3177
--- /dev/null
+++ b/tests/captcha/turnstile_test.php
@@ -0,0 +1,547 @@
+
+ * @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';
+}