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

839 lines 25.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 * @param array $values
10 * @param bool|string[] $exclude
11 * @return array
12 */
13 public static function validate( $values, $exclude = false ) {
14 FrmEntry::sanitize_entry_post( $values );
15 $errors = array();
16
17 if ( ! isset( $values['form_id'] ) || ! isset( $values['item_meta'] ) ) {
18 $errors['form'] = __( 'There was a problem with your submission. Please try again.', 'formidable' );
19
20 return $errors;
21 }
22
23 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' ) ) ) {
24 $frm_settings = FrmAppHelper::get_settings();
25 $errors['form'] = $frm_settings->admin_permission;
26 }
27
28 self::maybe_fix_item_meta();
29 self::set_item_key( $values );
30
31 $posted_fields = self::get_fields_to_validate( $values, $exclude );
32
33 // Pass exclude value to validate_field function so it can be used for repeating sections
34 $args = array( 'exclude' => $exclude );
35
36 foreach ( $posted_fields as $posted_field ) {
37 self::validate_field( $posted_field, $errors, $values, $args );
38 unset( $posted_field );
39 }
40
41 if ( empty( $errors ) ) {
42 self::spam_check( $exclude, $values, $errors );
43 }
44
45 /**
46 * Allows modifying the validation errors after validating all fields.
47 *
48 * @since 5.0.04 Added `posted_fields` to the third param.
49 *
50 * @param array $errors Errors data.
51 * @param array $values Value data of the form.
52 * @param array $args Custom arguments. Contains `exclude` and `posted_fields`.
53 */
54 $filtered_errors = apply_filters( 'frm_validate_entry', $errors, $values, compact( 'exclude', 'posted_fields' ) );
55
56 if ( is_array( $filtered_errors ) ) {
57 $errors = $filtered_errors;
58 } else {
59 _doing_it_wrong( __FUNCTION__, 'Only arrays should be returned when using the frm_validate_entry filter.', '6.3' );
60 }
61
62 return $errors;
63 }
64
65 /**
66 * In case $_POST['item_meta'] is not an array, change it to an empty array.
67 * This helps to avoid some warnings and errors when $_POST['item_meta'] is updated.
68 *
69 * @since 6.6
70 *
71 * @return void
72 */
73 private static function maybe_fix_item_meta() {
74 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
75 if ( ! isset( $_POST['item_meta'] ) || ! is_array( $_POST['item_meta'] ) ) {
76 $_POST['item_meta'] = array();
77 }
78 }
79
80 private static function set_item_key( &$values ) {
81 if ( ! isset( $values['item_key'] ) || $values['item_key'] == '' ) {
82 global $wpdb;
83 $values['item_key'] = FrmAppHelper::get_unique_key( '', $wpdb->prefix . 'frm_items', 'item_key' );
84 $_POST['item_key'] = $values['item_key'];
85 }
86 }
87
88 private static function get_fields_to_validate( $values, $exclude ) {
89 $where = apply_filters( 'frm_posted_field_ids', array( 'fi.form_id' => $values['form_id'] ) );
90
91 // Don't get subfields
92 $where['fr.parent_form_id'] = array( null, 0 );
93
94 // Don't get excluded fields (like file upload fields in the ajax validation)
95 if ( ! empty( $exclude ) ) {
96 $where['fi.type not'] = $exclude;
97 }
98
99 $fields = FrmField::getAll( $where, 'field_order' );
100
101 /**
102 * Allows modifying fields to validate.
103 *
104 * @since 5.0.06
105 *
106 * @param array $fields List of fields.
107 * @param array $args Includes `values`, `exclude`, `where`.
108 */
109 return apply_filters( 'frm_fields_to_validate', $fields, compact( 'values', 'exclude', 'where' ) );
110 }
111
112 public static function validate_field( $posted_field, &$errors, $values, $args = array() ) {
113 $defaults = array(
114 'id' => $posted_field->id,
115 // The id of the repeat or embed form.
116 'parent_field_id' => '',
117 // The pointer in the posted array.
118 'key_pointer' => '',
119 // Exclude these field types from validation.
120 'exclude' => array(),
121
122 );
123 $args = wp_parse_args( $args, $defaults );
124
125 if ( empty( $args['parent_field_id'] ) ) {
126 $value = isset( $values['item_meta'][ $args['id'] ] ) ? $values['item_meta'][ $args['id'] ] : '';
127 } else {
128 // value is from a nested form
129 $value = $values;
130 }
131
132 // Check for values in "Other" fields
133 FrmEntriesHelper::maybe_set_other_validation( $posted_field, $value, $args );
134
135 self::maybe_clear_value_for_default_blank_setting( $posted_field, $value );
136
137 $should_trim = is_array( $value ) && count( $value ) == 1 && isset( $value[0] ) && $posted_field->type !== 'checkbox';
138 if ( $should_trim ) {
139 $value = reset( $value );
140 }
141
142 if ( ! is_array( $value ) ) {
143 $value = trim( $value );
144 }
145
146 if ( $posted_field->required == '1' && FrmAppHelper::is_empty_value( $value ) ) {
147 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $posted_field, 'blank' );
148 } elseif ( ! isset( $_POST['item_name'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
149 self::maybe_add_item_name( $value, $posted_field );
150 }
151
152 FrmEntriesHelper::set_posted_value( $posted_field, $value, $args );
153
154 self::validate_field_types( $errors, $posted_field, $value, $args );
155
156 // Field might want to modify value before other parts of the system
157 // e.g. trim off excess values like in the case of fields with limit.
158 $value = apply_filters( 'frm_modify_posted_field_value', $value, $errors, $posted_field, $args );
159
160 if ( $value != '' ) {
161 self::validate_phone_field( $errors, $posted_field, $value, $args );
162 }
163
164 $errors = apply_filters( 'frm_validate_' . $posted_field->type . '_field_entry', $errors, $posted_field, $value, $args );
165 $errors = apply_filters( 'frm_validate_field_entry', $errors, $posted_field, $value, $args );
166
167 if ( ! FrmAppHelper::pro_is_installed() && empty( $args['other'] ) ) {
168 FrmEntriesHelper::get_posted_value( $posted_field, $value, $args );
169 }
170 }
171
172 /**
173 * Maybe add item_name to $_POST to save it in items table.
174 *
175 * @since 5.2.02
176 *
177 * @param array|string $value Field value.
178 * @param object $field Field object.
179 */
180 private static function maybe_add_item_name( $value, $field ) {
181 $item_name = false;
182 if ( 'name' === $field->type ) {
183 $field_obj = FrmFieldFactory::get_field_object( $field );
184 $item_name = $field_obj->get_display_value( $value );
185 } elseif ( 'text' === $field->type ) {
186 $item_name = $value;
187 }
188
189 if ( false !== $item_name ) {
190 // Item name has a max length of 255 characters so truncate it so it doesn't fail to save in the database.
191 $_POST['item_name'] = substr( $item_name, 0, 255 );
192 }
193 }
194
195 /**
196 * Set $value to an empty string if it matches its label
197 *
198 * @param object $field
199 * @param string $value
200 */
201 private static function maybe_clear_value_for_default_blank_setting( $field, &$value ) {
202 $position = FrmField::get_option( $field, 'label' );
203 if ( ! $position ) {
204 $position = FrmStylesController::get_style_val( 'position', $field->form_id );
205 }
206
207 if ( $position === 'inside' && FrmFieldsHelper::is_placeholder_field_type( $field->type ) && $value === $field->name ) {
208 $value = '';
209 }
210 }
211
212 public static function validate_field_types( &$errors, $posted_field, $value, $args ) {
213 $field_obj = FrmFieldFactory::get_field_object( $posted_field );
214 $args['value'] = $value;
215 $args['errors'] = $errors;
216
217 $new_errors = $field_obj->validate( $args );
218 if ( ! empty( $new_errors ) ) {
219 $errors = array_merge( $errors, $new_errors );
220 }
221 }
222
223 public static function validate_phone_field( &$errors, $field, $value, $args ) {
224 if ( $field->type === 'phone' || ( $field->type === 'text' && FrmField::is_option_true_in_object( $field, 'format' ) ) ) {
225
226 $pattern = self::phone_format( $field );
227
228 if ( ! preg_match( $pattern, $value ) ) {
229 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
230 }
231 }
232 }
233
234 public static function phone_format( $field ) {
235 if ( FrmField::is_option_empty( $field, 'format' ) ) {
236 $pattern = self::default_phone_format();
237 } else {
238 $pattern = FrmField::get_option( $field, 'format' );
239 }
240
241 // Ampersands are saved as &amp;.
242 // Reverse it here so we are checking for the correct character.
243 $pattern = html_entity_decode( $pattern );
244 $pattern = apply_filters( 'frm_phone_pattern', $pattern, $field );
245
246 // Create a regexp if format is not already a regexp
247 if ( strpos( $pattern, '^' ) !== 0 ) {
248 $pattern = self::create_regular_expression_from_format( $pattern );
249 }
250
251 $pattern = '/' . $pattern . '/';
252
253 return $pattern;
254 }
255
256 /**
257 * @since 3.01
258 */
259 private static function default_phone_format() {
260 return '^((\+\d{1,3}(-|.| )?\(?\d\)?(-| |.)?\d{1,5})|(\(?\d{2,6}\)?))(-|.| )?(\d{3,4})(-|.| )?(\d{4})(( x| ext)\d{1,5}){0,1}$';
261 }
262
263 /**
264 * Create a regular expression from a phone number format
265 *
266 * @since 2.02.02
267 *
268 * @param string $pattern
269 *
270 * @return string
271 */
272 private static function create_regular_expression_from_format( $pattern ) {
273 $pattern = preg_quote( $pattern );
274
275 // Firefox doesn't like escaped dashes or colons
276 $pattern = str_replace( array( '\-', '\:' ), array( '-', ':' ), $pattern );
277
278 // Switch generic values out for their regular expression
279 $pattern = preg_replace( '/\d/', '\d', $pattern );
280 $pattern = str_replace( 'A', '[A-Z]', $pattern );
281 $pattern = str_replace( 'a', '[a-zA-Z]', $pattern );
282 $pattern = str_replace( '*', 'w', $pattern );
283 $pattern = str_replace( '/', '\/', $pattern );
284
285 if ( strpos( $pattern, '\?' ) !== false ) {
286 $parts = explode( '\?', $pattern );
287 $pattern = '';
288 foreach ( $parts as $part ) {
289 if ( empty( $pattern ) ) {
290 $pattern .= $part;
291 } else {
292 $pattern .= '(' . $part . ')?';
293 }
294 }
295 }
296 $pattern = '^' . $pattern . '$';
297
298 return $pattern;
299 }
300
301 /**
302 * Check for spam
303 *
304 * @param bool $exclude
305 * @param array $values
306 * @param array $errors By reference.
307 */
308 public static function spam_check( $exclude, $values, &$errors ) {
309 if ( ! empty( $exclude ) || empty( $values['item_meta'] ) || ! empty( $errors ) ) {
310 // only check spam if there are no other errors
311 return;
312 }
313
314 $antispam_check = self::is_antispam_check( $values['form_id'] );
315 if ( is_string( $antispam_check ) ) {
316 $errors['spam'] = $antispam_check;
317 } elseif ( self::is_honeypot_spam( $values ) || self::is_spam_bot() ) {
318 $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
319 } elseif ( self::blacklist_check( $values ) ) {
320 $errors['spam'] = __( 'Your entry appears to be blocked spam!', 'formidable' );
321 }
322
323 if ( isset( $errors['spam'] ) || self::form_is_in_progress( $values ) ) {
324 return;
325 }
326
327 if ( self::is_akismet_enabled_for_user( $values['form_id'] ) && self::is_akismet_spam( $values ) ) {
328 $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
329 }
330 }
331
332 /**
333 * Checks if form is in progress.
334 *
335 * @since 5.0.13
336 *
337 * @param array $values The values.
338 * @return bool
339 */
340 private static function form_is_in_progress( $values ) {
341 return FrmAppHelper::pro_is_installed() &&
342 ( isset( $values[ 'frm_page_order_' . $values['form_id'] ] ) || FrmAppHelper::get_post_param( 'frm_next_page' ) ) &&
343 FrmField::get_all_types_in_form( $values['form_id'], 'break' );
344 }
345
346 /**
347 * @param int $form_id
348 *
349 * @return bool|string
350 */
351 private static function is_antispam_check( $form_id ) {
352 $aspm = new FrmAntiSpam( $form_id );
353 return $aspm->validate();
354 }
355
356 /**
357 * @param array $values
358 * @return bool
359 */
360 private static function is_honeypot_spam( $values ) {
361 $honeypot = new FrmHoneypot( $values['form_id'] );
362 return ! $honeypot->validate();
363 }
364
365 /**
366 * @return bool
367 */
368 private static function is_spam_bot() {
369 $ip = FrmAppHelper::get_ip_address();
370
371 return empty( $ip );
372 }
373
374 /**
375 * @param array $values
376 * @return bool
377 */
378 private static function is_akismet_spam( $values ) {
379 global $wpcom_api_key;
380
381 return ( is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values ) );
382 }
383
384 /**
385 * @param int $form_id
386 * @return bool
387 */
388 private static function is_akismet_enabled_for_user( $form_id ) {
389 $form = FrmForm::getOne( $form_id );
390
391 return ( ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() ) );
392 }
393
394 public static function blacklist_check( $values ) {
395 if ( ! apply_filters( 'frm_check_blacklist', true, $values ) ) {
396 return false;
397 }
398
399 $mod_keys = trim( self::get_disallowed_words() );
400 if ( empty( $mod_keys ) ) {
401 return false;
402 }
403
404 $content = FrmEntriesHelper::entry_array_to_string( $values );
405
406 self::prepare_values_for_spam_check( $values );
407 $ip = FrmAppHelper::get_ip_address();
408 $user_agent = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
409 $user_info = self::get_spam_check_user_info( $values );
410
411 return self::check_disallowed_words( $user_info['comment_author'], $user_info['comment_author_email'], $user_info['comment_author_url'], $content, $ip, $user_agent );
412 }
413
414 /**
415 * For WP 5.5 compatibility.
416 *
417 * @since 4.06.02
418 */
419 private static function check_disallowed_words( $author, $email, $url, $content, $ip, $user_agent ) {
420 if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
421 return wp_check_comment_disallowed_list( $author, $email, $url, $content, $ip, $user_agent );
422 }
423 // phpcs:ignore WordPress.WP.DeprecatedFunctions.wp_blacklist_checkFound
424 return wp_blacklist_check( $author, $email, $url, $content, $ip, $user_agent );
425 }
426
427 /**
428 * For WP 5.5 compatibility.
429 *
430 * @since 4.06.02
431 */
432 private static function get_disallowed_words() {
433 $keys = get_option( 'disallowed_keys' );
434 if ( false === $keys ) {
435 // Fallback for WP < 5.5.
436 // phpcs:ignore WordPress.WP.DeprecatedParameterValues.Found
437 $keys = get_option( 'blacklist_keys' );
438 }
439 return $keys;
440 }
441
442 /**
443 * Check entries for Akismet spam
444 *
445 * @return bool true if is spam
446 */
447 public static function akismet( $values ) {
448 if ( empty( $values['item_meta'] ) ) {
449 return false;
450 }
451
452 $datas = array(
453 'comment_type' => 'formidable',
454 );
455 self::parse_akismet_array( $datas, $values );
456
457 /**
458 * Allows modifying the values sent to Akismet.
459 *
460 * @since 5.0.07
461 *
462 * @param array $datas The array of values being sent to Akismet.
463 */
464 $datas = apply_filters( 'frm_akismet_values', $datas );
465
466 $query_string = _http_build_query( $datas, '', '&' );
467 $response = Akismet::http_post( $query_string, 'comment-check' );
468
469 return ( is_array( $response ) && $response[1] === 'true' );
470 }
471
472 /**
473 * @since 2.0
474 */
475 private static function parse_akismet_array( &$datas, $values ) {
476 self::add_site_info_to_akismet( $datas );
477 self::add_server_values_to_akismet( $datas );
478
479 self::prepare_values_for_spam_check( $values );
480
481 self::add_user_info_to_akismet( $datas, $values );
482 self::add_comment_content_to_akismet( $datas, $values );
483 }
484
485 private static function add_site_info_to_akismet( &$datas ) {
486 $datas['blog'] = FrmAppHelper::site_url();
487 $datas['user_ip'] = preg_replace( '/[^0-9., ]/', '', FrmAppHelper::get_ip_address() );
488 $datas['user_agent'] = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
489 $datas['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) ? FrmAppHelper::get_server_value( 'HTTP_REFERER' ) : false;
490 $datas['blog_lang'] = get_locale();
491 $datas['blog_charset'] = get_option( 'blog_charset' );
492
493 if ( akismet_test_mode() ) {
494 $datas['is_test'] = 'true';
495 }
496 }
497
498 private static function add_user_info_to_akismet( &$datas, $values ) {
499 $user_info = self::get_spam_check_user_info( $values );
500 $datas = $datas + $user_info;
501
502 if ( isset( $user_info['user_ID'] ) ) {
503 $datas['user_role'] = Akismet::get_user_roles( $user_info['user_ID'] );
504 }
505 }
506
507 /**
508 * Gets user info for Akismet spam check.
509 *
510 * @since 5.0.13 Separate code for guest. Handle value of embedded|repeater.
511 *
512 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
513 * @return array
514 */
515 private static function get_spam_check_user_info( $values ) {
516 if ( ! is_user_logged_in() ) {
517 return self::get_spam_check_user_info_for_guest( $values );
518 }
519
520 $user = wp_get_current_user();
521
522 return array(
523 'user_ID' => $user->ID,
524 'user_id' => $user->ID,
525 'comment_author' => $user->display_name,
526 'comment_author_email' => $user->user_email,
527 'comment_author_url' => $user->user_url,
528 );
529 }
530
531 /**
532 * Gets user info for Akismet spam check for guest.
533 *
534 * @since 5.0.13
535 *
536 * @param array $values Entry values after flattened.
537 * @return array
538 */
539 private static function get_spam_check_user_info_for_guest( $values ) {
540 $datas = array(
541 'comment_author' => '',
542 'comment_author_email' => '',
543 'comment_author_url' => '',
544 'name_field_ids' => $values['name_field_ids'],
545 'missing_keys' => array( 'comment_author_email', 'comment_author_url', 'comment_author' ),
546 'frm_duplicated' => array(),
547 );
548
549 if ( isset( $values['item_meta'] ) ) {
550 $values = $values['item_meta'];
551 }
552
553 $values = array_filter( $values );
554
555 self::recursive_add_akismet_guest_info( $datas, $values );
556 unset( $datas['name_field_ids'] );
557 unset( $datas['missing_keys'] );
558
559 return $datas;
560 }
561
562 /**
563 * Recursive adds akismet guest info.
564 *
565 * @since 5.0.13
566 *
567 * @param array $datas Guest data.
568 * @param array $values The values.
569 * @param int|null $custom_index Custom index (or field ID).
570 */
571 private static function recursive_add_akismet_guest_info( &$datas, $values, $custom_index = null ) {
572 foreach ( $values as $index => $value ) {
573 if ( ! $datas['missing_keys'] ) {
574 // Found all info.
575 return;
576 }
577
578 if ( is_array( $value ) ) {
579 self::recursive_add_akismet_guest_info( $datas, $value, $index );
580 continue;
581 }
582
583 $field_id = ! is_null( $custom_index ) ? $custom_index : $index;
584 foreach ( $datas['missing_keys'] as $key_index => $key ) {
585 $found = self::is_akismet_guest_info_value( $key, $value, $field_id, $datas['name_field_ids'] );
586 if ( $found ) {
587 $datas[ $key ] = $value;
588 $datas['frm_duplicated'][] = $field_id;
589 unset( $datas['missing_keys'][ $key_index ] );
590 }
591 }
592 }//end foreach
593 }
594
595 /**
596 * Checks if given value is an akismet guest info.
597 *
598 * @since 5.0.13
599 *
600 * @param string $key Guest info key.
601 * @param string $value Value to check.
602 * @param int $field_id Field ID.
603 * @param array $name_field_ids Name field IDs.
604 * @return bool
605 */
606 private static function is_akismet_guest_info_value( $key, $value, $field_id, $name_field_ids ) {
607 if ( ! $value || is_numeric( $value ) ) {
608 return false;
609 }
610
611 switch ( $key ) {
612 case 'comment_author_email':
613 return strpos( $value, '@' ) && is_email( $value );
614
615 case 'comment_author_url':
616 return 0 === strpos( $value, 'http' );
617
618 case 'comment_author':
619 if ( $name_field_ids ) {
620 // If there is name field in the form, we should always use it as author name.
621 return in_array( $field_id, $name_field_ids, true );
622 }
623 return strlen( $value ) < 200;
624 }
625
626 return false;
627 }
628
629 private static function add_server_values_to_akismet( &$datas ) {
630 foreach ( $_SERVER as $key => $value ) {
631 $include_value = is_string( $value ) && ! preg_match( '/^HTTP_COOKIE/', $key ) && preg_match( '/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/', $key );
632
633 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
634 if ( $include_value ) {
635 $datas[ $key ] = $value;
636 }
637 unset( $key, $value );
638 }
639 }
640
641 /**
642 * Adds comment content to Akismet data.
643 *
644 * @since 5.0.09
645 *
646 * @param array $datas The array of values being sent to Akismet.
647 * @param array $values Entry values.
648 */
649 private static function add_comment_content_to_akismet( &$datas, $values ) {
650 if ( isset( $datas['frm_duplicated'] ) ) {
651 foreach ( $datas['frm_duplicated'] as $index ) {
652 if ( isset( $values['item_meta'][ $index ] ) ) {
653 unset( $values['item_meta'][ $index ] );
654 } else {
655 unset( $values[ $index ] );
656 }
657 }
658 unset( $datas['frm_duplicated'] );
659 }
660
661 self::skip_adding_values_to_akismet( $values );
662
663 $datas['comment_content'] = FrmEntriesHelper::entry_array_to_string( $values );
664 }
665
666 /**
667 * Skips adding field values to Akismet.
668 *
669 * @since 5.0.09
670 *
671 * @param array $values Entry values.
672 */
673 private static function skip_adding_values_to_akismet( &$values ) {
674 $skipped_fields = self::get_akismet_skipped_field_ids( $values );
675 foreach ( $skipped_fields as $skipped_field ) {
676 if ( ! isset( $values['item_meta'][ $skipped_field->id ] ) ) {
677 continue;
678 }
679
680 if ( self::should_really_skip_field( $skipped_field, $values ) ) {
681 unset( $values['item_meta'][ $skipped_field->id ] );
682 if ( isset( $values['item_meta']['other'][ $skipped_field->id ] ) ) {
683 unset( $values['item_meta']['other'][ $skipped_field->id ] );
684 }
685 }
686 }
687 }
688
689 /**
690 * Checks if a skip field should be really skipped.
691 *
692 * @since 5.02.04
693 *
694 * @param object $field_data Object contains `id` and `options`.
695 * @param array $values Entry values.
696 * @return bool
697 */
698 private static function should_really_skip_field( $field_data, $values ) {
699 if ( empty( $field_data->options ) ) {
700 // This is skipped field types.
701 return true;
702 }
703
704 FrmAppHelper::unserialize_or_decode( $field_data->options );
705 if ( ! $field_data->options ) {
706 // Check if an error happens when unserializing, or empty options.
707 return true;
708 }
709
710 end( $field_data->options );
711 $last_key = key( $field_data->options );
712
713 // If a choice field has no Other option.
714 if ( is_numeric( $last_key ) || 0 !== strpos( $last_key, 'other_' ) ) {
715 return true;
716 }
717
718 // If a choice field has Other option, but Other is not selected.
719 if ( empty( $values['item_meta']['other'][ $field_data->id ] ) ) {
720 return true;
721 }
722
723 // Check if submitted value is same as one of field option.
724 foreach ( $field_data->options as $option ) {
725 $option_value = ! is_array( $option ) ? $option : ( isset( $option['value'] ) ? $option['value'] : '' );
726 if ( $values['item_meta']['other'][ $field_data->id ] === $option_value ) {
727 return true;
728 }
729 }
730
731 return false;
732 }
733
734 /**
735 * Gets field IDs that are skipped from sending to Akismet spam check.
736 *
737 * @since 5.0.09
738 * @since 5.0.13 Move out get_all_form_ids_and_flatten_meta() call and get `form_ids` from `$values`.
739 * @since 5.2.04 This method returns array of object contains `id` and `options` instead of array of `id` only.
740 *
741 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
742 * @return array
743 */
744 private static function get_akismet_skipped_field_ids( $values ) {
745 if ( empty( $values['form_ids'] ) ) {
746 return array();
747 }
748
749 $skipped_types = array( 'divider', 'form', 'hidden', 'user_id', 'file', 'date', 'time', 'scale', 'star', 'range', 'toggle', 'data', 'lookup', 'likert', 'nps' );
750 $has_other_types = array( 'radio', 'checkbox', 'select' );
751
752 $where = array(
753 array(
754 'form_id' => $values['form_ids'],
755 'type' => array_merge( $skipped_types, $has_other_types ),
756 ),
757 );
758
759 return FrmDb::get_results( 'frm_fields', $where, 'id,options' );
760 }
761
762 /**
763 * Prepares values array for spam check.
764 *
765 * @since 5.0.13
766 *
767 * @param array $values Entry values.
768 */
769 private static function prepare_values_for_spam_check( &$values ) {
770 $form_ids = self::get_all_form_ids_and_flatten_meta( $values );
771 $values['form_ids'] = $form_ids;
772 }
773
774 /**
775 * Gets all form IDs (include child form IDs) and flatten item_meta array. Used for skipping values sent to Akismet.
776 * This also removes some unused data from the item_meta.
777 *
778 * @since 5.0.09
779 * @since 5.0.13 Convert name field value to string.
780 *
781 * @param array $values Entry values.
782 * @return array Form IDs.
783 */
784 private static function get_all_form_ids_and_flatten_meta( &$values ) {
785 $values['name_field_ids'] = array();
786
787 // Blacklist check for File field in the old version doesn't contain `form_id`.
788 $form_ids = isset( $values['form_id'] ) ? array( absint( $values['form_id'] ) ) : array();
789 foreach ( $values['item_meta'] as $field_id => $value ) {
790 if ( ! is_numeric( $field_id ) ) {
791 // Maybe `other`.
792 continue;
793 }
794
795 // Convert name array to string.
796 if ( isset( $value['first'] ) && isset( $value['last'] ) ) {
797 $values['item_meta'][ $field_id ] = trim( implode( ' ', $value ) );
798 $values['name_field_ids'][] = $field_id;
799 continue;
800 }
801
802 if ( ! is_array( $value ) || empty( $value['form'] ) ) {
803 continue;
804 }
805
806 $form_ids[] = absint( $value['form'] );
807
808 foreach ( $value as $subindex => $subvalue ) {
809 if ( ! is_numeric( $subindex ) || ! is_array( $subvalue ) ) {
810 continue;
811 }
812
813 foreach ( $subvalue as $subsubindex => $subsubvalue ) {
814 if ( ! $subsubvalue ) {
815 continue;
816 }
817
818 if ( ! isset( $values['item_meta'][ $subsubindex ] ) ) {
819 $values['item_meta'][ $subsubindex ] = array();
820 }
821
822 // Convert name array to string.
823 if ( isset( $subsubvalue['first'] ) && isset( $subsubvalue['last'] ) ) {
824 $subsubvalue = trim( implode( ' ', $subsubvalue ) );
825
826 $values['name_field_ids'][] = $subsubindex;
827 }
828
829 $values['item_meta'][ $subsubindex ][] = $subsubvalue;
830 }
831 }//end foreach
832
833 unset( $values['item_meta'][ $field_id ] );
834 }//end foreach
835
836 return $form_ids;
837 }
838 }
839