PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.5.2
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.5.2
3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / addons / ShieldSilentCaptcha / ShieldSilentCaptcha.php
everest-forms / addons / ShieldSilentCaptcha Last commit date
ShieldSilentCaptcha.php 1 month ago
ShieldSilentCaptcha.php
522 lines
1 <?php
2 /**
3 * Shield silentCAPTCHA integration.
4 *
5 * @since 3.7.1
6 * @package EverestForms\Addons\ShieldSilentCaptcha
7 */
8
9 namespace EverestForms\Addons\ShieldSilentCaptcha;
10
11 use EverestForms\Traits\Singleton;
12
13 /**
14 * Shield silentCAPTCHA integration.
15 *
16 * @since 3.7.1
17 */
18 class ShieldSilentCaptcha {
19
20 use Singleton;
21
22 /**
23 * Shield global option key.
24 */
25 const SETTING_KEY = 'shield_silent_captcha';
26
27 /**
28 * Help URL.
29 */
30 const HELP_URL = 'https://clk.shldscrty.com/silentcaptchaintegrationhelp';
31
32 /**
33 * Cached Shield availability.
34 *
35 * @var bool|null
36 */
37 protected $shield_available = null;
38
39 /**
40 * Cached Shield threshold-zero status.
41 *
42 * @var bool|null
43 */
44 protected $shield_threshold_zero = null;
45
46 /**
47 * Constructor.
48 *
49 * @since 3.7.1
50 */
51 public function __construct() {
52 $this->setup();
53 }
54
55 /**
56 * Setup hooks.
57 *
58 * @since 3.7.1
59 */
60 public function setup() {
61 add_filter( 'everest_forms_recaptcha_integration_settings', array( $this, 'add_recaptcha_integration_settings' ) );
62 add_action( 'everest_forms_inline_security_settings', array( $this, 'add_inline_security_settings' ) );
63 add_action( 'everest_forms_admin_field_shield_silent_captcha_info', array( $this, 'render_global_info_field' ) );
64 add_filter( 'everest_forms_process_initial_errors', array( $this, 'filter_initial_errors' ), 10, 2 );
65 }
66
67 /**
68 * Add Shield provider to CAPTCHA integration settings.
69 *
70 * @since 3.7.1
71 *
72 * @param array $settings Settings array.
73 * @return array
74 */
75 public function add_recaptcha_integration_settings( $settings ) {
76 $is_available = $this->is_shield_available();
77 $toggle_field = $this->get_global_toggle_field( $is_available );
78 $info_field = array(
79 'type' => 'shield_silent_captcha_info',
80 'desc' => $this->get_global_toggle_desc( $is_available ),
81 );
82 $shield_accordion = array(
83 'title' => esc_html__( 'Shield silentCAPTCHA', 'everest-forms' ),
84 'icon' => plugins_url( 'assets/images/captcha/shield-security-logo.png', EVF_PLUGIN_FILE ),
85 'is_enabled' => $this->is_global_shield_enabled(),
86 'fields' => array( $toggle_field, $info_field ),
87 );
88
89 foreach ( $settings as &$setting ) {
90 if ( ! isset( $setting['type'] ) || 'accordion' !== $setting['type'] ) {
91 continue;
92 }
93
94 if ( ! isset( $setting['items'] ) || ! is_array( $setting['items'] ) ) {
95 continue;
96 }
97
98 $setting['items'][] = $shield_accordion;
99 break;
100 }
101
102 return $settings;
103 }
104
105 /**
106 * Render Shield global info paragraph below toggle control.
107 *
108 * @since 3.7.1
109 *
110 * @param array $field Field config.
111 */
112 public function render_global_info_field( $field ) {
113 if ( empty( $field['desc'] ) ) {
114 return;
115 }
116 ?>
117 <div class="everest-forms-global-settings">
118 <div class="everest-forms-global-settings--field">
119 <p class="description"><?php echo wp_kses_post( $field['desc'] ); ?></p>
120 </div>
121 </div>
122 <?php
123 }
124
125 /**
126 * Add Shield per-form toggle in builder security panel.
127 *
128 * @since 3.7.1
129 *
130 * @param object $builder Builder object.
131 */
132 public function add_inline_security_settings( $builder ) {
133 $is_available = $this->is_shield_available();
134 $toggle_args = array(
135 'default' => '0',
136 'tooltip' => esc_html__( 'Enable Shield silentCAPTCHA. Submission is checked only when this toggle and the global toggle are both enabled.', 'everest-forms' ),
137 );
138
139 echo '<div class="everest-forms-border-container">';
140 echo '<h4 class="everest-forms-border-container-title">' . esc_html__( 'Shield silentCAPTCHA', 'everest-forms' ) . '</h4>';
141
142 everest_forms_panel_field(
143 'toggle',
144 'settings',
145 self::SETTING_KEY,
146 $builder->form_data,
147 esc_html__( 'Enable Shield silentCAPTCHA bot protection', 'everest-forms' ),
148 $toggle_args
149 );
150
151 if ( ! $is_available ) {
152 printf(
153 '<p class="everest-forms-notice everest-forms-notice-info">%s</p>',
154 wp_kses_post( $this->get_unavailable_notice_html() )
155 );
156 }
157
158 echo '</div>';
159 }
160
161 /**
162 * Filter initial errors with Shield decision gate.
163 *
164 * @since 3.7.1
165 *
166 * @param array $errors Current error bag.
167 * @param array $form_data Current form data.
168 * @return array
169 */
170 public function filter_initial_errors( $errors, $form_data ) {
171 if ( isset( $_POST['__amp_form_verify'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
172 return $errors;
173 }
174
175 if ( ! is_array( $form_data ) || ! isset( $form_data['id'] ) ) {
176 return $errors;
177 }
178
179 $form_id = absint( $form_data['id'] );
180 if ( ! $form_id ) {
181 return $errors;
182 }
183
184 if ( isset( $errors[ $form_id ] ) && ! empty( $errors[ $form_id ] ) ) {
185 return $errors;
186 }
187
188 if ( ! $this->is_global_shield_enabled() ) {
189 return $errors;
190 }
191
192 if ( ! $this->is_form_shield_enabled( $form_data ) ) {
193 return $errors;
194 }
195
196 $request_ip = evf_get_ip_address();
197 $verdict = $this->get_shield_bot_verdict_for_ip( $request_ip );
198
199 if ( true !== $verdict ) {
200 return $errors;
201 }
202
203 $errors[ $form_id ]['header'] = apply_filters(
204 'everest_forms_process_form_error_header',
205 __( 'Form has not been submitted, please see the errors below.', 'everest-forms' )
206 );
207
208 return $errors;
209 }
210
211 /**
212 * Detect whether Shield callable is available.
213 *
214 * @since 3.7.1
215 *
216 * @return bool
217 */
218 public function is_shield_available() {
219 if ( null !== $this->shield_available ) {
220 return $this->shield_available;
221 }
222
223 $this->shield_available = false;
224
225 foreach ( $this->get_shield_callables() as $callable ) {
226 if ( is_callable( $callable ) ) {
227 $this->shield_available = true;
228 break;
229 }
230 }
231
232 return $this->shield_available;
233 }
234
235 /**
236 * Resolve Shield verdict for a given request IP.
237 *
238 * @since 3.7.1
239 *
240 * @param string $request_ip Request IP.
241 * @return bool|null
242 */
243 public function get_shield_bot_verdict_for_ip( $request_ip ) {
244 foreach ( $this->get_shield_callables() as $callable ) {
245 if ( ! is_callable( $callable ) ) {
246 continue;
247 }
248
249 try {
250 $verdict = call_user_func( $callable, $request_ip );
251 } catch ( \Throwable $e ) {
252 continue;
253 }
254
255 $normalized = $this->normalize_shield_verdict( $verdict );
256
257 if ( null !== $normalized ) {
258 return $normalized;
259 }
260 }
261
262 return null;
263 }
264
265 /**
266 * Get Shield callable priority list.
267 *
268 * @since 3.7.1
269 *
270 * @return array
271 */
272 protected function get_shield_callables() {
273 return array(
274 '\\FernleafSystems\\Wordpress\\Plugin\\Shield\\Functions\\test_ip_is_bot',
275 'shield_test_ip_is_bot',
276 );
277 }
278
279 /**
280 * Get Shield threshold callable priority list.
281 *
282 * @since 3.7.1
283 *
284 * @return array
285 */
286 protected function get_shield_threshold_callables() {
287 return array(
288 '\\FernleafSystems\\Wordpress\\Plugin\\Shield\\Functions\\get_silentcaptcha_bot_threshold',
289 'shield_get_silentcaptcha_bot_threshold',
290 );
291 }
292
293 /**
294 * Build notice HTML with standard Shield help link.
295 *
296 * @since 3.7.1
297 *
298 * @param string $notice_text Notice text.
299 * @return string
300 */
301 protected function get_notice_with_help_link_html( $notice_text ) {
302 return sprintf(
303 /* translators: 1: notice text, 2: Shield help URL. */
304 __( '%1$s <a href="%2$s" target="_blank" rel="noopener noreferrer">Learn More</a>.', 'everest-forms' ),
305 esc_html( $notice_text ),
306 esc_url( self::HELP_URL )
307 );
308 }
309
310 /**
311 * Get lock attributes used when Shield is unavailable.
312 *
313 * @since 3.7.1
314 *
315 * @return array
316 */
317 protected function get_lock_attributes() {
318 return array(
319 'disabled' => 'disabled',
320 'data-shield-silent-captcha-lock' => '1',
321 'data-shield-silent-captcha-available' => '0',
322 );
323 }
324
325 /**
326 * Get unavailable notice plain text.
327 *
328 * @since 3.7.1
329 *
330 * @return string
331 */
332 protected function get_unavailable_notice_text() {
333 return esc_html__( 'Shield Security bot detection is not currently detected, so this setting has no effect right now. Install and activate Shield Security to enable silentCAPTCHA bot checks.', 'everest-forms' );
334 }
335
336 /**
337 * Get unavailable notice HTML with help link.
338 *
339 * @since 3.7.1
340 *
341 * @return string
342 */
343 protected function get_unavailable_notice_html() {
344 return $this->get_notice_with_help_link_html( $this->get_unavailable_notice_text() );
345 }
346
347 /**
348 * Get threshold-zero notice plain text.
349 *
350 * @since 3.7.1
351 *
352 * @return string
353 */
354 protected function get_threshold_zero_notice_text() {
355 return esc_html__( 'Shield is active and running, but your Shield silentcaptcha bot threshold is set to zero.', 'everest-forms' );
356 }
357
358 /**
359 * Get threshold-zero notice HTML with help link.
360 *
361 * @since 3.7.1
362 *
363 * @return string
364 */
365 protected function get_threshold_zero_notice_html() {
366 return $this->get_notice_with_help_link_html( $this->get_threshold_zero_notice_text() );
367 }
368
369 /**
370 * Build Shield global toggle field.
371 *
372 * @since 3.7.1
373 *
374 * @param bool $is_available Availability state.
375 * @return array
376 */
377 protected function get_global_toggle_field( $is_available ) {
378 $field = array(
379 'title' => esc_html__( 'Enable Shield silentCAPTCHA', 'everest-forms' ),
380 'type' => 'toggle',
381 'desc' => '',
382 'id' => self::SETTING_KEY,
383 'default' => 'no',
384 'desc_tip' => false,
385 );
386
387 return $field;
388 }
389
390 /**
391 * Get global toggle description.
392 *
393 * @since 3.7.1
394 *
395 * @param bool $is_available Availability state.
396 * @return string
397 */
398 protected function get_global_toggle_desc( $is_available ) {
399 if ( ! $is_available ) {
400 return $this->get_unavailable_notice_html();
401 }
402
403 if ( $this->is_shield_silentcaptcha_threshold_zero() ) {
404 return $this->get_threshold_zero_notice_html();
405 }
406
407 return sprintf(
408 '%s<br/>%s %s',
409 /* translators: %s - Shield help URL. */
410 __( 'With Shield silentCAPTCHA Bot SPAM Protection, form submissions are blocked when requests are identified as coming from automated bots.', 'everest-forms' ),
411 __( "silentCAPTCHA is 100% invisible to your site visitors and you'll need to install the Shield Security plugin to use this feature.", 'everest-forms' ),
412 /* translators: %s: Shield help URL. */
413 sprintf( __( '<a href="%s" target="_blank" rel="noopener noreferrer">Learn More</a>.', 'everest-forms' ), esc_url( self::HELP_URL ) )
414 );
415 }
416
417 /**
418 * Determine whether Shield silentCAPTCHA threshold is explicitly set to zero.
419 *
420 * @since 3.7.1
421 *
422 * @return bool
423 */
424 protected function is_shield_silentcaptcha_threshold_zero() {
425 if ( null !== $this->shield_threshold_zero ) {
426 return $this->shield_threshold_zero;
427 }
428
429 $this->shield_threshold_zero = false;
430
431 if ( ! $this->is_shield_available() ) {
432 return $this->shield_threshold_zero;
433 }
434
435 foreach ( $this->get_shield_threshold_callables() as $callable ) {
436 if ( ! is_callable( $callable ) ) {
437 continue;
438 }
439
440 try {
441 $threshold = call_user_func( $callable );
442 } catch ( \Throwable $e ) {
443 continue;
444 }
445
446 if ( ! is_numeric( $threshold ) ) {
447 continue;
448 }
449
450 $this->shield_threshold_zero = 0 === (int) $threshold;
451 break;
452 }
453
454 return $this->shield_threshold_zero;
455 }
456
457 /**
458 * Get admin control config for script locking.
459 *
460 * @since 3.7.1
461 *
462 * @return array
463 */
464 protected function get_admin_control_config() {
465 return array(
466 array(
467 'shieldSilentCaptchaToggleSelector' => '#shield_silent_captcha',
468 'shieldSilentCaptchaHiddenSelector' => '',
469 ),
470 array(
471 'shieldSilentCaptchaToggleSelector' => '#everest-forms-panel-field-settings-shield_silent_captcha',
472 'shieldSilentCaptchaHiddenSelector' => 'input[type="hidden"][name="settings[shield_silent_captcha]"]',
473 ),
474 );
475 }
476
477 /**
478 * Normalize Shield callable result into strict tri-state.
479 *
480 * @since 3.7.1
481 *
482 * @param mixed $verdict Raw callable result.
483 * @return bool|null
484 */
485 protected function normalize_shield_verdict( $verdict ) {
486 if ( true === $verdict ) {
487 return true;
488 }
489
490 if ( false === $verdict ) {
491 return false;
492 }
493
494 return null;
495 }
496
497 /**
498 * Check whether Shield is globally enabled.
499 *
500 * @since 3.7.1
501 *
502 * @return bool
503 */
504 protected function is_global_shield_enabled() {
505 return 'yes' === get_option( self::SETTING_KEY, 'no' );
506 }
507
508 /**
509 * Check whether Shield is enabled for the form.
510 *
511 * @since 3.7.1
512 *
513 * @param array $form_data Form data.
514 * @return bool
515 */
516 protected function is_form_shield_enabled( $form_data ) {
517 $setting_value = isset( $form_data['settings'][ self::SETTING_KEY ] ) ? $form_data['settings'][ self::SETTING_KEY ] : '0';
518
519 return '1' === (string) $setting_value;
520 }
521 }
522