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

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