PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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
defender-security / src / component / class-captcha.php

class-captcha.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.2, at src/component/class-captcha.php

270 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles CAPTCHA functionality.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use Calotes\Base\Component;
11 use WP_Defender\Integrations\Buddypress;
12 use WP_Defender\Integrations\Woocommerce;
13 use WP_Defender\Model\Setting\Captcha as Captcha_Model;
14
15 /**
16 * Provides methods to handle CAPTCHA integration, including rendering, validation, and script management.
17 */
18 class Captcha extends Component {
19
20 /**
21 * Default form identifiers for CAPTCHA integration.
22 */
23 public const DEFAULT_LOGIN_FORM = 'login',
24 DEFAULT_REGISTER_FORM = 'register',
25 DEFAULT_LOST_PASSWORD_FORM = 'lost_password',
26 DEFAULT_COMMENT_FORM = 'comments';
27
28 /**
29 * The CAPTCHA settings model.
30 *
31 * @var Captcha_Model
32 */
33 protected Captcha_Model $model;
34
35 /**
36 * WooCommerce integration instance.
37 *
38 * @var Woocommerce|null
39 */
40 private ?Woocommerce $woo;
41 /**
42 * BuddyPress integration instance.
43 *
44 * @var Buddypress|null
45 */
46 private ?Buddypress $buddypress;
47
48 /**
49 * Captcha constructor.
50 *
51 * @param Captcha_Model $model The CAPTCHA settings model.
52 */
53 public function __construct( Captcha_Model $model ) {
54 $this->model = $model;
55 $this->woo = wd_di()->get( Woocommerce::class );
56 $this->buddypress = wd_di()->get( Buddypress::class );
57 }
58
59 /**
60 * Determines if any CAPTCHA location is enabled.
61 *
62 * @param bool $exist_woo Whether WooCommerce is active.
63 * @param bool $exist_bp Whether BuddyPress is active.
64 *
65 * @return bool True if any location is enabled, false otherwise.
66 */
67 public function enable_any_location( $exist_woo, $exist_bp ): bool {
68 return $this->model->enable_default_location()
69 || $this->model->check_woo_locations( $exist_woo )
70 || $this->model->check_buddypress_locations( $exist_bp );
71 }
72
73 /**
74 * Excludes CAPTCHA for specific requests.
75 *
76 * @return bool
77 */
78 public function exclude_captcha_for_requests(): bool {
79 if ( $this->is_recaptcha_active_provider() ) {
80 return $this->exclude_recaptcha_for_requests();
81 } else {
82 return $this->exclude_cloudflare_turnstile_for_requests();
83 }
84 }
85
86 /**
87 * Excludes reCAPTCHA for specific requests.
88 *
89 * @return bool
90 */
91 private function exclude_recaptcha_for_requests(): bool {
92 $current_request = defender_get_data_from_request( 'REQUEST_URI', 's' ) ?? '/';
93 $excluded_requests = (array) apply_filters( 'wd_recaptcha_excluded_requests', array() );
94
95 return in_array( $current_request, $excluded_requests, true );
96 }
97
98 /**
99 * Excludes Cloudflare Turnstile for specific requests.
100 *
101 * @return bool
102 * @since v5.7.0.
103 */
104 private function exclude_cloudflare_turnstile_for_requests(): bool {
105 $current_request = defender_get_data_from_request( 'REQUEST_URI', 's' ) ?? '/';
106 $excluded_requests = (array) apply_filters( 'wd_cloudflare_turnstile_excluded_requests', array() );
107
108 return in_array( $current_request, $excluded_requests, true );
109 }
110
111 /**
112 * Removes duplicate CAPTCHA scripts.
113 *
114 * @return bool|void False if no scripts are registered, or void otherwise.
115 */
116 public function remove_duplicate_captcha_scripts() {
117 global $wp_scripts;
118
119 if ( ! is_object( $wp_scripts ) ) {
120 return false;
121 }
122 /**
123 * Exclude scripts from Defender and Forminator to display reCAPTCHA.
124 *
125 * @since 5.1.0
126 */
127 $excluded_handles = (array) apply_filters(
128 $this->is_recaptcha_active_provider() ? 'wd_recaptcha_excluded_handles' : 'wd_turnstile_excluded_handles',
129 array(
130 'wpdef_captcha_api',
131 'forminator-google-recaptcha',
132 'forminator-turnstile',
133 )
134 );
135 foreach ( $wp_scripts->registered as $script_name => $args ) {
136 $search_pattern = $this->is_recaptcha_active_provider() ? '|google\.com/recaptcha/api\.js|' : '|challenges\.cloudflare\.com/turnstile/v0/api\.js|';
137 if ( is_string( $args->src ) && preg_match( $search_pattern, $args->src )
138 && ! in_array( $script_name, $excluded_handles, true )
139 ) {
140 wp_dequeue_script( $script_name );
141 }
142 }
143 }
144
145 /**
146 * Returns a custom error message for reCAPTCHA validation failure.
147 *
148 * @return string The formatted error message.
149 */
150 public function error_message(): string {
151 $default_values = $this->model->get_default_values();
152 if ( $this->is_recaptcha_active_provider() ) {
153 $message = '' === $this->model->message ? $default_values['message'] : $this->model->message;
154 } else {
155 $message = array() !== $this->model->data_turnstile && '' !== $this->model->data_turnstile['message'] ? $this->model->data_turnstile['message'] : $default_values['turnstile_message'];
156 }
157 return sprintf( '<strong>%s:</strong> %s', esc_html__( 'Error', 'defender-security' ), $message );
158 }
159
160 /**
161 * Sends an HTTP POST request to the Google reCAPTCHA API and returns the validation result.
162 *
163 * @param array $post_body The POST request body.
164 *
165 * @return bool True if the reCAPTCHA validation is successful, false otherwise.
166 */
167 public function captcha_post_request( array $post_body ): bool {
168 $args = array(
169 'body' => $post_body,
170 'sslverify' => false,
171 );
172 $url = $this->is_recaptcha_active_provider() ? 'https://www.google.com/recaptcha/api/siteverify' : 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
173 $request = wp_remote_post( $url, $args );
174
175 if ( is_wp_error( $request ) ) {
176 return false;
177 }
178
179 $response_body = wp_remote_retrieve_body( $request );
180 $response_keys = json_decode( $response_body, true );
181 if ( 'v3_recaptcha' === $this->model->active_type ) {
182 if (
183 $response_keys['success']
184 && isset( $this->model->data_v3_recaptcha['threshold'], $response_keys['score'] )
185 && is_numeric( $this->model->data_v3_recaptcha['threshold'] )
186 && is_numeric( $response_keys['score'] )
187 ) {
188 $is_success = $response_keys['score'] >= (float) $this->model->data_v3_recaptcha['threshold'];
189 } else {
190 $is_success = false;
191 }
192 } else {
193 $is_success = (bool) $response_keys['success'];
194 }
195
196 return $is_success;
197 }
198
199 /**
200 * Retrieves the list of default forms where CAPTCHA can be integrated.
201 *
202 * @return array An associative array of form identifiers and their display names.
203 */
204 public static function get_forms(): array {
205 return array(
206 self::DEFAULT_LOGIN_FORM => esc_html__( 'Login', 'defender-security' ),
207 self::DEFAULT_REGISTER_FORM => esc_html__( 'Register', 'defender-security' ),
208 self::DEFAULT_LOST_PASSWORD_FORM => esc_html__( 'Lost Password', 'defender-security' ),
209 self::DEFAULT_COMMENT_FORM => esc_html__( 'Comments', 'defender-security' ),
210 );
211 }
212 /**
213 * Validate the token
214 *
215 * @param string $token The token from the admin-side widget.
216 * @return bool Indicating if validation was successful.
217 */
218 public function verify_turnstile_key( string $token ): bool {
219 $key = $this->model->data_turnstile['key'] ?? '';
220 $secret = $this->model->data_turnstile['secret'] ?? '';
221 // Skip validation only if both secret and site key are empty.
222 if ( '' === $secret && '' === $key ) {
223 return true;
224 }
225 // If secret key is provided but site key is not (or vice versa), require both.
226 if ( '' === $secret || '' === $key ) {
227 return false;
228 }
229 // If no token provided, validation cannot proceed.
230 if ( '' === $token ) {
231 return false;
232 }
233 return $this->captcha_post_request(
234 array(
235 'secret' => $this->model->data_turnstile['secret'],
236 'response' => $token,
237 )
238 );
239 }
240
241 /**
242 * Check if the Google reCAPTCHA is active provider.
243 *
244 * @return bool
245 */
246 public function is_recaptcha_active_provider(): bool {
247 return 'recaptcha' === $this->model->provider;
248 }
249
250 /**
251 * Skip turnstile check as per current sprint plan.
252 *
253 * @return bool true if validation should skip, false otherwise.
254 */
255 public function should_skip_turnstile_check(): bool {
256 // Only apply for Turnstile provider.
257 if ( $this->is_recaptcha_active_provider() ) {
258 return false;
259 }
260
261 // Skip if neither WooCommerce nor BuddyPress is active.
262 if ( ! $this->woo->is_activated() && ! $this->buddypress->is_activated() ) {
263 return false;
264 }
265
266 // Skip turnstile check if in WooCommerce or BuddyPress login context.
267 return $this->woo->is_wc_login_context() || $this->buddypress->is_login_context();
268 }
269 }
270