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

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