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 -1065 5.8.1 → 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 = '' !== $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,304 +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 esc_attr( $from_width ); ?>px !important;
516 - }
517 -
518 - #login_error,
519 - .message {
520 - width: <?php echo esc_attr( $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 - echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
551 - }
552 -
553 - /**
554 - * Display the output of the recaptcha.
555 - *
556 - * @return string
557 - */
558 - protected function display_recaptcha(): string {
559 - $deps = null;
560 - $content = '<div class="captcha_wrap wpdef_captcha_' . $this->recaptcha_type . '">';
561 - if ( ! $this->private_key || ! $this->public_key || '' === $this->recaptcha_type ) {
562 - // Display nothing.
563 - $content .= '</div>';
564 -
565 - return $content;
566 - }
567 -
568 - $api_url = $this->get_api_url();
569 -
570 - // Generate random id value if there's content with pagination plugin for not getting duplicate id values.
571 - $id = Crypt::random_int( 0, mt_getrandmax() );
572 - if ( in_array( $this->recaptcha_type, array( 'v2_checkbox', 'v2_invisible' ), true ) ) {
573 - $content .= '<div id="wpdef_recaptcha_' . $id . '" class="wpdef_recaptcha"></div>
574 - <noscript>
575 - <div style="width: 302px;">
576 - <div style="width: 302px; height: 422px; position: relative;">
577 - <div style="width: 302px; height: 422px; position: absolute;">
578 - <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>
579 - </div>
580 - </div>
581 - <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;">
582 - <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>
583 - </div>
584 - </div>
585 - </noscript>';
586 - $deps = array( 'jquery' );
587 - } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
588 - $content .= '<input type="hidden" class="g-recaptcha-response" name="g-recaptcha-response" />';
589 - } elseif ( 'turnstile' === $this->recaptcha_type ) {
590 - $content .= sprintf( '<div id="wpdef_turnstile_%s"></div>', $id );
591 - $content .= '<noscript>';
592 - $content .= '<div style="padding: 10px; border: 1px solid #cccccc; background-color: #f9f9f9;margin-top: 10px;">';
593 - $content .= sprintf( '<p>%s</p>', esc_html__( 'Please enable JavaScript to complete the security verification.', 'defender-security' ) );
594 - $content .= '</div>';
595 - $content .= '</noscript>';
596 - }
597 - $content .= '</div>';
598 -
599 - // Register reCAPTCHA script.
600 - $locations = $this->model->locations;
601 - if ( ! wp_script_is( 'wpdef_captcha_api', 'registered' ) ) {
602 -
603 - if ( 'v3_recaptcha' === $this->recaptcha_type ) {
604 - wp_register_script( 'wpdef_captcha_api', $api_url, array(), DEFENDER_VERSION, false );
605 - } else {
606 - wp_register_script( 'wpdef_captcha_api', $api_url, $deps, $this->service->is_recaptcha_active_provider() ? DEFENDER_VERSION : null, true );
607 - }
608 - add_action( 'wp_footer', array( $this, 'add_captcha_scripts' ) );
609 - if (
610 - in_array( Captcha_Component::DEFAULT_LOGIN_FORM, $locations, true )
611 - || in_array( Captcha_Component::DEFAULT_REGISTER_FORM, $locations, true )
612 - || in_array( Captcha_Component::DEFAULT_LOST_PASSWORD_FORM, $locations, true )
613 - ) {
614 - add_action( 'login_footer', array( $this, 'add_captcha_scripts' ) );
615 - }
616 - }
617 -
618 - return $content;
619 - }
620 -
621 - /**
622 295 * Check the current page from is from the Woo plugin.
623 296 *
624 297 * @return bool
625 298 */
@@ -638,363 +311,56 @@
638 311 return false;
639 312 }
640 313
641 314 /**
642 - * Verify the captcha code on the Login page.
315 + * Provides data for the frontend.
643 316 *
644 - * @param WP_User|WP_Error $user WP_User or WP_Error object if a previous callback failed authentication.
645 - *
646 - * @return WP_Error|WP_User
317 + * @return array An array of data for the frontend.
647 318 */
648 - public function validate_captcha_field_on_login( $user ) {
649 - // Only Recaptcha type has Integration fields in v5.7.0.
650 - if ( $this->service->is_recaptcha_active_provider() && $this->is_woocommerce_page() ) {
651 - return $user;
652 - }
653 - // Skip check if connecting to XMLRPC.
654 - if ( defined( 'XMLRPC_REQUEST' ) ) {
655 - return $user;
656 - }
657 - // Skip turnstile verification for current sprint plan.
658 - if ( $this->service->should_skip_turnstile_check() ) {
659 - return $user;
660 - }
661 - // Is Recaptcha-request from 'Ultimate Member' plugin?
662 - $um_request = defender_get_data_from_request( 'um_request', 'p' );
663 - if ( $this->service->is_recaptcha_active_provider() &&
664 - is_string( $um_request ) &&
665 - '' !== $um_request &&
666 - function_exists( 'um_recaptcha_validate' )
667 - ) {
668 - return $user;
669 - }
670 -
671 - if ( ! $this->captcha_response( 'default_login' ) ) {
672 - if ( is_wp_error( $user ) ) {
673 - $user->add( 'invalid_captcha', $this->service->error_message() );
674 -
675 - return $user;
676 - }
677 - return new WP_Error( 'invalid_captcha', $this->service->error_message() );
678 - }
679 -
680 - return $user;
681 - }
682 -
683 - /**
684 - * Verify the recaptcha code on the Registration page.
685 - *
686 - * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
687 - *
688 - * @return WP_Error
689 - */
690 - public function validate_captcha_field_on_registration( WP_Error $errors ) {
691 - // Skip check if connecting to XMLRPC.
692 - if ( defined( 'XMLRPC_REQUEST' ) ) {
693 - return $errors;
694 - }
695 -
696 - if ( ! $this->captcha_response( 'default_registration' ) ) {
697 - $errors->add( 'invalid_captcha', $this->service->error_message() );
698 - }
699 - $_POST['g-recaptcha-response-check'] = true;
700 -
701 - return $errors;
702 - }
703 -
704 - /**
705 - * Add captcha to the multisite signup form.
706 - *
707 - * @param WP_Error $errors A WP_Error object possibly containing 'blogname' or 'blog_title' errors.
708 - *
709 - * @return void
710 - */
711 - public function display_signup_captcha( WP_Error $errors ): void {
712 - $error_message = $errors->get_error_message( 'invalid_captcha' );
713 - if ( is_string( $error_message ) && '' !== $error_message ) {
714 - printf( '<p class="error">%s</p>', wp_kses_post( $error_message ) );
715 - }
716 - echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
717 - }
718 -
719 - /**
720 - * Verify the recaptcha code on the multisite signup page.
721 - *
722 - * @param array $result An array of errors.
723 - *
724 - * @return array
725 - */
726 - public function validate_captcha_field_on_wpmu_registration( array $result ): array {
727 - global $current_user;
728 - if ( is_admin() && ! defined( 'DOING_AJAX' ) && isset( $current_user->data->ID ) && 0 < (int) $current_user->data->ID ) {
729 - return $result;
730 - }
731 -
732 - // Skip if BuddyPress is handling the registration.
733 - if (
734 - $this->is_buddypress_activated &&
735 - $this->service->is_recaptcha_active_provider() &&
736 - in_array( Buddypress::REGISTER_FORM, $this->model->buddypress_checked_locations, true )
737 - ) {
738 - return $result;
739 - }
740 -
741 - if ( ! $this->captcha_response( 'wpmu_registration' ) ) {
742 - if ( isset( $result['errors'] ) && $result['errors'] instanceof \WP_Error ) {
743 - $errors = $result['errors'];
744 - } else {
745 - $errors = new WP_Error();
746 - }
747 - $errors->add( 'invalid_captcha', $this->service->error_message() );
748 - $result['errors'] = $errors;
749 -
750 - return $result;
751 - }
752 -
753 - return $result;
754 - }
755 -
756 - /**
757 - * Verify the recaptcha code on Woo login page.
758 - *
759 - * @param WP_Error $errors A WP_Error object containing any errors encountered during login.
760 - *
761 - * @return WP_Error
762 - */
763 - public function validate_captcha_field_on_woo_login( WP_Error $errors ) {
764 - // Skip check if connecting to XMLRPC.
765 - if ( defined( 'XMLRPC_REQUEST' ) ) {
766 - return $errors;
767 - }
768 -
769 - if ( ! $this->captcha_response( 'woo_login' ) ) {
770 - // Remove 'Error: ' because Woo has it by default.
771 - $message = str_replace(
772 - sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
773 - '',
774 - $this->service->error_message()
775 - );
776 - $errors->add( 'invalid_captcha', $message );
777 - }
778 -
779 - return $errors;
780 - }
781 -
782 - /**
783 - * Check recaptcha on Woo registration form.
784 - *
785 - * @param WP_Error $errors A WP_Error object containing any errors encountered during registration.
786 - *
787 - * @return WP_Error
788 - */
789 - public function validate_captcha_field_on_woo_registration( WP_Error $errors ) {
790 - if ( defined( 'WOOCOMMERCE_CHECKOUT' ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
791 - return $errors;
792 - }
793 - if ( ! $this->captcha_response( 'woo_registration' ) ) {
794 - // Remove 'Error: ' because Woo has it by default.
795 - $message = str_replace(
796 - sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
797 - '',
798 - $this->service->error_message()
799 - );
800 - $errors->add( 'invalid_captcha', $message );
801 - }
802 -
803 - return $errors;
804 - }
805 -
806 - /**
807 - * Fires before errors are returned from a password reset request.
808 - * Without 2nd `$user_data` parameter because it's since WP 5.4.0.
809 - *
810 - * @param WP_Error $errors A WP_Error object containing any errors encountered during password reset.
811 - *
812 - * @return void
813 - */
814 - public function validate_captcha_field_on_lostpassword( WP_Error $errors ): void {
815 - if ( ! $this->captcha_response( 'default_lost_password' ) ) {
816 - $errors->add( 'invalid_captcha', $this->service->error_message() );
817 - }
818 - }
819 -
820 - /**
821 - * Validates the reCAPTCHA field on the WooCommerce checkout form.
822 - *
823 - * @param array $fields The fields of the checkout form.
824 - * @param WP_Error $errors The errors encountered during the checkout process.
825 - *
826 - * @return WP_Error The updated errors with the reCAPTCHA validation result.
827 - */
828 - public function validate_captcha_field_on_woo_checkout( $fields, $errors ): WP_Error {
829 - if ( ! $this->captcha_response( 'woo_checkout' ) ) {
830 - // Remove 'Error: ' because Woo has it by default.
831 - $message = str_replace(
832 - sprintf( '<strong>%s:</strong> ', esc_html__( 'Error', 'defender-security' ) ),
833 - '',
834 - $this->service->error_message()
835 - );
836 - $errors->add( 'invalid_captcha', $message );
837 - }
838 -
839 - return $errors;
840 - }
841 -
842 - /**
843 - * Display google recaptcha on comments form.
844 - *
845 - * @param array $defaults The default comment form arguments.
846 - *
847 - * @return array
848 - */
849 - public function comment_form_defaults( array $defaults ) {
850 - $defaults['comment_notes_after'] .= '<p>' . $this->display_recaptcha() . '</p>';
851 -
852 - return $defaults;
853 - }
854 -
855 - /**
856 - * Check JS enabled for comment form.
857 - *
858 - * @param int $comment_post_id Post ID.
859 - *
860 - * @return void
861 - */
862 - public function validate_captcha_field_on_comment( $comment_post_id ) {
863 - if ( $this->service->exclude_captcha_for_requests() ) {
864 - return;
865 - }
866 - // Skip if it's from WooCommerce review form.
867 - if ( 'product' === get_post_type( $comment_post_id ) ) {
868 - return;
869 - }
870 -
871 - if ( ! $this->captcha_response( 'default_comments' ) ) {
872 - // @since v2.5.6
873 - wp_die(
874 - wp_kses_post(
875 - apply_filters(
876 - 'wd_recaptcha_require_valid_comment',
877 - $this->service->error_message()
878 - )
879 - )
880 - );
881 - }
882 - }
883 -
884 - /**
885 - * Validates the reCAPTCHA response for a given form.
886 - *
887 - * @param string $form The form being verified.
888 - *
889 - * @return bool The result of the reCAPTCHA verification.
890 - * @since 2.5.6
891 - */
892 - protected function captcha_response( string $form ): bool {
893 - $response_field = 'turnstile' === $this->recaptcha_type ? 'wpdef-turnstile-response' : 'g-recaptcha-response';
894 - $response = stripslashes( defender_get_data_from_request( $response_field, 'p' ) );
895 - if ( ! is_string( $this->private_key ) || '' === $this->private_key || '' === $response ) {
896 - return false;
897 - }
898 - // reCAPTCHA response post data.
899 - $remote_ip = filter_var( defender_get_data_from_request( 'REMOTE_ADDR', 's' ), FILTER_VALIDATE_IP );
900 - /**
901 - * Filters to Get the remote IP address.
902 - *
903 - * @param mixed $remote_ip The remote IP address.
904 - *
905 - * @since 2.5.6
906 - */
907 - $remote_ip = (string) apply_filters( 'wd_recaptcha_remote_ip', $remote_ip );
908 -
909 - $post_body = array(
910 - 'secret' => $this->private_key,
911 - 'response' => $response,
912 - '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
913 325 );
914 326
915 - $result = $this->service->captcha_post_request( $post_body );
916 -
917 327 /**
918 - * Filters to check the result of a reCAPTCHA verification.
919 - *
920 - * @param mixed $result The result of the reCAPTCHA verification.
921 - * @param mixed $form The form being verified.
922 - *
923 - * @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.
924 332 */
925 - return apply_filters( 'wd_recaptcha_check_result', $result, $form );
926 - }
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' );
927 334
928 - /**
929 - * Display the BuddyPress reCAPTCHA.
930 - *
931 - * @return void
932 - */
933 - public function display_buddypress_recaptcha(): void {
934 - $error = '';
935 - $obj = buddypress();
936 - if ( isset( $obj->signup->errors['failed_recaptcha_verification'] ) && '' !== $obj->signup->errors['failed_recaptcha_verification'] ) {
937 - $error = $obj->signup->errors['failed_recaptcha_verification'];
335 + if ( ( new WPMUDEV() )->show_support_links() ) {
336 + $ticket_text .= defender_support_ticket_text();
938 337 }
939 338
940 - if ( '' !== $error ) {
941 - $output = '<div class="error">';
942 - $output .= $error;
943 - $output .= '</div>';
944 -
945 - echo wp_kses_post( $output );
946 - }
947 -
948 - echo wp_kses( $this->display_recaptcha(), $this->get_allowed_html() );
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 + );
949 357 }
950 358
951 359 /**
952 - * Validates the reCAPTCHA field on the BuddyPress registration form.
953 - *
954 - * @return void
955 - */
956 - public function validate_captcha_field_on_buddypress_registration(): void {
957 - if ( ! $this->captcha_response( 'buddypress_registration' ) ) {
958 - buddypress()->signup->errors['failed_recaptcha_verification'] = $this->service->error_message();
959 - }
960 - }
961 -
962 - /**
963 - * Verify BuddyPress group form captcha.
964 - *
965 - * @return bool|void
966 - */
967 - public function validate_captcha_field_on_buddypress_group() {
968 - if ( ! bp_is_group_creation_step( 'group-details' ) ) {
969 - return false;
970 - }
971 -
972 - if ( ! $this->captcha_response( 'buddypress_create_group' ) ) {
973 - bp_core_add_message( $this->service->error_message(), 'error' );
974 - bp_core_redirect( bp_get_root_domain() . '/' . bp_get_groups_root_slug() . '/create/step/group-details/' );
975 - } else {
976 - return false;
977 - }
978 - }
979 -
980 - /**
981 - * Provide data to the frontend via localized script.
982 - *
983 - * @param array $data Data collection is ready to passed.
984 - *
985 - * @return array Modified data array with added this controller data.
986 - */
987 - public function script_data( array $data ): array {
988 - $data['captcha'] = $this->data_frontend();
989 -
990 - return $data;
991 - }
992 -
993 - /**
994 360 * Save settings.
995 361 *
996 - * @param Request $request The request object containing new settings data.
362 + * @param Request $request The request object containing new settings data.
997 363 *
998 364 * @return Response
999 365 * @defender_route
1000 366 */
@@ -1000,33 +366,10 @@
1000 366 */
1001 367 public function save_settings( Request $request ): Response {
1002 368 $data = $request->get_data_by_model( $this->model );
1003 369 $this->model->import( $data );
1004 - if ( ! $this->service->is_recaptcha_active_provider() ) {
1005 - $valid_token = $request->get_data(
1006 - array(
1007 - 'token' => array(
1008 - 'type' => 'string',
1009 - ),
1010 - )
1011 - );
1012 - $token = $valid_token['token'] ?? '';
1013 - $is_valid = $this->service->verify_turnstile_key( $token );
1014 - if ( $is_valid ) {
1015 - $this->model->active_type = 'turnstile';
1016 - } else {
1017 - return new Response(
1018 - false,
1019 - // Merge stored data to avoid errors.
1020 - array_merge(
1021 - array(
1022 - 'message' => __( 'Please make sure your Cloudflare API keys are correct and that Preview Widget verification is successful.', 'defender-security' ),
1023 - 'error_keys' => array( 'data_turnstile.secret' ),
1024 - ),
1025 - $this->data_frontend()
1026 - )
1027 - );
1028 - }
370 + if ( Captcha_Model::TURNSTILE === $this->model->provider ) {
371 + $this->model->active_type = Captcha_Model::TURNSTILE;
1029 372 }
1030 373 if ( $this->model->validate() ) {
1031 374 $this->model->save();
1032 375 Config_Hub_Helper::set_clear_active_flag();
@@ -1056,150 +399,75 @@
1056 399 );
1057 400 }
1058 401
1059 402 /**
1060 - * Load the CAPTCHA preview based on the provided request data.
403 + * Verify a CAPTCHA response token.
1061 404 *
1062 - * @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.
1063 406 *
1064 - * @return Response The response object with the preview HTML and status.
407 + * @return Response
1065 408 * @defender_route
1066 409 */
1067 - public function load_captcha_preview( Request $request ): Response {
1068 - $onload = null;
1069 - $js = null;
1070 - $data = $request->get_data(
410 + public function verify_response_token( Request $request ): Response {
411 + $data = $request->get_data(
1071 412 array(
1072 - 'captcha_type' => array(
1073 - 'type' => 'string',
413 + 'token' => array(
414 + 'type' => 'string',
415 + 'sanitize' => 'sanitize_text_field',
1074 416 ),
417 + 'key' => array(
418 + 'type' => 'string',
419 + 'sanitize' => 'sanitize_text_field',
420 + ),
1075 421 )
1076 422 );
1077 - $this->recaptcha_type = $data['captcha_type'];
1078 423
1079 - $model = $this->model;
1080 -
1081 - if ( 'turnstile' === $this->recaptcha_type ) {
424 + $token = $data['token'] ?? '';
425 + $key = $data['key'] ?? '';
426 + if ( '' === $token || '' === $key ) {
1082 427 return new Response(
1083 - $model->is_active(),
428 + false,
1084 429 array(
1085 - 'preview' => $model->is_active(),
430 + 'message' => esc_html__( 'Missing CAPTCHA verification data.', 'defender-security' ),
1086 431 )
1087 432 );
1088 433 }
1089 - $language = '' !== $model->language && 'automatic' !== $model->language ? $model->language : get_locale();
1090 434
1091 - $notice = '<div class="sui-notice sui-notice-default">';
1092 - $notice .= '<div class="sui-notice-content">';
1093 - $notice .= '<div class="sui-notice-message">';
1094 - $notice .= '<i class="sui-notice-icon sui-icon-info sui-md" aria-hidden="true"></i>';
1095 - $notice .= '<p>' . esc_html__( 'Save your API keys to load the reCAPTCHA preview.', 'defender-security' ) . '</p>';
1096 - $notice .= '</div>';
1097 - $notice .= '</div>';
1098 - $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 + );
1099 443
1100 - $theme = 'light';
1101 - $captcha_size = 'invisible';
1102 - $data_args = '';
1103 - if ( 'v2_checkbox' === $this->recaptcha_type ) {
1104 - $this->public_key = $model->data_v2_checkbox['key'];
1105 - $theme = $model->data_v2_checkbox['style'];
1106 - $captcha_size = $model->data_v2_checkbox['size'];
1107 - $onload = 'defender_render_admin_captcha_v2';
1108 - // Onload method for Recaptcha works only for js 'var'.
1109 - $js = "<script>var defender_render_admin_captcha_v2 = function () {
1110 - setTimeout( function () {
1111 - var captcha_v2 = jQuery( '.defender-g-recaptcha-v2_checkbox' ),
1112 - sitekey_v2 = captcha_v2.data('sitekey'),
1113 - theme_v2 = captcha_v2.data('theme'),
1114 - size_v2 = captcha_v2.data('size');
1115 - if (captcha_v2[0] && captcha_v2[0].hasChildNodes()) {
1116 - return;
1117 - }
1118 - window.grecaptcha.render( captcha_v2[0], {
1119 - sitekey: sitekey_v2,
1120 - theme: theme_v2,
1121 - size: size_v2,
1122 - 'error-callback': function() {
1123 - jQuery('#v2_checkbox_notice_1').hide();
1124 - jQuery('#v2_checkbox_notice_2').show();
1125 - }
1126 - } );
1127 - }, 100 );
1128 - };</script>";
1129 - } elseif ( 'v2_invisible' === $this->recaptcha_type ) {
1130 - $this->public_key = $model->data_v2_invisible['key'];
1131 - $onload = 'defender_render_admin_captcha_v2_invisible';
1132 - $data_args = 'data-badge="inline" data-callback="setResponse"';
1133 - $js = "<script>var defender_render_admin_captcha_v2_invisible = function () {
1134 - setTimeout( function () {
1135 - var captcha = jQuery( '.defender-g-recaptcha-v2_invisible' ),
1136 - sitekey = captcha.data('sitekey'),
1137 - theme = captcha.data('theme'),
1138 - size = captcha.data('size');
1139 - if (captcha[0] && captcha[0].hasChildNodes()) {
1140 - return;
1141 - }
1142 - window.grecaptcha.render( captcha[0], {
1143 - sitekey: sitekey,
1144 - theme: theme,
1145 - size: size,
1146 - badge: 'inline',
1147 - 'error-callback': function() {
1148 - jQuery('#v2_invisible_notice_1').hide();
1149 - jQuery('#v2_invisible_notice_2').show();
1150 - }
1151 - } );
1152 - }, 100 );
1153 - };</script>";
1154 - } elseif ( 'v3_recaptcha' === $this->recaptcha_type ) {
1155 - $this->public_key = $model->data_v3_recaptcha['key'];
1156 - $onload = 'defender_render_admin_captcha_v3';
1157 - $js = "<script>var defender_render_admin_captcha_v3 = function () {
1158 - setTimeout( function () {
1159 - var captcha = jQuery( '.defender-g-recaptcha-v3_recaptcha' ),
1160 - sitekey = captcha.data('sitekey'),
1161 - theme = captcha.data('theme'),
1162 - size = captcha.data('size');
1163 - if (captcha[0] && captcha[0].hasChildNodes()) {
1164 - return;
1165 - }
1166 - window.grecaptcha.render( captcha[0], {
1167 - sitekey: sitekey,
1168 - theme: theme,
1169 - size: size,
1170 - badge: 'inline',
1171 - 'error-callback': function() {
1172 - jQuery('#v3_recaptcha_notice_1').hide();
1173 - jQuery('#v3_recaptcha_notice_2').show();
1174 - }
1175 - } );
1176 - }, 100 );
1177 - };</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();
1178 464 }
1179 465
1180 - $html = '';
1181 - if ( isset( $this->recaptcha_type ) && '' !== $this->public_key ) {
1182 - // Google reCAPTCHA script must be connected with dynamic parameters (onload, render, etc.).
1183 - $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
1184 -
1185 - $html .= sprintf(
1186 - '<div class="%s" data-sitekey="%s" data-theme="%s" data-size="%s" %s></div>',
1187 - 'defender-g-recaptcha-' . $this->recaptcha_type,
1188 - esc_attr( $this->public_key ),
1189 - esc_attr( $theme ),
1190 - esc_attr( $captcha_size ),
1191 - $data_args
1192 - );
1193 - } else {
1194 - $html .= $notice;
1195 - }
1196 -
1197 466 return new Response(
1198 - true,
467 + $success,
1199 468 array(
1200 - 'preview' => true,
1201 - 'html' => $html,
469 + 'success' => $success,
1202 470 )
1203 471 );
1204 472 }
1205 473
@@ -1225,93 +493,8 @@
1225 493 return array();
1226 494 }
1227 495
1228 496 /**
1229 - * Provides data for the frontend.
1230 - *
1231 - * @return array An array of data for the frontend.
1232 - */
1233 - public function data_frontend(): array {
1234 - $model = $this->model;
1235 - $is_active = $model->is_active();
1236 - /**
1237 - * Different cases for entered keys and locations:
1238 - * success - one default, Woo or BuddyPress location is checked at least,
1239 - * warning - default, Woo and BuddyPress locations are unchecked,
1240 - * warning - default location is unchecked, also Woo and BuddyPress is deactivated,
1241 - * warning - non-entered keys.
1242 - */
1243 - if ( $is_active ) {
1244 - if ( $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) {
1245 - $notice_type = 'success';
1246 - $notice_text = false;
1247 - } elseif ( ! $this->is_woo_activated && ! $this->is_buddypress_activated && ! $model->enable_default_location() ) {
1248 - $notice_type = 'warning';
1249 - $notice_text = sprintf(
1250 - /* translators: 1. Captcha Provider name. 2. Captcha Type */
1251 - 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' ),
1252 - 'turnstile' === $model->provider ? esc_html__( 'Cloudflare Turnstile', 'defender-security' ) : esc_html__( 'Google reCAPTCHA', 'defender-security' ),
1253 - 'turnstile' === $model->provider ? esc_html__( 'Turnstile', 'defender-security' ) : esc_html__( 'reCAPTCHA', 'defender-security' ),
1254 - );
1255 - } elseif (
1256 - ! $model->enable_default_location()
1257 - && (
1258 - ( $this->is_woo_activated && ! $model->enable_woo_location() )
1259 - || ( $this->is_buddypress_activated && ! $model->enable_buddypress_location() )
1260 - )
1261 - ) {
1262 - $notice_type = 'warning';
1263 - $notice_text = sprintf(
1264 - /* translators: 1. reCAPTCHA & Woo location. 2. BuddyPress location. */
1265 - 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' ),
1266 - '<b>' . esc_html__( 'reCAPTCHA Locations', 'defender-security' ) . '</b>, <b>' . esc_html__( 'WooCommerce', 'defender-security' ) . '</b>',
1267 - '<b>' . esc_html__( 'BuddyPress', 'defender-security' ) . '</b>'
1268 - );
1269 - }
1270 - } else {
1271 - // Inactive case.
1272 - $notice_type = 'error';
1273 - $notice_text = esc_html__(
1274 - 'Configure a CAPTCHA provider to verify users and prevent automated bots. Both reCAPTCHA and Cloudflare Turnstile are supported.',
1275 - 'defender-security'
1276 - );
1277 - }
1278 -
1279 - /**
1280 - * Cases:
1281 - * Invalid domain for Site Key,
1282 - * Google ReCAPTCHA is in localhost,
1283 - * Cannot contact reCAPTCHA. Check your connection.
1284 - */
1285 - $ticket_text = esc_html__(
1286 - '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.',
1287 - 'defender-security'
1288 - );
1289 -
1290 - if ( ( new WPMUDEV() )->show_support_links() ) {
1291 - $ticket_text .= defender_support_ticket_text();
1292 - }
1293 -
1294 - return array_merge(
1295 - array(
1296 - 'model' => $model->export(),
1297 - 'is_active' => $is_active,
1298 - 'default_message' => $this->default_msg,
1299 - 'default_turnstile_message' => $this->model->get_default_values()['turnstile_message'],
1300 - 'default_locations' => Captcha_Component::get_forms(),
1301 - 'notice_type' => $notice_type,
1302 - 'notice_text' => $notice_text,
1303 - 'ticket_text' => $ticket_text,
1304 - 'is_woo_active' => $this->is_woo_activated,
1305 - 'woo_locations' => Woocommerce::get_forms(),
1306 - 'is_buddypress_active' => $this->is_buddypress_activated,
1307 - 'buddypress_locations' => Buddypress::get_forms(),
1308 - ),
1309 - $this->dump_routes_and_nonces()
1310 - );
1311 - }
1312 -
1313 - /**
1314 497 * Provides data for the dashboard widget.
1315 498 *
1316 499 * @return array An array of dashboard widget data.
1317 500 */
@@ -1316,16 +499,9 @@
1316 499 * @return array An array of dashboard widget data.
1317 500 */
1318 501 public function dashboard_widget(): array {
1319 502 $model = $this->model;
1320 - $notice_type = ( $model->is_active()
1321 - && $this->service->enable_any_location(
1322 - $this->is_woo_activated,
1323 - $this->is_buddypress_activated
1324 - )
1325 - )
1326 - ? 'success'
1327 - : 'warning';
503 + $notice_type = ( $model->is_active() && $this->service->enable_any_location( $this->is_woo_activated, $this->is_buddypress_activated ) ) ? 'success' : 'warning';
1328 504
1329 505 return array(
1330 506 'model' => $model->export(),
1331 507 'notice_type' => $notice_type,
@@ -1334,9 +510,9 @@
1334 510
1335 511 /**
1336 512 * Imports data into the model.
1337 513 *
1338 - * @param array $data Data to be imported into the model.
514 + * @param array $data Data to be imported into the model.
1339 515 */
1340 516 public function import_data( array $data ) {
1341 517 $model = $this->model;
1342 518
@@ -1355,27 +531,11 @@
1355 531 return array( $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ) );
1356 532 }
1357 533
1358 534 /**
1359 - * Maybe validate reCaptcha for lost password.
1360 - *
1361 - * @return bool
1362 - * @since 3.2.0
1363 - */
1364 - protected function maybe_validate_captcha_for_lostpassword(): bool {
1365 - $post_data = defender_get_data_from_request( null, 'p' );
1366 - $action = $post_data['action'] ?? '';
1367 -
1368 - return ! $this->is_woocommerce_page() &&
1369 - ! isset( $post_data['wc_reset_password'], $post_data['user_login'] ) &&
1370 - ! ( is_admin() && 'send-password-reset' === $action ) &&
1371 - 'pp_ajax_passwordreset' !== $action;
1372 - }
1373 -
1374 - /**
1375 535 * Enable/disable module.
1376 536 *
1377 - * @param Request $request The request object.
537 + * @param Request $request The request object.
1378 538 *
1379 539 * @return Response
1380 540 * @defender_route
1381 541 * @since 3.12.0
@@ -1410,39 +570,7 @@
1410 570 'auto_close' => true,
1411 571 ),
1412 572 $this->data_frontend()
1413 573 )
1414 - );
1415 - }
1416 -
1417 - /**
1418 - * Get allowed HTML tags and attributes for reCaptcha output sanitization.
1419 - *
1420 - * @return array
1421 - */
1422 - private function get_allowed_html(): array {
1423 - return array(
1424 - 'div' => array(
1425 - 'class' => array(),
1426 - 'id' => array(),
1427 - 'style' => array(),
1428 - ),
1429 - 'iframe' => array(
1430 - 'src' => array(),
1431 - 'frameborder' => array(),
1432 - 'scrolling' => array(),
1433 - 'style' => array(),
1434 - ),
1435 - 'noscript' => array(),
1436 - 'textarea' => array(
1437 - 'name' => array( 'g-recaptcha-response' ),
1438 - 'class' => array( 'g-recaptcha-response' ),
1439 - 'style' => array(),
1440 - ),
1441 - 'input' => array(
1442 - 'type' => array( 'hidden' ),
1443 - 'class' => array( 'g-recaptcha-response' ),
1444 - 'name' => array( 'g-recaptcha-response' ),
1445 - ),
1446 574 );
1447 575 }
1448 576 }