PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
← All changes | src/controller/class-captcha.php +193 -1039 5.7.0 → 6.3.0 View file →
@@ -6,16 +6,13 @@
6 6 */
7 7
8 8 namespace WP_Defender\Controller;
9 9
10 -use WP_User;
11 -use WP_Error;
12 10 use WP_Defender\Event;
13 11 use Calotes\Component\Request;
14 12 use Calotes\Component\Response;
15 -use WP_Defender\Component\Crypt;
16 13 use WP_Defender\Behavior\WPMUDEV;
17 -use WP_Defender\Traits\Hummingbird;
14 +use WP_Defender\Integrations\Hummingbird;
18 15 use WP_Defender\Integrations\Buddypress;
19 16 use WP_Defender\Integrations\Woocommerce;
20 17 use WP_Defender\Component\Config\Config_Hub_Helper;
21 18 use WP_Defender\Component\Captcha as Captcha_Component;
@@ -27,66 +24,14 @@
27 24 * @since 2.5.4
28 25 */
29 26 class Captcha extends Event {
30 27
31 - use Hummingbird;
32 -
33 28 /**
34 - * Accepted values: v2_checkbox, v2_invisible, v3_recaptcha, turnstile.
35 - *
36 - * @var string
37 - */
38 - private $recaptcha_type;
39 -
40 - /**
41 - * Accepted values: light and dark.
42 - *
43 - * @var string
44 - */
45 - private $recaptcha_theme;
46 -
47 - /**
48 - * Accepted values: normal and compact.
49 - *
50 - * @var string
51 - */
52 - private $recaptcha_size;
53 -
54 - /**
55 - * Recaptcha API public key.
56 - *
57 - * @var string
58 - */
59 - private $public_key;
60 -
61 - /**
62 - * Recaptcha API private key.
63 - *
64 - * @var string
65 - */
66 - private $private_key;
67 -
68 - /**
69 - * Language for the reCAPTCHA.
70 - *
71 - * @var string
72 - */
73 - private $language;
74 -
75 - /**
76 - * Default message for the reCAPTCHA.
77 - *
78 - * @var string
79 - */
80 - private $default_msg;
81 -
82 - /**
83 29 * The model for handling the data.
84 30 *
85 31 * @var Captcha_Model
86 32 */
87 33 public $model;
88 -
89 34 /**
90 35 * Service for handling logic.
91 36 *
92 37 * @var Captcha_Component
@@ -107,79 +52,43 @@
107 52 */
108 53 private $is_buddypress_activated;
109 54
110 55 /**
56 + * Buddypress integration module.
57 + *
58 + * @var Buddypress|null
59 + */
60 + private ?Buddypress $buddypress;
61 +
62 + /**
63 + * Woocommerce integration module.
64 + *
65 + * @var Woocommerce|null
66 + */
67 + private ?Woocommerce $woo;
68 +
69 + /**
111 70 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
112 71 */
113 72 public function __construct() {
114 - $this->model = wd_di()->get( Captcha_Model::class );
73 + $this->model = wd_di()->get( Captcha_Model::class );
74 +
115 75 $this->service = new Captcha_Component( $this->model );
116 - // Use default msg to avoid empty message error.
117 - $default_values = $this->model->get_default_values();
118 - $this->default_msg = $default_values['message'];
119 76 $this->register_routes();
120 - $this->is_woo_activated = wd_di()->get( Woocommerce::class )->is_activated();
121 - $this->is_buddypress_activated = wd_di()->get( Buddypress::class )->is_activated();
122 - add_filter( 'wp_defender_advanced_tools_data', array( $this, 'script_data' ) );
77 + $this->woo = wd_di()->get( Woocommerce::class );
78 + $this->is_woo_activated = $this->woo->is_activated();
79 + $this->buddypress = wd_di()->get( Buddypress::class );
80 + $this->is_buddypress_activated = $this->buddypress->is_activated();
123 81
124 - if (
125 - $this->model->is_active()
126 - // No need the check by Woo and Buddypress are activated because we use this below.
127 - && $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated )
128 - && ! $this->service->exclude_captcha_for_requests()
129 - ) {
130 - $this->declare_variables();
82 + if ( $this->model->is_active() // No need the check by Woo and Buddypress are activated because we use this below.
83 + && $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) && ! $this->service->exclude_captcha_for_requests() ) {
131 84 $this->add_actions();
132 85
133 - add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 2 );
86 + add_filter( 'script_loader_tag', array( $this->service, 'script_loader_tag' ), 10, 2 );
134 87 }
135 88 }
136 89
137 90 /**
138 - * Declares the necessary variables for the reCAPTCHA functionality.
139 - *
140 - * @return void
141 - */
142 - protected function declare_variables(): void {
143 - $this->recaptcha_type = $this->model->active_type;
144 - $this->recaptcha_theme = 'light';
145 - $this->recaptcha_size = 'invisible';
146 - $this->language = ! empty( $this->model->language ) && 'automatic' !== $this->model->language
147 - ? $this->model->language
148 - : get_locale();
149 -
150 - // Add the CAPTCHA keys depending on the CAPTCHA type.
151 - if ( 'v2_checkbox' === $this->recaptcha_type ) {
152 - $this->public_key = $this->model->data_v2_checkbox['key'];
153 - $this->private_key = $this->model->data_v2_checkbox['secret'];
154 - $this->recaptcha_theme = $this->model->data_v2_checkbox['style'];
155 - $this->recaptcha_size = $this->model->data_v2_checkbox['size'];
156 - } elseif ( 'v2_invisible' === $this->recaptcha_type ) {
157 - $this->public_key = $this->model->data_v2_invisible['key'];
158 - $this->private_key = $this->model->data_v2_invisible['secret'];
159 - } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
160 - $this->public_key = $this->model->data_v3_recaptcha['key'];
161 - $this->private_key = $this->model->data_v3_recaptcha['secret'];
162 - } elseif ( 'turnstile' === $this->recaptcha_type ) {
163 - $this->public_key = $this->model->data_turnstile['key'];
164 - $this->private_key = $this->model->data_turnstile['secret'];
165 - $this->recaptcha_theme = $this->model->data_turnstile['style'] ?? 'auto';
166 - $this->recaptcha_size = $this->model->data_turnstile['size'] ?? 'normal';
167 - $this->language = $this->model->data_turnstile['language'] ?? 'auto';
168 - }
169 - }
170 -
171 - /**
172 - * Is it Defender's CAPTCHA page?
173 - *
174 - * @return bool
175 - */
176 - protected function is_captcha_settings(): bool {
177 - $view = defender_get_data_from_request( 'view', 'g' );
178 - return 'wdf-advanced-tools' === defender_get_current_page() && 'captcha' === $view;
179 - }
180 -
181 - /**
182 91 * Add actions for CAPTCHA.
183 92 *
184 93 * @return void
185 94 */
@@ -187,15 +96,9 @@
187 96 $extra_conditions = is_admin() && ! ( defined( 'DOING_AJAX' ) && $this->is_captcha_settings() );
188 97 // Since v5.7.0.
189 98 do_action( 'wd_captcha_before_actions', $extra_conditions );
190 99
191 - do_action_deprecated(
192 - 'wd_recaptcha_before_actions',
193 - array( $extra_conditions ),
194 - '5.7.0',
195 - 'wd_captcha_before_actions',
196 - __( 'This hook is deprecated and will be removed in future versions.', 'defender-security' )
197 - );
100 + do_action_deprecated( 'wd_recaptcha_before_actions', array( $extra_conditions ), '5.7.0', 'wd_captcha_before_actions', __( 'This hook is deprecated and will be removed in future versions.', 'defender-security' ) );
198 101 if ( $extra_conditions ) {
199 102 return;
200 103 }
201 104
@@ -200,36 +103,46 @@
200 103 }
201 104
202 105 $display_for_known_users = $this->model->display_for_known_users();
203 106 $locations = $this->model->locations;
204 - if (
205 - in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true )
206 - || in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true )
207 - || in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true )
208 - ) {
107 + if ( in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true ) || in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true ) || in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true ) ) {
209 108 add_filter( 'cfturnstile_widget_disable', '__return_true' );
210 109 add_filter( 'easy_cloudflare_turnstile_render_list', '__return_empty_array' );
211 110 add_filter( 'easy_cloudflare_turnstile_verify_list', '__return_empty_array' );
212 - add_action( 'login_enqueue_scripts', array( $this->service, 'remove_duplicate_captcha_scripts' ), PHP_INT_MAX );
111 + add_action(
112 + 'login_enqueue_scripts',
113 + array(
114 + $this->service,
115 + 'remove_duplicate_captcha_scripts',
116 + ),
117 + PHP_INT_MAX
118 + );
213 119 }
214 120 // Default login form.
215 121 if ( in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true ) ) {
216 - add_filter( 'authenticate', array( $this, 'validate_login_captcha' ), 9999 );
217 - add_action( 'login_form', array( $this, 'display_login_captcha' ) );
218 - add_filter( 'wp_authenticate_user', array( $this, 'validate_captcha_field_on_login' ), 8 );
122 + add_filter( 'authenticate', array( $this->service, 'validate_login_captcha' ), 9999 );
123 + add_action( 'login_form', array( $this->service, 'display_login_captcha' ) );
124 + add_filter( 'wp_authenticate_user', array( $this->service, 'validate_captcha_field_on_login' ), 8 );
219 125 }
220 126 // Default register form.
221 127 if ( in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true ) ) {
222 128 if ( ! is_multisite() ) {
223 - add_action( 'register_form', array( $this, 'display_login_captcha' ) );
224 - add_filter( 'registration_errors', array( $this, 'validate_captcha_field_on_registration' ), 10 );
129 + add_action( 'register_form', array( $this->service, 'display_login_captcha' ) );
130 + add_filter(
131 + 'registration_errors',
132 + array(
133 + $this->service,
134 + 'validate_captcha_field_on_registration',
135 + ),
136 + 10
137 + );
225 138 } else {
226 - add_action( 'signup_extra_fields', array( $this, 'display_signup_captcha' ) );
227 - add_action( 'signup_blogform', array( $this, 'display_signup_captcha' ) );
139 + add_action( 'signup_extra_fields', array( $this->service, 'display_signup_captcha' ) );
140 + add_action( 'signup_blogform', array( $this->service, 'display_signup_captcha' ) );
228 141 add_filter(
229 142 'wpmu_validate_user_signup',
230 143 array(
231 - $this,
144 + $this->service,
232 145 'validate_captcha_field_on_wpmu_registration',
233 146 ),
234 147 10
235 148 );
@@ -236,35 +149,34 @@
236 149 }
237 150 }
238 151 // Default lost password form.
239 152 if ( in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true ) ) {
240 - add_action( 'lostpassword_form', array( $this, 'display_login_captcha' ) );
153 + add_action( 'lostpassword_form', array( $this->service, 'display_login_captcha' ) );
241 154 if ( $this->maybe_validate_captcha_for_lostpassword() ) {
242 - add_action( 'lostpassword_post', array( $this, 'validate_captcha_field_on_lostpassword' ) );
155 + add_action( 'lostpassword_post', array( $this->service, 'validate_captcha_field_on_lostpassword' ) );
243 156 }
244 157 }
245 158 // Default comment form.
246 159 if ( $display_for_known_users && in_array( Captcha_Component::DEFAULT_COMMENT_FORM, $locations, true ) ) {
247 160 // @since v3.4.0 Change from 'comment_form_after_fields' to 'comment_form_defaults'.
248 - add_filter( 'comment_form_defaults', array( $this, 'comment_form_defaults' ), 10 );
249 - add_action( 'pre_comment_on_post', array( $this, 'validate_captcha_field_on_comment' ) );
161 + add_filter( 'comment_form_defaults', array( $this->service, 'comment_form_defaults' ), 10 );
162 + add_action( 'pre_comment_on_post', array( $this->service, 'validate_captcha_field_on_comment' ) );
250 163 // When comments are loaded via Hummingbird's lazy load feature.
251 - if ( $this->is_lazy_load_comments_enabled() ) {
252 - add_action( 'wp_footer', array( $this, 'add_scripts_for_lazy_load' ) );
164 + if ( wd_di()->get( Hummingbird::class )->is_lazy_load_comments_enabled() ) {
165 + add_action( 'wp_footer', array( $this->service, 'add_scripts_for_lazy_load' ) );
253 166 }
254 167 }
255 168 // Todo: move code to related class.
256 - // Only Recaptcha type has Integration fields in v5.7.0.
257 169 // For Woo forms. Mandatory check for the activated Woo before.
258 - if ( $this->service->is_recaptcha_active_provider() && $this->model->check_woo_locations( $this->is_woo_activated ) ) {
170 + if ( $this->model->check_woo_locations( $this->is_woo_activated ) ) {
259 171 $woo_locations = $this->model->woo_checked_locations;
260 172 // Woo login form.
261 173 if ( in_array( Woocommerce::WOO_LOGIN_FORM, $woo_locations, true ) ) {
262 - add_action( 'woocommerce_login_form', array( $this, 'display_login_captcha' ) );
174 + add_action( 'woocommerce_login_form', array( $this->service, 'display_login_captcha' ) );
263 175 add_filter(
264 176 'woocommerce_process_login_errors',
265 177 array(
266 - $this,
178 + $this->service,
267 179 'validate_captcha_field_on_woo_login',
268 180 ),
269 181 10
270 182 );
@@ -270,13 +182,13 @@
270 182 );
271 183 }
272 184 // Woo register form.
273 185 if ( in_array( Woocommerce::WOO_REGISTER_FORM, $woo_locations, true ) ) {
274 - add_action( 'woocommerce_register_form', array( $this, 'display_login_captcha' ) );
186 + add_action( 'woocommerce_register_form', array( $this->service, 'display_login_captcha' ) );
275 187 add_filter(
276 188 'woocommerce_registration_errors',
277 189 array(
278 - $this,
190 + $this->service,
279 191 'validate_captcha_field_on_woo_registration',
280 192 ),
281 193 10
282 194 );
@@ -282,22 +194,34 @@
282 194 );
283 195 }
284 196 // Woo lost password form.
285 197 if ( in_array( Woocommerce::WOO_LOST_PASSWORD_FORM, $woo_locations, true ) ) {
286 - add_action( 'woocommerce_lostpassword_form', array( $this, 'display_login_captcha' ) );
198 + add_action( 'woocommerce_lostpassword_form', array( $this->service, 'display_login_captcha' ) );
287 199 // Use default WP hook because Woo doesn't have own hook, so there's the extra check for Woo form.
288 200 $post_data = defender_get_data_from_request( null, 'p' );
289 201 if ( isset( $post_data['wc_reset_password'], $post_data['user_login'] ) ) {
290 - add_action( 'lostpassword_post', array( $this, 'validate_captcha_field_on_lostpassword' ) );
202 + add_action(
203 + 'lostpassword_post',
204 + array(
205 + $this->service,
206 + 'validate_captcha_field_on_lostpassword',
207 + )
208 + );
291 209 }
292 210 }
293 211 // Woo checkout form.
294 212 if ( $display_for_known_users && in_array( Woocommerce::WOO_CHECKOUT_FORM, $woo_locations, true ) ) {
295 - add_action( 'woocommerce_after_checkout_billing_form', array( $this, 'display_login_captcha' ) );
296 213 add_action(
214 + 'woocommerce_after_checkout_billing_form',
215 + array(
216 + $this->service,
217 + 'display_login_captcha',
218 + )
219 + );
220 + add_action(
297 221 'woocommerce_after_checkout_validation',
298 222 array(
299 - $this,
223 + $this->service,
300 224 'validate_captcha_field_on_woo_checkout',
301 225 ),
302 226 10,
303 227 2
@@ -303,20 +227,24 @@
303 227 2
304 228 );
305 229 }
306 230 }
307 - // Todo: move code to related class.
308 - // Only Recaptcha type has Integration fields in v5.7.0.
309 231 // For BuddyPress forms. Mandatory check for the activated BuddyPress before.
310 - if ( $this->service->is_recaptcha_active_provider() && $this->model->check_buddypress_locations( $this->is_buddypress_activated ) ) {
232 + if ( $this->model->check_buddypress_locations( $this->is_buddypress_activated ) ) {
311 233 $buddypress_locations = $this->model->buddypress_checked_locations;
312 234 // Register form.
313 235 if ( in_array( Buddypress::REGISTER_FORM, $buddypress_locations, true ) ) {
314 - add_action( 'bp_before_registration_submit_buttons', array( $this, 'display_buddypress_recaptcha' ) );
315 - add_filter(
236 + add_action(
237 + 'bp_before_registration_submit_buttons',
238 + array(
239 + $this->service,
240 + 'display_buddypress_recaptcha',
241 + )
242 + );
243 + add_action(
316 244 'bp_signup_validate',
317 245 array(
318 - $this,
246 + $this->service,
319 247 'validate_captcha_field_on_buddypress_registration',
320 248 ),
321 249 10
322 250 );
@@ -322,328 +250,49 @@
322 250 );
323 251 }
324 252 // Group form.
325 253 if ( $display_for_known_users && in_array( Buddypress::NEW_GROUP_FORM, $buddypress_locations, true ) ) {
326 - add_action( 'bp_after_group_details_creation_step', array( $this, 'display_login_captcha' ) );
327 - add_action( 'groups_group_before_save', array( $this, 'validate_captcha_field_on_buddypress_group' ) );
254 + add_action( 'bp_after_group_details_creation_step', array( $this->service, 'display_login_captcha' ) );
255 + add_action(
256 + 'groups_group_before_save',
257 + array(
258 + $this->service,
259 + 'validate_captcha_field_on_buddypress_group',
260 + )
261 + );
328 262 }
329 263 }
330 264 // Since v5.7.0.
331 265 do_action( 'wd_captcha_after_actions', $display_for_known_users );
332 266
333 - do_action_deprecated(
334 - 'wd_recaptcha_after_actions',
335 - array( $display_for_known_users ),
336 - '5.7.0',
337 - 'wd_captcha_after_actions',
338 - __( 'This hook is deprecated and will be removed in a future version.', 'defender-security' )
339 - );
267 + do_action_deprecated( 'wd_recaptcha_after_actions', array( $display_for_known_users ), '5.7.0', 'wd_captcha_after_actions', __( 'This hook is deprecated and will be removed in a future version.', 'defender-security' ) );
340 268 }
341 269
342 270 /**
343 - * Validates the CAPTCHA response for the login form.
271 + * Is it Defender's CAPTCHA page?
344 272 *
345 - * @param null|WP_Error $error WP_Error object if validation fails, else null.
346 - *
347 - * @return null|WP_Error WP_Error object if validation fails else null.
273 + * @return bool
348 274 */
349 - public function validate_login_captcha( $error ) {
350 - $post_data = defender_get_data_from_request( null, 'p' );
351 - if ( array() === $post_data ) {
352 - return $error;
353 - }
354 - // Skip turnstile verification for current sprint plan.
355 - if ( $this->service->should_skip_turnstile_check() ) {
356 - return $error;
357 - }
358 - // Check if the $_POST array is not empty and if 'g-recaptcha-response' key is also empty.
359 - $response_field = 'turnstile' === $this->recaptcha_type ? 'wpdef-turnstile-response' : 'g-recaptcha-response';
360 - $captcha_response = stripslashes( defender_get_data_from_request( $response_field, 'p' ) );
361 - if ( '' === $captcha_response ) {
362 - $code = 'captcha_error';
363 - $message = __( 'Please verify that you are not a robot.', 'defender-security' );
275 + protected function is_captcha_settings(): bool {
276 + $view = defender_get_data_from_request( 'view', 'g' );
364 277
365 - if ( is_wp_error( $error ) ) {
366 - $error->add( $code, $message );
367 - } else {
368 - // Replace $user with a new WP_Error object with an error message.
369 - $error = new WP_Error( $code, $message );
370 - }
371 - }
372 -
373 - // Return the $error variable.
374 - return $error;
278 + return 'wdf-advanced-tools' === defender_get_current_page() && 'captcha' === $view;
375 279 }
376 280
377 281 /**
378 - * Modifies the script loader tag for the 'wpdef_captcha_api' handle.
282 + * Maybe validate reCaptcha for lost password.
379 283 *
380 - * @param string $tag The original script loader tag.
381 - * @param string $handle The handle being loaded.
382 - *
383 - * @return string The modified script loader tag.
284 + * @return bool
285 + * @since 3.2.0
384 286 */
385 - public function script_loader_tag( string $tag, string $handle ) {
386 - if ( 'wpdef_captcha_api' === $handle ) {
387 - $tag = str_replace( ' src', ' data-cfasync="false" async="async" defer="defer" src', $tag );
388 - }
287 + protected function maybe_validate_captcha_for_lostpassword(): bool {
288 + $post_data = defender_get_data_from_request( null, 'p' );
289 + $action = $post_data['action'] ?? '';
389 290
390 - return $tag;
291 + return ! $this->is_woocommerce_page() && ! isset( $post_data['wc_reset_password'], $post_data['user_login'] ) && ! ( is_admin() && 'send-password-reset' === $action ) && 'pp_ajax_passwordreset' !== $action;
391 292 }
392 293
393 294 /**
394 - * Returns the API URL for reCAPTCHA based on the recaptcha_type property.
395 - *
396 - * @return string The API URL for reCAPTCHA. Returns an empty string if the recaptcha_type is not set.
397 - */
398 - protected function get_api_url(): string {
399 - if ( isset( $this->recaptcha_type ) ) {
400 - if ( 'v3_recaptcha' === $this->recaptcha_type ) {
401 - return sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s&render=%s', $this->language, $this->public_key );
402 - } elseif ( 'turnstile' === $this->recaptcha_type ) {
403 - return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=wpdefTurnstileCallback';
404 - } elseif ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) {
405 - return sprintf( 'https://www.google.com/recaptcha/api.js?hl=%s&render=explicit', $this->language );
406 - }
407 - }
408 -
409 - return '';
410 - }
411 -
412 - /**
413 - * Enqueues the necessary scripts for the reCAPTCHA frontend.
414 - *
415 - * @return void
416 - */
417 - public function add_captcha_scripts(): void {
418 - if ( isset( $this->recaptcha_type ) ) {
419 - $this->service->remove_duplicate_captcha_scripts();
420 - }
421 - if ( 'turnstile' === $this->recaptcha_type ) {
422 - $handle = 'wpdef_turnstile_script';
423 - $src_url = 'assets/js/turnstile.js';
424 - } else {
425 - $handle = 'wpdef_recaptcha_script';
426 - $src_url = 'assets/js/recaptcha_frontend.js';
427 - }
428 - if ( ! wp_script_is( $handle, 'enqueued' ) ) {
429 - wp_enqueue_script(
430 - $handle,
431 - plugins_url( $src_url, WP_DEFENDER_FILE ),
432 - array(
433 - 'jquery',
434 - 'wpdef_captcha_api',
435 - ),
436 - DEFENDER_VERSION,
437 - true
438 - );
439 - }
440 -
441 - // @since 2.5.6
442 - do_action( 'wd_recaptcha_extra_assets' );
443 -
444 - $error_text =
445 - sprintf(
446 - /* translators: 1: Captcha Type, 2: Captcha Type */
447 - esc_html__( 'More than one %1$s has been found in the current form. Please remove all unnecessary %2$s fields to make it work properly.', 'defender-security' ),
448 - 'turnstile' === $this->recaptcha_type ? 'turnstile' : 'reCAPTCHA',
449 - 'turnstile' === $this->recaptcha_type ? 'turnstile' : 'reCAPTCHA'
450 - );
451 - $options = array(
452 - 'size' => $this->recaptcha_size,
453 - 'sitekey' => $this->public_key,
454 - 'error' => sprintf( '<strong>%s</strong>:&nbsp;%s', esc_html__( 'Warning', 'defender-security' ), $error_text ),
455 - // For default comment form.
456 - 'disable' => '',
457 - );
458 - if ( 'turnstile' === $this->recaptcha_type ) {
459 - $options['theme'] = $this->recaptcha_theme;
460 - $options['lang'] = $this->language;
461 - } else {
462 - $options['version'] = $this->recaptcha_type;
463 - $options['hl'] = $this->language;
464 - if ( 'v2_checkbox' === $this->recaptcha_type ) {
465 - $options['theme'] = $this->recaptcha_theme;
466 - }
467 - }
468 -
469 - wp_localize_script(
470 - $handle,
471 - 'WPDEF',
472 - array(
473 - 'options' => $options,
474 - 'vars' => array(
475 - 'visibility' => ( 'login_footer' === current_filter() ),
476 - ),
477 - )
478 - );
479 - }
480 -
481 - /**
482 - * Add scripts when comments are lazy loaded.
483 - *
484 - * @return void
485 - * @since 2.6.1
486 - */
487 - public function add_scripts_for_lazy_load(): void {
488 - if (
489 - in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true )
490 - && ( is_single() || is_page() )
491 - && comments_open()
492 - ) {
493 - if ( ! wp_script_is( 'wpdef_captcha_api', 'registered' ) ) {
494 - $api_url = $this->get_api_url();
495 - $deps = array( 'jquery' );
496 - wp_register_script( 'wpdef_captcha_api', $api_url, $deps, DEFENDER_VERSION, true );
497 - }
498 -
499 - $this->add_captcha_scripts();
500 - }
501 - }
502 -
503 - /**
504 - * Display the CAPTCHA field.
505 - *
506 - * @return void
507 - */
508 - public function display_login_captcha(): void {
509 - if ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'turnstile' ), true ) ) {
510 - $from_width = 302; ?>
511 - <style media="screen">
512 - .login-action-login #loginform,
513 - .login-action-lostpassword #lostpasswordform,
514 - .login-action-register #registerform {
515 - width: <?php echo (int) $from_width; ?>px !important;
516 - }
517 -
518 - #login_error,
519 - .message {
520 - width: <?php echo (int) $from_width + 20; ?>px !important;
521 - }
522 -
523 - .login-action-login #loginform .captcha_wrap,
524 - .login-action-lostpassword #lostpasswordform .captcha_wrap,
525 - .login-action-register #registerform .captcha_wrap {
526 - margin-bottom: 10px;
527 - }
528 -
529 - #group-create-body .captcha_wrap {
530 - margin-top: 15px;
531 - }
532 - </style>
533 - <?php
534 - } elseif ( 'v2_invisible' === $this->recaptcha_type ) {
535 - ?>
536 - <style>
537 - .login-action-lostpassword #lostpasswordform .captcha_wrap,
538 - .login-action-login #loginform .captcha_wrap,
539 - .login-action-register #registerform .captcha_wrap {
540 - margin-bottom: 10px;
541 - }
542 -
543 - #signup-content .captcha_wrap,
544 - #group-create-body .captcha_wrap {
545 - margin-top: 10px;
546 - }
547 - </style>
548 - <?php
549 - }
550 - $allowed_html = array(
551 - 'div' => array(
552 - 'class' => array(),
553 - 'id' => array(),
554 - 'style' => array(),
555 - ),
556 - 'iframe' => array(
557 - 'src' => array(),
558 - 'frameborder' => array(),
559 - 'scrolling' => array(),
560 - 'style' => array(),
561 - ),
562 - 'noscript' => array(),
563 - 'textarea' => array(
564 - 'name' => array( 'g-recaptcha-response' ),
565 - 'class' => array( 'g-recaptcha-response' ),
566 - 'style' => array(),
567 - ),
568 - 'input' => array(
569 - 'type' => array( 'hidden' ),
570 - 'class' => array( 'g-recaptcha-response' ),
571 - 'name' => array( 'g-recaptcha-response' ),
572 - ),
573 - );
574 - echo wp_kses( $this->display_recaptcha(), $allowed_html );
575 - }
576 -
577 - /**
578 - * Display the output of the recaptcha.
579 - *
580 - * @return string
581 - */
582 - protected function display_recaptcha(): string {
583 - $deps = null;
584 - $content = '<div class="captcha_wrap wpdef_captcha_' . $this->recaptcha_type . '">';
585 - if ( ! $this->private_key || ! $this->public_key || '' === $this->recaptcha_type ) {
586 - // Display nothing.
587 - $content .= '</div>';
588 -
589 - return $content;
590 - }
591 -
592 - $api_url = $this->get_api_url();
593 -
594 - // Generate random id value if there's content with pagination plugin for not getting duplicate id values.
595 - $id = Crypt::random_int( 0, mt_getrandmax() );
596 - if ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) {
597 - $content .= '<div id="wpdef_recaptcha_' . $id . '" class="wpdef_recaptcha"></div>
598 - <noscript>
599 - <div style="width: 302px;">
600 - <div style="width: 302px; height: 422px; position: relative;">
601 - <div style="width: 302px; height: 422px; position: absolute;">
602 - <iframe src="https://www.google.com/recaptcha/api/fallback?k=' . esc_attr( $this->public_key ) . '" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
603 - </div>
604 - </div>
605 - <div style="border-style: none; bottom: 12px; left: 25px; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px; height: 60px; width: 300px;">
606 - <textarea name="g-recaptcha-response" class="g-recaptcha-response" style="width: 250px !important; height: 40px !important; border: 1px solid #c1c1c1 !important; margin: 10px 25px !important; padding: 0px !important; resize: none !important;"></textarea>
607 - </div>
608 - </div>
609 - </noscript>';
610 - $deps = array( 'jquery' );
611 - } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
612 - $content .= '<input type="hidden" class="g-recaptcha-response" name="g-recaptcha-response" />';
613 - } elseif ( 'turnstile' === $this->recaptcha_type ) {
614 - $content .= sprintf( '<div id="wpdef_turnstile_%s"></div>', $id );
615 - $content .= '<noscript>';
616 - $content .= '<div style="padding: 10px; border: 1px solid #cccccc; background-color: #f9f9f9;margin-top: 10px;">';
617 - $content .= sprintf( '<p>%s</p>', esc_html__( 'Please enable JavaScript to complete the security verification.', 'defender-security' ) );
618 - $content .= '</div>';
619 - $content .= '</noscript>';
620 - }
621 - $content .= '</div>';
622 -
623 - // Register reCAPTCHA script.
624 - $locations = $this->model->locations;
625 - if ( ! wp_script_is( 'wpdef_captcha_api', 'registered' ) ) {
626 -
627 - if ( 'v3_recaptcha' === $this->recaptcha_type ) {
628 - wp_register_script( 'wpdef_captcha_api', $api_url, array(), DEFENDER_VERSION, false );
629 - } else {
630 - wp_register_script( 'wpdef_captcha_api', $api_url, $deps, $this->service->is_recaptcha_active_provider() ? DEFENDER_VERSION : null, true );
631 - }
632 - add_action( 'wp_footer', array( $this, 'add_captcha_scripts' ) );
633 - if (
634 - in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true )
635 - || in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true )
636 - || in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true )
637 - ) {
638 - add_action( 'login_footer', array( $this, 'add_captcha_scripts' ) );
639 - }
640 - }
641 -
642 - return $content;
643 - }
644 -
645 - /**
646 295 * Check the current page from is from the Woo plugin.
647 296 *
648 297 * @return bool
649 298 */
@@ -662,345 +311,56 @@
662 311 return false;
663 312 }
664 313
665 314 /**
666 - * Verify the captcha code on the Login page.
315 + * Provides data for the frontend.
667 316 *
668 - * @param WP_User|WP_Error $user WP_User or WP_Error object if a previous callback failed authentication.
669 - *
670 - * @return WP_Error|WP_User
317 + * @return array An array of data for the frontend.
671 318 */
672 - public function validate_captcha_field_on_login( $user ) {
673 - // Only Recaptcha type has Integration fields in v5.7.0.
674 - if ( $this->service->is_recaptcha_active_provider() && $this->is_woocommerce_page() ) {
675 - return $user;
676 - }
677 - // Skip check if connecting to XMLRPC.
678 - if ( defined( 'XMLRPC_REQUEST' ) ) {
679 - return $user;
680 - }
681 - // Skip turnstile verification for current sprint plan.
682 - if ( $this->service->should_skip_turnstile_check() ) {
683 - return $user;
684 - }
685 - // Is Recaptcha-request from 'Ultimate Member' plugin?
686 - if ( $this->service->is_recaptcha_active_provider() &&
687 - ! empty( defender_get_data_from_request( 'um_request', 'p' ) )
688 - && function_exists( 'um_recaptcha_validate' )
689 - ) {
690 - return $user;
691 - }
692 -
693 - if ( ! $this->captcha_response( 'default_login' ) ) {
694 - if ( is_wp_error( $user ) ) {
695 - $user->add( 'invalid_captcha', $this->service->error_message() );
696 -
697 - return $user;
698 - }
699 - return new WP_Error( 'invalid_captcha', $this->service->error_message() );
700 - }
701 -
702 - return $user;
703 - }
704 -
705 - /**
706 - * Verify the recaptcha code on the Registration page.
707 - *
708 - * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
709 - *
710 - * @return WP_Error
711 - */
712 - public function validate_captcha_field_on_registration( WP_Error $errors ) {
713 - // Skip check if connecting to XMLRPC.
714 - if ( defined( 'XMLRPC_REQUEST' ) ) {
715 - return $errors;
716 - }
717 -
718 - if ( ! $this->captcha_response( 'default_registration' ) ) {
719 - $errors->add( 'invalid_captcha', $this->service->error_message() );
720 - }
721 - $_POST['g-recaptcha-response-check'] = true;
722 -
723 - return $errors;
724 - }
725 -
726 - /**
727 - * Add captcha to the multisite signup form.
728 - *
729 - * @param WP_Error $errors A WP_Error object possibly containing 'blogname' or 'blog_title' errors.
730 - *
731 - * @return void
732 - */
733 - public function display_signup_captcha( WP_Error $errors ): void {
734 - $error_message = $errors->get_error_message( 'invalid_captcha' );
735 - if ( ! empty( $error_message ) ) {
736 - printf( '<p class="error">%s</p>', wp_kses_post( $error_message ) );
737 - }
738 - echo wp_kses_post( $this->display_recaptcha() );
739 - }
740 -
741 - /**
742 - * Verify the recaptcha code on the multisite signup page.
743 - *
744 - * @param array $result An array of errors.
745 - *
746 - * @return array
747 - */
748 - public function validate_captcha_field_on_wpmu_registration( array $result ): array {
749 - global $current_user;
750 - if ( is_admin() && ! defined( 'DOING_AJAX' ) && ! empty( $current_user->data->ID ) ) {
751 - return $result;
752 - }
753 -
754 - if ( ! $this->captcha_response( 'wpmu_registration' ) ) {
755 - if ( isset( $result['errors'] ) && ! empty( $result['errors'] ) ) {
756 - $errors = $result['errors'];
757 - } else {
758 - $errors = new WP_Error();
759 - }
760 - $errors->add( 'invalid_captcha', $this->service->error_message() );
761 - $result['errors'] = $errors;
762 -
763 - return $result;
764 - }
765 -
766 - return $result;
767 - }
768 -
769 - /**
770 - * Verify the recaptcha code on Woo login page.
771 - *
772 - * @param WP_Error $errors A WP_Error object containing any errors encountered during login.
773 - *
774 - * @return WP_Error
775 - */
776 - public function validate_captcha_field_on_woo_login( WP_Error $errors ) {
777 - // Skip check if connecting to XMLRPC.
778 - if ( defined( 'XMLRPC_REQUEST' ) ) {
779 - return $errors;
780 - }
781 -
782 - if ( ! $this->captcha_response( 'woo_login' ) ) {
783 - // Remove 'Error: ' because Woo has it by default.
784 - $message = str_replace(
785 - sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
786 - '',
787 - $this->service->error_message()
788 - );
789 - $errors->add( 'invalid_captcha', $message );
790 - }
791 -
792 - return $errors;
793 - }
794 -
795 - /**
796 - * Check recaptcha on Woo registration form.
797 - *
798 - * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
799 - *
800 - * @return WP_Error
801 - */
802 - public function validate_captcha_field_on_woo_registration( WP_Error $errors ) {
803 - if ( defined( 'WOOCOMMERCE_CHECKOUT' ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
804 - return $errors;
805 - }
806 - if ( ! $this->captcha_response( 'woo_registration' ) ) {
807 - // Remove 'Error: ' because Woo has it by default.
808 - $message = str_replace(
809 - sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
810 - '',
811 - $this->service->error_message()
812 - );
813 - $errors->add( 'invalid_captcha', $message );
814 - }
815 -
816 - return $errors;
817 - }
818 -
819 - /**
820 - * Fires before errors are returned from a password reset request.
821 - * Without 2nd `$user_data` parameter because it's since WP 5.4.0.
822 - *
823 - * @param WP_Error $errors A WP_Error object containing any errors encountered during password reset.
824 - *
825 - * @return void
826 - */
827 - public function validate_captcha_field_on_lostpassword( WP_Error $errors ): void {
828 - if ( ! $this->captcha_response( 'default_lost_password' ) ) {
829 - $errors->add( 'invalid_captcha', $this->service->error_message() );
830 - }
831 - }
832 -
833 - /**
834 - * Validates the reCAPTCHA field on the WooCommerce checkout form.
835 - *
836 - * @param array $fields The fields of the checkout form.
837 - * @param WP_Error $errors The errors encountered during the checkout process.
838 - *
839 - * @return WP_Error The updated errors with the reCAPTCHA validation result.
840 - */
841 - public function validate_captcha_field_on_woo_checkout( $fields, $errors ): WP_Error {
842 - if ( ! $this->captcha_response( 'woo_checkout' ) ) {
843 - // Remove 'Error: ' because Woo has it by default.
844 - $message = str_replace(
845 - sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
846 - '',
847 - $this->service->error_message()
848 - );
849 - $errors->add( 'invalid_captcha', $message );
850 - }
851 -
852 - return $errors;
853 - }
854 -
855 - /**
856 - * Display google recaptcha on comments form.
857 - *
858 - * @param array $defaults The default comment form arguments.
859 - *
860 - * @return array
861 - */
862 - public function comment_form_defaults( array $defaults ) {
863 - $defaults['comment_notes_after'] .= '<p>' . $this->display_recaptcha() . '</p>';
864 -
865 - return $defaults;
866 - }
867 -
868 - /**
869 - * Check JS enabled for comment form.
870 - *
871 - * @param int $comment_post_id Post ID.
872 - *
873 - * @return void
874 - */
875 - public function validate_captcha_field_on_comment( $comment_post_id ) {
876 - if ( $this->service->exclude_captcha_for_requests() ) {
877 - return;
878 - }
879 - // Skip if it's from WooCommerce review form.
880 - if ( 'product' === get_post_type( $comment_post_id ) ) {
881 - return;
882 - }
883 -
884 - if ( ! $this->captcha_response( 'default_comments' ) ) {
885 - // @since v2.5.6
886 - wp_die(
887 - wp_kses_post(
888 - apply_filters(
889 - 'wd_recaptcha_require_valid_comment',
890 - $this->service->error_message()
891 - )
892 - )
893 - );
894 - }
895 - }
896 -
897 - /**
898 - * Validates the reCAPTCHA response for a given form.
899 - *
900 - * @param string $form The form being verified.
901 - *
902 - * @return bool The result of the reCAPTCHA verification.
903 - * @since 2.5.6
904 - */
905 - protected function captcha_response( string $form ): bool {
906 - $response_field = 'turnstile' === $this->recaptcha_type ? 'wpdef-turnstile-response' : 'g-recaptcha-response';
907 - $response = stripslashes( defender_get_data_from_request( $response_field, 'p' ) );
908 - if ( empty( $this->private_key ) || empty( $response ) ) {
909 - return false;
910 - }
911 - // reCAPTCHA response post data.
912 - $remote_ip = filter_var( defender_get_data_from_request( 'REMOTE_ADDR', 's' ), FILTER_VALIDATE_IP );
913 - /**
914 - * Filters to Get the remote IP address.
915 - *
916 - * @param mixed $remote_ip The remote IP address.
917 - *
918 - * @since 2.5.6
919 - */
920 - $remote_ip = (string) apply_filters( 'wd_recaptcha_remote_ip', $remote_ip );
921 -
922 - $post_body = array(
923 - 'secret' => $this->private_key,
924 - 'response' => $response,
925 - 'remoteip' => $remote_ip,
319 + public function data_frontend(): array {
320 + $model = $this->model;
321 + $is_active = $model->is_active();
322 + $notice_data = $this->service->get_provider()->get_notice_data(
323 + $this->is_woo_activated,
324 + $this->is_buddypress_activated
926 325 );
927 326
928 - $result = $this->service->captcha_post_request( $post_body );
929 -
930 327 /**
931 - * Filters to check the result of a reCAPTCHA verification.
932 - *
933 - * @param mixed $result The result of the reCAPTCHA verification.
934 - * @param mixed $form The form being verified.
935 - *
936 - * @since 2.5.6
328 + * Cases:
329 + * Invalid domain for Site Key,
330 + * Google ReCAPTCHA is in localhost,
331 + * Cannot contact reCAPTCHA. Check your connection.
937 332 */
938 - return apply_filters( 'wd_recaptcha_check_result', $result, $form );
939 - }
333 + $ticket_text = esc_html__( 'If you see any errors in the preview, make sure the keys you’ve entered are valid, and you\'ve listed your domain name while generating the keys.', 'defender-security' );
940 334
941 - /**
942 - * Display the BuddyPress reCAPTCHA.
943 - *
944 - * @return void
945 - */
946 - public function display_buddypress_recaptcha(): void {
947 - if ( ! empty( buddypress()->signup->errors['failed_recaptcha_verification'] ) ) {
948 - $output = '<div class="error">';
949 - $output .= buddypress()->signup->errors['failed_recaptcha_verification'];
950 - $output .= '</div>';
951 -
952 - echo wp_kses_post( $output );
335 + if ( ( new WPMUDEV() )->show_support_links() ) {
336 + $ticket_text .= defender_support_ticket_text();
953 337 }
954 - echo wp_kses_post( $this->display_recaptcha() );
955 - }
956 338
957 - /**
958 - * Validates the reCAPTCHA field on the BuddyPress registration form.
959 - *
960 - * @return void
961 - */
962 - public function validate_captcha_field_on_buddypress_registration(): void {
963 - if ( ! $this->captcha_response( 'buddypress_registration' ) ) {
964 - buddypress()->signup->errors['failed_recaptcha_verification'] = $this->service->error_message();
965 - }
339 + return array_merge(
340 + array(
341 + 'model' => $model->export(),
342 + 'active_captcha' => $model->get_active_captcha_data(),
343 + 'is_active' => $is_active,
344 + 'default_message' => $this->model->get_default_values()['message'],
345 + 'default_turnstile_message' => $this->model->get_default_values()['turnstile_message'],
346 + 'default_locations' => Captcha_Component::get_forms(),
347 + 'notice_type' => $notice_data['notice_type'],
348 + 'notice_text' => $notice_data['notice_text'],
349 + 'ticket_text' => $ticket_text,
350 + 'is_woo_active' => $this->woo->is_activated(),
351 + 'woo_locations' => Woocommerce::get_forms(),
352 + 'is_buddypress_active' => $this->buddypress->is_activated(),
353 + 'buddypress_locations' => Buddypress::get_forms(),
354 + ),
355 + $this->dump_routes_and_nonces()
356 + );
966 357 }
967 358
968 359 /**
969 - * Verify BuddyPress group form captcha.
970 - *
971 - * @return bool|void
972 - */
973 - public function validate_captcha_field_on_buddypress_group() {
974 - if ( ! bp_is_group_creation_step( 'group-details' ) ) {
975 - return false;
976 - }
977 -
978 - if ( ! $this->captcha_response( 'buddypress_create_group' ) ) {
979 - bp_core_add_message( $this->service->error_message(), 'error' );
980 - bp_core_redirect( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/create/step/group-details/' );
981 - } else {
982 - return false;
983 - }
984 - }
985 -
986 - /**
987 - * Provide data to the frontend via localized script.
988 - *
989 - * @param array $data Data collection is ready to passed.
990 - *
991 - * @return array Modified data array with added this controller data.
992 - */
993 - public function script_data( array $data ): array {
994 - $data['captcha'] = $this->data_frontend();
995 -
996 - return $data;
997 - }
998 -
999 - /**
1000 360 * Save settings.
1001 361 *
1002 - * @param Request $request The request object containing new settings data.
362 + * @param Request $request The request object containing new settings data.
1003 363 *
1004 364 * @return Response
1005 365 * @defender_route
1006 366 */
@@ -1006,33 +366,10 @@
1006 366 */
1007 367 public function save_settings( Request $request ): Response {
1008 368 $data = $request->get_data_by_model( $this->model );
1009 369 $this->model->import( $data );
1010 - if ( ! $this->service->is_recaptcha_active_provider() ) {
1011 - $valid_token = $request->get_data(
1012 - array(
1013 - 'token' => array(
1014 - 'type' => 'string',
1015 - ),
1016 - )
1017 - );
1018 - $token = $valid_token['token'] ?? '';
1019 - $is_valid = $this->service->verify_turnstile_key( $token );
1020 - if ( $is_valid ) {
1021 - $this->model->active_type = 'turnstile';
1022 - } else {
1023 - return new Response(
1024 - false,
1025 - // Merge stored data to avoid errors.
1026 - array_merge(
1027 - array(
1028 - 'message' => __( 'Please make sure your Cloudflare API keys are correct and that Preview Widget verification is successful.', 'defender-security' ),
1029 - 'error_keys' => array( 'data_turnstile.secret' ),
1030 - ),
1031 - $this->data_frontend()
1032 - )
1033 - );
1034 - }
370 + if ( Captcha_Model::TURNSTILE === $this->model->provider ) {
371 + $this->model->active_type = Captcha_Model::TURNSTILE;
1035 372 }
1036 373 if ( $this->model->validate() ) {
1037 374 $this->model->save();
1038 375 Config_Hub_Helper::set_clear_active_flag();
@@ -1062,150 +399,75 @@
1062 399 );
1063 400 }
1064 401
1065 402 /**
1066 - * Load the CAPTCHA preview based on the provided request data.
403 + * Verify a CAPTCHA response token.
1067 404 *
1068 - * @param Request $request The request object containing the data needed to load the preview.
405 + * @param Request $request The request object containing token and key data.
1069 406 *
1070 - * @return Response The response object with the preview HTML and status.
407 + * @return Response
1071 408 * @defender_route
1072 409 */
1073 - public function load_captcha_preview( Request $request ): Response {
1074 - $onload = null;
1075 - $js = null;
1076 - $data = $request->get_data(
410 + public function verify_response_token( Request $request ): Response {
411 + $data = $request->get_data(
1077 412 array(
1078 - 'captcha_type' => array(
1079 - 'type' => 'string',
413 + 'token' => array(
414 + 'type' => 'string',
415 + 'sanitize' => 'sanitize_text_field',
1080 416 ),
417 + 'key' => array(
418 + 'type' => 'string',
419 + 'sanitize' => 'sanitize_text_field',
420 + ),
1081 421 )
1082 422 );
1083 - $this->recaptcha_type = $data['captcha_type'];
1084 423
1085 - $model = $this->model;
1086 -
1087 - if ( 'turnstile' === $this->recaptcha_type ) {
424 + $token = $data['token'] ?? '';
425 + $key = $data['key'] ?? '';
426 + if ( '' === $token || '' === $key ) {
1088 427 return new Response(
1089 - $model->is_active(),
428 + false,
1090 429 array(
1091 - 'preview' => $model->is_active(),
430 + 'message' => esc_html__( 'Missing CAPTCHA verification data.', 'defender-security' ),
1092 431 )
1093 432 );
1094 433 }
1095 - $language = ! empty( $model->language ) && 'automatic' !== $model->language ? $model->language : get_locale();
1096 434
1097 - $notice = '<div class="sui-notice sui-notice-default">';
1098 - $notice .= '<div class="sui-notice-content">';
1099 - $notice .= '<div class="sui-notice-message">';
1100 - $notice .= '<i class="sui-notice-icon sui-icon-info sui-md" aria-hidden="true"></i>';
1101 - $notice .= '<p>' . esc_html__( 'Save your API keys to load the reCAPTCHA preview.', 'defender-security' ) . '</p>';
1102 - $notice .= '</div>';
1103 - $notice .= '</div>';
1104 - $notice .= '</div>';
435 + $provider = $this->service->get_provider();
436 + $response = $provider->verify_response_token(
437 + $token,
438 + $provider->get_verify_url(),
439 + array(
440 + 'secret' => $key,
441 + )
442 + );
1105 443
1106 - $theme = 'light';
1107 - $captcha_size = 'invisible';
1108 - $data_args = '';
1109 - if ( 'v2_checkbox' === $this->recaptcha_type ) {
1110 - $this->public_key = $model->data_v2_checkbox['key'];
1111 - $theme = $model->data_v2_checkbox['style'];
1112 - $captcha_size = $model->data_v2_checkbox['size'];
1113 - $onload = 'defender_render_admin_captcha_v2';
1114 - // Onload method for Recaptcha works only for js 'var'.
1115 - $js = "<script>var defender_render_admin_captcha_v2 = function () {
1116 - setTimeout( function () {
1117 - var captcha_v2 = jQuery( '.defender-g-recaptcha-v2_checkbox' ),
1118 - sitekey_v2 = captcha_v2.data('sitekey'),
1119 - theme_v2 = captcha_v2.data('theme'),
1120 - size_v2 = captcha_v2.data('size');
1121 - if (captcha_v2[0] && captcha_v2[0].hasChildNodes()) {
1122 - return;
1123 - }
1124 - window.grecaptcha.render( captcha_v2[0], {
1125 - sitekey: sitekey_v2,
1126 - theme: theme_v2,
1127 - size: size_v2,
1128 - 'error-callback': function() {
1129 - jQuery('#v2_checkbox_notice_1').hide();
1130 - jQuery('#v2_checkbox_notice_2').show();
1131 - }
1132 - } );
1133 - }, 100 );
1134 - };</script>";
1135 - } elseif ( 'v2_invisible' === $this->recaptcha_type ) {
1136 - $this->public_key = $model->data_v2_invisible['key'];
1137 - $onload = 'defender_render_admin_captcha_v2_invisible';
1138 - $data_args = 'data-badge="inline" data-callback="setResponse"';
1139 - $js = "<script>var defender_render_admin_captcha_v2_invisible = function () {
1140 - setTimeout( function () {
1141 - var captcha = jQuery( '.defender-g-recaptcha-v2_invisible' ),
1142 - sitekey = captcha.data('sitekey'),
1143 - theme = captcha.data('theme'),
1144 - size = captcha.data('size');
1145 - if (captcha[0] && captcha[0].hasChildNodes()) {
1146 - return;
1147 - }
1148 - window.grecaptcha.render( captcha[0], {
1149 - sitekey: sitekey,
1150 - theme: theme,
1151 - size: size,
1152 - badge: 'inline',
1153 - 'error-callback': function() {
1154 - jQuery('#v2_invisible_notice_1').hide();
1155 - jQuery('#v2_invisible_notice_2').show();
1156 - }
1157 - } );
1158 - }, 100 );
1159 - };</script>";
1160 - } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
1161 - $this->public_key = $model->data_v3_recaptcha['key'];
1162 - $onload = 'defender_render_admin_captcha_v3';
1163 - $js = "<script>var defender_render_admin_captcha_v3 = function () {
1164 - setTimeout( function () {
1165 - var captcha = jQuery( '.defender-g-recaptcha-v3_recaptcha' ),
1166 - sitekey = captcha.data('sitekey'),
1167 - theme = captcha.data('theme'),
1168 - size = captcha.data('size');
1169 - if (captcha[0] && captcha[0].hasChildNodes()) {
1170 - return;
1171 - }
1172 - window.grecaptcha.render( captcha[0], {
1173 - sitekey: sitekey,
1174 - theme: theme,
1175 - size: size,
1176 - badge: 'inline',
1177 - 'error-callback': function() {
1178 - jQuery('#v3_recaptcha_notice_1').hide();
1179 - jQuery('#v3_recaptcha_notice_2').show();
1180 - }
1181 - } );
1182 - }, 100 );
1183 - };</script>";
444 + $success = (bool) ( $response['success'] ?? false );
445 + if ( $success ) {
446 + if ( Captcha_Model::TURNSTILE === $this->model->provider ) {
447 + $data = $this->model->data_turnstile;
448 + $data['verified'] = true;
449 + $this->model->data_turnstile = $data;
450 + } elseif ( 'v2_checkbox' === $this->model->active_type ) {
451 + $data = $this->model->data_v2_checkbox;
452 + $data['verified'] = true;
453 + $this->model->data_v2_checkbox = $data;
454 + } elseif ( 'v2_invisible' === $this->model->active_type ) {
455 + $data = $this->model->data_v2_invisible;
456 + $data['verified'] = true;
457 + $this->model->data_v2_invisible = $data;
458 + } elseif ( 'v3_recaptcha' === $this->model->active_type ) {
459 + $data = $this->model->data_v3_recaptcha;
460 + $data['verified'] = true;
461 + $this->model->data_v3_recaptcha = $data;
462 + }
463 + $this->model->save();
1184 464 }
1185 465
1186 - $html = '';
1187 - if ( isset( $this->recaptcha_type ) && ! empty( $this->public_key ) ) {
1188 - // Google reCAPTCHA script must be connected with dynamic parameters (onload, render, etc.).
1189 - $html .= '<script src="https://www.google.com/recaptcha/api.js?hl=' . esc_attr( $language ) . '&render=explicit&onload=' . $onload . '" async defer></script>' . $js; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
1190 -
1191 - $html .= sprintf(
1192 - '<div class="%s" data-sitekey="%s" data-theme="%s" data-size="%s" %s></div>',
1193 - 'defender-g-recaptcha-' . $this->recaptcha_type,
1194 - esc_attr( $this->public_key ),
1195 - esc_attr( $theme ),
1196 - esc_attr( $captcha_size ),
1197 - $data_args
1198 - );
1199 - } else {
1200 - $html .= $notice;
1201 - }
1202 -
1203 466 return new Response(
1204 - true,
467 + $success,
1205 468 array(
1206 - 'preview' => true,
1207 - 'html' => $html,
469 + 'success' => $success,
1208 470 )
1209 471 );
1210 472 }
1211 473
@@ -1231,93 +493,8 @@
1231 493 return array();
1232 494 }
1233 495
1234 496 /**
1235 - * Provides data for the frontend.
1236 - *
1237 - * @return array An array of data for the frontend.
1238 - */
1239 - public function data_frontend(): array {
1240 - $model = $this->model;
1241 - $is_active = $model->is_active();
1242 - /**
1243 - * Different cases for entered keys and locations:
1244 - * success - one default, Woo or BuddyPress location is checked at least,
1245 - * warning - default, Woo and BuddyPress locations are unchecked,
1246 - * warning - default location is unchecked, also Woo and BuddyPress is deactivated,
1247 - * warning - non-entered keys.
1248 - */
1249 - if ( $is_active ) {
1250 - if ( $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) {
1251 - $notice_type = 'success';
1252 - $notice_text = false;
1253 - } elseif ( ! $this->is_woo_activated && ! $this->is_buddypress_activated && ! $model->enable_default_location() ) {
1254 - $notice_type = 'warning';
1255 - $notice_text = sprintf(
1256 - /* translators: 1. Captcha Provider name. 2. Captcha Type */
1257 - esc_html__( '%1$s is currently inactive for all forms. You can deploy %2$s for specific forms in the reCAPTCHA Locations below.', 'defender-security' ),
1258 - 'turnstile' === $model->provider ? esc_html__( 'Cloudflare Turnstile', 'defender-security' ) : esc_html__( 'Google reCAPTCHA', 'defender-security' ),
1259 - 'turnstile' === $model->provider ? esc_html__( 'Turnstile', 'defender-security' ) : esc_html__( 'reCAPTCHA', 'defender-security' ),
1260 - );
1261 - } elseif (
1262 - ! $model->enable_default_location()
1263 - && (
1264 - ( $this->is_woo_activated && ! $model->enable_woo_location() )
1265 - || ( $this->is_buddypress_activated && ! $model->enable_buddypress_location() )
1266 - )
1267 - ) {
1268 - $notice_type = 'warning';
1269 - $notice_text = sprintf(
1270 - /* translators: 1. reCAPTCHA & Woo location. 2. BuddyPress location. */
1271 - esc_html__( 'Google reCAPTCHA is currently inactive for all forms. You can deploy reCAPTCHA for specific forms in the %1$s or %2$s settings below.', 'defender-security' ),
1272 - '<b>' . esc_html__( 'reCAPTCHA Locations', 'defender-security' ) . '</b>, <b>' . esc_html__( 'WooCommerce', 'defender-security' ) . '</b>',
1273 - '<b>' . esc_html__( 'BuddyPress', 'defender-security' ) . '</b>'
1274 - );
1275 - }
1276 - } else {
1277 - // Inactive case.
1278 - $notice_type = 'error';
1279 - $notice_text = esc_html__(
1280 - 'Configure a CAPTCHA provider to verify users and prevent automated bots. Both reCAPTCHA and Cloudflare Turnstile are supported.',
1281 - 'defender-security'
1282 - );
1283 - }
1284 -
1285 - /**
1286 - * Cases:
1287 - * Invalid domain for Site Key,
1288 - * Google ReCAPTCHA is in localhost,
1289 - * Cannot contact reCAPTCHA. Check your connection.
1290 - */
1291 - $ticket_text = esc_html__(
1292 - 'If you see any errors in the preview, make sure the keys you’ve entered are valid, and you\'ve listed your domain name while generating the keys.',
1293 - 'defender-security'
1294 - );
1295 -
1296 - if ( ( new WPMUDEV() )->show_support_links() ) {
1297 - $ticket_text .= defender_support_ticket_text();
1298 - }
1299 -
1300 - return array_merge(
1301 - array(
1302 - 'model' => $model->export(),
1303 - 'is_active' => $is_active,
1304 - 'default_message' => $this->default_msg,
1305 - 'default_turnstile_message' => $this->model->get_default_values()['turnstile_message'],
1306 - 'default_locations' => Captcha_Component::get_forms(),
1307 - 'notice_type' => $notice_type,
1308 - 'notice_text' => $notice_text,
1309 - 'ticket_text' => $ticket_text,
1310 - 'is_woo_active' => $this->is_woo_activated,
1311 - 'woo_locations' => Woocommerce::get_forms(),
1312 - 'is_buddypress_active' => $this->is_buddypress_activated,
1313 - 'buddypress_locations' => Buddypress::get_forms(),
1314 - ),
1315 - $this->dump_routes_and_nonces()
1316 - );
1317 - }
1318 -
1319 - /**
1320 497 * Provides data for the dashboard widget.
1321 498 *
1322 499 * @return array An array of dashboard widget data.
1323 500 */
@@ -1322,16 +499,9 @@
1322 499 * @return array An array of dashboard widget data.
1323 500 */
1324 501 public function dashboard_widget(): array {
1325 502 $model = $this->model;
1326 - $notice_type = ( $model->is_active()
1327 - && $this->service->enable_any_location(
1328 - $this->is_woo_activated,
1329 - $this->is_buddypress_activated
1330 - )
1331 - )
1332 - ? 'success'
1333 - : 'warning';
503 + $notice_type = ( $model->is_active() && $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) ? 'success' : 'warning';
1334 504
1335 505 return array(
1336 506 'model' => $model->export(),
1337 507 'notice_type' => $notice_type,
@@ -1340,9 +510,9 @@
1340 510
1341 511 /**
1342 512 * Imports data into the model.
1343 513 *
1344 - * @param array $data Data to be imported into the model.
514 + * @param array $data Data to be imported into the model.
1345 515 */
1346 516 public function import_data( array $data ) {
1347 517 $model = $this->model;
1348 518
@@ -1361,27 +531,11 @@
1361 531 return array( $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ) );
1362 532 }
1363 533
1364 534 /**
1365 - * Maybe validate reCaptcha for lost password.
1366 - *
1367 - * @return bool
1368 - * @since 3.2.0
1369 - */
1370 - protected function maybe_validate_captcha_for_lostpassword(): bool {
1371 - $post_data = defender_get_data_from_request( null, 'p' );
1372 - $action = $post_data['action'] ?? '';
1373 -
1374 - return ! $this->is_woocommerce_page() &&
1375 - ! isset( $post_data['wc_reset_password'], $post_data['user_login'] ) &&
1376 - ! ( is_admin() && 'send-password-reset' === $action ) &&
1377 - 'pp_ajax_passwordreset' !== $action;
1378 - }
1379 -
1380 - /**
1381 535 * Enable/disable module.
1382 536 *
1383 - * @param Request $request The request object.
537 + * @param Request $request The request object.
1384 538 *
1385 539 * @return Response
1386 540 * @defender_route
1387 541 * @since 3.12.0