PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.26
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.26
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 / FrmEntryValidate.php

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

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