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

838 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 self::skip_adding_values_to_akismet( $values );
481
482 self::add_user_info_to_akismet( $datas, $values );
483 self::add_comment_content_to_akismet( $datas, $values );
484 }
485
486 private static function add_site_info_to_akismet( &$datas ) {
487 $datas['blog'] = FrmAppHelper::site_url();
488 $datas['user_ip'] = preg_replace( '/[^0-9., ]/', '', FrmAppHelper::get_ip_address() );
489 $datas['user_agent'] = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
490 $datas['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) ? FrmAppHelper::get_server_value( 'HTTP_REFERER' ) : false;
491 $datas['blog_lang'] = get_locale();
492 $datas['blog_charset'] = get_option( 'blog_charset' );
493
494 if ( akismet_test_mode() ) {
495 $datas['is_test'] = 'true';
496 }
497 }
498
499 private static function add_user_info_to_akismet( &$datas, $values ) {
500 $user_info = self::get_spam_check_user_info( $values );
501 $datas = $datas + $user_info;
502
503 if ( isset( $user_info['user_ID'] ) ) {
504 $datas['user_role'] = Akismet::get_user_roles( $user_info['user_ID'] );
505 }
506 }
507
508 /**
509 * Gets user info for Akismet spam check.
510 *
511 * @since 5.0.13 Separate code for guest. Handle value of embedded|repeater.
512 *
513 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
514 * @return array
515 */
516 private static function get_spam_check_user_info( $values ) {
517 if ( ! is_user_logged_in() ) {
518 return self::get_spam_check_user_info_for_guest( $values );
519 }
520
521 $user = wp_get_current_user();
522
523 return array(
524 'user_ID' => $user->ID,
525 'user_id' => $user->ID,
526 'comment_author' => $user->display_name,
527 'comment_author_email' => $user->user_email,
528 'comment_author_url' => $user->user_url,
529 );
530 }
531
532 /**
533 * Gets user info for Akismet spam check for guest.
534 *
535 * @since 5.0.13
536 *
537 * @param array $values Entry values after flattened.
538 * @return array
539 */
540 private static function get_spam_check_user_info_for_guest( $values ) {
541 $datas = array(
542 'comment_author' => '',
543 'comment_author_email' => '',
544 'comment_author_url' => '',
545 'name_field_ids' => $values['name_field_ids'],
546 'missing_keys' => array( 'comment_author_email', 'comment_author_url', 'comment_author' ),
547 'frm_duplicated' => array(),
548 );
549
550 if ( isset( $values['item_meta'] ) ) {
551 $values = $values['item_meta'];
552 }
553
554 $values = array_filter( $values );
555
556 self::recursive_add_akismet_guest_info( $datas, $values );
557 unset( $datas['name_field_ids'] );
558 unset( $datas['missing_keys'] );
559
560 return $datas;
561 }
562
563 /**
564 * Recursive adds akismet guest info.
565 *
566 * @since 5.0.13
567 *
568 * @param array $datas Guest data.
569 * @param array $values The values.
570 * @param int|null $custom_index Custom index (or field ID).
571 */
572 private static function recursive_add_akismet_guest_info( &$datas, $values, $custom_index = null ) {
573 foreach ( $values as $index => $value ) {
574 if ( ! $datas['missing_keys'] ) {
575 // Found all info.
576 return;
577 }
578
579 if ( is_array( $value ) ) {
580 self::recursive_add_akismet_guest_info( $datas, $value, $index );
581 continue;
582 }
583
584 $field_id = ! is_null( $custom_index ) ? $custom_index : $index;
585 foreach ( $datas['missing_keys'] as $key_index => $key ) {
586 $found = self::is_akismet_guest_info_value( $key, $value, $field_id, $datas['name_field_ids'] );
587 if ( $found ) {
588 $datas[ $key ] = $value;
589 $datas['frm_duplicated'][] = $field_id;
590 unset( $datas['missing_keys'][ $key_index ] );
591 }
592 }
593 }//end foreach
594 }
595
596 /**
597 * Checks if given value is an akismet guest info.
598 *
599 * @since 5.0.13
600 *
601 * @param string $key Guest info key.
602 * @param string $value Value to check.
603 * @param int $field_id Field ID.
604 * @param array $name_field_ids Name field IDs.
605 * @return bool
606 */
607 private static function is_akismet_guest_info_value( $key, $value, $field_id, $name_field_ids ) {
608 if ( ! $value || is_numeric( $value ) ) {
609 return false;
610 }
611
612 switch ( $key ) {
613 case 'comment_author_email':
614 return strpos( $value, '@' ) && is_email( $value );
615
616 case 'comment_author_url':
617 return 0 === strpos( $value, 'http' );
618
619 case 'comment_author':
620 if ( $name_field_ids ) {
621 // If there is name field in the form, we should always use it as author name.
622 return in_array( $field_id, $name_field_ids, true );
623 }
624 return strlen( $value ) < 200;
625 }
626
627 return false;
628 }
629
630 private static function add_server_values_to_akismet( &$datas ) {
631 foreach ( $_SERVER as $key => $value ) {
632 $include_value = is_string( $value ) && ! preg_match( '/^HTTP_COOKIE/', $key ) && preg_match( '/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/', $key );
633
634 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
635 if ( $include_value ) {
636 $datas[ $key ] = $value;
637 }
638 unset( $key, $value );
639 }
640 }
641
642 /**
643 * Adds comment content to Akismet data.
644 *
645 * @since 5.0.09
646 *
647 * @param array $datas The array of values being sent to Akismet.
648 * @param array $values Entry values.
649 */
650 private static function add_comment_content_to_akismet( &$datas, $values ) {
651 if ( isset( $datas['frm_duplicated'] ) ) {
652 foreach ( $datas['frm_duplicated'] as $index ) {
653 if ( isset( $values['item_meta'][ $index ] ) ) {
654 unset( $values['item_meta'][ $index ] );
655 } else {
656 unset( $values[ $index ] );
657 }
658 }
659 unset( $datas['frm_duplicated'] );
660 }
661
662 $datas['comment_content'] = FrmEntriesHelper::entry_array_to_string( $values );
663 }
664
665 /**
666 * Skips adding field values to Akismet.
667 *
668 * @since 5.0.09
669 *
670 * @param array $values Entry values.
671 */
672 private static function skip_adding_values_to_akismet( &$values ) {
673 $skipped_fields = self::get_akismet_skipped_field_ids( $values );
674 foreach ( $skipped_fields as $skipped_field ) {
675 if ( ! isset( $values['item_meta'][ $skipped_field->id ] ) ) {
676 continue;
677 }
678
679 if ( self::should_really_skip_field( $skipped_field, $values ) ) {
680 unset( $values['item_meta'][ $skipped_field->id ] );
681 if ( isset( $values['item_meta']['other'][ $skipped_field->id ] ) ) {
682 unset( $values['item_meta']['other'][ $skipped_field->id ] );
683 }
684 }
685 }
686 }
687
688 /**
689 * Checks if a skip field should be really skipped.
690 *
691 * @since 5.02.04
692 *
693 * @param object $field_data Object contains `id` and `options`.
694 * @param array $values Entry values.
695 * @return bool
696 */
697 private static function should_really_skip_field( $field_data, $values ) {
698 if ( empty( $field_data->options ) ) {
699 // This is skipped field types.
700 return true;
701 }
702
703 FrmAppHelper::unserialize_or_decode( $field_data->options );
704 if ( ! $field_data->options ) {
705 // Check if an error happens when unserializing, or empty options.
706 return true;
707 }
708
709 end( $field_data->options );
710 $last_key = key( $field_data->options );
711
712 // If a choice field has no Other option.
713 if ( is_numeric( $last_key ) || 0 !== strpos( $last_key, 'other_' ) ) {
714 return true;
715 }
716
717 // If a choice field has Other option, but Other is not selected.
718 if ( empty( $values['item_meta']['other'][ $field_data->id ] ) ) {
719 return true;
720 }
721
722 // Check if submitted value is same as one of field option.
723 foreach ( $field_data->options as $option ) {
724 $option_value = ! is_array( $option ) ? $option : ( isset( $option['value'] ) ? $option['value'] : '' );
725 if ( $values['item_meta']['other'][ $field_data->id ] === $option_value ) {
726 return true;
727 }
728 }
729
730 return false;
731 }
732
733 /**
734 * Gets field IDs that are skipped from sending to Akismet spam check.
735 *
736 * @since 5.0.09
737 * @since 5.0.13 Move out get_all_form_ids_and_flatten_meta() call and get `form_ids` from `$values`.
738 * @since 5.2.04 This method returns array of object contains `id` and `options` instead of array of `id` only.
739 *
740 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
741 * @return array
742 */
743 private static function get_akismet_skipped_field_ids( $values ) {
744 if ( empty( $values['form_ids'] ) ) {
745 return array();
746 }
747
748 $skipped_types = array( 'divider', 'form', 'hidden', 'user_id', 'file', 'date', 'time', 'scale', 'star', 'range', 'toggle', 'data', 'lookup', 'likert', 'nps' );
749 $has_other_types = array( 'radio', 'checkbox', 'select' );
750
751 $where = array(
752 array(
753 'form_id' => $values['form_ids'],
754 'type' => array_merge( $skipped_types, $has_other_types ),
755 ),
756 );
757
758 return FrmDb::get_results( 'frm_fields', $where, 'id,options' );
759 }
760
761 /**
762 * Prepares values array for spam check.
763 *
764 * @since 5.0.13
765 *
766 * @param array $values Entry values.
767 */
768 private static function prepare_values_for_spam_check( &$values ) {
769 $form_ids = self::get_all_form_ids_and_flatten_meta( $values );
770 $values['form_ids'] = $form_ids;
771 }
772
773 /**
774 * Gets all form IDs (include child form IDs) and flatten item_meta array. Used for skipping values sent to Akismet.
775 * This also removes some unused data from the item_meta.
776 *
777 * @since 5.0.09
778 * @since 5.0.13 Convert name field value to string.
779 *
780 * @param array $values Entry values.
781 * @return array Form IDs.
782 */
783 private static function get_all_form_ids_and_flatten_meta( &$values ) {
784 $values['name_field_ids'] = array();
785
786 // Blacklist check for File field in the old version doesn't contain `form_id`.
787 $form_ids = isset( $values['form_id'] ) ? array( absint( $values['form_id'] ) ) : array();
788 foreach ( $values['item_meta'] as $field_id => $value ) {
789 if ( ! is_numeric( $field_id ) ) {
790 // Maybe `other`.
791 continue;
792 }
793
794 // Convert name array to string.
795 if ( isset( $value['first'] ) && isset( $value['last'] ) ) {
796 $values['item_meta'][ $field_id ] = trim( implode( ' ', $value ) );
797 $values['name_field_ids'][] = $field_id;
798 continue;
799 }
800
801 if ( ! is_array( $value ) || empty( $value['form'] ) ) {
802 continue;
803 }
804
805 $form_ids[] = absint( $value['form'] );
806
807 foreach ( $value as $subindex => $subvalue ) {
808 if ( ! is_numeric( $subindex ) || ! is_array( $subvalue ) ) {
809 continue;
810 }
811
812 foreach ( $subvalue as $subsubindex => $subsubvalue ) {
813 if ( ! $subsubvalue ) {
814 continue;
815 }
816
817 if ( ! isset( $values['item_meta'][ $subsubindex ] ) ) {
818 $values['item_meta'][ $subsubindex ] = array();
819 }
820
821 // Convert name array to string.
822 if ( isset( $subsubvalue['first'] ) && isset( $subsubvalue['last'] ) ) {
823 $subsubvalue = trim( implode( ' ', $subsubvalue ) );
824
825 $values['name_field_ids'][] = $subsubindex;
826 }
827
828 $values['item_meta'][ $subsubindex ][] = $subsubvalue;
829 }
830 }//end foreach
831
832 unset( $values['item_meta'][ $field_id ] );
833 }//end foreach
834
835 return $form_ids;
836 }
837 }
838