PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.26.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.26.1
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / models / FrmSpamCheckDenylist.php

FrmSpamCheckDenylist.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.26.1, at classes/models/FrmSpamCheckDenylist.php

613 lines 14.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Spam check using denylist
4 *
5 * @since 6.21
6 *
7 * @package Formidable
8 */
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 die( 'You are not allowed to call this page directly.' );
12 }
13
14 class FrmSpamCheckDenylist extends FrmSpamCheck {
15
16 const COMPARE_CONTAINS = '';
17
18 const COMPARE_EQUALS = 'equals';
19
20 /**
21 * @var array
22 */
23 protected $posted_fields;
24
25 /**
26 * @var array
27 */
28 protected $denylist;
29
30 /**
31 * @param array $values
32 */
33 public function __construct( $values ) {
34 $this->maybe_add_form_id_to_values( $values );
35
36 parent::__construct( $values );
37
38 $this->denylist = $this->get_denylist_array();
39 }
40
41 /**
42 * @return array
43 */
44 protected function get_posted_fields() {
45 if ( is_null( $this->posted_fields ) ) {
46 $this->posted_fields = FrmField::get_all_for_form( $this->values['form_id'] );
47 }
48 return $this->posted_fields;
49 }
50
51 /**
52 * Maybe add form ID to values. In file name validation, only item_meta in $values.
53 *
54 * @param array $values Spam check values.
55 *
56 * @return void
57 */
58 protected function maybe_add_form_id_to_values( &$values ) {
59 if ( ! empty( $values['form_id'] ) || empty( $values['item_meta'] ) ) {
60 return;
61 }
62
63 $field_id = key( $values['item_meta'] );
64 $field = FrmField::getOne( $field_id );
65
66 if ( $field ) {
67 $values['form_id'] = $field->form_id;
68 }
69 }
70
71 protected function is_enabled() {
72 $frm_settings = FrmAppHelper::get_settings();
73 $is_enabled = $frm_settings->denylist_check;
74
75 /**
76 * Allows disabling the denylist check.
77 *
78 * @since 6.21
79 *
80 * @param bool $is_enabled Whether the denylist check is enabled.
81 * @param array $values The entry values.
82 */
83 return apply_filters( 'frm_check_denylist', $is_enabled, $this->values );
84 }
85
86 /**
87 * Gets denylist data.
88 * See {@see FrmSpamCheckDenylist::fill_default_denylist_data()} for more details.
89 *
90 * @return array[]
91 */
92 protected function get_denylist_array() {
93 $denylist_data = array(
94 array(
95 'file' => FrmAppHelper::plugin_path() . '/denylist/domain-partial.txt',
96 ),
97 array(
98 'file' => FrmAppHelper::plugin_path() . '/denylist/splorp-wp-comment.txt',
99 'skip' => FrmAppHelper::current_user_can( 'frm_create_entries' ),
100 'skip_field_types' => array( 'file' ),
101 ),
102 array(
103 'words' => array(
104 'moncler|north face|vuitton|handbag|burberry|outlet|prada|cialis|viagra|maillot|oakley|ralph lauren|ray ban|iphone|プラダ',
105 ),
106 'field_types' => array( 'name' ),
107 'is_regex' => true,
108 ),
109 array(
110 'words' => array(
111 '@mail\.ru|@yandex\.',
112 ),
113 'field_types' => array( 'email' ),
114 'is_regex' => true,
115 ),
116 );
117
118 $custom_denylist = $this->get_words_from_setting( 'disallowed_words' );
119
120 if ( $custom_denylist ) {
121 $denylist_data['custom'] = array(
122 'words' => $custom_denylist,
123 );
124 }
125
126 /**
127 * Allows to modify the denylist data.
128 *
129 * @since 6.21
130 *
131 * @param array[] $denylist_data The denylist data.
132 */
133 return apply_filters( 'frm_denylist_data', $denylist_data );
134 }
135
136 /**
137 * Gets denylist IP addresses.
138 *
139 * @return array
140 */
141 protected function get_denylist_ips() {
142 return apply_filters(
143 'frm_denylist_ips_data',
144 array(
145 'files' => array(
146 FrmAppHelper::plugin_path() . '/denylist/ip.txt',
147 ),
148 'custom' => array(),
149 )
150 );
151 }
152
153 /**
154 * Checks spam.
155 *
156 * @return bool
157 */
158 public function check() {
159 if ( $this->check_ip() ) {
160 return true;
161 }
162
163 return $this->check_values();
164 }
165
166 /**
167 * Checks entry values.
168 *
169 * @return bool
170 */
171 protected function check_values() {
172 $allowed_words = $this->get_words_from_setting( 'allowed_words' );
173 $allowed_words = array_map( array( $this, 'convert_to_lowercase' ), $allowed_words );
174
175 foreach ( $this->denylist as $denylist ) {
176 if ( ! empty( $denylist['skip'] ) ) {
177 continue;
178 }
179
180 if ( empty( $denylist['file'] ) && empty( $denylist['words'] ) ) {
181 continue;
182 }
183
184 $this->fill_default_denylist_data( $denylist );
185 $denylist['allowed_words'] = $allowed_words;
186
187 if ( ! empty( $denylist['words'] ) ) {
188 foreach ( $denylist['words'] as $word ) {
189 if ( $this->single_line_check_values( $word, $denylist ) ) {
190 self::add_spam_keyword_to_option( $word );
191 return true;
192 }
193 }
194 } elseif ( file_exists( $denylist['file'] ) ) {
195 $is_spam = $this->read_lines_and_check( $denylist['file'], array( $this, 'single_line_check_values' ), $denylist );
196
197 if ( $is_spam ) {
198 return true;
199 }
200 }
201 }//end foreach
202
203 return false;
204 }
205
206 /**
207 * Fills default denylist data.
208 *
209 * @param array $denylist Denylist.
210 *
211 * @return void
212 */
213 protected function fill_default_denylist_data( &$denylist ) {
214 $denylist = wp_parse_args(
215 $denylist,
216 array(
217 'file' => '',
218 'words' => array(),
219 'is_regex' => false,
220 'field_types' => array(),
221 // Add `other` if you want to skip checking Other values of some field types.
222 'skip_field_types' => array(),
223 // Is ignore if `is_regex` is `true`.
224 'compare' => self::COMPARE_CONTAINS,
225 'extract_value' => '',
226 // If this is `true`, this denylist will be skipped.
227 'skip' => false,
228 )
229 );
230
231 // Some field types should never be checked.
232 $denylist['skip_field_types'] = array_merge(
233 $denylist['skip_field_types'],
234 array( 'password', 'captcha', 'signature', 'checkbox', 'radio', 'select' )
235 );
236 }
237
238 /**
239 * Gets words from setting.
240 *
241 * @param string $setting_key Setting key.
242 *
243 * @return array
244 */
245 protected function get_words_from_setting( $setting_key ) {
246 $frm_settings = FrmAppHelper::get_settings();
247 $words = $frm_settings->$setting_key ?? '';
248
249 if ( ! $words ) {
250 return array();
251 }
252
253 return array_filter(
254 array_map( 'trim', explode( "\n", $words ) )
255 );
256 }
257
258 /**
259 * Checks the values against each single word.
260 *
261 * @param string $line Single line.
262 * @param array $args Check args.
263 *
264 * @return bool
265 */
266 protected function single_line_check_values( $line, $args ) {
267 $line = $this->convert_to_lowercase( $line );
268
269 // Do not check if this word is in the allowed words.
270 if ( ! empty( $args['allowed_words'] ) && in_array( $line, $args['allowed_words'], true ) ) {
271 return false;
272 }
273
274 $values_to_check = $this->get_values_to_check( $args );
275
276 if ( ! $values_to_check ) {
277 // Nothing needs to be checked.
278 return false;
279 }
280
281 if ( ! empty( $args['is_regex'] ) ) {
282 return preg_match( '/' . trim( $line, '/' ) . '/i', $this->convert_values_to_string( $values_to_check ) );
283 }
284
285 if ( self::COMPARE_EQUALS === $args['compare'] ) {
286 foreach ( $values_to_check as $value ) {
287 $value = $this->convert_to_lowercase( $value );
288
289 if ( $line === $value ) {
290 return true;
291 }
292 }
293 return false;
294 }
295
296 $values_str = strtolower( $this->convert_values_to_string( $values_to_check ) );
297 return strpos( $values_str, $line ) !== false;
298 }
299
300 /**
301 * Converts values to string to check.
302 *
303 * @param array $values Values array.
304 *
305 * @return string
306 */
307 protected function convert_values_to_string( $values ) {
308 // Unslash the forward slashes so strings like /joomla/ are not stuck as \/joomla\/.
309 return str_replace( '\\/', '/', FrmAppHelper::maybe_json_encode( $values ) );
310 }
311
312 /**
313 * Converts string to lowercase.
314 *
315 * @param string $str String.
316 *
317 * @return string
318 */
319 protected function convert_to_lowercase( $str ) {
320 return strtolower( $str );
321 }
322
323 /**
324 * Get the field IDs to check.
325 *
326 * @param array $denylist The denylist data.
327 *
328 * @return array|false Return array of field IDs or false if do not need to check.
329 */
330 protected function get_field_ids_to_check( array $denylist ) {
331 $field_types = isset( $denylist['field_types'] ) && is_array( $denylist['field_types'] ) ? $denylist['field_types'] : array();
332 $skip_field_types = isset( $denylist['skip_field_types'] ) && is_array( $denylist['skip_field_types'] ) ? $denylist['skip_field_types'] : array();
333
334 if ( ! $field_types && ! $skip_field_types ) {
335 // This will check all fields.
336 return false;
337 }
338
339 $field_ids_to_check = array();
340
341 foreach ( $this->get_posted_fields() as $field ) {
342 $field_type = FrmField::get_field_type( $field );
343
344 if ( in_array( $field_type, $skip_field_types, true ) ) {
345 continue;
346 }
347
348 if ( $field_types && ! in_array( $field_type, $field_types, true ) ) {
349 continue;
350 }
351
352 $field_ids_to_check[] = intval( $field->id );
353 }
354
355 return $field_ids_to_check;
356 }
357
358 /**
359 * Gets values to check.
360 *
361 * @param array $denylist Single denylist data.
362 *
363 * @return array|false Return `false` if no values need to check, or return array of values.
364 */
365 protected function get_values_to_check( $denylist ) {
366 $field_ids_to_check = $this->get_field_ids_to_check( $denylist );
367
368 if ( array() === $field_ids_to_check ) {
369 // No values need to check.
370 return false;
371 }
372
373 $values_to_check = array();
374
375 foreach ( $this->values['item_meta'] as $key => $value ) {
376 if ( is_array( $value ) && isset( $value['form'] ) ) {
377 // This is a repeater value, loop through sub values.
378 unset( $value['form'] );
379 unset( $value['row_ids'] );
380
381 foreach ( $value as $sub_key => $sub_value ) {
382 if ( $this->should_check_this_field( $sub_key, $field_ids_to_check ) ) {
383 $this->add_to_values_to_check( $values_to_check, $sub_value );
384 }
385 }
386 } elseif ( 'other' === $key ) {
387 if ( ! in_array( 'other', $denylist['skip_field_types'], true ) ) {
388 // This is Other values, loop through this and add sub values.
389 foreach ( $value as $sub_value ) {
390 $this->add_to_values_to_check( $values_to_check, $sub_value );
391 }
392 }
393 } elseif ( $this->should_check_this_field( $key, $field_ids_to_check ) ) {
394 $this->add_to_values_to_check( $values_to_check, $value );
395 }
396 }//end foreach
397
398 if ( isset( $denylist['extract_value'] ) && is_callable( $denylist['extract_value'] ) ) {
399 $values_to_check = call_user_func( $denylist['extract_value'], $values_to_check, $denylist );
400 }
401
402 return $values_to_check;
403 }
404
405 /**
406 * Checks if should check the value of the given field ID.
407 *
408 * @param int $field_id Field ID.
409 * @param int[] $field_ids_to_check Field IDs to check.
410 *
411 * @return bool
412 */
413 protected function should_check_this_field( $field_id, $field_ids_to_check ) {
414 // Should check this field if no field types is specific or this field ID is in the field IDs to check array.
415 return false === $field_ids_to_check || in_array( $field_id, $field_ids_to_check, true );
416 }
417
418 /**
419 * Adds the value to values to check array.
420 *
421 * @param array $values_to_check Values to check array.
422 * @param mixed $value The value.
423 *
424 * @return void
425 */
426 protected function add_to_values_to_check( &$values_to_check, $value ) {
427 $values_to_check[] = is_array( $value ) ? implode( ' ', $value ) : $value;
428 }
429
430 /**
431 * Checks if IP is denied.
432 *
433 * @return bool
434 */
435 protected function check_ip() {
436 $ip = FrmAppHelper::get_ip_address();
437
438 if ( $this->is_allowed_ip( $ip ) ) {
439 return false;
440 }
441
442 $denylist_ips = $this->get_denylist_ips();
443
444 if ( ! empty( $denylist_ips['custom'] ) && $this->ip_matches_array( $ip, $denylist_ips['custom'] ) ) {
445 return true;
446 }
447
448 if ( empty( $denylist_ips['files'] ) || ! is_array( $denylist_ips['files'] ) ) {
449 return false;
450 }
451
452 foreach ( $denylist_ips['files'] as $file ) {
453 if ( ! file_exists( $file ) ) {
454 continue;
455 }
456
457 $is_spam = $this->read_lines_and_check(
458 $file,
459 array( $this, 'single_line_check_ip' ),
460 compact( 'ip' )
461 );
462
463 if ( $is_spam ) {
464 return true;
465 }
466 }
467
468 return false;
469 }
470
471 /**
472 * Reads lines in file and do the check.
473 *
474 * @param string $file_path File path.
475 * @param callable $callback Check callback.
476 * @param array $callback_args Callback args.
477 *
478 * @return bool
479 */
480 protected function read_lines_and_check( $file_path, $callback, $callback_args = array() ) {
481 if ( ! is_callable( $callback ) ) {
482 return false;
483 }
484
485 $fp = @fopen( $file_path, 'r' );
486
487 if ( ! $fp ) {
488 return false;
489 }
490
491 while ( ( $line = fgets( $fp ) ) !== false ) {
492 $line = trim( $line );
493
494 if ( $line === '' ) {
495 continue;
496 }
497
498 $is_spam = $callback( $line, $callback_args );
499
500 if ( $is_spam ) {
501 if ( is_array( $callback ) && isset( $callback[1] ) && 'single_line_check_values' === $callback[1] ) {
502 self::add_spam_keyword_to_option( $line );
503 }
504
505 fclose( $fp );
506 return true;
507 }
508 }
509
510 fclose( $fp );
511 return false;
512 }
513
514 /**
515 * Checks if the given IP is allowed.
516 *
517 * @param string $ip IP address.
518 *
519 * @return bool
520 */
521 protected function is_allowed_ip( $ip ) {
522 return $this->ip_matches_array( $ip, FrmAntiSpamController::get_allowed_ips() );
523 }
524
525 /**
526 * @param string $line
527 * @param array $args
528 *
529 * @return bool
530 */
531 protected function single_line_check_ip( $line, $args ) {
532 return $this->ip_matches( $args['ip'], $line );
533 }
534
535 /**
536 * Checks if the given IP address matches the IP address with CIDR format.
537 *
538 * @param string $ip IP address.
539 * @param string $cidr_ip IP address with CIDR format (x.x.x.x/24).
540 *
541 * @return bool
542 */
543 protected function ip_matches( $ip, $cidr_ip ) {
544 $cidr_parts = explode( '/', $cidr_ip );
545
546 // If the second IP doesn't have CIDR format, just use equals comparison.
547 if ( 1 === count( $cidr_parts ) ) {
548 return $ip === $cidr_ip;
549 }
550
551 if ( 0 === strpos( $ip . '/', $cidr_ip ) ) {
552 // 1.1.1.1 and 1.1.1.1/24 matches.
553 return true;
554 }
555
556 // Validate IP address format - only IPv4 is supported in the CIDR check.
557 if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
558 return false;
559 }
560
561 list( $net, $mask ) = explode( '/', $cidr_ip );
562
563 $ip_net = ip2long( $net );
564 $ip_mask = ~( ( 1 << ( 32 - intval( $mask ) ) ) - 1 ); // phpcs:ignore SlevomatCodingStandard.PHP.UselessParentheses.UselessParentheses
565
566 $ip_ip = ip2long( $ip );
567
568 return ( $ip_ip & $ip_mask ) === ( $ip_net & $ip_mask );
569 }
570
571 /**
572 * Checks if the given IP matches an IP in the array.
573 *
574 * @param string $ip The IP address.
575 * @param string[] $ip_array Array of IP addresses.
576 *
577 * @return bool
578 */
579 protected function ip_matches_array( $ip, $ip_array ) {
580 foreach ( $ip_array as $cidr_ip ) {
581 if ( $this->ip_matches( $ip, $cidr_ip ) ) {
582 return true;
583 }
584 }
585 return false;
586 }
587
588 protected function get_spam_message() {
589 return __( 'Your entry appears to be blocked spam!', 'formidable' );
590 }
591
592 /**
593 * @param string $keyword
594 *
595 * @return void
596 */
597 private function add_spam_keyword_to_option( $keyword ) {
598 $transient_name = 'frm_recent_spam_detected';
599 $transient = get_transient( $transient_name );
600
601 if ( ! is_array( $transient ) ) {
602 $transient = array();
603 }
604
605 if ( in_array( $keyword, $transient, true ) ) {
606 return;
607 }
608
609 $transient[] = $keyword;
610 set_transient( $transient_name, $transient, DAY_IN_SECONDS );
611 }
612 }
613