PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
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
← All changes | classes/models/FrmEntryValidate.php +492 -171 6.4.1 → trunk View file →
@@ -5,10 +5,18 @@
5 5
6 6 class FrmEntryValidate {
7 7
8 8 /**
9 + * @since 6.17
10 + *
11 + * @var array|null
12 + */
13 + private static $name_text_fields;
14 +
15 + /**
9 16 * @param array $values
10 - * @param string[]|bool $exclude
17 + * @param bool|string[] $exclude
18 + *
11 19 * @return array
12 20 */
13 21 public static function validate( $values, $exclude = false ) {
14 22 FrmEntry::sanitize_entry_post( $values );
@@ -15,17 +23,17 @@
15 23 $errors = array();
16 24
17 25 if ( ! isset( $values['form_id'] ) || ! isset( $values['item_meta'] ) ) {
18 26 $errors['form'] = __( 'There was a problem with your submission. Please try again.', 'formidable' );
19 -
20 27 return $errors;
21 28 }
22 29
23 - if ( FrmAppHelper::is_admin() && is_user_logged_in() && ( ! isset( $values[ 'frm_submit_entry_' . $values['form_id'] ] ) || ! wp_verify_nonce( $values[ 'frm_submit_entry_' . $values['form_id'] ], 'frm_submit_entry_nonce' ) ) ) {
30 + if ( FrmAppHelper::is_admin() && is_user_logged_in() && ( ! isset( $values[ 'frm_submit_entry_' . $values['form_id'] ] ) || ! wp_verify_nonce( $values[ 'frm_submit_entry_' . $values['form_id'] ], 'frm_submit_entry_nonce' ) ) ) { // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
24 31 $frm_settings = FrmAppHelper::get_settings();
25 32 $errors['form'] = $frm_settings->admin_permission;
26 33 }
27 34
35 + self::maybe_fix_item_meta();
28 36 self::set_item_key( $values );
29 37
30 38 $posted_fields = self::get_fields_to_validate( $values, $exclude );
31 39
@@ -36,9 +44,9 @@
36 44 self::validate_field( $posted_field, $errors, $values, $args );
37 45 unset( $posted_field );
38 46 }
39 47
40 - if ( empty( $errors ) ) {
48 + if ( ! $errors ) {
41 49 self::spam_check( $exclude, $values, $errors );
42 50 }
43 51
44 52 /**
@@ -54,22 +62,51 @@
54 62
55 63 if ( is_array( $filtered_errors ) ) {
56 64 $errors = $filtered_errors;
57 65 } else {
58 - _doing_it_wrong( __FUNCTION__, 'Only arrays should be returned when using the frm_validate_entry filter.', '6.3' );
66 + _doing_it_wrong( __METHOD__, 'Only arrays should be returned when using the frm_validate_entry filter.', '6.3' );
59 67 }
60 68
61 69 return $errors;
62 70 }
63 71
72 + /**
73 + * In case $_POST['item_meta'] is not an array, change it to an empty array.
74 + * This helps to avoid some warnings and errors when $_POST['item_meta'] is updated.
75 + *
76 + * @since 6.6
77 + *
78 + * @return void
79 + */
80 + private static function maybe_fix_item_meta() {
81 + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
82 + if ( ! isset( $_POST['item_meta'] ) || ! is_array( $_POST['item_meta'] ) ) {
83 + $_POST['item_meta'] = array();
84 + }
85 + }
86 +
87 + /**
88 + * @param array $values
89 + *
90 + * @return void
91 + */
64 92 private static function set_item_key( &$values ) {
65 - if ( ! isset( $values['item_key'] ) || $values['item_key'] == '' ) {
66 - global $wpdb;
67 - $values['item_key'] = FrmAppHelper::get_unique_key( '', $wpdb->prefix . 'frm_items', 'item_key' );
68 - $_POST['item_key'] = $values['item_key'];
93 + // phpcs:ignore Universal.Operators.StrictComparisons
94 + if ( isset( $values['item_key'] ) && $values['item_key'] != '' ) {
95 + return;
69 96 }
97 +
98 + global $wpdb;
99 + $values['item_key'] = FrmAppHelper::get_unique_key( '', $wpdb->prefix . 'frm_items', 'item_key' );
100 + $_POST['item_key'] = $values['item_key'];
70 101 }
71 102
103 + /**
104 + * @param array $values
105 + * @param array|string $exclude
106 + *
107 + * @return array
108 + */
72 109 private static function get_fields_to_validate( $values, $exclude ) {
73 110 $where = apply_filters( 'frm_posted_field_ids', array( 'fi.form_id' => $values['form_id'] ) );
74 111
75 112 // Don't get subfields
@@ -75,9 +112,9 @@
75 112 // Don't get subfields
76 113 $where['fr.parent_form_id'] = array( null, 0 );
77 114
78 115 // Don't get excluded fields (like file upload fields in the ajax validation)
79 - if ( ! empty( $exclude ) ) {
116 + if ( $exclude ) {
80 117 $where['fi.type not'] = $exclude;
81 118 }
82 119
83 120 $fields = FrmField::getAll( $where, 'field_order' );
@@ -92,30 +129,37 @@
92 129 */
93 130 return apply_filters( 'frm_fields_to_validate', $fields, compact( 'values', 'exclude', 'where' ) );
94 131 }
95 132
133 + /**
134 + * @param object $posted_field
135 + * @param array $errors
136 + * @param array $values
137 + * @param array $args
138 + *
139 + * @return void
140 + */
96 141 public static function validate_field( $posted_field, &$errors, $values, $args = array() ) {
97 142 $defaults = array(
98 143 'id' => $posted_field->id,
99 - 'parent_field_id' => '', // the id of the repeat or embed form
100 - 'key_pointer' => '', // the pointer in the posted array
101 - 'exclude' => array(), // exclude these field types from validation
144 + // The id of the repeat or embed form.
145 + 'parent_field_id' => '',
146 + // The pointer in the posted array.
147 + 'key_pointer' => '',
148 + // Exclude these field types from validation.
149 + 'exclude' => array(),
150 +
102 151 );
103 - $args = wp_parse_args( $args, $defaults );
152 + $args = wp_parse_args( $args, $defaults );
153 + $value = ! empty( $args['parent_field_id'] ) ? $values : ( $values['item_meta'][ $args['id'] ] ?? '' );
104 154
105 - if ( empty( $args['parent_field_id'] ) ) {
106 - $value = isset( $values['item_meta'][ $args['id'] ] ) ? $values['item_meta'][ $args['id'] ] : '';
107 - } else {
108 - // value is from a nested form
109 - $value = $values;
110 - }
111 -
112 155 // Check for values in "Other" fields
113 156 FrmEntriesHelper::maybe_set_other_validation( $posted_field, $value, $args );
114 157
115 158 self::maybe_clear_value_for_default_blank_setting( $posted_field, $value );
116 159
117 - $should_trim = is_array( $value ) && count( $value ) == 1 && isset( $value[0] ) && $posted_field->type !== 'checkbox';
160 + $should_trim = is_array( $value ) && count( $value ) === 1 && isset( $value[0] ) && $posted_field->type !== 'checkbox';
161 +
118 162 if ( $should_trim ) {
119 163 $value = reset( $value );
120 164 }
121 165
@@ -122,8 +166,9 @@
122 166 if ( ! is_array( $value ) ) {
123 167 $value = trim( $value );
124 168 }
125 169
170 + // phpcs:ignore Universal.Operators.StrictComparisons
126 171 if ( $posted_field->required == '1' && FrmAppHelper::is_empty_value( $value ) ) {
127 172 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $posted_field, 'blank' );
128 173 } elseif ( ! isset( $_POST['item_name'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
129 174 self::maybe_add_item_name( $value, $posted_field );
@@ -130,8 +175,9 @@
130 175 }
131 176
132 177 FrmEntriesHelper::set_posted_value( $posted_field, $value, $args );
133 178
179 + self::validate_options( $errors, $posted_field, $value, $args );
134 180 self::validate_field_types( $errors, $posted_field, $value, $args );
135 181
136 182 // Field might want to modify value before other parts of the system
137 183 // e.g. trim off excess values like in the case of fields with limit.
@@ -136,8 +182,9 @@
136 182 // Field might want to modify value before other parts of the system
137 183 // e.g. trim off excess values like in the case of fields with limit.
138 184 $value = apply_filters( 'frm_modify_posted_field_value', $value, $errors, $posted_field, $args );
139 185
186 + // phpcs:ignore Universal.Operators.StrictComparisons
140 187 if ( $value != '' ) {
141 188 self::validate_phone_field( $errors, $posted_field, $value, $args );
142 189 }
143 190
@@ -142,19 +189,212 @@
142 189 }
143 190
144 191 $errors = apply_filters( 'frm_validate_' . $posted_field->type . '_field_entry', $errors, $posted_field, $value, $args );
145 192 $errors = apply_filters( 'frm_validate_field_entry', $errors, $posted_field, $value, $args );
193 +
194 + if ( ! FrmAppHelper::pro_is_installed() && empty( $args['other'] ) ) {
195 + FrmEntriesHelper::get_posted_value( $posted_field, $value, $args );
196 + }
146 197 }
147 198
148 199 /**
200 + * @since 6.21
201 + *
202 + * @param array $errors
203 + * @param object $posted_field
204 + * @param array|string $value
205 + * @param array $args
206 + *
207 + * @return void
208 + */
209 + private static function validate_options( &$errors, $posted_field, $value, $args ) {
210 + if ( empty( $posted_field->options ) ) {
211 + return;
212 + }
213 +
214 + $option_is_valid = self::option_is_valid( $posted_field, $value, $posted_field->options );
215 +
216 + /**
217 + * @since 6.21
218 + *
219 + * @param bool $option_is_valid
220 + * @param array|string $value
221 + * @param object $field
222 + */
223 + $option_is_valid = (bool) apply_filters( 'frm_option_is_valid', $option_is_valid, $value, $posted_field );
224 +
225 + if ( ! $option_is_valid ) {
226 + $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $posted_field, 'invalid' );
227 + }
228 + }
229 +
230 + /**
231 + * Validate that value matches one of the options for the field.
232 + *
233 + * @since 6.21
234 + *
235 + * @param stdClass $field
236 + * @param array|string $value
237 + * @param array $options
238 + *
239 + * @return bool
240 + */
241 + private static function option_is_valid( $field, $value, $options ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
242 + if ( '' === $value ) {
243 + return true;
244 + }
245 +
246 + $field_object = FrmFieldFactory::get_field_type( $field->type, $field );
247 +
248 + if ( ! $field_object->field_type_has_options_settings() ) {
249 + return true;
250 + }
251 +
252 + if ( in_array( $field->type, array( 'likert', 'ranking' ), true ) ) {
253 + // Ignore these field types automatically.
254 + return true;
255 + }
256 +
257 + if ( 'product' === $field->type && 'user_def' === FrmField::get_option( $field, 'data_type' ) ) {
258 + return true;
259 + }
260 +
261 + if ( ! empty( $field->field_options['post_field'] ) ) {
262 + return true;
263 + }
264 +
265 + $value = (array) $value;
266 +
267 + foreach ( $value as $current_value ) {
268 + $match = false;
269 +
270 + foreach ( $options as $key => $option ) {
271 + if ( str_starts_with( $key, 'other_' ) ) {
272 + // Always return true if an other option is found.
273 + return true;
274 + }
275 +
276 + if ( is_array( $option ) ) {
277 + $separate_value = FrmField::get_option( $field, 'separate_value' );
278 + $option_value = $separate_value ? $option['value'] : $option['label'];
279 + } else {
280 + $option_value = $option;
281 + }
282 +
283 + /**
284 + * @var string $current_value
285 + */
286 + $match = trim( $current_value ) === trim( $option_value );
287 +
288 + if ( $match ) {
289 + break;
290 + }
291 +
292 + $match = trim( $current_value ) === trim( do_shortcode( $option_value ) );
293 +
294 + if ( $match ) {
295 + break;
296 + }
297 +
298 + $match = self::is_filtered_match( $current_value, $option_value );
299 +
300 + if ( $match ) {
301 + break;
302 + }
303 +
304 + if ( ! is_numeric( $current_value ) ) {
305 + continue;
306 + }
307 +
308 + $match = (int) $current_value === (int) $option_value;
309 +
310 + if ( $match ) {
311 + break;
312 + }
313 + }//end foreach
314 +
315 + if ( ! $match ) {
316 + return self::options_are_dynamic_based_on_hook( $field, $value );
317 + }
318 + }//end foreach
319 +
320 + return true;
321 + }
322 +
323 + /**
324 + * Make an extra check after passing $option_value through the_content filter.
325 + * This is to help catch cases where the option's formatting has been modified using
326 + * the_content filter.
327 + *
328 + * @since 6.22
329 + *
330 + * @param string $value
331 + * @param string $option_value
332 + *
333 + * @return bool
334 + */
335 + private static function is_filtered_match( $value, $option_value ) {
336 + // First remove the wpautop filter so it doesn't add extra tags to $option_value.
337 + $filter_priority = has_filter( 'the_content', 'wpautop' );
338 +
339 + if ( is_numeric( $filter_priority ) ) {
340 + remove_filter( 'the_content', 'wpautop', $filter_priority );
341 + }
342 +
343 + $filtered_option = apply_filters( 'the_content', $option_value );
344 +
345 + if ( is_numeric( $filter_priority ) ) {
346 + add_filter( 'the_content', 'wpautop', $filter_priority );
347 + }
348 +
349 + return trim( $value ) === trim( $filtered_option );
350 + }
351 +
352 + /**
353 + * Do not validate options if they have been modified with a hook.
354 + * This is to help avoid issues where the options could be based on a URL param for example.
355 + *
356 + * @since 6.21
357 + *
358 + * @param object $field_object The field object.
359 + * @param array|string $value The value to validate.
360 + *
361 + * @return bool
362 + */
363 + private static function options_are_dynamic_based_on_hook( $field_object, $value ) {
364 + $values = (array) $field_object;
365 + $values['value'] = $value;
366 + FrmFieldsHelper::prepare_new_front_field( $values, $field_object );
367 +
368 + $separate_value = FrmField::get_option( $field_object, 'separate_value' );
369 + $map_callback = function ( $option ) use ( $separate_value ) {
370 + if ( is_array( $option ) ) {
371 + $option_value = $separate_value ? $option['value'] : $option['label'];
372 + } else {
373 + $option_value = $option;
374 + }
375 + return do_shortcode( $option_value );
376 + };
377 +
378 + $values_options = array_map( $map_callback, $values['options'] );
379 + $field_object_options = array_map( $map_callback, $field_object->options );
380 +
381 + return $values_options !== $field_object_options;
382 + }
383 +
384 + /**
149 385 * Maybe add item_name to $_POST to save it in items table.
150 386 *
151 387 * @since 5.2.02
152 388 *
153 - * @param object $field Field object.
389 + * @param array|string $value Field value.
390 + * @param object $field Field object.
391 + *
392 + * @return void
154 393 */
155 394 private static function maybe_add_item_name( $value, $field ) {
156 395 $item_name = false;
396 +
157 397 if ( 'name' === $field->type ) {
158 398 $field_obj = FrmFieldFactory::get_field_object( $field );
159 399 $item_name = $field_obj->get_display_value( $value );
160 400 } elseif ( 'text' === $field->type ) {
@@ -162,9 +402,9 @@
162 402 }
163 403
164 404 if ( false !== $item_name ) {
165 405 // Item name has a max length of 255 characters so truncate it so it doesn't fail to save in the database.
166 - $_POST['item_name'] = substr( $item_name, 0, 255 );
406 + $_POST['item_name'] = FrmAppHelper::truncate( $item_name, 255, 1, '', true );
167 407 }
168 408 }
169 409
170 410 /**
@@ -171,11 +411,14 @@
171 411 * Set $value to an empty string if it matches its label
172 412 *
173 413 * @param object $field
174 414 * @param string $value
415 + *
416 + * @return void
175 417 */
176 418 private static function maybe_clear_value_for_default_blank_setting( $field, &$value ) {
177 419 $position = FrmField::get_option( $field, 'label' );
420 +
178 421 if ( ! $position ) {
179 422 $position = FrmStylesController::get_style_val( 'position', $field->form_id );
180 423 }
181 424
@@ -183,8 +426,16 @@
183 426 $value = '';
184 427 }
185 428 }
186 429
430 + /**
431 + * @param array $errors
432 + * @param object $posted_field
433 + * @param mixed $value
434 + * @param array $args
435 + *
436 + * @return void
437 + */
187 438 public static function validate_field_types( &$errors, $posted_field, $value, $args ) {
188 439 $field_obj = FrmFieldFactory::get_field_object( $posted_field );
189 440 $args['value'] = $value;
190 441 $args['errors'] = $errors;
@@ -189,24 +440,41 @@
189 440 $args['value'] = $value;
190 441 $args['errors'] = $errors;
191 442
192 443 $new_errors = $field_obj->validate( $args );
193 - if ( ! empty( $new_errors ) ) {
444 +
445 + if ( $new_errors ) {
194 446 $errors = array_merge( $errors, $new_errors );
195 447 }
196 448 }
197 449
450 + /**
451 + * @param array $errors
452 + * @param object $field
453 + * @param string $value
454 + * @param array $args
455 + *
456 + * @return void
457 + */
198 458 public static function validate_phone_field( &$errors, $field, $value, $args ) {
199 - if ( $field->type == 'phone' || ( $field->type == 'text' && FrmField::is_option_true_in_object( $field, 'format' ) ) ) {
459 + $format_value = FrmField::get_option( $field, 'format' );
200 460
201 - $pattern = self::phone_format( $field );
461 + if ( $field->type !== 'phone' && ( $field->type !== 'text' || ! $format_value || FrmCurrencyHelper::is_currency_format( $format_value ) ) ) {
462 + return;
463 + }
202 464
203 - if ( ! preg_match( $pattern, $value ) ) {
204 - $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
205 - }
465 + $pattern = self::phone_format( $field );
466 +
467 + if ( ! preg_match( $pattern, $value ) ) {
468 + $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
206 469 }
207 470 }
208 471
472 + /**
473 + * @param object $field
474 + *
475 + * @return string
476 + */
209 477 public static function phone_format( $field ) {
210 478 if ( FrmField::is_option_empty( $field, 'format' ) ) {
211 479 $pattern = self::default_phone_format();
212 480 } else {
@@ -212,22 +480,25 @@
212 480 } else {
213 481 $pattern = FrmField::get_option( $field, 'format' );
214 482 }
215 483
484 + // Ampersands are saved as &.
485 + // Reverse it here so we are checking for the correct character.
486 + $pattern = html_entity_decode( $pattern );
216 487 $pattern = apply_filters( 'frm_phone_pattern', $pattern, $field );
217 488
218 489 // Create a regexp if format is not already a regexp
219 - if ( strpos( $pattern, '^' ) !== 0 ) {
490 + if ( ! str_starts_with( $pattern, '^' ) ) {
220 491 $pattern = self::create_regular_expression_from_format( $pattern );
221 492 }
222 493
223 - $pattern = '/' . $pattern . '/';
224 -
225 - return $pattern;
494 + return '/' . $pattern . '/';
226 495 }
227 496
228 497 /**
229 498 * @since 3.01
499 + *
500 + * @return string
230 501 */
231 502 private static function default_phone_format() {
232 503 return '^((\+\d{1,3}(-|.| )?\(?\d\)?(-| |.)?\d{1,5})|(\(?\d{2,6}\)?))(-|.| )?(\d{3,4})(-|.| )?(\d{4})(( x| ext)\d{1,5}){0,1}$';
233 504 }
@@ -253,44 +524,57 @@
253 524 $pattern = str_replace( 'a', '[a-zA-Z]', $pattern );
254 525 $pattern = str_replace( '*', 'w', $pattern );
255 526 $pattern = str_replace( '/', '\/', $pattern );
256 527
257 - if ( strpos( $pattern, '\?' ) !== false ) {
528 + if ( str_contains( $pattern, '\?' ) ) {
258 529 $parts = explode( '\?', $pattern );
259 530 $pattern = '';
531 +
260 532 foreach ( $parts as $part ) {
261 - if ( empty( $pattern ) ) {
533 + if ( $pattern ) {
534 + $pattern .= '(' . $part . ')?';
535 + } else {
262 536 $pattern .= $part;
263 - } else {
264 - $pattern .= '(' . $part . ')?';
265 537 }
266 538 }
267 539 }
268 - $pattern = '^' . $pattern . '$';
269 540
270 - return $pattern;
541 + return '^' . $pattern . '$';
271 542 }
272 543
273 544 /**
274 - * Check for spam
545 + * Check for spam.
275 546 *
276 - * @param boolean $exclude
547 + * @param bool $exclude
277 548 * @param array $values
278 - * @param array $errors by reference
549 + * @param array $errors By reference.
550 + *
551 + * @return void
279 552 */
280 553 public static function spam_check( $exclude, $values, &$errors ) {
281 - if ( ! empty( $exclude ) || ! isset( $values['item_meta'] ) || empty( $values['item_meta'] ) || ! empty( $errors ) ) {
282 - // only check spam if there are no other errors
554 + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
555 + // Do not check spam on importing.
283 556 return;
284 557 }
285 558
559 + if ( $exclude || empty( $values['item_meta'] ) || $errors ) {
560 + // Only check spam if there are no other errors
561 + return;
562 + }
563 +
286 564 $antispam_check = self::is_antispam_check( $values['form_id'] );
565 + $spam_msg = FrmAntiSpamController::get_default_spam_message();
566 +
287 567 if ( is_string( $antispam_check ) ) {
288 568 $errors['spam'] = $antispam_check;
289 569 } elseif ( self::is_honeypot_spam( $values ) || self::is_spam_bot() ) {
290 - $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
291 - } elseif ( self::blacklist_check( $values ) ) {
292 - $errors['spam'] = __( 'Your entry appears to be blocked spam!', 'formidable' );
570 + $errors['spam'] = $spam_msg;
571 + } else {
572 + $is_spam = FrmAntiSpamController::is_spam( $values );
573 +
574 + if ( $is_spam ) {
575 + $errors['spam'] = $is_spam;
576 + }
293 577 }
294 578
295 579 if ( isset( $errors['spam'] ) || self::form_is_in_progress( $values ) ) {
296 580 return;
@@ -306,14 +590,17 @@
306 590 *
307 591 * @since 5.0.13
308 592 *
309 593 * @param array $values The values.
594 + *
310 595 * @return bool
311 596 */
312 597 private static function form_is_in_progress( $values ) {
598 + // phpcs:disable Generic.WhiteSpace.ScopeIndent
313 599 return FrmAppHelper::pro_is_installed() &&
314 600 ( isset( $values[ 'frm_page_order_' . $values['form_id'] ] ) || FrmAppHelper::get_post_param( 'frm_next_page' ) ) &&
315 601 FrmField::get_all_types_in_form( $values['form_id'], 'break' );
602 + // phpcs:enable Generic.WhiteSpace.ScopeIndent
316 603 }
317 604
318 605 /**
319 606 * @param int $form_id
@@ -326,9 +613,10 @@
326 613 }
327 614
328 615 /**
329 616 * @param array $values
330 - * @return boolean
617 + *
618 + * @return bool
331 619 */
332 620 private static function is_honeypot_spam( $values ) {
333 621 $honeypot = new FrmHoneypot( $values['form_id'] );
334 622 return ! $honeypot->validate();
@@ -334,87 +622,51 @@
334 622 return ! $honeypot->validate();
335 623 }
336 624
337 625 /**
338 - * @return boolean
626 + * @return bool
339 627 */
340 628 private static function is_spam_bot() {
341 - $ip = FrmAppHelper::get_ip_address();
342 -
343 - return empty( $ip );
629 + return ! FrmAppHelper::get_ip_address();
344 630 }
345 631
346 632 /**
347 633 * @param array $values
348 - * @return boolean
634 + *
635 + * @return bool
349 636 */
350 637 private static function is_akismet_spam( $values ) {
351 638 global $wpcom_api_key;
352 -
353 - return ( is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values ) );
639 + return is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values );
354 640 }
355 641
356 642 /**
357 643 * @param int $form_id
644 + *
358 645 * @return bool
359 646 */
360 647 private static function is_akismet_enabled_for_user( $form_id ) {
361 648 $form = FrmForm::getOne( $form_id );
362 -
363 - return ( ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() ) );
649 + return ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() );
364 650 }
365 651
366 - public static function blacklist_check( $values ) {
367 - if ( ! apply_filters( 'frm_check_blacklist', true, $values ) ) {
368 - return false;
369 - }
370 -
371 - $mod_keys = trim( self::get_disallowed_words() );
372 - if ( empty( $mod_keys ) ) {
373 - return false;
374 - }
375 -
376 - $content = FrmEntriesHelper::entry_array_to_string( $values );
377 -
378 - self::prepare_values_for_spam_check( $values );
379 - $ip = FrmAppHelper::get_ip_address();
380 - $user_agent = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
381 - $user_info = self::get_spam_check_user_info( $values );
382 -
383 - return self::check_disallowed_words( $user_info['comment_author'], $user_info['comment_author_email'], $user_info['comment_author_url'], $content, $ip, $user_agent );
384 - }
385 -
386 652 /**
387 - * For WP 5.5 compatibility.
653 + * Checks spam using WordPress disallowed words and Frm denylist.
388 654 *
389 - * @since 4.06.02
390 - */
391 - private static function check_disallowed_words( $author, $email, $url, $content, $ip, $user_agent ) {
392 - if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
393 - return wp_check_comment_disallowed_list( $author, $email, $url, $content, $ip, $user_agent );
394 - } else {
395 - return wp_blacklist_check( $author, $email, $url, $content, $ip, $user_agent );
396 - }
397 - }
398 -
399 - /**
400 - * For WP 5.5 compatibility.
655 + * @param array $values Entry values.
401 656 *
402 - * @since 4.06.02
657 + * @return bool
403 658 */
404 - private static function get_disallowed_words() {
405 - $keys = get_option( 'disallowed_keys' );
406 - if ( false === $keys ) {
407 - // Fallback for WP < 5.5.
408 - $keys = get_option( 'blacklist_keys' );
409 - }
410 - return $keys;
659 + public static function blacklist_check( $values ) {
660 + return FrmAntiSpamController::contains_wp_disallowed_words( $values ) || FrmAntiSpamController::is_denylist_spam( $values );
411 661 }
412 662
413 663 /**
414 664 * Check entries for Akismet spam
415 665 *
416 - * @return boolean true if is spam
666 + * @param array $values Entry values.
667 + *
668 + * @return bool true if is spam
417 669 */
418 670 public static function akismet( $values ) {
419 671 if ( empty( $values['item_meta'] ) ) {
420 672 return false;
@@ -436,13 +688,18 @@
436 688
437 689 $query_string = _http_build_query( $datas, '', '&' );
438 690 $response = Akismet::http_post( $query_string, 'comment-check' );
439 691
440 - return ( is_array( $response ) && $response[1] == 'true' );
692 + return is_array( $response ) && $response[1] === 'true';
441 693 }
442 694
443 695 /**
444 696 * @since 2.0
697 + *
698 + * @param array $datas The array of values being sent to Akismet.
699 + * @param array $values Entry values.
700 + *
701 + * @return void
445 702 */
446 703 private static function parse_akismet_array( &$datas, $values ) {
447 704 self::add_site_info_to_akismet( $datas );
448 705 self::add_server_values_to_akismet( $datas );
@@ -447,13 +704,19 @@
447 704 self::add_site_info_to_akismet( $datas );
448 705 self::add_server_values_to_akismet( $datas );
449 706
450 707 self::prepare_values_for_spam_check( $values );
708 + self::skip_adding_values_to_akismet( $values );
451 709
452 710 self::add_user_info_to_akismet( $datas, $values );
453 711 self::add_comment_content_to_akismet( $datas, $values );
454 712 }
455 713
714 + /**
715 + * @param array $datas
716 + *
717 + * @return void
718 + */
456 719 private static function add_site_info_to_akismet( &$datas ) {
457 720 $datas['blog'] = FrmAppHelper::site_url();
458 721 $datas['user_ip'] = preg_replace( '/[^0-9., ]/', '', FrmAppHelper::get_ip_address() );
459 722 $datas['user_agent'] = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
@@ -465,8 +728,14 @@
465 728 $datas['is_test'] = 'true';
466 729 }
467 730 }
468 731
732 + /**
733 + * @param array $datas
734 + * @param array $values
735 + *
736 + * @return void
737 + */
469 738 private static function add_user_info_to_akismet( &$datas, $values ) {
470 739 $user_info = self::get_spam_check_user_info( $values );
471 740 $datas = $datas + $user_info;
472 741
@@ -478,13 +747,15 @@
478 747 /**
479 748 * Gets user info for Akismet spam check.
480 749 *
481 750 * @since 5.0.13 Separate code for guest. Handle value of embedded|repeater.
751 + * @since 6.21 This changed from private to public.
482 752 *
483 753 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
754 + *
484 755 * @return array
485 756 */
486 - private static function get_spam_check_user_info( $values ) {
757 + public static function get_spam_check_user_info( $values ) {
487 758 if ( ! is_user_logged_in() ) {
488 759 return self::get_spam_check_user_info_for_guest( $values );
489 760 }
490 761
@@ -504,8 +775,9 @@
504 775 *
505 776 * @since 5.0.13
506 777 *
507 778 * @param array $values Entry values after flattened.
779 + *
508 780 * @return array
509 781 */
510 782 private static function get_spam_check_user_info_for_guest( $values ) {
511 783 $datas = array(
@@ -537,13 +809,16 @@
537 809 *
538 810 * @param array $datas Guest data.
539 811 * @param array $values The values.
540 812 * @param int|null $custom_index Custom index (or field ID).
813 + *
814 + * @return void
541 815 */
542 816 private static function recursive_add_akismet_guest_info( &$datas, $values, $custom_index = null ) {
543 817 foreach ( $values as $index => $value ) {
544 818 if ( ! $datas['missing_keys'] ) {
545 - return; // Found all info.
819 + // Found all info.
820 + return;
546 821 }
547 822
548 823 if ( is_array( $value ) ) {
549 824 self::recursive_add_akismet_guest_info( $datas, $value, $index );
@@ -550,17 +825,21 @@
550 825 continue;
551 826 }
552 827
553 828 $field_id = ! is_null( $custom_index ) ? $custom_index : $index;
829 +
554 830 foreach ( $datas['missing_keys'] as $key_index => $key ) {
555 - $found = self::is_akismet_guest_info_value( $key, $value, $field_id, $datas['name_field_ids'] );
556 - if ( $found ) {
557 - $datas[ $key ] = $value;
558 - $datas['frm_duplicated'][] = $field_id;
559 - unset( $datas['missing_keys'][ $key_index ] );
831 + $found = self::is_akismet_guest_info_value( $key, $value, $field_id, $datas['name_field_ids'], $values );
832 +
833 + if ( ! $found ) {
834 + continue;
560 835 }
836 +
837 + $datas[ $key ] = $value;
838 + $datas['frm_duplicated'][] = $field_id;
839 + unset( $datas['missing_keys'][ $key_index ] );
561 840 }
562 - }
841 + }//end foreach
563 842 }
564 843
565 844 /**
566 845 * Checks if given value is an akismet guest info.
@@ -570,11 +849,13 @@
570 849 * @param string $key Guest info key.
571 850 * @param string $value Value to check.
572 851 * @param int $field_id Field ID.
573 852 * @param array $name_field_ids Name field IDs.
853 + * @param array $values Array of posted values.
854 + *
574 855 * @return bool
575 856 */
576 - private static function is_akismet_guest_info_value( $key, $value, $field_id, $name_field_ids ) {
857 + private static function is_akismet_guest_info_value( $key, &$value, $field_id, $name_field_ids, $values ) {
577 858 if ( ! $value || is_numeric( $value ) ) {
578 859 return false;
579 860 }
580 861
@@ -579,24 +860,79 @@
579 860 }
580 861
581 862 switch ( $key ) {
582 863 case 'comment_author_email':
583 - return strpos( $value, '@' ) && is_email( $value );
864 + return str_contains( $value, '@' ) && is_email( $value );
584 865
585 866 case 'comment_author_url':
586 - return 0 === strpos( $value, 'http' );
867 + return str_starts_with( $value, 'http' );
587 868
588 869 case 'comment_author':
589 - if ( $name_field_ids ) {
870 + if ( $name_field_ids && in_array( $field_id, $name_field_ids, true ) ) {
590 871 // If there is name field in the form, we should always use it as author name.
591 - return in_array( $field_id, $name_field_ids, true );
872 + return true;
592 873 }
593 - return strlen( $value ) < 200;
874 +
875 + $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
876 + $fields = self::get_name_text_fields( $form_id );
877 +
878 + foreach ( $fields as $index => $field ) {
879 + if ( 'Name' !== $field->name ) {
880 + continue;
881 + }
882 +
883 + if ( isset( $fields[ $index + 1 ] ) && 'Last' === $fields[ $index + 1 ]->name ) {
884 + if ( empty( $values[ absint( $fields[ $index + 1 ]->id ) ] ) ) {
885 + continue;
886 + }
887 +
888 + $value .= ' ' . $values[ $fields[ $index + 1 ]->id ];
889 + return true;
890 + }
891 + }
892 + }//end switch
893 +
894 + return false;
895 + }
896 +
897 + /**
898 + * Returns fields that have 'Name' and 'Last' as their name.
899 + *
900 + * @since 6.17
901 + *
902 + * @param int $form_id
903 + *
904 + * @return array
905 + */
906 + private static function get_name_text_fields( $form_id ) {
907 + $name_text_fields_is_initialized = is_array( self::$name_text_fields );
908 +
909 + if ( $name_text_fields_is_initialized && isset( self::$name_text_fields[ $form_id ] ) ) {
910 + return self::$name_text_fields[ $form_id ];
594 911 }
595 912
596 - return false;
913 + if ( ! $name_text_fields_is_initialized ) {
914 + self::$name_text_fields = array();
915 + }
916 + self::$name_text_fields[ $form_id ] = FrmDb::get_results(
917 + 'frm_fields',
918 + array(
919 + 'form_id' => $form_id,
920 + 'type' => 'text',
921 + 'name' => array( 'Name', 'Last' ),
922 + ),
923 + 'id,name',
924 + array( 'order_by' => 'field_order ASC' )
925 + );
926 +
927 + return self::$name_text_fields[ $form_id ];
597 928 }
598 929
930 + /**
931 + * @param array $datas
932 + *
933 + * @return void
934 + */
599 935 private static function add_server_values_to_akismet( &$datas ) {
600 936 foreach ( $_SERVER as $key => $value ) {
601 937 $include_value = is_string( $value ) && ! preg_match( '/^HTTP_COOKIE/', $key ) && preg_match( '/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/', $key );
602 938
@@ -614,8 +950,10 @@
614 950 * @since 5.0.09
615 951 *
616 952 * @param array $datas The array of values being sent to Akismet.
617 953 * @param array $values Entry values.
954 + *
955 + * @return void
618 956 */
619 957 private static function add_comment_content_to_akismet( &$datas, $values ) {
620 958 if ( isset( $datas['frm_duplicated'] ) ) {
621 959 foreach ( $datas['frm_duplicated'] as $index ) {
@@ -627,10 +965,8 @@
627 965 }
628 966 unset( $datas['frm_duplicated'] );
629 967 }
630 968
631 - self::skip_adding_values_to_akismet( $values );
632 -
633 969 $datas['comment_content'] = FrmEntriesHelper::entry_array_to_string( $values );
634 970 }
635 971
636 972 /**
@@ -638,22 +974,28 @@
638 974 *
639 975 * @since 5.0.09
640 976 *
641 977 * @param array $values Entry values.
978 + *
979 + * @return void
642 980 */
643 981 private static function skip_adding_values_to_akismet( &$values ) {
644 982 $skipped_fields = self::get_akismet_skipped_field_ids( $values );
983 +
645 984 foreach ( $skipped_fields as $skipped_field ) {
646 985 if ( ! isset( $values['item_meta'][ $skipped_field->id ] ) ) {
647 986 continue;
648 987 }
649 988
650 - if ( self::should_really_skip_field( $skipped_field, $values ) ) {
651 - unset( $values['item_meta'][ $skipped_field->id ] );
652 - if ( isset( $values['item_meta']['other'][ $skipped_field->id ] ) ) {
653 - unset( $values['item_meta']['other'][ $skipped_field->id ] );
654 - }
989 + if ( ! self::should_really_skip_field( $skipped_field, $values ) ) {
990 + continue;
655 991 }
992 +
993 + unset( $values['item_meta'][ $skipped_field->id ] );
994 +
995 + if ( isset( $values['item_meta']['other'][ $skipped_field->id ] ) ) {
996 + unset( $values['item_meta']['other'][ $skipped_field->id ] );
997 + }
656 998 }
657 999 }
658 1000
659 1001 /**
@@ -662,25 +1004,28 @@
662 1004 * @since 5.02.04
663 1005 *
664 1006 * @param object $field_data Object contains `id` and `options`.
665 1007 * @param array $values Entry values.
1008 + *
666 1009 * @return bool
667 1010 */
668 1011 private static function should_really_skip_field( $field_data, $values ) {
669 - if ( empty( $field_data->options ) ) { // This is skipped field types.
1012 + if ( empty( $field_data->options ) ) {
1013 + // This is skipped field types.
670 1014 return true;
671 1015 }
672 1016
673 1017 FrmAppHelper::unserialize_or_decode( $field_data->options );
674 - if ( ! $field_data->options ) { // Check if an error happens when unserializing, or empty options.
1018 +
1019 + if ( ! $field_data->options ) {
1020 + // Check if an error happens when unserializing, or empty options.
675 1021 return true;
676 1022 }
677 1023
678 - end( $field_data->options );
679 - $last_key = key( $field_data->options );
1024 + $last_key = array_key_last( $field_data->options );
680 1025
681 1026 // If a choice field has no Other option.
682 - if ( is_numeric( $last_key ) || 0 !== strpos( $last_key, 'other_' ) ) {
1027 + if ( is_numeric( $last_key ) || ! str_starts_with( $last_key, 'other_' ) ) {
683 1028 return true;
684 1029 }
685 1030
686 1031 // If a choice field has Other option, but Other is not selected.
@@ -689,9 +1034,10 @@
689 1034 }
690 1035
691 1036 // Check if submitted value is same as one of field option.
692 1037 foreach ( $field_data->options as $option ) {
693 - $option_value = ! is_array( $option ) ? $option : ( isset( $option['value'] ) ? $option['value'] : '' );
1038 + $option_value = is_array( $option ) ? ( $option['value'] ?? '' ) : $option;
1039 +
694 1040 if ( $values['item_meta']['other'][ $field_data->id ] === $option_value ) {
695 1041 return true;
696 1042 }
697 1043 }
@@ -706,8 +1052,9 @@
706 1052 * @since 5.0.13 Move out get_all_form_ids_and_flatten_meta() call and get `form_ids` from `$values`.
707 1053 * @since 5.2.04 This method returns array of object contains `id` and `options` instead of array of `id` only.
708 1054 *
709 1055 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
1056 + *
710 1057 * @return array
711 1058 */
712 1059 private static function get_akismet_skipped_field_ids( $values ) {
713 1060 if ( empty( $values['form_ids'] ) ) {
@@ -730,14 +1077,16 @@
730 1077 /**
731 1078 * Prepares values array for spam check.
732 1079 *
733 1080 * @since 5.0.13
1081 + * @since 6.21 This changed from private to public.
734 1082 *
735 1083 * @param array $values Entry values.
1084 + *
1085 + * @return void
736 1086 */
737 - private static function prepare_values_for_spam_check( &$values ) {
738 - $form_ids = self::get_all_form_ids_and_flatten_meta( $values );
739 - $values['form_ids'] = $form_ids;
1087 + public static function prepare_values_for_spam_check( &$values ) {
1088 + $values['form_ids'] = self::get_all_form_ids_and_flatten_meta( $values );
740 1089 }
741 1090
742 1091 /**
743 1092 * Gets all form IDs (include child form IDs) and flatten item_meta array. Used for skipping values sent to Akismet.
@@ -746,17 +1095,20 @@
746 1095 * @since 5.0.09
747 1096 * @since 5.0.13 Convert name field value to string.
748 1097 *
749 1098 * @param array $values Entry values.
1099 + *
750 1100 * @return array Form IDs.
751 1101 */
752 - private static function get_all_form_ids_and_flatten_meta( &$values ) {
1102 + private static function get_all_form_ids_and_flatten_meta( &$values ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
753 1103 $values['name_field_ids'] = array();
754 1104
755 1105 // Blacklist check for File field in the old version doesn't contain `form_id`.
756 1106 $form_ids = isset( $values['form_id'] ) ? array( absint( $values['form_id'] ) ) : array();
1107 +
757 1108 foreach ( $values['item_meta'] as $field_id => $value ) {
758 - if ( ! is_numeric( $field_id ) ) { // Maybe `other`.
1109 + if ( ! is_numeric( $field_id ) ) {
1110 + // Maybe `other`.
759 1111 continue;
760 1112 }
761 1113
762 1114 // Convert name array to string.
@@ -787,51 +1139,20 @@
787 1139 }
788 1140
789 1141 // Convert name array to string.
790 1142 if ( isset( $subsubvalue['first'] ) && isset( $subsubvalue['last'] ) ) {
791 - $subsubvalue = trim( implode( ' ', $subsubvalue ) );
792 -
1143 + $subsubvalue = trim( implode( ' ', $subsubvalue ) );
793 1144 $values['name_field_ids'][] = $subsubindex;
794 1145 }
795 1146
796 - $values['item_meta'][ $subsubindex ][] = $subsubvalue;
1147 + if ( is_array( $values['item_meta'][ $subsubindex ] ) ) {
1148 + $values['item_meta'][ $subsubindex ][] = $subsubvalue;
1149 + }
797 1150 }
798 - }
1151 + }//end foreach
799 1152
800 1153 unset( $values['item_meta'][ $field_id ] );
801 - }
1154 + }//end foreach
802 1155
803 1156 return $form_ids;
804 - }
805 -
806 - /**
807 - * @deprecated 3.0
808 - * @codeCoverageIgnore
809 - */
810 - public static function validate_url_field( &$errors, $field, $value, $args ) {
811 - FrmDeprecated::validate_url_field( $errors, $field, $value, $args );
812 - }
813 -
814 - /**
815 - * @deprecated 3.0
816 - * @codeCoverageIgnore
817 - */
818 - public static function validate_email_field( &$errors, $field, $value, $args ) {
819 - FrmDeprecated::validate_email_field( $errors, $field, $value, $args );
820 - }
821 -
822 - /**
823 - * @deprecated 3.0
824 - * @codeCoverageIgnore
825 - */
826 - public static function validate_number_field( &$errors, $field, $value, $args ) {
827 - FrmDeprecated::validate_number_field( $errors, $field, $value, $args );
828 - }
829 -
830 - /**
831 - * @deprecated 3.0
832 - * @codeCoverageIgnore
833 - */
834 - public static function validate_recaptcha( &$errors, $field, $args ) {
835 - FrmDeprecated::validate_recaptcha( $errors, $field, $args );
836 1157 }
837 1158 }