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

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