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

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