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

836 lines 24.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmEntryValidate {
7
8 /**
9 * @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 $pattern = apply_filters( 'frm_phone_pattern', $pattern, $field );
242
243 // Create a regexp if format is not already a regexp
244 if ( strpos( $pattern, '^' ) !== 0 ) {
245 $pattern = self::create_regular_expression_from_format( $pattern );
246 }
247
248 $pattern = '/' . $pattern . '/';
249
250 return $pattern;
251 }
252
253 /**
254 * @since 3.01
255 */
256 private static function default_phone_format() {
257 return '^((\+\d{1,3}(-|.| )?\(?\d\)?(-| |.)?\d{1,5})|(\(?\d{2,6}\)?))(-|.| )?(\d{3,4})(-|.| )?(\d{4})(( x| ext)\d{1,5}){0,1}$';
258 }
259
260 /**
261 * Create a regular expression from a phone number format
262 *
263 * @since 2.02.02
264 *
265 * @param string $pattern
266 *
267 * @return string
268 */
269 private static function create_regular_expression_from_format( $pattern ) {
270 $pattern = preg_quote( $pattern );
271
272 // Firefox doesn't like escaped dashes or colons
273 $pattern = str_replace( array( '\-', '\:' ), array( '-', ':' ), $pattern );
274
275 // Switch generic values out for their regular expression
276 $pattern = preg_replace( '/\d/', '\d', $pattern );
277 $pattern = str_replace( 'A', '[A-Z]', $pattern );
278 $pattern = str_replace( 'a', '[a-zA-Z]', $pattern );
279 $pattern = str_replace( '*', 'w', $pattern );
280 $pattern = str_replace( '/', '\/', $pattern );
281
282 if ( strpos( $pattern, '\?' ) !== false ) {
283 $parts = explode( '\?', $pattern );
284 $pattern = '';
285 foreach ( $parts as $part ) {
286 if ( empty( $pattern ) ) {
287 $pattern .= $part;
288 } else {
289 $pattern .= '(' . $part . ')?';
290 }
291 }
292 }
293 $pattern = '^' . $pattern . '$';
294
295 return $pattern;
296 }
297
298 /**
299 * Check for spam
300 *
301 * @param bool $exclude
302 * @param array $values
303 * @param array $errors By reference.
304 */
305 public static function spam_check( $exclude, $values, &$errors ) {
306 if ( ! empty( $exclude ) || empty( $values['item_meta'] ) || ! empty( $errors ) ) {
307 // only check spam if there are no other errors
308 return;
309 }
310
311 $antispam_check = self::is_antispam_check( $values['form_id'] );
312 if ( is_string( $antispam_check ) ) {
313 $errors['spam'] = $antispam_check;
314 } elseif ( self::is_honeypot_spam( $values ) || self::is_spam_bot() ) {
315 $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
316 } elseif ( self::blacklist_check( $values ) ) {
317 $errors['spam'] = __( 'Your entry appears to be blocked spam!', 'formidable' );
318 }
319
320 if ( isset( $errors['spam'] ) || self::form_is_in_progress( $values ) ) {
321 return;
322 }
323
324 if ( self::is_akismet_enabled_for_user( $values['form_id'] ) && self::is_akismet_spam( $values ) ) {
325 $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
326 }
327 }
328
329 /**
330 * Checks if form is in progress.
331 *
332 * @since 5.0.13
333 *
334 * @param array $values The values.
335 * @return bool
336 */
337 private static function form_is_in_progress( $values ) {
338 return FrmAppHelper::pro_is_installed() &&
339 ( isset( $values[ 'frm_page_order_' . $values['form_id'] ] ) || FrmAppHelper::get_post_param( 'frm_next_page' ) ) &&
340 FrmField::get_all_types_in_form( $values['form_id'], 'break' );
341 }
342
343 /**
344 * @param int $form_id
345 *
346 * @return bool|string
347 */
348 private static function is_antispam_check( $form_id ) {
349 $aspm = new FrmAntiSpam( $form_id );
350 return $aspm->validate();
351 }
352
353 /**
354 * @param array $values
355 * @return bool
356 */
357 private static function is_honeypot_spam( $values ) {
358 $honeypot = new FrmHoneypot( $values['form_id'] );
359 return ! $honeypot->validate();
360 }
361
362 /**
363 * @return bool
364 */
365 private static function is_spam_bot() {
366 $ip = FrmAppHelper::get_ip_address();
367
368 return empty( $ip );
369 }
370
371 /**
372 * @param array $values
373 * @return bool
374 */
375 private static function is_akismet_spam( $values ) {
376 global $wpcom_api_key;
377
378 return ( is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values ) );
379 }
380
381 /**
382 * @param int $form_id
383 * @return bool
384 */
385 private static function is_akismet_enabled_for_user( $form_id ) {
386 $form = FrmForm::getOne( $form_id );
387
388 return ( ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() ) );
389 }
390
391 public static function blacklist_check( $values ) {
392 if ( ! apply_filters( 'frm_check_blacklist', true, $values ) ) {
393 return false;
394 }
395
396 $mod_keys = trim( self::get_disallowed_words() );
397 if ( empty( $mod_keys ) ) {
398 return false;
399 }
400
401 $content = FrmEntriesHelper::entry_array_to_string( $values );
402
403 self::prepare_values_for_spam_check( $values );
404 $ip = FrmAppHelper::get_ip_address();
405 $user_agent = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
406 $user_info = self::get_spam_check_user_info( $values );
407
408 return self::check_disallowed_words( $user_info['comment_author'], $user_info['comment_author_email'], $user_info['comment_author_url'], $content, $ip, $user_agent );
409 }
410
411 /**
412 * For WP 5.5 compatibility.
413 *
414 * @since 4.06.02
415 */
416 private static function check_disallowed_words( $author, $email, $url, $content, $ip, $user_agent ) {
417 if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
418 return wp_check_comment_disallowed_list( $author, $email, $url, $content, $ip, $user_agent );
419 }
420 // phpcs:ignore WordPress.WP.DeprecatedFunctions.wp_blacklist_checkFound
421 return wp_blacklist_check( $author, $email, $url, $content, $ip, $user_agent );
422 }
423
424 /**
425 * For WP 5.5 compatibility.
426 *
427 * @since 4.06.02
428 */
429 private static function get_disallowed_words() {
430 $keys = get_option( 'disallowed_keys' );
431 if ( false === $keys ) {
432 // Fallback for WP < 5.5.
433 // phpcs:ignore WordPress.WP.DeprecatedParameterValues.Found
434 $keys = get_option( 'blacklist_keys' );
435 }
436 return $keys;
437 }
438
439 /**
440 * Check entries for Akismet spam
441 *
442 * @return bool true if is spam
443 */
444 public static function akismet( $values ) {
445 if ( empty( $values['item_meta'] ) ) {
446 return false;
447 }
448
449 $datas = array(
450 'comment_type' => 'formidable',
451 );
452 self::parse_akismet_array( $datas, $values );
453
454 /**
455 * Allows modifying the values sent to Akismet.
456 *
457 * @since 5.0.07
458 *
459 * @param array $datas The array of values being sent to Akismet.
460 */
461 $datas = apply_filters( 'frm_akismet_values', $datas );
462
463 $query_string = _http_build_query( $datas, '', '&' );
464 $response = Akismet::http_post( $query_string, 'comment-check' );
465
466 return ( is_array( $response ) && $response[1] === 'true' );
467 }
468
469 /**
470 * @since 2.0
471 */
472 private static function parse_akismet_array( &$datas, $values ) {
473 self::add_site_info_to_akismet( $datas );
474 self::add_server_values_to_akismet( $datas );
475
476 self::prepare_values_for_spam_check( $values );
477
478 self::add_user_info_to_akismet( $datas, $values );
479 self::add_comment_content_to_akismet( $datas, $values );
480 }
481
482 private static function add_site_info_to_akismet( &$datas ) {
483 $datas['blog'] = FrmAppHelper::site_url();
484 $datas['user_ip'] = preg_replace( '/[^0-9., ]/', '', FrmAppHelper::get_ip_address() );
485 $datas['user_agent'] = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
486 $datas['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) ? FrmAppHelper::get_server_value( 'HTTP_REFERER' ) : false;
487 $datas['blog_lang'] = get_locale();
488 $datas['blog_charset'] = get_option( 'blog_charset' );
489
490 if ( akismet_test_mode() ) {
491 $datas['is_test'] = 'true';
492 }
493 }
494
495 private static function add_user_info_to_akismet( &$datas, $values ) {
496 $user_info = self::get_spam_check_user_info( $values );
497 $datas = $datas + $user_info;
498
499 if ( isset( $user_info['user_ID'] ) ) {
500 $datas['user_role'] = Akismet::get_user_roles( $user_info['user_ID'] );
501 }
502 }
503
504 /**
505 * Gets user info for Akismet spam check.
506 *
507 * @since 5.0.13 Separate code for guest. Handle value of embedded|repeater.
508 *
509 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
510 * @return array
511 */
512 private static function get_spam_check_user_info( $values ) {
513 if ( ! is_user_logged_in() ) {
514 return self::get_spam_check_user_info_for_guest( $values );
515 }
516
517 $user = wp_get_current_user();
518
519 return array(
520 'user_ID' => $user->ID,
521 'user_id' => $user->ID,
522 'comment_author' => $user->display_name,
523 'comment_author_email' => $user->user_email,
524 'comment_author_url' => $user->user_url,
525 );
526 }
527
528 /**
529 * Gets user info for Akismet spam check for guest.
530 *
531 * @since 5.0.13
532 *
533 * @param array $values Entry values after flattened.
534 * @return array
535 */
536 private static function get_spam_check_user_info_for_guest( $values ) {
537 $datas = array(
538 'comment_author' => '',
539 'comment_author_email' => '',
540 'comment_author_url' => '',
541 'name_field_ids' => $values['name_field_ids'],
542 'missing_keys' => array( 'comment_author_email', 'comment_author_url', 'comment_author' ),
543 'frm_duplicated' => array(),
544 );
545
546 if ( isset( $values['item_meta'] ) ) {
547 $values = $values['item_meta'];
548 }
549
550 $values = array_filter( $values );
551
552 self::recursive_add_akismet_guest_info( $datas, $values );
553 unset( $datas['name_field_ids'] );
554 unset( $datas['missing_keys'] );
555
556 return $datas;
557 }
558
559 /**
560 * Recursive adds akismet guest info.
561 *
562 * @since 5.0.13
563 *
564 * @param array $datas Guest data.
565 * @param array $values The values.
566 * @param int|null $custom_index Custom index (or field ID).
567 */
568 private static function recursive_add_akismet_guest_info( &$datas, $values, $custom_index = null ) {
569 foreach ( $values as $index => $value ) {
570 if ( ! $datas['missing_keys'] ) {
571 // Found all info.
572 return;
573 }
574
575 if ( is_array( $value ) ) {
576 self::recursive_add_akismet_guest_info( $datas, $value, $index );
577 continue;
578 }
579
580 $field_id = ! is_null( $custom_index ) ? $custom_index : $index;
581 foreach ( $datas['missing_keys'] as $key_index => $key ) {
582 $found = self::is_akismet_guest_info_value( $key, $value, $field_id, $datas['name_field_ids'] );
583 if ( $found ) {
584 $datas[ $key ] = $value;
585 $datas['frm_duplicated'][] = $field_id;
586 unset( $datas['missing_keys'][ $key_index ] );
587 }
588 }
589 }//end foreach
590 }
591
592 /**
593 * Checks if given value is an akismet guest info.
594 *
595 * @since 5.0.13
596 *
597 * @param string $key Guest info key.
598 * @param string $value Value to check.
599 * @param int $field_id Field ID.
600 * @param array $name_field_ids Name field IDs.
601 * @return bool
602 */
603 private static function is_akismet_guest_info_value( $key, $value, $field_id, $name_field_ids ) {
604 if ( ! $value || is_numeric( $value ) ) {
605 return false;
606 }
607
608 switch ( $key ) {
609 case 'comment_author_email':
610 return strpos( $value, '@' ) && is_email( $value );
611
612 case 'comment_author_url':
613 return 0 === strpos( $value, 'http' );
614
615 case 'comment_author':
616 if ( $name_field_ids ) {
617 // If there is name field in the form, we should always use it as author name.
618 return in_array( $field_id, $name_field_ids, true );
619 }
620 return strlen( $value ) < 200;
621 }
622
623 return false;
624 }
625
626 private static function add_server_values_to_akismet( &$datas ) {
627 foreach ( $_SERVER as $key => $value ) {
628 $include_value = is_string( $value ) && ! preg_match( '/^HTTP_COOKIE/', $key ) && preg_match( '/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/', $key );
629
630 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
631 if ( $include_value ) {
632 $datas[ $key ] = $value;
633 }
634 unset( $key, $value );
635 }
636 }
637
638 /**
639 * Adds comment content to Akismet data.
640 *
641 * @since 5.0.09
642 *
643 * @param array $datas The array of values being sent to Akismet.
644 * @param array $values Entry values.
645 */
646 private static function add_comment_content_to_akismet( &$datas, $values ) {
647 if ( isset( $datas['frm_duplicated'] ) ) {
648 foreach ( $datas['frm_duplicated'] as $index ) {
649 if ( isset( $values['item_meta'][ $index ] ) ) {
650 unset( $values['item_meta'][ $index ] );
651 } else {
652 unset( $values[ $index ] );
653 }
654 }
655 unset( $datas['frm_duplicated'] );
656 }
657
658 self::skip_adding_values_to_akismet( $values );
659
660 $datas['comment_content'] = FrmEntriesHelper::entry_array_to_string( $values );
661 }
662
663 /**
664 * Skips adding field values to Akismet.
665 *
666 * @since 5.0.09
667 *
668 * @param array $values Entry values.
669 */
670 private static function skip_adding_values_to_akismet( &$values ) {
671 $skipped_fields = self::get_akismet_skipped_field_ids( $values );
672 foreach ( $skipped_fields as $skipped_field ) {
673 if ( ! isset( $values['item_meta'][ $skipped_field->id ] ) ) {
674 continue;
675 }
676
677 if ( self::should_really_skip_field( $skipped_field, $values ) ) {
678 unset( $values['item_meta'][ $skipped_field->id ] );
679 if ( isset( $values['item_meta']['other'][ $skipped_field->id ] ) ) {
680 unset( $values['item_meta']['other'][ $skipped_field->id ] );
681 }
682 }
683 }
684 }
685
686 /**
687 * Checks if a skip field should be really skipped.
688 *
689 * @since 5.02.04
690 *
691 * @param object $field_data Object contains `id` and `options`.
692 * @param array $values Entry values.
693 * @return bool
694 */
695 private static function should_really_skip_field( $field_data, $values ) {
696 if ( empty( $field_data->options ) ) {
697 // This is skipped field types.
698 return true;
699 }
700
701 FrmAppHelper::unserialize_or_decode( $field_data->options );
702 if ( ! $field_data->options ) {
703 // Check if an error happens when unserializing, or empty options.
704 return true;
705 }
706
707 end( $field_data->options );
708 $last_key = key( $field_data->options );
709
710 // If a choice field has no Other option.
711 if ( is_numeric( $last_key ) || 0 !== strpos( $last_key, 'other_' ) ) {
712 return true;
713 }
714
715 // If a choice field has Other option, but Other is not selected.
716 if ( empty( $values['item_meta']['other'][ $field_data->id ] ) ) {
717 return true;
718 }
719
720 // Check if submitted value is same as one of field option.
721 foreach ( $field_data->options as $option ) {
722 $option_value = ! is_array( $option ) ? $option : ( isset( $option['value'] ) ? $option['value'] : '' );
723 if ( $values['item_meta']['other'][ $field_data->id ] === $option_value ) {
724 return true;
725 }
726 }
727
728 return false;
729 }
730
731 /**
732 * Gets field IDs that are skipped from sending to Akismet spam check.
733 *
734 * @since 5.0.09
735 * @since 5.0.13 Move out get_all_form_ids_and_flatten_meta() call and get `form_ids` from `$values`.
736 * @since 5.2.04 This method returns array of object contains `id` and `options` instead of array of `id` only.
737 *
738 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
739 * @return array
740 */
741 private static function get_akismet_skipped_field_ids( $values ) {
742 if ( empty( $values['form_ids'] ) ) {
743 return array();
744 }
745
746 $skipped_types = array( 'divider', 'form', 'hidden', 'user_id', 'file', 'date', 'time', 'scale', 'star', 'range', 'toggle', 'data', 'lookup', 'likert', 'nps' );
747 $has_other_types = array( 'radio', 'checkbox', 'select' );
748
749 $where = array(
750 array(
751 'form_id' => $values['form_ids'],
752 'type' => array_merge( $skipped_types, $has_other_types ),
753 ),
754 );
755
756 return FrmDb::get_results( 'frm_fields', $where, 'id,options' );
757 }
758
759 /**
760 * Prepares values array for spam check.
761 *
762 * @since 5.0.13
763 *
764 * @param array $values Entry values.
765 */
766 private static function prepare_values_for_spam_check( &$values ) {
767 $form_ids = self::get_all_form_ids_and_flatten_meta( $values );
768 $values['form_ids'] = $form_ids;
769 }
770
771 /**
772 * Gets all form IDs (include child form IDs) and flatten item_meta array. Used for skipping values sent to Akismet.
773 * This also removes some unused data from the item_meta.
774 *
775 * @since 5.0.09
776 * @since 5.0.13 Convert name field value to string.
777 *
778 * @param array $values Entry values.
779 * @return array Form IDs.
780 */
781 private static function get_all_form_ids_and_flatten_meta( &$values ) {
782 $values['name_field_ids'] = array();
783
784 // Blacklist check for File field in the old version doesn't contain `form_id`.
785 $form_ids = isset( $values['form_id'] ) ? array( absint( $values['form_id'] ) ) : array();
786 foreach ( $values['item_meta'] as $field_id => $value ) {
787 if ( ! is_numeric( $field_id ) ) {
788 // Maybe `other`.
789 continue;
790 }
791
792 // Convert name array to string.
793 if ( isset( $value['first'] ) && isset( $value['last'] ) ) {
794 $values['item_meta'][ $field_id ] = trim( implode( ' ', $value ) );
795 $values['name_field_ids'][] = $field_id;
796 continue;
797 }
798
799 if ( ! is_array( $value ) || empty( $value['form'] ) ) {
800 continue;
801 }
802
803 $form_ids[] = absint( $value['form'] );
804
805 foreach ( $value as $subindex => $subvalue ) {
806 if ( ! is_numeric( $subindex ) || ! is_array( $subvalue ) ) {
807 continue;
808 }
809
810 foreach ( $subvalue as $subsubindex => $subsubvalue ) {
811 if ( ! $subsubvalue ) {
812 continue;
813 }
814
815 if ( ! isset( $values['item_meta'][ $subsubindex ] ) ) {
816 $values['item_meta'][ $subsubindex ] = array();
817 }
818
819 // Convert name array to string.
820 if ( isset( $subsubvalue['first'] ) && isset( $subsubvalue['last'] ) ) {
821 $subsubvalue = trim( implode( ' ', $subsubvalue ) );
822
823 $values['name_field_ids'][] = $subsubindex;
824 }
825
826 $values['item_meta'][ $subsubindex ][] = $subsubvalue;
827 }
828 }//end foreach
829
830 unset( $values['item_meta'][ $field_id ] );
831 }//end foreach
832
833 return $form_ids;
834 }
835 }
836