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

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