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

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