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

623 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmEntryValidate {
7 public static function validate( $values, $exclude = false ) {
8 FrmEntry::sanitize_entry_post( $values );
9 $errors = array();
10
11 if ( ! isset( $values['form_id'] ) || ! isset( $values['item_meta'] ) ) {
12 $errors['form'] = __( 'There was a problem with your submission. Please try again.', 'formidable' );
13
14 return $errors;
15 }
16
17 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' ) ) ) {
18 $errors['form'] = __( 'You do not have permission to do that', 'formidable' );
19 }
20
21 self::set_item_key( $values );
22
23 $posted_fields = self::get_fields_to_validate( $values, $exclude );
24
25 // Pass exclude value to validate_field function so it can be used for repeating sections
26 $args = array( 'exclude' => $exclude );
27
28 foreach ( $posted_fields as $posted_field ) {
29 self::validate_field( $posted_field, $errors, $values, $args );
30 unset( $posted_field );
31 }
32
33 if ( empty( $errors ) ) {
34 self::spam_check( $exclude, $values, $errors );
35 }
36
37 /**
38 * Allows modifying the validation errors after validating all fields.
39 *
40 * @since 5.0.04 Added `posted_fields` to the third param.
41 *
42 * @param array $errors Errors data.
43 * @param array $values Value data of the form.
44 * @param array $args Custom arguments. Contains `exclude` and `posted_fields`.
45 */
46 $errors = apply_filters( 'frm_validate_entry', $errors, $values, compact( 'exclude', 'posted_fields' ) );
47
48 return $errors;
49 }
50
51 private static function set_item_key( &$values ) {
52 if ( ! isset( $values['item_key'] ) || $values['item_key'] == '' ) {
53 global $wpdb;
54 $values['item_key'] = FrmAppHelper::get_unique_key( '', $wpdb->prefix . 'frm_items', 'item_key' );
55 $_POST['item_key'] = $values['item_key'];
56 }
57 }
58
59 private static function get_fields_to_validate( $values, $exclude ) {
60 $where = apply_filters( 'frm_posted_field_ids', array( 'fi.form_id' => $values['form_id'] ) );
61
62 // Don't get subfields
63 $where['fr.parent_form_id'] = array( null, 0 );
64
65 // Don't get excluded fields (like file upload fields in the ajax validation)
66 if ( ! empty( $exclude ) ) {
67 $where['fi.type not'] = $exclude;
68 }
69
70 $fields = FrmField::getAll( $where, 'field_order' );
71
72 /**
73 * Allows modifying fields to validate.
74 *
75 * @since 5.0.06
76 *
77 * @param array $fields List of fields.
78 * @param array $args Includes `values`, `exclude`, `where`.
79 */
80 return apply_filters( 'frm_fields_to_validate', $fields, compact( 'values', 'exclude', 'where' ) );
81 }
82
83 public static function validate_field( $posted_field, &$errors, $values, $args = array() ) {
84 $defaults = array(
85 'id' => $posted_field->id,
86 'parent_field_id' => '', // the id of the repeat or embed form
87 'key_pointer' => '', // the pointer in the posted array
88 'exclude' => array(), // exclude these field types from validation
89 );
90 $args = wp_parse_args( $args, $defaults );
91
92 if ( empty( $args['parent_field_id'] ) ) {
93 $value = isset( $values['item_meta'][ $args['id'] ] ) ? $values['item_meta'][ $args['id'] ] : '';
94 } else {
95 // value is from a nested form
96 $value = $values;
97 }
98
99 // Check for values in "Other" fields
100 FrmEntriesHelper::maybe_set_other_validation( $posted_field, $value, $args );
101
102 self::maybe_clear_value_for_default_blank_setting( $posted_field, $value );
103
104 $should_trim = is_array( $value ) && count( $value ) == 1 && isset( $value[0] ) && $posted_field->type !== 'checkbox';
105 if ( $should_trim ) {
106 $value = reset( $value );
107 }
108
109 if ( ! is_array( $value ) ) {
110 $value = trim( $value );
111 }
112
113 if ( $posted_field->required == '1' && FrmAppHelper::is_empty_value( $value ) ) {
114 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $posted_field, 'blank' );
115 } elseif ( $posted_field->type == 'text' && ! isset( $_POST['item_name'] ) ) { // WPCS: CSRF ok.
116 $_POST['item_name'] = $value;
117 }
118
119 FrmEntriesHelper::set_posted_value( $posted_field, $value, $args );
120
121 self::validate_field_types( $errors, $posted_field, $value, $args );
122
123 // Field might want to modify value before other parts of the system
124 // e.g. trim off excess values like in the case of fields with limit.
125 $value = apply_filters( 'frm_modify_posted_field_value', $value, $errors, $posted_field, $args );
126
127 if ( $value != '' ) {
128 self::validate_phone_field( $errors, $posted_field, $value, $args );
129 }
130
131 $errors = apply_filters( 'frm_validate_' . $posted_field->type . '_field_entry', $errors, $posted_field, $value, $args );
132 $errors = apply_filters( 'frm_validate_field_entry', $errors, $posted_field, $value, $args );
133 }
134
135 /**
136 * Set $value to an empty string if it matches its label
137 *
138 * @param object $field
139 * @param string $value
140 */
141 private static function maybe_clear_value_for_default_blank_setting( $field, &$value ) {
142 $position = FrmField::get_option( $field, 'label' );
143 if ( ! $position ) {
144 $position = FrmStylesController::get_style_val( 'position', $field->form_id );
145 }
146
147 if ( $position === 'inside' && FrmFieldsHelper::is_placeholder_field_type( $field->type ) && $value === $field->name ) {
148 $value = '';
149 }
150 }
151
152 public static function validate_field_types( &$errors, $posted_field, $value, $args ) {
153 $field_obj = FrmFieldFactory::get_field_object( $posted_field );
154 $args['value'] = $value;
155 $args['errors'] = $errors;
156
157 $new_errors = $field_obj->validate( $args );
158 if ( ! empty( $new_errors ) ) {
159 $errors = array_merge( $errors, $new_errors );
160 }
161 }
162
163 public static function validate_phone_field( &$errors, $field, $value, $args ) {
164 if ( $field->type == 'phone' || ( $field->type == 'text' && FrmField::is_option_true_in_object( $field, 'format' ) ) ) {
165
166 $pattern = self::phone_format( $field );
167
168 if ( ! preg_match( $pattern, $value ) ) {
169 $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $field, 'invalid' );
170 }
171 }
172 }
173
174 public static function phone_format( $field ) {
175 if ( FrmField::is_option_empty( $field, 'format' ) ) {
176 $pattern = self::default_phone_format();
177 } else {
178 $pattern = FrmField::get_option( $field, 'format' );
179 }
180
181 $pattern = apply_filters( 'frm_phone_pattern', $pattern, $field );
182
183 // Create a regexp if format is not already a regexp
184 if ( strpos( $pattern, '^' ) !== 0 ) {
185 $pattern = self::create_regular_expression_from_format( $pattern );
186 }
187
188 $pattern = '/' . $pattern . '/';
189
190 return $pattern;
191 }
192
193 /**
194 * @since 3.01
195 */
196 private static function default_phone_format() {
197 return '^((\+\d{1,3}(-|.| )?\(?\d\)?(-| |.)?\d{1,5})|(\(?\d{2,6}\)?))(-|.| )?(\d{3,4})(-|.| )?(\d{4})(( x| ext)\d{1,5}){0,1}$';
198 }
199
200 /**
201 * Create a regular expression from a phone number format
202 *
203 * @since 2.02.02
204 *
205 * @param string $pattern
206 *
207 * @return string
208 */
209 private static function create_regular_expression_from_format( $pattern ) {
210 $pattern = preg_quote( $pattern );
211
212 // Firefox doesn't like escaped dashes or colons
213 $pattern = str_replace( array( '\-', '\:' ), array( '-', ':' ), $pattern );
214
215 // Switch generic values out for their regular expression
216 $pattern = preg_replace( '/\d/', '\d', $pattern );
217 $pattern = str_replace( 'A', '[A-Z]', $pattern );
218 $pattern = str_replace( 'a', '[a-zA-Z]', $pattern );
219 $pattern = str_replace( '*', 'w', $pattern );
220 $pattern = str_replace( '/', '\/', $pattern );
221
222 if ( strpos( $pattern, '\?' ) !== false ) {
223 $parts = explode( '\?', $pattern );
224 $pattern = '';
225 foreach ( $parts as $part ) {
226 if ( empty( $pattern ) ) {
227 $pattern .= $part;
228 } else {
229 $pattern .= '(' . $part . ')?';
230 }
231 }
232 }
233 $pattern = '^' . $pattern . '$';
234
235 return $pattern;
236 }
237
238 /**
239 * Check for spam
240 *
241 * @param boolean $exclude
242 * @param array $values
243 * @param array $errors by reference
244 */
245 public static function spam_check( $exclude, $values, &$errors ) {
246 if ( ! empty( $exclude ) || ! isset( $values['item_meta'] ) || empty( $values['item_meta'] ) || ! empty( $errors ) ) {
247 // only check spam if there are no other errors
248 return;
249 }
250
251 $antispam_check = self::is_antispam_check( $values['form_id'] );
252 if ( is_string( $antispam_check ) ) {
253 $errors['spam'] = $antispam_check;
254 } elseif ( self::is_honeypot_spam( $values ) || self::is_spam_bot() ) {
255 $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
256 } elseif ( self::blacklist_check( $values ) ) {
257 $errors['spam'] = __( 'Your entry appears to be blocked spam!', 'formidable' );
258 } elseif ( self::is_akismet_enabled_for_user( $values['form_id'] ) && self::is_akismet_spam( $values ) ) {
259 $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
260 }
261 }
262
263 /**
264 * @param int $form_id
265 * @return boolean
266 */
267 private static function is_antispam_check( $form_id ) {
268 $aspm = new FrmAntiSpam( $form_id );
269 return $aspm->validate();
270 }
271
272 /**
273 * @param array $values
274 * @return boolean
275 */
276 private static function is_honeypot_spam( $values ) {
277 $honeypot = new FrmHoneypot( $values['form_id'] );
278 return ! $honeypot->validate();
279 }
280
281 /**
282 * @return boolean
283 */
284 private static function is_spam_bot() {
285 $ip = FrmAppHelper::get_ip_address();
286
287 return empty( $ip );
288 }
289
290 /**
291 * @param array $values
292 * @return boolean
293 */
294 private static function is_akismet_spam( $values ) {
295 global $wpcom_api_key;
296
297 return ( is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values ) );
298 }
299
300 /**
301 * @param int $form_id
302 * @return bool
303 */
304 private static function is_akismet_enabled_for_user( $form_id ) {
305 $form = FrmForm::getOne( $form_id );
306
307 return ( ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() ) );
308 }
309
310 public static function blacklist_check( $values ) {
311 if ( ! apply_filters( 'frm_check_blacklist', true, $values ) ) {
312 return false;
313 }
314
315 $mod_keys = trim( self::get_disallowed_words() );
316 if ( empty( $mod_keys ) ) {
317 return false;
318 }
319
320 $content = FrmEntriesHelper::entry_array_to_string( $values );
321 if ( empty( $content ) ) {
322 return false;
323 }
324
325 $ip = FrmAppHelper::get_ip_address();
326 $user_agent = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
327 $user_info = self::get_spam_check_user_info( $values );
328
329 return self::check_disallowed_words( $user_info['comment_author'], $user_info['comment_author_email'], $user_info['comment_author_url'], $content, $ip, $user_agent );
330 }
331
332 /**
333 * For WP 5.5 compatibility.
334 *
335 * @since 4.06.02
336 */
337 private static function check_disallowed_words( $author, $email, $url, $content, $ip, $user_agent ) {
338 if ( function_exists( 'wp_check_comment_disallowed_list' ) ) {
339 return wp_check_comment_disallowed_list( $author, $email, $url, $content, $ip, $user_agent );
340 } else {
341 return wp_blacklist_check( $author, $email, $url, $content, $ip, $user_agent );
342 }
343 }
344
345 /**
346 * For WP 5.5 compatibility.
347 *
348 * @since 4.06.02
349 */
350 private static function get_disallowed_words() {
351 $keys = get_option( 'disallowed_keys' );
352 if ( false === $keys ) {
353 // Fallback for WP < 5.5.
354 $keys = get_option( 'blacklist_keys' );
355 }
356 return $keys;
357 }
358
359 /**
360 * Check entries for Akismet spam
361 *
362 * @return boolean true if is spam
363 */
364 public static function akismet( $values ) {
365 if ( empty( $values['item_meta'] ) ) {
366 return false;
367 }
368
369 $datas = array(
370 'comment_type' => 'formidable',
371 );
372 self::parse_akismet_array( $datas, $values );
373
374 /**
375 * Allows modifying the values sent to Akismet.
376 *
377 * @since 5.0.07
378 *
379 * @param array $datas The array of values being sent to Akismet.
380 */
381 $datas = apply_filters( 'frm_akismet_values', $datas );
382
383 $query_string = _http_build_query( $datas, '', '&' );
384 $response = Akismet::http_post( $query_string, 'comment-check' );
385
386 return ( is_array( $response ) && $response[1] == 'true' );
387 }
388
389 /**
390 * @since 2.0
391 */
392 private static function parse_akismet_array( &$datas, $values ) {
393 self::add_site_info_to_akismet( $datas );
394 self::add_user_info_to_akismet( $datas, $values );
395 self::add_server_values_to_akismet( $datas );
396 self::add_comment_content_to_akismet( $datas, $values );
397 }
398
399 private static function add_site_info_to_akismet( &$datas ) {
400 $datas['blog'] = FrmAppHelper::site_url();
401 $datas['user_ip'] = preg_replace( '/[^0-9., ]/', '', FrmAppHelper::get_ip_address() );
402 $datas['user_agent'] = FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' );
403 $datas['referrer'] = isset( $_SERVER['HTTP_REFERER'] ) ? FrmAppHelper::get_server_value( 'HTTP_REFERER' ) : false;
404 $datas['blog_lang'] = get_locale();
405 $datas['blog_charset'] = get_option( 'blog_charset' );
406
407 if ( akismet_test_mode() ) {
408 $datas['is_test'] = 'true';
409 }
410 }
411
412 private static function add_user_info_to_akismet( &$datas, $values ) {
413 $user_info = self::get_spam_check_user_info( $values );
414 $datas = $datas + $user_info;
415
416 if ( isset( $user_info['user_ID'] ) ) {
417 $datas['user_role'] = Akismet::get_user_roles( $user_info['user_ID'] );
418 }
419 }
420
421 private static function get_spam_check_user_info( $values ) {
422 $datas = array();
423
424 if ( is_user_logged_in() ) {
425 $user = wp_get_current_user();
426
427 $datas['user_ID'] = $user->ID;
428 $datas['user_id'] = $user->ID;
429 $datas['comment_author'] = $user->display_name;
430 $datas['comment_author_email'] = $user->user_email;
431 $datas['comment_author_url'] = $user->user_url;
432 } else {
433 $datas['comment_author'] = '';
434 $datas['comment_author_email'] = '';
435 $datas['comment_author_url'] = '';
436
437 if ( isset( $values['item_meta'] ) ) {
438 $values = $values['item_meta'];
439 }
440
441 $values = array_filter( $values );
442
443 $datas['frm_duplicated'] = array();
444 foreach ( $values as $index => $value ) {
445 if ( ! is_array( $value ) ) {
446 if ( $datas['comment_author_email'] == '' && strpos( $value, '@' ) && is_email( $value ) ) {
447 $datas['comment_author_email'] = $value;
448 $datas['frm_duplicated'][] = $index;
449 } elseif ( $datas['comment_author_url'] == '' && strpos( $value, 'http' ) === 0 ) {
450 $datas['comment_author_url'] = $value;
451 $datas['frm_duplicated'][] = $index;
452 } elseif ( $datas['comment_author'] == '' && ! is_numeric( $value ) && strlen( $value ) < 200 ) {
453 $datas['comment_author'] = $value;
454 $datas['frm_duplicated'][] = $index;
455 }
456 }
457 }
458 }
459
460 return $datas;
461 }
462
463 private static function add_server_values_to_akismet( &$datas ) {
464 foreach ( $_SERVER as $key => $value ) {
465 $include_value = is_string( $value ) && ! preg_match( '/^HTTP_COOKIE/', $key ) && preg_match( '/^(HTTP_|REMOTE_ADDR|REQUEST_URI|DOCUMENT_URI)/', $key );
466
467 // Send any potentially useful $_SERVER vars, but avoid sending junk we don't need.
468 if ( $include_value ) {
469 $datas[ $key ] = $value;
470 }
471 unset( $key, $value );
472 }
473 }
474
475 /**
476 * Adds comment content to Akismet data.
477 *
478 * @since 5.0.09
479 *
480 * @param array $datas The array of values being sent to Akismet.
481 * @param array $values Entry values.
482 */
483 private static function add_comment_content_to_akismet( &$datas, $values ) {
484 if ( isset( $datas['frm_duplicated'] ) ) {
485 foreach ( $datas['frm_duplicated'] as $index ) {
486 if ( isset( $values['item_meta'][ $index ] ) ) {
487 unset( $values['item_meta'][ $index ] );
488 } else {
489 unset( $values[ $index ] );
490 }
491 }
492 unset( $datas['frm_duplicated'] );
493 }
494
495 self::skip_adding_values_to_akismet( $values );
496
497 $datas['comment_content'] = FrmEntriesHelper::entry_array_to_string( $values );
498 }
499
500 /**
501 * Skips adding field values to Akismet.
502 *
503 * @since 5.0.09
504 *
505 * @param array $values Entry values.
506 */
507 private static function skip_adding_values_to_akismet( &$values ) {
508 $skipped_field_ids = self::get_akismet_skipped_field_ids( $values );
509 foreach ( $skipped_field_ids as $field_id ) {
510 if ( isset( $values['item_meta'][ $field_id ] ) ) {
511 unset( $values['item_meta'][ $field_id ] );
512 }
513 }
514 }
515
516 /**
517 * Gets field IDs that are skipped from sending to Akismet spam check.
518 *
519 * @since 5.0.09
520 *
521 * @param array $values Entry values.
522 * @return array
523 */
524 private static function get_akismet_skipped_field_ids( $values ) {
525 $form_ids = self::get_all_form_ids_and_flatten_meta( $values );
526 $skipped_types = array( 'divider', 'form', 'hidden', 'user_id', 'file', 'date', 'time', 'scale', 'star', 'range', 'toggle', 'data', 'lookup', 'likert', 'nps' );
527 $has_other_types = array( 'radio', 'checkbox', 'select' );
528
529 $where = array(
530 array(
531 'form_id' => $form_ids,
532 array(
533 array(
534 'field_options not like' => ';s:5:"other";s:1:"1"',
535 'type' => $has_other_types,
536 ),
537 'or' => 1,
538 'type' => $skipped_types,
539 ),
540 ),
541 );
542
543 return FrmDb::get_col( 'frm_fields', $where );
544 }
545
546 /**
547 * Gets all form IDs (include child form IDs) and flatten item_meta array. Used for skipping values sent to Akismet.
548 * This also removes some unused data from the item_meta.
549 *
550 * @since 5.0.09
551 *
552 * @param array $values Entry values.
553 * @return array Form IDs.
554 */
555 private static function get_all_form_ids_and_flatten_meta( &$values ) {
556 $form_ids = array( absint( $values['form_id'] ) );
557 foreach ( $values['item_meta'] as $field_id => $value ) {
558 if ( ! is_numeric( $field_id ) ) { // Maybe `other`.
559 continue;
560 }
561
562 if ( ! is_array( $value ) || empty( $value['form'] ) ) {
563 continue;
564 }
565
566 $form_ids[] = absint( $value['form'] );
567
568 foreach ( $value as $subindex => $subvalue ) {
569 if ( ! is_numeric( $subindex ) || ! is_array( $subvalue ) ) {
570 continue;
571 }
572
573 foreach ( $subvalue as $subsubindex => $subsubvalue ) {
574 if ( ! $subsubvalue ) {
575 continue;
576 }
577
578 if ( ! isset( $values['item_meta'][ $subsubindex ] ) ) {
579 $values['item_meta'][ $subsubindex ] = array();
580 }
581 $values['item_meta'][ $subsubindex ][] = $subsubvalue;
582 }
583 }
584
585 unset( $values['item_meta'][ $field_id ] );
586 }
587
588 return $form_ids;
589 }
590
591 /**
592 * @deprecated 3.0
593 * @codeCoverageIgnore
594 */
595 public static function validate_url_field( &$errors, $field, $value, $args ) {
596 FrmDeprecated::validate_url_field( $errors, $field, $value, $args );
597 }
598
599 /**
600 * @deprecated 3.0
601 * @codeCoverageIgnore
602 */
603 public static function validate_email_field( &$errors, $field, $value, $args ) {
604 FrmDeprecated::validate_email_field( $errors, $field, $value, $args );
605 }
606
607 /**
608 * @deprecated 3.0
609 * @codeCoverageIgnore
610 */
611 public static function validate_number_field( &$errors, $field, $value, $args ) {
612 FrmDeprecated::validate_number_field( $errors, $field, $value, $args );
613 }
614
615 /**
616 * @deprecated 3.0
617 * @codeCoverageIgnore
618 */
619 public static function validate_recaptcha( &$errors, $field, $args ) {
620 FrmDeprecated::validate_recaptcha( $errors, $field, $args );
621 }
622 }
623