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

557 lines 14.3 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 // Unslash the forward slashes so strings like /joomla/ are not stuck as \/joomla\/.
282 return str_replace( '\\/', '/', FrmAppHelper::maybe_json_encode( $values ) );
283 }
284
285 /**
286 * Converts string to lowercase.
287 *
288 * @param string $str String.
289 * @return string
290 */
291 protected function convert_to_lowercase( $str ) {
292 return strtolower( $str );
293 }
294
295 /**
296 * Get the field IDs to check.
297 *
298 * @param array $denylist The denylist data.
299 *
300 * @return array|false Return array of field IDs or false if do not need to check.
301 */
302 protected function get_field_ids_to_check( array $denylist ) {
303 $field_types = isset( $denylist['field_types'] ) && is_array( $denylist['field_types'] ) ? $denylist['field_types'] : array();
304 $skip_field_types = isset( $denylist['skip_field_types'] ) && is_array( $denylist['skip_field_types'] ) ? $denylist['skip_field_types'] : array();
305
306 if ( ! $field_types && ! $skip_field_types ) {
307 // This will check all fields.
308 return false;
309 }
310
311 $field_ids_to_check = array();
312 foreach ( $this->get_posted_fields() as $field ) {
313 $field_type = FrmField::get_field_type( $field );
314 if ( in_array( $field_type, $skip_field_types, true ) ) {
315 continue;
316 }
317
318 if ( $field_types && ! in_array( $field_type, $field_types, true ) ) {
319 continue;
320 }
321
322 $field_ids_to_check[] = intval( $field->id );
323 }
324
325 return $field_ids_to_check;
326 }
327
328 /**
329 * Gets values to check.
330 *
331 * @param array $denylist Single denylist data.
332 * @return array|false Return `false` if no values need to check, or return array of values.
333 */
334 protected function get_values_to_check( $denylist ) {
335 $field_ids_to_check = $this->get_field_ids_to_check( $denylist );
336 if ( array() === $field_ids_to_check ) {
337 // No values need to check.
338 return false;
339 }
340
341 $values_to_check = array();
342 foreach ( $this->values['item_meta'] as $key => $value ) {
343 if ( is_array( $value ) && isset( $value['form'] ) ) {
344 // This is a repeater value, loop through sub values.
345 unset( $value['form'] );
346 unset( $value['row_ids'] );
347
348 foreach ( $value as $sub_key => $sub_value ) {
349 if ( $this->should_check_this_field( $sub_key, $field_ids_to_check ) ) {
350 $this->add_to_values_to_check( $values_to_check, $sub_value );
351 }
352 }
353 } elseif ( 'other' === $key ) {
354 if ( ! in_array( 'other', $denylist['skip_field_types'], true ) ) {
355 // This is Other values, loop through this and add sub values.
356 foreach ( $value as $sub_value ) {
357 $this->add_to_values_to_check( $values_to_check, $sub_value );
358 }
359 }
360 } elseif ( $this->should_check_this_field( $key, $field_ids_to_check ) ) {
361 $this->add_to_values_to_check( $values_to_check, $value );
362 }
363 }//end foreach
364
365 if ( isset( $denylist['extract_value'] ) && is_callable( $denylist['extract_value'] ) ) {
366 $values_to_check = call_user_func( $denylist['extract_value'], $values_to_check, $denylist );
367 }
368
369 return $values_to_check;
370 }
371
372 /**
373 * Checks if should check the value of the given field ID.
374 *
375 * @param int $field_id Field ID.
376 * @param int[] $field_ids_to_check Field IDs to check.
377 * @return bool
378 */
379 protected function should_check_this_field( $field_id, $field_ids_to_check ) {
380 // Should check this field if no field types is specific or this field ID is in the field IDs to check array.
381 return false === $field_ids_to_check || in_array( $field_id, $field_ids_to_check, true );
382 }
383
384 /**
385 * Adds the value to values to check array.
386 *
387 * @param array $values_to_check Values to check array.
388 * @param mixed $value The value.
389 */
390 protected function add_to_values_to_check( &$values_to_check, $value ) {
391 $values_to_check[] = is_array( $value ) ? implode( ' ', $value ) : $value;
392 }
393
394 /**
395 * Checks if IP is denied.
396 *
397 * @return bool
398 */
399 protected function check_ip() {
400 $ip = FrmAppHelper::get_ip_address();
401 if ( $this->is_allowed_ip( $ip ) ) {
402 return false;
403 }
404
405 $denylist_ips = $this->get_denylist_ips();
406
407 if ( ! empty( $denylist_ips['custom'] ) && $this->ip_matches_array( $ip, $denylist_ips['custom'] ) ) {
408 return true;
409 }
410
411 if ( empty( $denylist_ips['files'] ) || ! is_array( $denylist_ips['files'] ) ) {
412 return false;
413 }
414
415 foreach ( $denylist_ips['files'] as $file ) {
416 if ( ! file_exists( $file ) ) {
417 continue;
418 }
419
420 $is_spam = $this->read_lines_and_check(
421 $file,
422 array( $this, 'single_line_check_ip' ),
423 compact( 'ip' )
424 );
425
426 if ( $is_spam ) {
427 return true;
428 }
429 }
430
431 return false;
432 }
433
434 /**
435 * Reads lines in file and do the check.
436 *
437 * @param string $file_path File path.
438 * @param callable $callback Check callback.
439 * @param array $callback_args Callback args.
440 * @return bool
441 */
442 protected function read_lines_and_check( $file_path, $callback, $callback_args = array() ) {
443 if ( ! is_callable( $callback ) ) {
444 return false;
445 }
446
447 $fp = @fopen( $file_path, 'r' );
448 if ( ! $fp ) {
449 return false;
450 }
451
452 while ( ( $line = fgets( $fp ) ) !== false ) {
453 $line = trim( $line );
454 if ( $line === '' ) {
455 continue;
456 }
457
458 $is_spam = $callback( $line, $callback_args );
459 if ( $is_spam ) {
460 if ( is_array( $callback ) && isset( $callback[1] ) && 'single_line_check_values' === $callback[1] ) {
461 self::add_spam_keyword_to_option( $line );
462 }
463
464 fclose( $fp );
465 return true;
466 }
467 }
468
469 fclose( $fp );
470 return false;
471 }
472
473 /**
474 * Checks if the given IP is allowed.
475 *
476 * @param string $ip IP address.
477 * @return bool
478 */
479 protected function is_allowed_ip( $ip ) {
480 return $this->ip_matches_array( $ip, FrmAntiSpamController::get_allowed_ips() );
481 }
482
483 protected function single_line_check_ip( $line, $args ) {
484 return $this->ip_matches( $args['ip'], $line );
485 }
486
487 /**
488 * Checks if the given IP address matches the IP address with CIDR format.
489 *
490 * @param string $ip IP address.
491 * @param string $cidr_ip IP address with CIDR format (x.x.x.x/24).
492 * @return bool
493 */
494 protected function ip_matches( $ip, $cidr_ip ) {
495 $cidr_parts = explode( '/', $cidr_ip );
496
497 // If the second IP doesn't have CIDR format, just use equals comparison.
498 if ( 1 === count( $cidr_parts ) ) {
499 return $ip === $cidr_ip;
500 }
501
502 if ( 0 === strpos( $ip . '/', $cidr_ip ) ) {
503 // 1.1.1.1 and 1.1.1.1/24 matches.
504 return true;
505 }
506
507 // Validate IP address format - only IPv4 is supported in the CIDR check.
508 if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
509 return false;
510 }
511
512 list( $net, $mask ) = explode( '/', $cidr_ip );
513
514 $ip_net = ip2long( $net );
515 $ip_mask = ~( ( 1 << ( 32 - intval( $mask ) ) ) - 1 ); // phpcs:ignore SlevomatCodingStandard.PHP.UselessParentheses.UselessParentheses
516
517 $ip_ip = ip2long( $ip );
518
519 return ( $ip_ip & $ip_mask ) === ( $ip_net & $ip_mask );
520 }
521
522 /**
523 * Checks if the given IP matches an IP in the array.
524 *
525 * @param string $ip The IP address.
526 * @param string[] $ip_array Array of IP addresses.
527 * @return bool
528 */
529 protected function ip_matches_array( $ip, $ip_array ) {
530 foreach ( $ip_array as $cidr_ip ) {
531 if ( $this->ip_matches( $ip, $cidr_ip ) ) {
532 return true;
533 }
534 }
535 return false;
536 }
537
538 protected function get_spam_message() {
539 return __( 'Your entry appears to be blocked spam!', 'formidable' );
540 }
541
542 private function add_spam_keyword_to_option( $keyword ) {
543 $transient_name = 'frm_recent_spam_detected';
544 $transient = get_transient( $transient_name );
545 if ( ! is_array( $transient ) ) {
546 $transient = array();
547 }
548
549 if ( in_array( $keyword, $transient, true ) ) {
550 return;
551 }
552
553 $transient[] = $keyword;
554 set_transient( $transient_name, $transient, DAY_IN_SECONDS );
555 }
556 }
557