PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.22
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.22
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.22, at classes/models/FrmSpamCheckDenylist.php

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