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

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