PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
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.8.1, at src/component/class-captcha.php

271 lines 8.1 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 $response_success = isset( $response_keys['success'] ) ? (bool) $response_keys['success'] : false;
182 if ( 'v3_recaptcha' === $this->model->active_type ) {
183 if (
184 $response_success
185 && isset( $this->model->data_v3_recaptcha['threshold'], $response_keys['score'] )
186 && is_numeric( $this->model->data_v3_recaptcha['threshold'] )
187 && is_numeric( $response_keys['score'] )
188 ) {
189 $is_success = $response_keys['score'] >= (float) $this->model->data_v3_recaptcha['threshold'];
190 } else {
191 $is_success = false;
192 }
193 } else {
194 $is_success = $response_success;
195 }
196
197 return $is_success;
198 }
199
200 /**
201 * Retrieves the list of default forms where CAPTCHA can be integrated.
202 *
203 * @return array An associative array of form identifiers and their display names.
204 */
205 public static function get_forms(): array {
206 return array(
207 self::DEFAULT_LOGIN_FORM => esc_html__( 'Login', 'defender-security' ),
208 self::DEFAULT_REGISTER_FORM => esc_html__( 'Register', 'defender-security' ),
209 self::DEFAULT_LOST_PASSWORD_FORM => esc_html__( 'Lost Password', 'defender-security' ),
210 self::DEFAULT_COMMENT_FORM => esc_html__( 'Comments', 'defender-security' ),
211 );
212 }
213 /**
214 * Validate the token
215 *
216 * @param string $token The token from the admin-side widget.
217 * @return bool Indicating if validation was successful.
218 */
219 public function verify_turnstile_key( string $token ): bool {
220 $key = $this->model->data_turnstile['key'] ?? '';
221 $secret = $this->model->data_turnstile['secret'] ?? '';
222 // Skip validation only if both secret and site key are empty.
223 if ( '' === $secret && '' === $key ) {
224 return true;
225 }
226 // If secret key is provided but site key is not (or vice versa), require both.
227 if ( '' === $secret || '' === $key ) {
228 return false;
229 }
230 // If no token provided, validation cannot proceed.
231 if ( '' === $token ) {
232 return false;
233 }
234 return $this->captcha_post_request(
235 array(
236 'secret' => $this->model->data_turnstile['secret'],
237 'response' => $token,
238 )
239 );
240 }
241
242 /**
243 * Check if the Google reCAPTCHA is active provider.
244 *
245 * @return bool
246 */
247 public function is_recaptcha_active_provider(): bool {
248 return 'recaptcha' === $this->model->provider;
249 }
250
251 /**
252 * Skip turnstile check as per current sprint plan.
253 *
254 * @return bool true if validation should skip, false otherwise.
255 */
256 public function should_skip_turnstile_check(): bool {
257 // Only apply for Turnstile provider.
258 if ( $this->is_recaptcha_active_provider() ) {
259 return false;
260 }
261
262 // Skip if neither WooCommerce nor BuddyPress is active.
263 if ( ! $this->woo->is_activated() && ! $this->buddypress->is_activated() ) {
264 return false;
265 }
266
267 // Skip turnstile check if in WooCommerce or BuddyPress login context.
268 return $this->woo->is_wc_login_context() || $this->buddypress->is_login_context();
269 }
270 }
271