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

1,158 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 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 ( $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 $value = empty( $args['parent_field_id'] ) ? ( $values['item_meta'][ $args['id'] ] ?? '' ) : $values;
152
153 // Check for values in "Other" fields
154 FrmEntriesHelper::maybe_set_other_validation( $posted_field, $value, $args );
155
156 self::maybe_clear_value_for_default_blank_setting( $posted_field, $value );
157
158 $should_trim = is_array( $value ) && count( $value ) === 1 && isset( $value[0] ) && $posted_field->type !== 'checkbox';
159
160 if ( $should_trim ) {
161 $value = reset( $value );
162 }
163
164 if ( ! is_array( $value ) ) {
165 $value = trim( $value );
166 }
167
168 // phpcs:ignore Universal.Operators.StrictComparisons
169 if ( $posted_field->required == '1' && FrmAppHelper::is_empty_value( $value ) ) {
170 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $posted_field, 'blank' );
171 } elseif ( ! isset( $_POST['item_name'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
172 self::maybe_add_item_name( $value, $posted_field );
173 }
174
175 FrmEntriesHelper::set_posted_value( $posted_field, $value, $args );
176
177 self::validate_options( $errors, $posted_field, $value, $args );
178 self::validate_field_types( $errors, $posted_field, $value, $args );
179
180 // Field might want to modify value before other parts of the system
181 // e.g. trim off excess values like in the case of fields with limit.
182 $value = apply_filters( 'frm_modify_posted_field_value', $value, $errors, $posted_field, $args );
183
184 // phpcs:ignore Universal.Operators.StrictComparisons
185 if ( $value != '' ) {
186 self::validate_phone_field( $errors, $posted_field, $value, $args );
187 }
188
189 $errors = apply_filters( 'frm_validate_' . $posted_field->type . '_field_entry', $errors, $posted_field, $value, $args );
190 $errors = apply_filters( 'frm_validate_field_entry', $errors, $posted_field, $value, $args );
191
192 if ( ! FrmAppHelper::pro_is_installed() && empty( $args['other'] ) ) {
193 FrmEntriesHelper::get_posted_value( $posted_field, $value, $args );
194 }
195 }
196
197 /**
198 * @since 6.21
199 *
200 * @param array $errors
201 * @param object $posted_field
202 * @param array|string $value
203 * @param array $args
204 *
205 * @return void
206 */
207 private static function validate_options( &$errors, $posted_field, $value, $args ) {
208 if ( empty( $posted_field->options ) ) {
209 return;
210 }
211
212 $option_is_valid = self::option_is_valid( $posted_field, $value, $posted_field->options );
213
214 /**
215 * @since 6.21
216 *
217 * @param bool $option_is_valid
218 * @param array|string $value
219 * @param object $field
220 */
221 $option_is_valid = (bool) apply_filters( 'frm_option_is_valid', $option_is_valid, $value, $posted_field );
222
223 if ( ! $option_is_valid ) {
224 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $posted_field, 'invalid' );
225 }
226 }
227
228 /**
229 * Validate that value matches one of the options for the field.
230 *
231 * @since 6.21
232 *
233 * @param stdClass $field
234 * @param array|string $value
235 * @param array $options
236 *
237 * @return bool
238 */
239 private static function option_is_valid( $field, $value, $options ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
240 if ( '' === $value ) {
241 return true;
242 }
243
244 $field_object = FrmFieldFactory::get_field_type( $field->type, $field );
245
246 if ( ! $field_object->field_type_has_options_settings() ) {
247 return true;
248 }
249
250 if ( in_array( $field->type, array( 'likert', 'ranking' ), true ) ) {
251 // Ignore these field types automatically.
252 return true;
253 }
254
255 if ( 'product' === $field->type && 'user_def' === FrmField::get_option( $field, 'data_type' ) ) {
256 return true;
257 }
258
259 if ( ! empty( $field->field_options['post_field'] ) ) {
260 return true;
261 }
262
263 $value = (array) $value;
264
265 foreach ( $value as $current_value ) {
266 $match = false;
267
268 foreach ( $options as $key => $option ) {
269 if ( str_starts_with( $key, 'other_' ) ) {
270 // Always return true if an other option is found.
271 return true;
272 }
273
274 if ( is_array( $option ) ) {
275 $separate_value = FrmField::get_option( $field, 'separate_value' );
276 $option_value = $separate_value ? $option['value'] : $option['label'];
277 } else {
278 $option_value = $option;
279 }
280
281 /**
282 * @var string $current_value
283 */
284 $match = trim( $current_value ) === trim( $option_value );
285
286 if ( $match ) {
287 break;
288 }
289
290 $match = trim( $current_value ) === trim( do_shortcode( $option_value ) );
291
292 if ( $match ) {
293 break;
294 }
295
296 $match = self::is_filtered_match( $current_value, $option_value );
297
298 if ( $match ) {
299 break;
300 }
301
302 if ( ! is_numeric( $current_value ) ) {
303 continue;
304 }
305
306 $match = (int) $current_value === (int) $option_value;
307
308 if ( $match ) {
309 break;
310 }
311 }//end foreach
312
313 if ( ! $match ) {
314 return self::options_are_dynamic_based_on_hook( $field, $value );
315 }
316 }//end foreach
317
318 return true;
319 }
320
321 /**
322 * Make an extra check after passing $option_value through the_content filter.
323 * This is to help catch cases where the option's formatting has been modified using
324 * the_content filter.
325 *
326 * @since 6.22
327 *
328 * @param string $value
329 * @param string $option_value
330 *
331 * @return bool
332 */
333 private static function is_filtered_match( $value, $option_value ) {
334 // First remove the wpautop filter so it doesn't add extra tags to $option_value.
335 $filter_priority = has_filter( 'the_content', 'wpautop' );
336
337 if ( is_numeric( $filter_priority ) ) {
338 remove_filter( 'the_content', 'wpautop', $filter_priority );
339 }
340
341 $filtered_option = apply_filters( 'the_content', $option_value );
342
343 if ( is_numeric( $filter_priority ) ) {
344 add_filter( 'the_content', 'wpautop', $filter_priority );
345 }
346
347 return trim( $value ) === trim( $filtered_option );
348 }
349
350 /**
351 * Do not validate options if they have been modified with a hook.
352 * This is to help avoid issues where the options could be based on a URL param for example.
353 *
354 * @since 6.21
355 *
356 * @param object $field_object The field object.
357 * @param array|string $value The value to validate.
358 *
359 * @return bool
360 */
361 private static function options_are_dynamic_based_on_hook( $field_object, $value ) {
362 $values = (array) $field_object;
363 $values['value'] = $value;
364 FrmFieldsHelper::prepare_new_front_field( $values, $field_object );
365
366 $separate_value = FrmField::get_option( $field_object, 'separate_value' );
367 $map_callback = function ( $option ) use ( $separate_value ) {
368 if ( is_array( $option ) ) {
369 $option_value = $separate_value ? $option['value'] : $option['label'];
370 } else {
371 $option_value = $option;
372 }
373 return do_shortcode( $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 ( $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 return;
461 }
462
463 $pattern = self::phone_format( $field );
464
465 if ( ! preg_match( $pattern, $value ) ) {
466 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
467 }
468 }
469
470 /**
471 * @param object $field
472 *
473 * @return string
474 */
475 public static function phone_format( $field ) {
476 if ( FrmField::is_option_empty( $field, 'format' ) ) {
477 $pattern = self::default_phone_format();
478 } else {
479 $pattern = FrmField::get_option( $field, 'format' );
480 }
481
482 // Ampersands are saved as &amp;.
483 // Reverse it here so we are checking for the correct character.
484 $pattern = html_entity_decode( $pattern );
485 $pattern = apply_filters( 'frm_phone_pattern', $pattern, $field );
486
487 // Create a regexp if format is not already a regexp
488 if ( ! str_starts_with( $pattern, '^' ) ) {
489 $pattern = self::create_regular_expression_from_format( $pattern );
490 }
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 ( str_contains( $pattern, '\?' ) ) {
527 $parts = explode( '\?', $pattern );
528 $pattern = '';
529
530 foreach ( $parts as $part ) {
531 if ( ! $pattern ) {
532 $pattern .= $part;
533 } else {
534 $pattern .= '(' . $part . ')?';
535 }
536 }
537 }
538
539 return '^' . $pattern . '$';
540 }
541
542 /**
543 * Check for spam.
544 *
545 * @param bool $exclude
546 * @param array $values
547 * @param array $errors By reference.
548 *
549 * @return void
550 */
551 public static function spam_check( $exclude, $values, &$errors ) {
552 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
553 // Do not check spam on importing.
554 return;
555 }
556
557 if ( $exclude || empty( $values['item_meta'] ) || $errors ) {
558 // Only check spam if there are no other errors
559 return;
560 }
561
562 $antispam_check = self::is_antispam_check( $values['form_id'] );
563 $spam_msg = FrmAntiSpamController::get_default_spam_message();
564
565 if ( is_string( $antispam_check ) ) {
566 $errors['spam'] = $antispam_check;
567 } elseif ( self::is_honeypot_spam( $values ) || self::is_spam_bot() ) {
568 $errors['spam'] = $spam_msg;
569 } else {
570 $is_spam = FrmAntiSpamController::is_spam( $values );
571
572 if ( $is_spam ) {
573 $errors['spam'] = $is_spam;
574 }
575 }
576
577 if ( isset( $errors['spam'] ) || self::form_is_in_progress( $values ) ) {
578 return;
579 }
580
581 if ( self::is_akismet_enabled_for_user( $values['form_id'] ) && self::is_akismet_spam( $values ) ) {
582 $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
583 }
584 }
585
586 /**
587 * Checks if form is in progress.
588 *
589 * @since 5.0.13
590 *
591 * @param array $values The values.
592 *
593 * @return bool
594 */
595 private static function form_is_in_progress( $values ) {
596 // phpcs:disable Generic.WhiteSpace.ScopeIndent
597 return FrmAppHelper::pro_is_installed() &&
598 ( isset( $values[ 'frm_page_order_' . $values['form_id'] ] ) || FrmAppHelper::get_post_param( 'frm_next_page' ) ) &&
599 FrmField::get_all_types_in_form( $values['form_id'], 'break' );
600 // phpcs:enable Generic.WhiteSpace.ScopeIndent
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 return empty( $ip );
629 }
630
631 /**
632 * @param array $values
633 *
634 * @return bool
635 */
636 private static function is_akismet_spam( $values ) {
637 global $wpcom_api_key;
638 return is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values );
639 }
640
641 /**
642 * @param int $form_id
643 *
644 * @return bool
645 */
646 private static function is_akismet_enabled_for_user( $form_id ) {
647 $form = FrmForm::getOne( $form_id );
648 return ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() );
649 }
650
651 /**
652 * Checks spam using WordPress disallowed words and Frm denylist.
653 *
654 * @param array $values Entry values.
655 *
656 * @return bool
657 */
658 public static function blacklist_check( $values ) {
659 return FrmAntiSpamController::contains_wp_disallowed_words( $values ) || FrmAntiSpamController::is_denylist_spam( $values );
660 }
661
662 /**
663 * Check entries for Akismet spam
664 *
665 * @param array $values Entry values.
666 *
667 * @return bool true if is spam
668 */
669 public static function akismet( $values ) {
670 if ( empty( $values['item_meta'] ) ) {
671 return false;
672 }
673
674 $datas = array(
675 'comment_type' => 'formidable',
676 );
677 self::parse_akismet_array( $datas, $values );
678
679 /**
680 * Allows modifying the values sent to Akismet.
681 *
682 * @since 5.0.07
683 *
684 * @param array $datas The array of values being sent to Akismet.
685 */
686 $datas = apply_filters( 'frm_akismet_values', $datas );
687
688 $query_string = _http_build_query( $datas, '', '&' );
689 $response = Akismet::http_post( $query_string, 'comment-check' );
690
691 return is_array( $response ) && $response[1] === 'true';
692 }
693
694 /**
695 * @since 2.0
696 *
697 * @param array $datas The array of values being sent to Akismet.
698 * @param array $values Entry values.
699 *
700 * @return void
701 */
702 private static function parse_akismet_array( &$datas, $values ) {
703 self::add_site_info_to_akismet( $datas );
704 self::add_server_values_to_akismet( $datas );
705
706 self::prepare_values_for_spam_check( $values );
707 self::skip_adding_values_to_akismet( $values );
708
709 self::add_user_info_to_akismet( $datas, $values );
710 self::add_comment_content_to_akismet( $datas, $values );
711 }
712
713 /**
714 * @param array $datas
715 *
716 * @return void
717 */
718 private static function add_site_info_to_akismet( &$datas ) {
719 $datas['blog'] = FrmAppHelper::site_url();
720 $datas['user_ip'] = preg_replace( '/[^0-9., ]/', '', FrmAppHelper::get_ip_address() );
721 $datas['user_agent'] = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
722 $datas['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) ? FrmAppHelper::get_server_value( 'HTTP_REFERER' ) : false;
723 $datas['blog_lang'] = get_locale();
724 $datas['blog_charset'] = get_option( 'blog_charset' );
725
726 if ( akismet_test_mode() ) {
727 $datas['is_test'] = 'true';
728 }
729 }
730
731 /**
732 * @param array $datas
733 * @param array $values
734 *
735 * @return void
736 */
737 private static function add_user_info_to_akismet( &$datas, $values ) {
738 $user_info = self::get_spam_check_user_info( $values );
739 $datas = $datas + $user_info;
740
741 if ( isset( $user_info['user_ID'] ) ) {
742 $datas['user_role'] = Akismet::get_user_roles( $user_info['user_ID'] );
743 }
744 }
745
746 /**
747 * Gets user info for Akismet spam check.
748 *
749 * @since 5.0.13 Separate code for guest. Handle value of embedded|repeater.
750 * @since 6.21 This changed from private to public.
751 *
752 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
753 *
754 * @return array
755 */
756 public static function get_spam_check_user_info( $values ) {
757 if ( ! is_user_logged_in() ) {
758 return self::get_spam_check_user_info_for_guest( $values );
759 }
760
761 $user = wp_get_current_user();
762
763 return array(
764 'user_ID' => $user->ID,
765 'user_id' => $user->ID,
766 'comment_author' => $user->display_name,
767 'comment_author_email' => $user->user_email,
768 'comment_author_url' => $user->user_url,
769 );
770 }
771
772 /**
773 * Gets user info for Akismet spam check for guest.
774 *
775 * @since 5.0.13
776 *
777 * @param array $values Entry values after flattened.
778 *
779 * @return array
780 */
781 private static function get_spam_check_user_info_for_guest( $values ) {
782 $datas = array(
783 'comment_author' => '',
784 'comment_author_email' => '',
785 'comment_author_url' => '',
786 'name_field_ids' => $values['name_field_ids'],
787 'missing_keys' => array( 'comment_author_email', 'comment_author_url', 'comment_author' ),
788 'frm_duplicated' => array(),
789 );
790
791 if ( isset( $values['item_meta'] ) ) {
792 $values = $values['item_meta'];
793 }
794
795 $values = array_filter( $values );
796
797 self::recursive_add_akismet_guest_info( $datas, $values );
798 unset( $datas['name_field_ids'] );
799 unset( $datas['missing_keys'] );
800
801 return $datas;
802 }
803
804 /**
805 * Recursive adds akismet guest info.
806 *
807 * @since 5.0.13
808 *
809 * @param array $datas Guest data.
810 * @param array $values The values.
811 * @param int|null $custom_index Custom index (or field ID).
812 *
813 * @return void
814 */
815 private static function recursive_add_akismet_guest_info( &$datas, $values, $custom_index = null ) {
816 foreach ( $values as $index => $value ) {
817 if ( ! $datas['missing_keys'] ) {
818 // Found all info.
819 return;
820 }
821
822 if ( is_array( $value ) ) {
823 self::recursive_add_akismet_guest_info( $datas, $value, $index );
824 continue;
825 }
826
827 $field_id = ! is_null( $custom_index ) ? $custom_index : $index;
828
829 foreach ( $datas['missing_keys'] as $key_index => $key ) {
830 $found = self::is_akismet_guest_info_value( $key, $value, $field_id, $datas['name_field_ids'], $values );
831
832 if ( ! $found ) {
833 continue;
834 }
835
836 $datas[ $key ] = $value;
837 $datas['frm_duplicated'][] = $field_id;
838 unset( $datas['missing_keys'][ $key_index ] );
839 }
840 }//end foreach
841 }
842
843 /**
844 * Checks if given value is an akismet guest info.
845 *
846 * @since 5.0.13
847 *
848 * @param string $key Guest info key.
849 * @param string $value Value to check.
850 * @param int $field_id Field ID.
851 * @param array $name_field_ids Name field IDs.
852 * @param array $values Array of posted values.
853 *
854 * @return bool
855 */
856 private static function is_akismet_guest_info_value( $key, &$value, $field_id, $name_field_ids, $values ) {
857 if ( ! $value || is_numeric( $value ) ) {
858 return false;
859 }
860
861 switch ( $key ) {
862 case 'comment_author_email':
863 return str_contains( $value, '@' ) && is_email( $value );
864
865 case 'comment_author_url':
866 return str_starts_with( $value, 'http' );
867
868 case 'comment_author':
869 if ( $name_field_ids && in_array( $field_id, $name_field_ids, true ) ) {
870 // If there is name field in the form, we should always use it as author name.
871 return true;
872 }
873
874 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
875 $fields = self::get_name_text_fields( $form_id );
876
877 foreach ( $fields as $index => $field ) {
878 if ( 'Name' !== $field->name ) {
879 continue;
880 }
881
882 if ( isset( $fields[ $index + 1 ] ) && 'Last' === $fields[ $index + 1 ]->name ) {
883 if ( empty( $values[ absint( $fields[ $index + 1 ]->id ) ] ) ) {
884 continue;
885 }
886
887 $value .= ' ' . $values[ $fields[ $index + 1 ]->id ];
888 return true;
889 }
890 }
891 }//end switch
892
893 return false;
894 }
895
896 /**
897 * Returns fields that have 'Name' and 'Last' as their name.
898 *
899 * @since 6.17
900 *
901 * @param int $form_id
902 *
903 * @return array
904 */
905 private static function get_name_text_fields( $form_id ) {
906 $name_text_fields_is_initialized = is_array( self::$name_text_fields );
907
908 if ( $name_text_fields_is_initialized && isset( self::$name_text_fields[ $form_id ] ) ) {
909 return self::$name_text_fields[ $form_id ];
910 }
911
912 if ( ! $name_text_fields_is_initialized ) {
913 self::$name_text_fields = array();
914 }
915 self::$name_text_fields[ $form_id ] = FrmDb::get_results(
916 'frm_fields',
917 array(
918 'form_id' => $form_id,
919 'type' => 'text',
920 'name' => array( 'Name', 'Last' ),
921 ),
922 'id,name',
923 array( 'order_by' => 'field_order ASC' )
924 );
925
926 return self::$name_text_fields[ $form_id ];
927 }
928
929 /**
930 * @param array $datas
931 *
932 * @return void
933 */
934 private static function add_server_values_to_akismet( &$datas ) {
935 foreach ( $_SERVER as $key => $value ) {
936 $include_value = is_string( $value ) && ! preg_match( '/^HTTP_COOKIE/', $key ) && preg_match( '/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/', $key );
937
938 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
939 if ( $include_value ) {
940 $datas[ $key ] = $value;
941 }
942 unset( $key, $value );
943 }
944 }
945
946 /**
947 * Adds comment content to Akismet data.
948 *
949 * @since 5.0.09
950 *
951 * @param array $datas The array of values being sent to Akismet.
952 * @param array $values Entry values.
953 *
954 * @return void
955 */
956 private static function add_comment_content_to_akismet( &$datas, $values ) {
957 if ( isset( $datas['frm_duplicated'] ) ) {
958 foreach ( $datas['frm_duplicated'] as $index ) {
959 if ( isset( $values['item_meta'][ $index ] ) ) {
960 unset( $values['item_meta'][ $index ] );
961 } else {
962 unset( $values[ $index ] );
963 }
964 }
965 unset( $datas['frm_duplicated'] );
966 }
967
968 $datas['comment_content'] = FrmEntriesHelper::entry_array_to_string( $values );
969 }
970
971 /**
972 * Skips adding field values to Akismet.
973 *
974 * @since 5.0.09
975 *
976 * @param array $values Entry values.
977 *
978 * @return void
979 */
980 private static function skip_adding_values_to_akismet( &$values ) {
981 $skipped_fields = self::get_akismet_skipped_field_ids( $values );
982
983 foreach ( $skipped_fields as $skipped_field ) {
984 if ( ! isset( $values['item_meta'][ $skipped_field->id ] ) ) {
985 continue;
986 }
987
988 if ( ! self::should_really_skip_field( $skipped_field, $values ) ) {
989 continue;
990 }
991
992 unset( $values['item_meta'][ $skipped_field->id ] );
993
994 if ( isset( $values['item_meta']['other'][ $skipped_field->id ] ) ) {
995 unset( $values['item_meta']['other'][ $skipped_field->id ] );
996 }
997 }
998 }
999
1000 /**
1001 * Checks if a skip field should be really skipped.
1002 *
1003 * @since 5.02.04
1004 *
1005 * @param object $field_data Object contains `id` and `options`.
1006 * @param array $values Entry values.
1007 *
1008 * @return bool
1009 */
1010 private static function should_really_skip_field( $field_data, $values ) {
1011 if ( empty( $field_data->options ) ) {
1012 // This is skipped field types.
1013 return true;
1014 }
1015
1016 FrmAppHelper::unserialize_or_decode( $field_data->options );
1017
1018 if ( ! $field_data->options ) {
1019 // Check if an error happens when unserializing, or empty options.
1020 return true;
1021 }
1022
1023 $last_key = array_key_last( $field_data->options );
1024
1025 // If a choice field has no Other option.
1026 if ( is_numeric( $last_key ) || ! str_starts_with( $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['value'] ?? '' ) : $option;
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 $values['form_ids'] = self::get_all_form_ids_and_flatten_meta( $values );
1088 }
1089
1090 /**
1091 * Gets all form IDs (include child form IDs) and flatten item_meta array. Used for skipping values sent to Akismet.
1092 * This also removes some unused data from the item_meta.
1093 *
1094 * @since 5.0.09
1095 * @since 5.0.13 Convert name field value to string.
1096 *
1097 * @param array $values Entry values.
1098 *
1099 * @return array Form IDs.
1100 */
1101 private static function get_all_form_ids_and_flatten_meta( &$values ) { // phpcs:ignore SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
1102 $values['name_field_ids'] = array();
1103
1104 // Blacklist check for File field in the old version doesn't contain `form_id`.
1105 $form_ids = isset( $values['form_id'] ) ? array( absint( $values['form_id'] ) ) : array();
1106
1107 foreach ( $values['item_meta'] as $field_id => $value ) {
1108 if ( ! is_numeric( $field_id ) ) {
1109 // Maybe `other`.
1110 continue;
1111 }
1112
1113 // Convert name array to string.
1114 if ( isset( $value['first'] ) && isset( $value['last'] ) ) {
1115 $values['item_meta'][ $field_id ] = trim( implode( ' ', $value ) );
1116 $values['name_field_ids'][] = $field_id;
1117 continue;
1118 }
1119
1120 if ( ! is_array( $value ) || empty( $value['form'] ) ) {
1121 continue;
1122 }
1123
1124 $form_ids[] = absint( $value['form'] );
1125
1126 foreach ( $value as $subindex => $subvalue ) {
1127 if ( ! is_numeric( $subindex ) || ! is_array( $subvalue ) ) {
1128 continue;
1129 }
1130
1131 foreach ( $subvalue as $subsubindex => $subsubvalue ) {
1132 if ( ! $subsubvalue ) {
1133 continue;
1134 }
1135
1136 if ( ! isset( $values['item_meta'][ $subsubindex ] ) ) {
1137 $values['item_meta'][ $subsubindex ] = array();
1138 }
1139
1140 // Convert name array to string.
1141 if ( isset( $subsubvalue['first'] ) && isset( $subsubvalue['last'] ) ) {
1142 $subsubvalue = trim( implode( ' ', $subsubvalue ) );
1143 $values['name_field_ids'][] = $subsubindex;
1144 }
1145
1146 if ( is_array( $values['item_meta'][ $subsubindex ] ) ) {
1147 $values['item_meta'][ $subsubindex ][] = $subsubvalue;
1148 }
1149 }
1150 }//end foreach
1151
1152 unset( $values['item_meta'][ $field_id ] );
1153 }//end foreach
1154
1155 return $form_ids;
1156 }
1157 }
1158