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

980 lines 28.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_options( $errors, $posted_field, $value, $args );
162 self::validate_field_types( $errors, $posted_field, $value, $args );
163
164 // Field might want to modify value before other parts of the system
165 // e.g. trim off excess values like in the case of fields with limit.
166 $value = apply_filters( 'frm_modify_posted_field_value', $value, $errors, $posted_field, $args );
167
168 if ( $value != '' ) {
169 self::validate_phone_field( $errors, $posted_field, $value, $args );
170 }
171
172 $errors = apply_filters( 'frm_validate_' . $posted_field->type . '_field_entry', $errors, $posted_field, $value, $args );
173 $errors = apply_filters( 'frm_validate_field_entry', $errors, $posted_field, $value, $args );
174
175 if ( ! FrmAppHelper::pro_is_installed() && empty( $args['other'] ) ) {
176 FrmEntriesHelper::get_posted_value( $posted_field, $value, $args );
177 }
178 }
179
180 /**
181 * @since 6.21
182 *
183 * @param array $errors
184 * @param object $posted_field
185 * @param array|string $value
186 * @param array $args
187 *
188 * @return void
189 */
190 private static function validate_options( &$errors, $posted_field, $value, $args ) {
191 if ( empty( $posted_field->options ) ) {
192 return;
193 }
194
195 $option_is_valid = self::option_is_valid( $posted_field, $value, $posted_field->options );
196
197 /**
198 * @since 6.21
199 *
200 * @param bool $option_is_valid
201 * @param array|string $value
202 * @param object $field
203 */
204 $option_is_valid = (bool) apply_filters( 'frm_option_is_valid', $option_is_valid, $value, $posted_field );
205
206 if ( ! $option_is_valid ) {
207 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $posted_field, 'invalid' );
208 }
209 }
210
211 /**
212 * Validate that value matches one of the options for the field.
213 *
214 * @since 6.21
215 *
216 * @param stdClass $field
217 * @param array|string $value
218 * @param array $options
219 * @return bool
220 */
221 private static function option_is_valid( $field, $value, $options ) {
222 if ( '' === $value ) {
223 return true;
224 }
225
226 if ( in_array( $field->type, array( 'likert', 'ranking' ), true ) ) {
227 // Ignore these field types automatically.
228 return true;
229 }
230
231 if ( 'product' === $field->type && 'user_def' === FrmField::get_option( $field, 'data_type' ) ) {
232 return true;
233 }
234
235 $value = (array) $value;
236
237 foreach ( $value as $current_value ) {
238 $match = false;
239
240 foreach ( $options as $key => $option ) {
241 if ( strpos( $key, 'other_' ) === 0 ) {
242 // Always return true if an other option is found.
243 return true;
244 }
245
246 $option_value = is_array( $option ) ? $option['value'] : $option;
247 $match = $current_value === $option_value;
248 if ( $match ) {
249 break;
250 }
251
252 if ( is_numeric( $current_value ) ) {
253 $match = (int) $current_value === (int) $option_value;
254 if ( $match ) {
255 break;
256 }
257 }
258 }
259
260 if ( ! $match ) {
261 return self::options_are_dynamic_based_on_hook( $field, $value );
262 }
263 }//end foreach
264
265 return true;
266 }
267
268 /**
269 * Do not validate options if they have been modified with a hook.
270 * This is to help avoid issues where the options could be based on a URL param for example.
271 *
272 * @since 6.21
273 *
274 * @return bool
275 */
276 private static function options_are_dynamic_based_on_hook( $field_object, $value ) {
277 $values = (array) $field_object;
278 $values['value'] = $value;
279 FrmFieldsHelper::prepare_new_front_field( $values, $field_object );
280
281 $map_callback = function ( $option ) {
282 return is_array( $option ) ? $option['value'] : $option;
283 };
284
285 $values_options = array_map( $map_callback, $values['options'] );
286 $field_object_options = array_map( $map_callback, $field_object->options );
287
288 return $values_options !== $field_object_options;
289 }
290
291 /**
292 * Maybe add item_name to $_POST to save it in items table.
293 *
294 * @since 5.2.02
295 *
296 * @param array|string $value Field value.
297 * @param object $field Field object.
298 */
299 private static function maybe_add_item_name( $value, $field ) {
300 $item_name = false;
301 if ( 'name' === $field->type ) {
302 $field_obj = FrmFieldFactory::get_field_object( $field );
303 $item_name = $field_obj->get_display_value( $value );
304 } elseif ( 'text' === $field->type ) {
305 $item_name = $value;
306 }
307
308 if ( false !== $item_name ) {
309 // Item name has a max length of 255 characters so truncate it so it doesn't fail to save in the database.
310 $_POST['item_name'] = substr( $item_name, 0, 255 );
311 }
312 }
313
314 /**
315 * Set $value to an empty string if it matches its label
316 *
317 * @param object $field
318 * @param string $value
319 */
320 private static function maybe_clear_value_for_default_blank_setting( $field, &$value ) {
321 $position = FrmField::get_option( $field, 'label' );
322 if ( ! $position ) {
323 $position = FrmStylesController::get_style_val( 'position', $field->form_id );
324 }
325
326 if ( $position === 'inside' && FrmFieldsHelper::is_placeholder_field_type( $field->type ) && $value === $field->name ) {
327 $value = '';
328 }
329 }
330
331 public static function validate_field_types( &$errors, $posted_field, $value, $args ) {
332 $field_obj = FrmFieldFactory::get_field_object( $posted_field );
333 $args['value'] = $value;
334 $args['errors'] = $errors;
335
336 $new_errors = $field_obj->validate( $args );
337 if ( ! empty( $new_errors ) ) {
338 $errors = array_merge( $errors, $new_errors );
339 }
340 }
341
342 public static function validate_phone_field( &$errors, $field, $value, $args ) {
343 $format_value = FrmField::get_option( $field, 'format' );
344
345 if ( $field->type === 'phone' || ( $field->type === 'text' && $format_value && ! FrmCurrencyHelper::is_currency_format( $format_value ) ) ) {
346 $pattern = self::phone_format( $field );
347
348 if ( ! preg_match( $pattern, $value ) ) {
349 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
350 }
351 }
352 }
353
354 public static function phone_format( $field ) {
355 if ( FrmField::is_option_empty( $field, 'format' ) ) {
356 $pattern = self::default_phone_format();
357 } else {
358 $pattern = FrmField::get_option( $field, 'format' );
359 }
360
361 // Ampersands are saved as &amp;.
362 // Reverse it here so we are checking for the correct character.
363 $pattern = html_entity_decode( $pattern );
364 $pattern = apply_filters( 'frm_phone_pattern', $pattern, $field );
365
366 // Create a regexp if format is not already a regexp
367 if ( strpos( $pattern, '^' ) !== 0 ) {
368 $pattern = self::create_regular_expression_from_format( $pattern );
369 }
370
371 $pattern = '/' . $pattern . '/';
372
373 return $pattern;
374 }
375
376 /**
377 * @since 3.01
378 */
379 private static function default_phone_format() {
380 return '^((\+\d{1,3}(-|.| )?\(?\d\)?(-| |.)?\d{1,5})|(\(?\d{2,6}\)?))(-|.| )?(\d{3,4})(-|.| )?(\d{4})(( x| ext)\d{1,5}){0,1}$';
381 }
382
383 /**
384 * Create a regular expression from a phone number format
385 *
386 * @since 2.02.02
387 *
388 * @param string $pattern
389 *
390 * @return string
391 */
392 private static function create_regular_expression_from_format( $pattern ) {
393 $pattern = preg_quote( $pattern );
394
395 // Firefox doesn't like escaped dashes or colons
396 $pattern = str_replace( array( '\-', '\:' ), array( '-', ':' ), $pattern );
397
398 // Switch generic values out for their regular expression
399 $pattern = preg_replace( '/\d/', '\d', $pattern );
400 $pattern = str_replace( 'A', '[A-Z]', $pattern );
401 $pattern = str_replace( 'a', '[a-zA-Z]', $pattern );
402 $pattern = str_replace( '*', 'w', $pattern );
403 $pattern = str_replace( '/', '\/', $pattern );
404
405 if ( strpos( $pattern, '\?' ) !== false ) {
406 $parts = explode( '\?', $pattern );
407 $pattern = '';
408 foreach ( $parts as $part ) {
409 if ( empty( $pattern ) ) {
410 $pattern .= $part;
411 } else {
412 $pattern .= '(' . $part . ')?';
413 }
414 }
415 }
416 $pattern = '^' . $pattern . '$';
417
418 return $pattern;
419 }
420
421 /**
422 * Check for spam.
423 *
424 * @param bool $exclude
425 * @param array $values
426 * @param array $errors By reference.
427 */
428 public static function spam_check( $exclude, $values, &$errors ) {
429 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
430 // Do not check spam on importing.
431 return;
432 }
433
434 if ( ! empty( $exclude ) || empty( $values['item_meta'] ) || ! empty( $errors ) ) {
435 // only check spam if there are no other errors
436 return;
437 }
438
439 $antispam_check = self::is_antispam_check( $values['form_id'] );
440 $spam_msg = FrmAntiSpamController::get_default_spam_message();
441 if ( is_string( $antispam_check ) ) {
442 $errors['spam'] = $antispam_check;
443 } elseif ( self::is_honeypot_spam( $values ) || self::is_spam_bot() ) {
444 $errors['spam'] = $spam_msg;
445 } else {
446 $is_spam = FrmAntiSpamController::is_spam( $values );
447 if ( $is_spam ) {
448 $errors['spam'] = $is_spam;
449 }
450 }
451
452 if ( isset( $errors['spam'] ) || self::form_is_in_progress( $values ) ) {
453 return;
454 }
455
456 if ( self::is_akismet_enabled_for_user( $values['form_id'] ) && self::is_akismet_spam( $values ) ) {
457 $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
458 }
459 }
460
461 /**
462 * Checks if form is in progress.
463 *
464 * @since 5.0.13
465 *
466 * @param array $values The values.
467 * @return bool
468 */
469 private static function form_is_in_progress( $values ) {
470 return FrmAppHelper::pro_is_installed() &&
471 ( isset( $values[ 'frm_page_order_' . $values['form_id'] ] ) || FrmAppHelper::get_post_param( 'frm_next_page' ) ) &&
472 FrmField::get_all_types_in_form( $values['form_id'], 'break' );
473 }
474
475 /**
476 * @param int $form_id
477 *
478 * @return bool|string
479 */
480 private static function is_antispam_check( $form_id ) {
481 $aspm = new FrmAntiSpam( $form_id );
482 return $aspm->validate();
483 }
484
485 /**
486 * @param array $values
487 * @return bool
488 */
489 private static function is_honeypot_spam( $values ) {
490 $honeypot = new FrmHoneypot( $values['form_id'] );
491 return ! $honeypot->validate();
492 }
493
494 /**
495 * @return bool
496 */
497 private static function is_spam_bot() {
498 $ip = FrmAppHelper::get_ip_address();
499
500 return empty( $ip );
501 }
502
503 /**
504 * @param array $values
505 * @return bool
506 */
507 private static function is_akismet_spam( $values ) {
508 global $wpcom_api_key;
509
510 return ( is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values ) );
511 }
512
513 /**
514 * @param int $form_id
515 * @return bool
516 */
517 private static function is_akismet_enabled_for_user( $form_id ) {
518 $form = FrmForm::getOne( $form_id );
519
520 return ( ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() ) );
521 }
522
523 /**
524 * Checks spam using WordPress disallowed words and Frm denylist.
525 *
526 * @param array $values Entry values.
527 *
528 * @return bool
529 */
530 public static function blacklist_check( $values ) {
531 return FrmAntiSpamController::contains_wp_disallowed_words( $values ) || FrmAntiSpamController::is_denylist_spam( $values );
532 }
533
534 /**
535 * Check entries for Akismet spam
536 *
537 * @return bool true if is spam
538 */
539 public static function akismet( $values ) {
540 if ( empty( $values['item_meta'] ) ) {
541 return false;
542 }
543
544 $datas = array(
545 'comment_type' => 'formidable',
546 );
547 self::parse_akismet_array( $datas, $values );
548
549 /**
550 * Allows modifying the values sent to Akismet.
551 *
552 * @since 5.0.07
553 *
554 * @param array $datas The array of values being sent to Akismet.
555 */
556 $datas = apply_filters( 'frm_akismet_values', $datas );
557
558 $query_string = _http_build_query( $datas, '', '&' );
559 $response = Akismet::http_post( $query_string, 'comment-check' );
560
561 return ( is_array( $response ) && $response[1] === 'true' );
562 }
563
564 /**
565 * @since 2.0
566 */
567 private static function parse_akismet_array( &$datas, $values ) {
568 self::add_site_info_to_akismet( $datas );
569 self::add_server_values_to_akismet( $datas );
570
571 self::prepare_values_for_spam_check( $values );
572 self::skip_adding_values_to_akismet( $values );
573
574 self::add_user_info_to_akismet( $datas, $values );
575 self::add_comment_content_to_akismet( $datas, $values );
576 }
577
578 private static function add_site_info_to_akismet( &$datas ) {
579 $datas['blog'] = FrmAppHelper::site_url();
580 $datas['user_ip'] = preg_replace( '/[^0-9., ]/', '', FrmAppHelper::get_ip_address() );
581 $datas['user_agent'] = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
582 $datas['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) ? FrmAppHelper::get_server_value( 'HTTP_REFERER' ) : false;
583 $datas['blog_lang'] = get_locale();
584 $datas['blog_charset'] = get_option( 'blog_charset' );
585
586 if ( akismet_test_mode() ) {
587 $datas['is_test'] = 'true';
588 }
589 }
590
591 private static function add_user_info_to_akismet( &$datas, $values ) {
592 $user_info = self::get_spam_check_user_info( $values );
593 $datas = $datas + $user_info;
594
595 if ( isset( $user_info['user_ID'] ) ) {
596 $datas['user_role'] = Akismet::get_user_roles( $user_info['user_ID'] );
597 }
598 }
599
600 /**
601 * Gets user info for Akismet spam check.
602 *
603 * @since 5.0.13 Separate code for guest. Handle value of embedded|repeater.
604 * @since 6.21 This changed from private to public.
605 *
606 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
607 * @return array
608 */
609 public static function get_spam_check_user_info( $values ) {
610 if ( ! is_user_logged_in() ) {
611 return self::get_spam_check_user_info_for_guest( $values );
612 }
613
614 $user = wp_get_current_user();
615
616 return array(
617 'user_ID' => $user->ID,
618 'user_id' => $user->ID,
619 'comment_author' => $user->display_name,
620 'comment_author_email' => $user->user_email,
621 'comment_author_url' => $user->user_url,
622 );
623 }
624
625 /**
626 * Gets user info for Akismet spam check for guest.
627 *
628 * @since 5.0.13
629 *
630 * @param array $values Entry values after flattened.
631 * @return array
632 */
633 private static function get_spam_check_user_info_for_guest( $values ) {
634 $datas = array(
635 'comment_author' => '',
636 'comment_author_email' => '',
637 'comment_author_url' => '',
638 'name_field_ids' => $values['name_field_ids'],
639 'missing_keys' => array( 'comment_author_email', 'comment_author_url', 'comment_author' ),
640 'frm_duplicated' => array(),
641 );
642
643 if ( isset( $values['item_meta'] ) ) {
644 $values = $values['item_meta'];
645 }
646
647 $values = array_filter( $values );
648
649 self::recursive_add_akismet_guest_info( $datas, $values );
650 unset( $datas['name_field_ids'] );
651 unset( $datas['missing_keys'] );
652
653 return $datas;
654 }
655
656 /**
657 * Recursive adds akismet guest info.
658 *
659 * @since 5.0.13
660 *
661 * @param array $datas Guest data.
662 * @param array $values The values.
663 * @param int|null $custom_index Custom index (or field ID).
664 */
665 private static function recursive_add_akismet_guest_info( &$datas, $values, $custom_index = null ) {
666 foreach ( $values as $index => $value ) {
667 if ( ! $datas['missing_keys'] ) {
668 // Found all info.
669 return;
670 }
671
672 if ( is_array( $value ) ) {
673 self::recursive_add_akismet_guest_info( $datas, $value, $index );
674 continue;
675 }
676
677 $field_id = ! is_null( $custom_index ) ? $custom_index : $index;
678 foreach ( $datas['missing_keys'] as $key_index => $key ) {
679 $found = self::is_akismet_guest_info_value( $key, $value, $field_id, $datas['name_field_ids'], $values );
680 if ( $found ) {
681 $datas[ $key ] = $value;
682 $datas['frm_duplicated'][] = $field_id;
683 unset( $datas['missing_keys'][ $key_index ] );
684 }
685 }
686 }//end foreach
687 }
688
689 /**
690 * Checks if given value is an akismet guest info.
691 *
692 * @since 5.0.13
693 *
694 * @param string $key Guest info key.
695 * @param string $value Value to check.
696 * @param int $field_id Field ID.
697 * @param array $name_field_ids Name field IDs.
698 * @param array $values Array of posted values.
699 *
700 * @return bool
701 */
702 private static function is_akismet_guest_info_value( $key, &$value, $field_id, $name_field_ids, $values ) {
703 if ( ! $value || is_numeric( $value ) ) {
704 return false;
705 }
706
707 switch ( $key ) {
708 case 'comment_author_email':
709 return strpos( $value, '@' ) && is_email( $value );
710
711 case 'comment_author_url':
712 return 0 === strpos( $value, 'http' );
713
714 case 'comment_author':
715 if ( $name_field_ids && in_array( $field_id, $name_field_ids, true ) ) {
716 // If there is name field in the form, we should always use it as author name.
717 return true;
718 }
719 $form_id = FrmAppHelper::get_post_param( 'form_id', 0, 'absint' );
720 $fields = self::get_name_text_fields( $form_id );
721
722 foreach ( $fields as $index => $field ) {
723 if ( 'Name' !== $field->name ) {
724 continue;
725 }
726 if ( isset( $fields[ $index + 1 ] ) && 'Last' === $fields[ $index + 1 ]->name ) {
727 if ( empty( $values[ absint( $fields[ $index + 1 ]->id ) ] ) ) {
728 continue;
729 }
730 $value .= ' ' . $values[ $fields[ $index + 1 ]->id ];
731 return true;
732 }
733 }
734 }//end switch
735
736 return false;
737 }
738
739 /**
740 * Returns fields that have 'Name' and 'Last' as their name.
741 *
742 * @since 6.17
743 *
744 * @param int $form_id
745 * @return array
746 */
747 private static function get_name_text_fields( $form_id ) {
748 $name_text_fields_is_initialized = is_array( self::$name_text_fields );
749 if ( $name_text_fields_is_initialized && isset( self::$name_text_fields[ $form_id ] ) ) {
750 return self::$name_text_fields[ $form_id ];
751 }
752 if ( ! $name_text_fields_is_initialized ) {
753 self::$name_text_fields = array();
754 }
755 self::$name_text_fields[ $form_id ] = FrmDb::get_results(
756 'frm_fields',
757 array(
758 'form_id' => $form_id,
759 'type' => 'text',
760 'name' => array( 'Name', 'Last' ),
761 ),
762 'id,name',
763 array( 'order_by' => 'field_order ASC' )
764 );
765
766 return self::$name_text_fields[ $form_id ];
767 }
768
769 private static function add_server_values_to_akismet( &$datas ) {
770 foreach ( $_SERVER as $key => $value ) {
771 $include_value = is_string( $value ) && ! preg_match( '/^HTTP_COOKIE/', $key ) && preg_match( '/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/', $key );
772
773 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
774 if ( $include_value ) {
775 $datas[ $key ] = $value;
776 }
777 unset( $key, $value );
778 }
779 }
780
781 /**
782 * Adds comment content to Akismet data.
783 *
784 * @since 5.0.09
785 *
786 * @param array $datas The array of values being sent to Akismet.
787 * @param array $values Entry values.
788 */
789 private static function add_comment_content_to_akismet( &$datas, $values ) {
790 if ( isset( $datas['frm_duplicated'] ) ) {
791 foreach ( $datas['frm_duplicated'] as $index ) {
792 if ( isset( $values['item_meta'][ $index ] ) ) {
793 unset( $values['item_meta'][ $index ] );
794 } else {
795 unset( $values[ $index ] );
796 }
797 }
798 unset( $datas['frm_duplicated'] );
799 }
800
801 $datas['comment_content'] = FrmEntriesHelper::entry_array_to_string( $values );
802 }
803
804 /**
805 * Skips adding field values to Akismet.
806 *
807 * @since 5.0.09
808 *
809 * @param array $values Entry values.
810 */
811 private static function skip_adding_values_to_akismet( &$values ) {
812 $skipped_fields = self::get_akismet_skipped_field_ids( $values );
813 foreach ( $skipped_fields as $skipped_field ) {
814 if ( ! isset( $values['item_meta'][ $skipped_field->id ] ) ) {
815 continue;
816 }
817
818 if ( self::should_really_skip_field( $skipped_field, $values ) ) {
819 unset( $values['item_meta'][ $skipped_field->id ] );
820 if ( isset( $values['item_meta']['other'][ $skipped_field->id ] ) ) {
821 unset( $values['item_meta']['other'][ $skipped_field->id ] );
822 }
823 }
824 }
825 }
826
827 /**
828 * Checks if a skip field should be really skipped.
829 *
830 * @since 5.02.04
831 *
832 * @param object $field_data Object contains `id` and `options`.
833 * @param array $values Entry values.
834 * @return bool
835 */
836 private static function should_really_skip_field( $field_data, $values ) {
837 if ( empty( $field_data->options ) ) {
838 // This is skipped field types.
839 return true;
840 }
841
842 FrmAppHelper::unserialize_or_decode( $field_data->options );
843 if ( ! $field_data->options ) {
844 // Check if an error happens when unserializing, or empty options.
845 return true;
846 }
847
848 end( $field_data->options );
849 $last_key = key( $field_data->options );
850
851 // If a choice field has no Other option.
852 if ( is_numeric( $last_key ) || 0 !== strpos( $last_key, 'other_' ) ) {
853 return true;
854 }
855
856 // If a choice field has Other option, but Other is not selected.
857 if ( empty( $values['item_meta']['other'][ $field_data->id ] ) ) {
858 return true;
859 }
860
861 // Check if submitted value is same as one of field option.
862 foreach ( $field_data->options as $option ) {
863 $option_value = ! is_array( $option ) ? $option : ( isset( $option['value'] ) ? $option['value'] : '' );
864 if ( $values['item_meta']['other'][ $field_data->id ] === $option_value ) {
865 return true;
866 }
867 }
868
869 return false;
870 }
871
872 /**
873 * Gets field IDs that are skipped from sending to Akismet spam check.
874 *
875 * @since 5.0.09
876 * @since 5.0.13 Move out get_all_form_ids_and_flatten_meta() call and get `form_ids` from `$values`.
877 * @since 5.2.04 This method returns array of object contains `id` and `options` instead of array of `id` only.
878 *
879 * @param array $values Entry values after running through {@see FrmEntryValidate::prepare_values_for_spam_check()}.
880 * @return array
881 */
882 private static function get_akismet_skipped_field_ids( $values ) {
883 if ( empty( $values['form_ids'] ) ) {
884 return array();
885 }
886
887 $skipped_types = array( 'divider', 'form', 'hidden', 'user_id', 'file', 'date', 'time', 'scale', 'star', 'range', 'toggle', 'data', 'lookup', 'likert', 'nps' );
888 $has_other_types = array( 'radio', 'checkbox', 'select' );
889
890 $where = array(
891 array(
892 'form_id' => $values['form_ids'],
893 'type' => array_merge( $skipped_types, $has_other_types ),
894 ),
895 );
896
897 return FrmDb::get_results( 'frm_fields', $where, 'id,options' );
898 }
899
900 /**
901 * Prepares values array for spam check.
902 *
903 * @since 5.0.13
904 * @since 6.21 This changed from private to public.
905 *
906 * @param array $values Entry values.
907 */
908 public static function prepare_values_for_spam_check( &$values ) {
909 $form_ids = self::get_all_form_ids_and_flatten_meta( $values );
910 $values['form_ids'] = $form_ids;
911 }
912
913 /**
914 * Gets all form IDs (include child form IDs) and flatten item_meta array. Used for skipping values sent to Akismet.
915 * This also removes some unused data from the item_meta.
916 *
917 * @since 5.0.09
918 * @since 5.0.13 Convert name field value to string.
919 *
920 * @param array $values Entry values.
921 * @return array Form IDs.
922 */
923 private static function get_all_form_ids_and_flatten_meta( &$values ) {
924 $values['name_field_ids'] = array();
925
926 // Blacklist check for File field in the old version doesn't contain `form_id`.
927 $form_ids = isset( $values['form_id'] ) ? array( absint( $values['form_id'] ) ) : array();
928 foreach ( $values['item_meta'] as $field_id => $value ) {
929 if ( ! is_numeric( $field_id ) ) {
930 // Maybe `other`.
931 continue;
932 }
933
934 // Convert name array to string.
935 if ( isset( $value['first'] ) && isset( $value['last'] ) ) {
936 $values['item_meta'][ $field_id ] = trim( implode( ' ', $value ) );
937 $values['name_field_ids'][] = $field_id;
938 continue;
939 }
940
941 if ( ! is_array( $value ) || empty( $value['form'] ) ) {
942 continue;
943 }
944
945 $form_ids[] = absint( $value['form'] );
946
947 foreach ( $value as $subindex => $subvalue ) {
948 if ( ! is_numeric( $subindex ) || ! is_array( $subvalue ) ) {
949 continue;
950 }
951
952 foreach ( $subvalue as $subsubindex => $subsubvalue ) {
953 if ( ! $subsubvalue ) {
954 continue;
955 }
956
957 if ( ! isset( $values['item_meta'][ $subsubindex ] ) ) {
958 $values['item_meta'][ $subsubindex ] = array();
959 }
960
961 // Convert name array to string.
962 if ( isset( $subsubvalue['first'] ) && isset( $subsubvalue['last'] ) ) {
963 $subsubvalue = trim( implode( ' ', $subsubvalue ) );
964
965 $values['name_field_ids'][] = $subsubindex;
966 }
967
968 if ( is_array( $values['item_meta'][ $subsubindex ] ) ) {
969 $values['item_meta'][ $subsubindex ][] = $subsubvalue;
970 }
971 }
972 }//end foreach
973
974 unset( $values['item_meta'][ $field_id ] );
975 }//end foreach
976
977 return $form_ids;
978 }
979 }
980