| @@ -2,8 +2,9 @@ | ||
| 2 | 2 | /** |
| 3 | 3 | * Spam check using denylist |
| 4 | 4 | * |
| 5 | 5 | * @since 6.21 |
| 6 | + * | |
| 6 | 7 | * @package Formidable |
| 7 | 8 | */ |
| 8 | 9 | |
| 9 | 10 | if ( ! defined( 'ABSPATH' ) ) { |
| @@ -15,12 +16,46 @@ | ||
| 15 | 16 | const COMPARE_CONTAINS = ''; |
| 16 | 17 | |
| 17 | 18 | const COMPARE_EQUALS = 'equals'; |
| 18 | 19 | |
| 20 | + /** | |
| 21 | + * How many leading characters of a denylist line are used as its index key. | |
| 22 | + * Four measured best on the shipped denylists: shorter keys are not selective | |
| 23 | + * enough, and longer ones push more lines below the length the index needs. | |
| 24 | + * | |
| 25 | + * @since 6.35 | |
| 26 | + */ | |
| 27 | + const PREFIX_LENGTH = 4; | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * Values shorter than this are not indexed. Comparing them is already cheap | |
| 31 | + * enough that building the index would cost more than it saves. | |
| 32 | + * | |
| 33 | + * @since 6.35 | |
| 34 | + */ | |
| 35 | + const MIN_LENGTH_TO_INDEX = 1024; | |
| 36 | + | |
| 37 | + /** | |
| 38 | + * Values longer than this are not indexed, so the index cannot grow without | |
| 39 | + * bound on an unusually large submission. | |
| 40 | + * | |
| 41 | + * @since 6.35 | |
| 42 | + */ | |
| 43 | + const MAX_LENGTH_TO_INDEX = 524288; | |
| 44 | + | |
| 45 | + /** | |
| 46 | + * @var array|null | |
| 47 | + */ | |
| 19 | 48 | protected $posted_fields; |
| 20 | 49 | |
| 50 | + /** | |
| 51 | + * @var array | |
| 52 | + */ | |
| 21 | 53 | protected $denylist; |
| 22 | 54 | |
| 55 | + /** | |
| 56 | + * @param array $values | |
| 57 | + */ | |
| 23 | 58 | public function __construct( $values ) { |
| 24 | 59 | $this->maybe_add_form_id_to_values( $values ); |
| 25 | 60 | |
| 26 | 61 | parent::__construct( $values ); |
| @@ -27,8 +62,11 @@ | ||
| 27 | 62 | |
| 28 | 63 | $this->denylist = $this->get_denylist_array(); |
| 29 | 64 | } |
| 30 | 65 | |
| 66 | + /** | |
| 67 | + * @return array | |
| 68 | + */ | |
| 31 | 69 | protected function get_posted_fields() { |
| 32 | 70 | if ( is_null( $this->posted_fields ) ) { |
| 33 | 71 | $this->posted_fields = FrmField::get_all_for_form( $this->values['form_id'] ); |
| 34 | 72 | } |
| @@ -38,8 +76,10 @@ | ||
| 38 | 76 | /** |
| 39 | 77 | * Maybe add form ID to values. In file name validation, only item_meta in $values. |
| 40 | 78 | * |
| 41 | 79 | * @param array $values Spam check values. |
| 80 | + * | |
| 81 | + * @return void | |
| 42 | 82 | */ |
| 43 | 83 | protected function maybe_add_form_id_to_values( &$values ) { |
| 44 | 84 | if ( ! empty( $values['form_id'] ) || empty( $values['item_meta'] ) ) { |
| 45 | 85 | return; |
| @@ -46,8 +86,9 @@ | ||
| 46 | 86 | } |
| 47 | 87 | |
| 48 | 88 | $field_id = key( $values['item_meta'] ); |
| 49 | 89 | $field = FrmField::getOne( $field_id ); |
| 90 | + | |
| 50 | 91 | if ( $field ) { |
| 51 | 92 | $values['form_id'] = $field->form_id; |
| 52 | 93 | } |
| 53 | 94 | } |
| @@ -79,9 +120,9 @@ | ||
| 79 | 120 | 'file' => FrmAppHelper::plugin_path() . '/denylist/domain-partial.txt', |
| 80 | 121 | ), |
| 81 | 122 | array( |
| 82 | 123 | 'file' => FrmAppHelper::plugin_path() . '/denylist/splorp-wp-comment.txt', |
| 83 | - 'skip' => FrmAppHelper::current_user_can( 'frm_create_entries' ), | |
| 124 | + 'skip' => current_user_can( 'frm_create_entries' ), | |
| 84 | 125 | 'skip_field_types' => array( 'file' ), |
| 85 | 126 | ), |
| 86 | 127 | array( |
| 87 | 128 | 'words' => array( |
| @@ -99,8 +140,9 @@ | ||
| 99 | 140 | ), |
| 100 | 141 | ); |
| 101 | 142 | |
| 102 | 143 | $custom_denylist = $this->get_words_from_setting( 'disallowed_words' ); |
| 144 | + | |
| 103 | 145 | if ( $custom_denylist ) { |
| 104 | 146 | $denylist_data['custom'] = array( |
| 105 | 147 | 'words' => $custom_denylist, |
| 106 | 148 | ); |
| @@ -138,13 +180,9 @@ | ||
| 138 | 180 | * |
| 139 | 181 | * @return bool |
| 140 | 182 | */ |
| 141 | 183 | public function check() { |
| 142 | - if ( $this->check_ip() ) { | |
| 143 | - return true; | |
| 144 | - } | |
| 145 | - | |
| 146 | - return $this->check_values(); | |
| 184 | + return $this->check_ip() ? true : $this->check_values(); | |
| 147 | 185 | } |
| 148 | 186 | |
| 149 | 187 | /** |
| 150 | 188 | * Checks entry values. |
| @@ -166,8 +204,13 @@ | ||
| 166 | 204 | |
| 167 | 205 | $this->fill_default_denylist_data( $denylist ); |
| 168 | 206 | $denylist['allowed_words'] = $allowed_words; |
| 169 | 207 | |
| 208 | + if ( ! $this->add_values_to_check( $denylist ) ) { | |
| 209 | + // Nothing in this submission needs to be checked against this denylist. | |
| 210 | + continue; | |
| 211 | + } | |
| 212 | + | |
| 170 | 213 | if ( ! empty( $denylist['words'] ) ) { |
| 171 | 214 | foreach ( $denylist['words'] as $word ) { |
| 172 | 215 | if ( $this->single_line_check_values( $word, $denylist ) ) { |
| 173 | 216 | self::add_spam_keyword_to_option( $word ); |
| @@ -175,8 +218,9 @@ | ||
| 175 | 218 | } |
| 176 | 219 | } |
| 177 | 220 | } elseif ( file_exists( $denylist['file'] ) ) { |
| 178 | 221 | $is_spam = $this->read_lines_and_check( $denylist['file'], array( $this, 'single_line_check_values' ), $denylist ); |
| 222 | + | |
| 179 | 223 | if ( $is_spam ) { |
| 180 | 224 | return true; |
| 181 | 225 | } |
| 182 | 226 | } |
| @@ -188,8 +232,10 @@ | ||
| 188 | 232 | /** |
| 189 | 233 | * Fills default denylist data. |
| 190 | 234 | * |
| 191 | 235 | * @param array $denylist Denylist. |
| 236 | + * | |
| 237 | + * @return void | |
| 192 | 238 | */ |
| 193 | 239 | protected function fill_default_denylist_data( &$denylist ) { |
| 194 | 240 | $denylist = wp_parse_args( |
| 195 | 241 | $denylist, |
| @@ -210,21 +256,118 @@ | ||
| 210 | 256 | |
| 211 | 257 | // Some field types should never be checked. |
| 212 | 258 | $denylist['skip_field_types'] = array_merge( |
| 213 | 259 | $denylist['skip_field_types'], |
| 214 | - array( 'password', 'captcha', 'signature', 'checkbox', 'radio', 'select' ) | |
| 260 | + array( 'password', 'captcha', 'signature', 'checkbox', 'radio', 'select', 'ranking' ) | |
| 215 | 261 | ); |
| 216 | 262 | } |
| 217 | 263 | |
| 218 | 264 | /** |
| 265 | + * Extracts the submitted values this denylist needs to check, and the string | |
| 266 | + * forms those values are compared against. | |
| 267 | + * | |
| 268 | + * The values depend on the denylist configuration, not on the word or file line | |
| 269 | + * being compared, so they are extracted once here and carried on the denylist. | |
| 270 | + * The shipped denylist files hold tens of thousands of lines and a large form | |
| 271 | + * posts more than a thousand values, so extracting per line is quadratic. | |
| 272 | + * | |
| 273 | + * @since 6.35 | |
| 274 | + * | |
| 275 | + * @param array $denylist Denylist data, with the defaults already filled in. | |
| 276 | + * | |
| 277 | + * @return bool False if this submission has no values for this denylist to check. | |
| 278 | + */ | |
| 279 | + protected function add_values_to_check( &$denylist ) { | |
| 280 | + $values_to_check = $this->get_values_to_check( $denylist ); | |
| 281 | + | |
| 282 | + if ( ! $values_to_check ) { | |
| 283 | + return false; | |
| 284 | + } | |
| 285 | + | |
| 286 | + $values_string = $this->convert_values_to_string( $values_to_check ); | |
| 287 | + | |
| 288 | + $denylist['values_to_check'] = $values_to_check; | |
| 289 | + $denylist['values_string'] = $values_string; | |
| 290 | + $denylist['values_string_lower'] = $this->convert_to_lowercase( $values_string ); | |
| 291 | + $denylist['values_prefix_index'] = $this->get_values_prefix_index( $denylist ); | |
| 292 | + | |
| 293 | + return true; | |
| 294 | + } | |
| 295 | + | |
| 296 | + /** | |
| 297 | + * Indexes every PREFIX_LENGTH character window of the values. | |
| 298 | + * | |
| 299 | + * A line can only be inside the values if its own first PREFIX_LENGTH | |
| 300 | + * characters are somewhere in them, so a line whose prefix is missing from | |
| 301 | + * this index cannot match and does not need to be compared at all. The shipped | |
| 302 | + * denylists hold tens of thousands of lines and each comparison reads the whole | |
| 303 | + * values string, so ruling a line out with one array lookup is worth the index. | |
| 304 | + * | |
| 305 | + * Returns an empty array when the index would not answer for this denylist, or | |
| 306 | + * would not pay for itself. Every line is then compared as before. | |
| 307 | + * | |
| 308 | + * @since 6.35 | |
| 309 | + * | |
| 310 | + * @param array $denylist Denylist data, holding the values strings. | |
| 311 | + * | |
| 312 | + * @return array Index of value prefixes, or an empty array for no index. | |
| 313 | + */ | |
| 314 | + protected function get_values_prefix_index( $denylist ) { | |
| 315 | + if ( ! empty( $denylist['is_regex'] ) || self::COMPARE_CONTAINS !== $denylist['compare'] ) { | |
| 316 | + // A regex line is a pattern rather than a literal, so its leading | |
| 317 | + // characters are not text to look for. Only "contains" is indexable. | |
| 318 | + return array(); | |
| 319 | + } | |
| 320 | + | |
| 321 | + $values = $denylist['values_string_lower']; | |
| 322 | + $length = strlen( $values ); | |
| 323 | + | |
| 324 | + if ( $length < self::MIN_LENGTH_TO_INDEX || $length > self::MAX_LENGTH_TO_INDEX ) { | |
| 325 | + return array(); | |
| 326 | + } | |
| 327 | + | |
| 328 | + $index = array(); | |
| 329 | + $last = $length - self::PREFIX_LENGTH; | |
| 330 | + | |
| 331 | + for ( $i = 0; $i <= $last; $i++ ) { | |
| 332 | + $index[ substr( $values, $i, self::PREFIX_LENGTH ) ] = true; | |
| 333 | + } | |
| 334 | + | |
| 335 | + return $index; | |
| 336 | + } | |
| 337 | + | |
| 338 | + /** | |
| 339 | + * Checks the values index to rule a line out before comparing it. | |
| 340 | + * | |
| 341 | + * A `false` here does not mean the line matches, only that the index cannot | |
| 342 | + * rule it out, so the caller still has to compare it. | |
| 343 | + * | |
| 344 | + * @since 6.35 | |
| 345 | + * | |
| 346 | + * @param string $line The lowercased denylist line. | |
| 347 | + * @param array $args Check args, holding the index when there is one. | |
| 348 | + * | |
| 349 | + * @return bool True when the line cannot be inside the values. | |
| 350 | + */ | |
| 351 | + protected function line_is_ruled_out( $line, $args ) { | |
| 352 | + if ( empty( $args['values_prefix_index'] ) || strlen( $line ) < self::PREFIX_LENGTH ) { | |
| 353 | + return false; | |
| 354 | + } | |
| 355 | + | |
| 356 | + return ! isset( $args['values_prefix_index'][ substr( $line, 0, self::PREFIX_LENGTH ) ] ); | |
| 357 | + } | |
| 358 | + | |
| 359 | + /** | |
| 219 | 360 | * Gets words from setting. |
| 220 | 361 | * |
| 221 | 362 | * @param string $setting_key Setting key. |
| 363 | + * | |
| 222 | 364 | * @return array |
| 223 | 365 | */ |
| 224 | 366 | protected function get_words_from_setting( $setting_key ) { |
| 225 | 367 | $frm_settings = FrmAppHelper::get_settings(); |
| 226 | 368 | $words = $frm_settings->$setting_key ?? ''; |
| 369 | + | |
| 227 | 370 | if ( ! $words ) { |
| 228 | 371 | return array(); |
| 229 | 372 | } |
| 230 | 373 | |
| @@ -236,40 +379,47 @@ | ||
| 236 | 379 | /** |
| 237 | 380 | * Checks the values against each single word. |
| 238 | 381 | * |
| 239 | 382 | * @param string $line Single line. |
| 240 | - * @param array $args Check args. | |
| 383 | + * @param array $args Check args. Carries the values to check when they have | |
| 384 | + * already been extracted by {@see FrmSpamCheckDenylist::add_values_to_check()}. | |
| 385 | + * | |
| 241 | 386 | * @return bool |
| 242 | 387 | */ |
| 243 | 388 | protected function single_line_check_values( $line, $args ) { |
| 244 | 389 | $line = $this->convert_to_lowercase( $line ); |
| 390 | + | |
| 245 | 391 | // Do not check if this word is in the allowed words. |
| 246 | 392 | if ( ! empty( $args['allowed_words'] ) && in_array( $line, $args['allowed_words'], true ) ) { |
| 247 | 393 | return false; |
| 248 | 394 | } |
| 249 | 395 | |
| 250 | - $values_to_check = $this->get_values_to_check( $args ); | |
| 251 | - if ( ! $values_to_check ) { | |
| 396 | + if ( ! isset( $args['values_to_check'] ) && ! $this->add_values_to_check( $args ) ) { | |
| 252 | 397 | // Nothing needs to be checked. |
| 253 | 398 | return false; |
| 254 | 399 | } |
| 255 | 400 | |
| 256 | 401 | if ( ! empty( $args['is_regex'] ) ) { |
| 257 | - return preg_match( '/' . trim( $line, '/' ) . '/i', $this->convert_values_to_string( $values_to_check ) ); | |
| 402 | + return preg_match( '/' . trim( $line, '/' ) . '/i', $args['values_string'] ); | |
| 258 | 403 | } |
| 259 | 404 | |
| 260 | 405 | if ( self::COMPARE_EQUALS === $args['compare'] ) { |
| 261 | - foreach ( $values_to_check as $value ) { | |
| 406 | + foreach ( $args['values_to_check'] as $value ) { | |
| 262 | 407 | $value = $this->convert_to_lowercase( $value ); |
| 408 | + | |
| 263 | 409 | if ( $line === $value ) { |
| 264 | 410 | return true; |
| 265 | 411 | } |
| 266 | 412 | } |
| 413 | + | |
| 267 | 414 | return false; |
| 268 | 415 | } |
| 269 | 416 | |
| 270 | - $values_str = strtolower( $this->convert_values_to_string( $values_to_check ) ); | |
| 271 | - return strpos( $values_str, $line ) !== false; | |
| 417 | + if ( $this->line_is_ruled_out( $line, $args ) ) { | |
| 418 | + return false; | |
| 419 | + } | |
| 420 | + | |
| 421 | + return str_contains( $args['values_string_lower'], $line ); | |
| 272 | 422 | } |
| 273 | 423 | |
| 274 | 424 | /** |
| 275 | 425 | * Converts values to string to check. |
| @@ -274,8 +424,9 @@ | ||
| 274 | 424 | /** |
| 275 | 425 | * Converts values to string to check. |
| 276 | 426 | * |
| 277 | 427 | * @param array $values Values array. |
| 428 | + * | |
| 278 | 429 | * @return string |
| 279 | 430 | */ |
| 280 | 431 | protected function convert_values_to_string( $values ) { |
| 281 | 432 | // Unslash the forward slashes so strings like /joomla/ are not stuck as \/joomla\/. |
| @@ -285,8 +436,9 @@ | ||
| 285 | 436 | /** |
| 286 | 437 | * Converts string to lowercase. |
| 287 | 438 | * |
| 288 | 439 | * @param string $str String. |
| 440 | + * | |
| 289 | 441 | * @return string |
| 290 | 442 | */ |
| 291 | 443 | protected function convert_to_lowercase( $str ) { |
| 292 | 444 | return strtolower( $str ); |
| @@ -308,10 +460,12 @@ | ||
| 308 | 460 | return false; |
| 309 | 461 | } |
| 310 | 462 | |
| 311 | 463 | $field_ids_to_check = array(); |
| 464 | + | |
| 312 | 465 | foreach ( $this->get_posted_fields() as $field ) { |
| 313 | 466 | $field_type = FrmField::get_field_type( $field ); |
| 467 | + | |
| 314 | 468 | if ( in_array( $field_type, $skip_field_types, true ) ) { |
| 315 | 469 | continue; |
| 316 | 470 | } |
| 317 | 471 | |
| @@ -328,12 +482,14 @@ | ||
| 328 | 482 | /** |
| 329 | 483 | * Gets values to check. |
| 330 | 484 | * |
| 331 | 485 | * @param array $denylist Single denylist data. |
| 486 | + * | |
| 332 | 487 | * @return array|false Return `false` if no values need to check, or return array of values. |
| 333 | 488 | */ |
| 334 | 489 | protected function get_values_to_check( $denylist ) { |
| 335 | 490 | $field_ids_to_check = $this->get_field_ids_to_check( $denylist ); |
| 491 | + | |
| 336 | 492 | if ( array() === $field_ids_to_check ) { |
| 337 | 493 | // No values need to check. |
| 338 | 494 | return false; |
| 339 | 495 | } |
| @@ -338,8 +494,9 @@ | ||
| 338 | 494 | return false; |
| 339 | 495 | } |
| 340 | 496 | |
| 341 | 497 | $values_to_check = array(); |
| 498 | + | |
| 342 | 499 | foreach ( $this->values['item_meta'] as $key => $value ) { |
| 343 | 500 | if ( is_array( $value ) && isset( $value['form'] ) ) { |
| 344 | 501 | // This is a repeater value, loop through sub values. |
| 345 | 502 | unset( $value['form'] ); |
| @@ -362,9 +519,9 @@ | ||
| 362 | 519 | } |
| 363 | 520 | }//end foreach |
| 364 | 521 | |
| 365 | 522 | if ( isset( $denylist['extract_value'] ) && is_callable( $denylist['extract_value'] ) ) { |
| 366 | - $values_to_check = call_user_func( $denylist['extract_value'], $values_to_check, $denylist ); | |
| 523 | + return call_user_func( $denylist['extract_value'], $values_to_check, $denylist ); | |
| 367 | 524 | } |
| 368 | 525 | |
| 369 | 526 | return $values_to_check; |
| 370 | 527 | } |
| @@ -371,10 +528,11 @@ | ||
| 371 | 528 | |
| 372 | 529 | /** |
| 373 | 530 | * Checks if should check the value of the given field ID. |
| 374 | 531 | * |
| 375 | - * @param int $field_id Field ID. | |
| 376 | - * @param int[] $field_ids_to_check Field IDs to check. | |
| 532 | + * @param int $field_id Field ID. | |
| 533 | + * @param false|int[] $field_ids_to_check Field IDs to check. | |
| 534 | + * | |
| 377 | 535 | * @return bool |
| 378 | 536 | */ |
| 379 | 537 | protected function should_check_this_field( $field_id, $field_ids_to_check ) { |
| 380 | 538 | // Should check this field if no field types is specific or this field ID is in the field IDs to check array. |
| @@ -385,11 +543,13 @@ | ||
| 385 | 543 | * Adds the value to values to check array. |
| 386 | 544 | * |
| 387 | 545 | * @param array $values_to_check Values to check array. |
| 388 | 546 | * @param mixed $value The value. |
| 547 | + * | |
| 548 | + * @return void | |
| 389 | 549 | */ |
| 390 | 550 | protected function add_to_values_to_check( &$values_to_check, $value ) { |
| 391 | - $values_to_check[] = is_array( $value ) ? implode( ' ', $value ) : $value; | |
| 551 | + $values_to_check[] = is_array( $value ) ? FrmAppHelper::safe_implode( ' ', $value ) : $value; | |
| 392 | 552 | } |
| 393 | 553 | |
| 394 | 554 | /** |
| 395 | 555 | * Checks if IP is denied. |
| @@ -397,8 +557,9 @@ | ||
| 397 | 557 | * @return bool |
| 398 | 558 | */ |
| 399 | 559 | protected function check_ip() { |
| 400 | 560 | $ip = FrmAppHelper::get_ip_address(); |
| 561 | + | |
| 401 | 562 | if ( $this->is_allowed_ip( $ip ) ) { |
| 402 | 563 | return false; |
| 403 | 564 | } |
| 404 | 565 | |
| @@ -436,8 +597,9 @@ | ||
| 436 | 597 | * |
| 437 | 598 | * @param string $file_path File path. |
| 438 | 599 | * @param callable $callback Check callback. |
| 439 | 600 | * @param array $callback_args Callback args. |
| 601 | + * | |
| 440 | 602 | * @return bool |
| 441 | 603 | */ |
| 442 | 604 | protected function read_lines_and_check( $file_path, $callback, $callback_args = array() ) { |
| 443 | 605 | if ( ! is_callable( $callback ) ) { |
| @@ -444,8 +606,9 @@ | ||
| 444 | 606 | return false; |
| 445 | 607 | } |
| 446 | 608 | |
| 447 | 609 | $fp = @fopen( $file_path, 'r' ); |
| 610 | + | |
| 448 | 611 | if ( ! $fp ) { |
| 449 | 612 | return false; |
| 450 | 613 | } |
| 451 | 614 | |
| @@ -450,21 +613,25 @@ | ||
| 450 | 613 | } |
| 451 | 614 | |
| 452 | 615 | while ( ( $line = fgets( $fp ) ) !== false ) { |
| 453 | 616 | $line = trim( $line ); |
| 617 | + | |
| 454 | 618 | if ( $line === '' ) { |
| 455 | 619 | continue; |
| 456 | 620 | } |
| 457 | 621 | |
| 458 | 622 | $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 | 623 | |
| 464 | - fclose( $fp ); | |
| 465 | - return true; | |
| 624 | + if ( ! $is_spam ) { | |
| 625 | + continue; | |
| 466 | 626 | } |
| 627 | + | |
| 628 | + if ( is_array( $callback ) && isset( $callback[1] ) && 'single_line_check_values' === $callback[1] ) { | |
| 629 | + self::add_spam_keyword_to_option( $line ); | |
| 630 | + } | |
| 631 | + | |
| 632 | + fclose( $fp ); | |
| 633 | + return true; | |
| 467 | 634 | } |
| 468 | 635 | |
| 469 | 636 | fclose( $fp ); |
| 470 | 637 | return false; |
| @@ -473,8 +640,9 @@ | ||
| 473 | 640 | /** |
| 474 | 641 | * Checks if the given IP is allowed. |
| 475 | 642 | * |
| 476 | 643 | * @param string $ip IP address. |
| 644 | + * | |
| 477 | 645 | * @return bool |
| 478 | 646 | */ |
| 479 | 647 | protected function is_allowed_ip( $ip ) { |
| 480 | 648 | return $this->ip_matches_array( $ip, FrmAntiSpamController::get_allowed_ips() ); |
| @@ -479,8 +647,14 @@ | ||
| 479 | 647 | protected function is_allowed_ip( $ip ) { |
| 480 | 648 | return $this->ip_matches_array( $ip, FrmAntiSpamController::get_allowed_ips() ); |
| 481 | 649 | } |
| 482 | 650 | |
| 651 | + /** | |
| 652 | + * @param string $line | |
| 653 | + * @param array $args | |
| 654 | + * | |
| 655 | + * @return bool | |
| 656 | + */ | |
| 483 | 657 | protected function single_line_check_ip( $line, $args ) { |
| 484 | 658 | return $this->ip_matches( $args['ip'], $line ); |
| 485 | 659 | } |
| 486 | 660 | |
| @@ -488,8 +662,9 @@ | ||
| 488 | 662 | * Checks if the given IP address matches the IP address with CIDR format. |
| 489 | 663 | * |
| 490 | 664 | * @param string $ip IP address. |
| 491 | 665 | * @param string $cidr_ip IP address with CIDR format (x.x.x.x/24). |
| 666 | + * | |
| 492 | 667 | * @return bool |
| 493 | 668 | */ |
| 494 | 669 | protected function ip_matches( $ip, $cidr_ip ) { |
| 495 | 670 | $cidr_parts = explode( '/', $cidr_ip ); |
| @@ -498,9 +673,9 @@ | ||
| 498 | 673 | if ( 1 === count( $cidr_parts ) ) { |
| 499 | 674 | return $ip === $cidr_ip; |
| 500 | 675 | } |
| 501 | 676 | |
| 502 | - if ( 0 === strpos( $ip . '/', $cidr_ip ) ) { | |
| 677 | + if ( str_starts_with( $ip . '/', $cidr_ip ) ) { | |
| 503 | 678 | // 1.1.1.1 and 1.1.1.1/24 matches. |
| 504 | 679 | return true; |
| 505 | 680 | } |
| 506 | 681 | |
| @@ -523,8 +698,9 @@ | ||
| 523 | 698 | * Checks if the given IP matches an IP in the array. |
| 524 | 699 | * |
| 525 | 700 | * @param string $ip The IP address. |
| 526 | 701 | * @param string[] $ip_array Array of IP addresses. |
| 702 | + * | |
| 527 | 703 | * @return bool |
| 528 | 704 | */ |
| 529 | 705 | protected function ip_matches_array( $ip, $ip_array ) { |
| 530 | 706 | foreach ( $ip_array as $cidr_ip ) { |
| @@ -538,11 +714,17 @@ | ||
| 538 | 714 | protected function get_spam_message() { |
| 539 | 715 | return __( 'Your entry appears to be blocked spam!', 'formidable' ); |
| 540 | 716 | } |
| 541 | 717 | |
| 718 | + /** | |
| 719 | + * @param string $keyword | |
| 720 | + * | |
| 721 | + * @return void | |
| 722 | + */ | |
| 542 | 723 | private function add_spam_keyword_to_option( $keyword ) { |
| 543 | 724 | $transient_name = 'frm_recent_spam_detected'; |
| 544 | 725 | $transient = get_transient( $transient_name ); |
| 726 | + | |
| 545 | 727 | if ( ! is_array( $transient ) ) { |
| 546 | 728 | $transient = array(); |
| 547 | 729 | } |
| 548 | 730 | |