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

1,159 lines 31.9 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 return $errors;
28 }
29
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
31 $frm_settings = FrmAppHelper::get_settings();
32 $errors['form'] = $frm_settings->admin_permission;
33 }
34
35 self::maybe_fix_item_meta();
36 self::set_item_key( $values );
37
38 $posted_fields = self::get_fields_to_validate( $values, $exclude );
39
40 // Pass exclude value to validate_field function so it can be used for repeating sections
41 $args = array( 'exclude' => $exclude );
42
43 foreach ( $posted_fields as $posted_field ) {
44 self::validate_field( $posted_field, $errors, $values, $args );
45 unset( $posted_field );
46 }
47
48 if ( ! $errors ) {
49 self::spam_check( $exclude, $values, $errors );
50 }
51
52 /**
53 * Allows modifying the validation errors after validating all fields.
54 *
55 * @since 5.0.04 Added `posted_fields` to the third param.
56 *
57 * @param array $errors Errors data.
58 * @param array $values Value data of the form.
59 * @param array $args Custom arguments. Contains `exclude` and `posted_fields`.
60 */
61 $filtered_errors = apply_filters( 'frm_validate_entry', $errors, $values, compact( 'exclude', 'posted_fields' ) );
62
63 if ( is_array( $filtered_errors ) ) {
64 $errors = $filtered_errors;
65 } else {
66 _doing_it_wrong( __METHOD__, 'Only arrays should be returned when using the frm_validate_entry filter.', '6.3' );
67 }
68
69 return $errors;
70 }
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 */
92 private static function set_item_key( &$values ) {
93 // phpcs:ignore Universal.Operators.StrictComparisons
94 if ( isset( $values['item_key'] ) && $values['item_key'] != '' ) {
95 return;
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'];
101 }
102
103 /**
104 * @param array $values
105 * @param array|string $exclude
106 *
107 * @return array
108 */
109 private static function get_fields_to_validate( $values, $exclude ) {
110 $where = apply_filters( 'frm_posted_field_ids', array( 'fi.form_id' => $values['form_id'] ) );
111
112 // Don't get subfields
113 $where['fr.parent_form_id'] = array( null, 0 );
114
115 // Don't get excluded fields (like file upload fields in the ajax validation)
116 if ( $exclude ) {
117 $where['fi.type not'] = $exclude;
118 }
119
120 $fields = FrmField::getAll( $where, 'field_order' );
121
122 /**
123 * Allows modifying fields to validate.
124 *
125 * @since 5.0.06
126 *
127 * @param array $fields List of fields.
128 * @param array $args Includes `values`, `exclude`, `where`.
129 */
130 return apply_filters( 'frm_fields_to_validate', $fields, compact( 'values', 'exclude', 'where' ) );
131 }
132
133 /**
134 * @param object $posted_field
135 * @param array $errors
136 * @param array $values
137 * @param array $args
138 *
139 * @return void
140 */
141 public static function validate_field( $posted_field, &$errors, $values, $args = array() ) {
142 $defaults = array(
143 'id' => $posted_field->id,
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
151 );
152 $args = wp_parse_args( $args, $defaults );
153 $value = ! empty( $args['parent_field_id'] ) ? $values : ( $values['item_meta'][ $args['id'] ] ?? '' );
154
155 // Check for values in "Other" fields
156 FrmEntriesHelper::maybe_set_other_validation( $posted_field, $value, $args );
157
158 self::maybe_clear_value_for_default_blank_setting( $posted_field, $value );
159
160 $should_trim = is_array( $value ) && count( $value ) === 1 && isset( $value[0] ) && $posted_field->type !== 'checkbox';
161
162 if ( $should_trim ) {
163 $value = reset( $value );
164 }
165
166 if ( ! is_array( $value ) ) {
167 $value = trim( $value );
168 }
169
170 // phpcs:ignore Universal.Operators.StrictComparisons
171 if ( $posted_field->required == '1' && FrmAppHelper::is_empty_value( $value ) ) {
172 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $posted_field, 'blank' );
173 } elseif ( ! isset( $_POST['item_name'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
174 self::maybe_add_item_name( $value, $posted_field );
175 }
176
177 FrmEntriesHelper::set_posted_value( $posted_field, $value, $args );
178
179 self::validate_options( $errors, $posted_field, $value, $args );
180 self::validate_field_types( $errors, $posted_field, $value, $args );
181
182 // Field might want to modify value before other parts of the system
183 // e.g. trim off excess values like in the case of fields with limit.
184 $value = apply_filters( 'frm_modify_posted_field_value', $value, $errors, $posted_field, $args );
185
186 // phpcs:ignore Universal.Operators.StrictComparisons
187 if ( $value != '' ) {
188 self::validate_phone_field( $errors, $posted_field, $value, $args );
189 }
190
191 $errors = apply_filters( 'frm_validate_' . $posted_field->type . '_field_entry', $errors, $posted_field, $value, $args );
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 }
197 }
198
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 /**
385 * Maybe add item_name to $_POST to save it in items table.
386 *
387 * @since 5.2.02
388 *
389 * @param array|string $value Field value.
390 * @param object $field Field object.
391 *
392 * @return void
393 */
394 private static function maybe_add_item_name( $value, $field ) {
395 $item_name = false;
396
397 if ( 'name' === $field->type ) {
398 $field_obj = FrmFieldFactory::get_field_object( $field );
399 $item_name = $field_obj->get_display_value( $value );
400 } elseif ( 'text' === $field->type ) {
401 $item_name = $value;
402 }
403
404 if ( false !== $item_name ) {
405 // Item name has a max length of 255 characters so truncate it so it doesn't fail to save in the database.
406 $_POST['item_name'] = FrmAppHelper::truncate( $item_name, 255, 1, '', true );
407 }
408 }
409
410 /**
411 * Set $value to an empty string if it matches its label
412 *
413 * @param object $field
414 * @param string $value
415 *
416 * @return void
417 */
418 private static function maybe_clear_value_for_default_blank_setting( $field, &$value ) {
419 $position = FrmField::get_option( $field, 'label' );
420
421 if ( ! $position ) {
422 $position = FrmStylesController::get_style_val( 'position', $field->form_id );
423 }
424
425 if ( $position === 'inside' && FrmFieldsHelper::is_placeholder_field_type( $field->type ) && $value === $field->name ) {
426 $value = '';
427 }
428 }
429
430 /**
431 * @param array $errors
432 * @param object $posted_field
433 * @param mixed $value
434 * @param array $args
435 *
436 * @return void
437 */
438 public static function validate_field_types( &$errors, $posted_field, $value, $args ) {
439 $field_obj = FrmFieldFactory::get_field_object( $posted_field );
440 $args['value'] = $value;
441 $args['errors'] = $errors;
442
443 $new_errors = $field_obj->validate( $args );
444
445 if ( $new_errors ) {
446 $errors = array_merge( $errors, $new_errors );
447 }
448 }
449
450 /**
451 * @param array $errors
452 * @param object $field
453 * @param string $value
454 * @param array $args
455 *
456 * @return void
457 */
458 public static function validate_phone_field( &$errors, $field, $value, $args ) {
459 $format_value = FrmField::get_option( $field, 'format' );
460
461 if ( $field->type !== 'phone' && ( $field->type !== 'text' || ! $format_value || FrmCurrencyHelper::is_currency_format( $format_value ) ) ) {
462 return;
463 }
464
465 $pattern = self::phone_format( $field );
466
467 if ( ! preg_match( $pattern, $value ) ) {
468 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
469 }
470 }
471
472 /**
473 * @param object $field
474 *
475 * @return string
476 */
477 public static function phone_format( $field ) {
478 if ( FrmField::is_option_empty( $field, 'format' ) ) {
479 $pattern = self::default_phone_format();
480 } else {
481 $pattern = FrmField::get_option( $field, 'format' );
482 }
483
484 // Ampersands are saved as &amp;.
485 // Reverse it here so we are checking for the correct character.
486 $pattern = html_entity_decode( $pattern );
487 $pattern = apply_filters( 'frm_phone_pattern', $pattern, $field );
488
489 // Create a regexp if format is not already a regexp
490 if ( ! str_starts_with( $pattern, '^' ) ) {
491 $pattern = self::create_regular_expression_from_format( $pattern );
492 }
493
494 return '/' . $pattern . '/';
495 }
496
497 /**
498 * @since 3.01
499 *
500 * @return string
501 */
502 private static function default_phone_format() {
503 return '^((\+\d{1,3}(-|.| )?\(?\d\)?(-| |.)?\d{1,5})|(\(?\d{2,6}\)?))(-|.| )?(\d{3,4})(-|.| )?(\d{4})(( x| ext)\d{1,5}){0,1}$';
504 }
505
506 /**
507 * Create a regular expression from a phone number format
508 *
509 * @since 2.02.02
510 *
511 * @param string $pattern
512 *
513 * @return string
514 */
515 private static function create_regular_expression_from_format( $pattern ) {
516 $pattern = preg_quote( $pattern );
517
518 // Firefox doesn't like escaped dashes or colons
519 $pattern = str_replace( array( '\-', '\:' ), array( '-', ':' ), $pattern );
520
521 // Switch generic values out for their regular expression
522 $pattern = preg_replace( '/\d/', '\d', $pattern );
523 $pattern = str_replace( 'A', '[A-Z]', $pattern );
524 $pattern = str_replace( 'a', '[a-zA-Z]', $pattern );
525 $pattern = str_replace( '*', 'w', $pattern );
526 $pattern = str_replace( '/', '\/', $pattern );
527
528 if ( str_contains( $pattern, '\?' ) ) {
529 $parts = explode( '\?', $pattern );
530 $pattern = '';
531
532 foreach ( $parts as $part ) {
533 if ( $pattern ) {
534 $pattern .= '(' . $part . ')?';
535 } else {
536 $pattern .= $part;
537 }
538 }
539 }
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 ( $exclude || empty( $values['item_meta'] ) || $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 // phpcs:disable Generic.WhiteSpace.ScopeIndent
599 return FrmAppHelper::pro_is_installed() &&
600 ( isset( $values[ 'frm_page_order_' . $values['form_id'] ] ) || FrmAppHelper::get_post_param( 'frm_next_page' ) ) &&
601 FrmField::get_all_types_in_form( $values['form_id'], 'break' );
602 // phpcs:enable Generic.WhiteSpace.ScopeIndent
603 }
604
605 /**
606 * @param int $form_id
607 *
608 * @return bool|string
609 */
610 private static function is_antispam_check( $form_id ) {
611 $aspm = new FrmAntiSpam( $form_id );
612 return $aspm->validate();
613 }
614
615 /**
616 * @param array $values
617 *
618 * @return bool
619 */
620 private static function is_honeypot_spam( $values ) {
621 $honeypot = new FrmHoneypot( $values['form_id'] );
622 return ! $honeypot->validate();
623 }
624
625 /**
626 * @return bool
627 */
628 private static function is_spam_bot() {
629 return ! FrmAppHelper::get_ip_address();
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 return is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values );
640 }
641
642 /**
643 * @param int $form_id
644 *
645 * @return bool
646 */
647 private static function is_akismet_enabled_for_user( $form_id ) {
648 $form = FrmForm::getOne( $form_id );
649 return ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() );
650 }
651
652 /**
653 * Checks spam using WordPress disallowed words and Frm denylist.
654 *
655 * @param array $values Entry values.
656 *
657 * @return bool
658 */
659 public static function blacklist_check( $values ) {
660 return FrmAntiSpamController::contains_wp_disallowed_words( $values ) || FrmAntiSpamController::is_denylist_spam( $values );
661 }
662
663 /**
664 * Check entries for Akismet spam
665 *
666 * @param array $values Entry values.
667 *
668 * @return bool true if is spam
669 */
670 public static function akismet( $values ) {
671 if ( empty( $values['item_meta'] ) ) {
672 return false;
673 }
674
675 $datas = array(
676 'comment_type' => 'formidable',
677 );
678 self::parse_akismet_array( $datas, $values );
679
680 /**
681 * Allows modifying the values sent to Akismet.
682 *
683 * @since 5.0.07
684 *
685 * @param array $datas The array of values being sent to Akismet.
686 */
687 $datas = apply_filters( 'frm_akismet_values', $datas );
688
689 $query_string = _http_build_query( $datas, '', '&' );
690 $response = Akismet::http_post( $query_string, 'comment-check' );
691
692 return is_array( $response ) && $response[1] === 'true';
693 }
694
695 /**
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
702 */
703 private static function parse_akismet_array( &$datas, $values ) {
704 self::add_site_info_to_akismet( $datas );
705 self::add_server_values_to_akismet( $datas );
706
707 self::prepare_values_for_spam_check( $values );
708 self::skip_adding_values_to_akismet( $values );
709
710 self::add_user_info_to_akismet( $datas, $values );
711 self::add_comment_content_to_akismet( $datas, $values );
712 }
713
714 /**
715 * @param array $datas
716 *
717 * @return void
718 */
719 private static function add_site_info_to_akismet( &$datas ) {
720 $datas['blog'] = FrmAppHelper::site_url();
721 $datas['user_ip'] = preg_replace( '/[^0-9., ]/', '', FrmAppHelper::get_ip_address() );
722 $datas['user_agent'] = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
723 $datas['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) ? FrmAppHelper::get_server_value( 'HTTP_REFERER' ) : false;
724 $datas['blog_lang'] = get_locale();
725 $datas['blog_charset'] = get_option( 'blog_charset' );
726
727 if ( akismet_test_mode() ) {
728 $datas['is_test'] = 'true';
729 }
730 }
731
732 /**
733 * @param array $datas
734 * @param array $values
735 *
736 * @return void
737 */
738 private static function add_user_info_to_akismet( &$datas, $values ) {
739 $user_info = self::get_spam_check_user_info( $values );
740 $datas = $datas + $user_info;
741
742 if ( isset( $user_info['user_ID'] ) ) {
743 $datas['user_role'] = Akismet::get_user_roles( $user_info['user_ID'] );
744 }
745 }
746
747 /**
748 * Gets user info for Akismet spam check.
749 *
750 * @since 5.0.13 Separate code for guest. Handle value of embedded|repeater.
751 * @since 6.21 This changed from private to public.
752 *
753 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
754 *
755 * @return array
756 */
757 public static function get_spam_check_user_info( $values ) {
758 if ( ! is_user_logged_in() ) {
759 return self::get_spam_check_user_info_for_guest( $values );
760 }
761
762 $user = wp_get_current_user();
763
764 return array(
765 'user_ID' => $user->ID,
766 'user_id' => $user->ID,
767 'comment_author' => $user->display_name,
768 'comment_author_email' => $user->user_email,
769 'comment_author_url' => $user->user_url,
770 );
771 }
772
773 /**
774 * Gets user info for Akismet spam check for guest.
775 *
776 * @since 5.0.13
777 *
778 * @param array $values Entry values after flattened.
779 *
780 * @return array
781 */
782 private static function get_spam_check_user_info_for_guest( $values ) {
783 $datas = array(
784 'comment_author' => '',
785 'comment_author_email' => '',
786 'comment_author_url' => '',
787 'name_field_ids' => $values['name_field_ids'],
788 'missing_keys' => array( 'comment_author_email', 'comment_author_url', 'comment_author' ),
789 'frm_duplicated' => array(),
790 );
791
792 if ( isset( $values['item_meta'] ) ) {
793 $values = $values['item_meta'];
794 }
795
796 $values = array_filter( $values );
797
798 self::recursive_add_akismet_guest_info( $datas, $values );
799 unset( $datas['name_field_ids'] );
800 unset( $datas['missing_keys'] );
801
802 return $datas;
803 }
804
805 /**
806 * Recursive adds akismet guest info.
807 *
808 * @since 5.0.13
809 *
810 * @param array $datas Guest data.
811 * @param array $values The values.
812 * @param int|null $custom_index Custom index (or field ID).
813 *
814 * @return void
815 */
816 private static function recursive_add_akismet_guest_info( &$datas, $values, $custom_index = null ) {
817 foreach ( $values as $index => $value ) {
818 if ( ! $datas['missing_keys'] ) {
819 // Found all info.
820 return;
821 }
822
823 if ( is_array( $value ) ) {
824 self::recursive_add_akismet_guest_info( $datas, $value, $index );
825 continue;
826 }
827
828 $field_id = ! is_null( $custom_index ) ? $custom_index : $index;
829
830 foreach ( $datas['missing_keys'] as $key_index => $key ) {
831 $found = self::is_akismet_guest_info_value( $key, $value, $field_id, $datas['name_field_ids'], $values );
832
833 if ( ! $found ) {
834 continue;
835 }
836
837 $datas[ $key ] = $value;
838 $datas['frm_duplicated'][] = $field_id;
839 unset( $datas['missing_keys'][ $key_index ] );
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 str_contains( $value, '@' ) && is_email( $value );
865
866 case 'comment_author_url':
867 return str_starts_with( $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 continue;
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 }
998 }
999 }
1000
1001 /**
1002 * Checks if a skip field should be really skipped.
1003 *
1004 * @since 5.02.04
1005 *
1006 * @param object $field_data Object contains `id` and `options`.
1007 * @param array $values Entry values.
1008 *
1009 * @return bool
1010 */
1011 private static function should_really_skip_field( $field_data, $values ) {
1012 if ( empty( $field_data->options ) ) {
1013 // This is skipped field types.
1014 return true;
1015 }
1016
1017 FrmAppHelper::unserialize_or_decode( $field_data->options );
1018
1019 if ( ! $field_data->options ) {
1020 // Check if an error happens when unserializing, or empty options.
1021 return true;
1022 }
1023
1024 $last_key = array_key_last( $field_data->options );
1025
1026 // If a choice field has no Other option.
1027 if ( is_numeric( $last_key ) || ! str_starts_with( $last_key, 'other_' ) ) {
1028 return true;
1029 }
1030
1031 // If a choice field has Other option, but Other is not selected.
1032 if ( empty( $values['item_meta']['other'][ $field_data->id ] ) ) {
1033 return true;
1034 }
1035
1036 // Check if submitted value is same as one of field option.
1037 foreach ( $field_data->options as $option ) {
1038 $option_value = is_array( $option ) ? ( $option['value'] ?? '' ) : $option;
1039
1040 if ( $values['item_meta']['other'][ $field_data->id ] === $option_value ) {
1041 return true;
1042 }
1043 }
1044
1045 return false;
1046 }
1047
1048 /**
1049 * Gets field IDs that are skipped from sending to Akismet spam check.
1050 *
1051 * @since 5.0.09
1052 * @since 5.0.13 Move out get_all_form_ids_and_flatten_meta() call and get `form_ids` from `$values`.
1053 * @since 5.2.04 This method returns array of object contains `id` and `options` instead of array of `id` only.
1054 *
1055 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
1056 *
1057 * @return array
1058 */
1059 private static function get_akismet_skipped_field_ids( $values ) {
1060 if ( empty( $values['form_ids'] ) ) {
1061 return array();
1062 }
1063
1064 $skipped_types = array( 'divider', 'form', 'hidden', 'user_id', 'file', 'date', 'time', 'scale', 'star', 'range', 'toggle', 'data', 'lookup', 'likert', 'nps' );
1065 $has_other_types = array( 'radio', 'checkbox', 'select' );
1066
1067 $where = array(
1068 array(
1069 'form_id' => $values['form_ids'],
1070 'type' => array_merge( $skipped_types, $has_other_types ),
1071 ),
1072 );
1073
1074 return FrmDb::get_results( 'frm_fields', $where, 'id,options' );
1075 }
1076
1077 /**
1078 * Prepares values array for spam check.
1079 *
1080 * @since 5.0.13
1081 * @since 6.21 This changed from private to public.
1082 *
1083 * @param array $values Entry values.
1084 *
1085 * @return void
1086 */
1087 public static function prepare_values_for_spam_check( &$values ) {
1088 $values['form_ids'] = self::get_all_form_ids_and_flatten_meta( $values );
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 ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
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 $values['name_field_ids'][] = $subsubindex;
1145 }
1146
1147 if ( is_array( $values['item_meta'][ $subsubindex ] ) ) {
1148 $values['item_meta'][ $subsubindex ][] = $subsubvalue;
1149 }
1150 }
1151 }//end foreach
1152
1153 unset( $values['item_meta'][ $field_id ] );
1154 }//end foreach
1155
1156 return $form_ids;
1157 }
1158 }
1159