PluginProbe ʕ •ᴥ•ʔ
WPForms – Easy Form Builder for WordPress – Contact Forms, Payment Forms, Surveys, & More / 1.7.9.1
WPForms – Easy Form Builder for WordPress – Contact Forms, Payment Forms, Surveys, & More v1.7.9.1
1.10.1.1 1.10.1 1.10.0.5 trunk 1.1.4 1.1.4.2 1.1.5 1.1.5.1 1.1.6 1.1.6.1 1.1.7 1.1.7.1 1.1.7.2 1.1.8 1.1.8.1 1.1.8.2 1.1.8.3 1.1.8.4 1.10.0.1 1.10.0.2 1.10.0.3 1.10.0.4 1.2.0 1.2.0.1 1.2.1 1.2.2 1.2.2.1 1.2.2.2 1.2.3 1.2.3.1 1.2.3.2 1.2.4 1.2.4.1 1.2.5 1.2.5.1 1.2.6 1.2.7 1.2.8 1.2.8.1 1.2.9 1.3.0 1.3.1 1.3.1.1 1.3.1.2 1.3.2 1.3.3 1.3.5 1.3.6 1.3.6.1 1.3.6.2 1.3.7.2 1.3.7.3 1.3.7.4 1.3.8 1.3.9.1 1.4.0.1 1.4.1.1 1.4.2 1.4.2.1 1.4.2.2 1.4.3 1.4.4 1.4.4.1 1.4.5 1.4.5.1 1.4.5.2 1.4.5.3 1.4.6 1.4.7.1 1.4.7.2 1.4.8.1 1.4.9 1.5.0.1 1.5.0.3 1.5.0.4 1.5.1 1.5.1.1 1.5.1.3 1.5.2.1 1.5.2.2 1.5.2.3 1.5.3 1.5.3.1 1.5.4.1 1.5.4.2 1.5.5 1.5.5.1 1.5.6 1.5.6.2 1.5.7 1.5.8.2 1.5.9.1 1.5.9.4 1.5.9.5 1.6.0.1 1.6.0.2 1.6.1 1.6.2.2 1.6.2.3 1.6.3.1 1.6.4 1.6.4.1 1.6.5 1.6.6 1.6.7 1.6.7.1 1.6.7.2 1.6.7.3 1.6.8 1.6.8.1 1.6.9 1.7.0 1.7.1.1 1.7.1.2 1.7.2 1.7.2.1 1.7.3 1.7.4 1.7.4.1 1.7.4.2 1.7.5.1 1.7.5.2 1.7.5.3 1.7.5.5 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.7.9.1 1.8.0.1 1.8.0.2 1.8.1.1 1.8.1.2 1.8.1.3 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.3.1 1.8.4 1.8.4.1 1.8.5.2 1.8.5.3 1.8.5.4 1.8.6.2 1.8.6.3 1.8.6.4 1.8.7.2 1.8.8.2 1.8.8.3 1.8.9.1 1.8.9.2 1.8.9.4 1.8.9.5 1.8.9.6 1.9.0.1 1.9.0.2 1.9.0.3 1.9.0.4 1.9.1.1 1.9.1.2 1.9.1.3 1.9.1.4 1.9.1.5 1.9.1.6 1.9.2.1 1.9.2.2 1.9.2.3 1.9.3.1 1.9.3.2 1.9.4.1 1.9.4.2 1.9.5 1.9.5.1 1.9.5.2 1.9.6 1.9.6.1 1.9.6.2 1.9.7.1 1.9.7.2 1.9.7.3 1.9.8.1 1.9.8.2 1.9.8.4 1.9.8.7 1.9.9.2 1.9.9.3 1.9.9.4
wpforms-lite / includes / class-process.php
wpforms-lite / includes Last commit date
admin 3 years ago emails 3 years ago fields 3 years ago providers 3 years ago templates 3 years ago class-db.php 3 years ago class-fields.php 3 years ago class-form.php 3 years ago class-frontend.php 3 years ago class-install.php 3 years ago class-process.php 3 years ago class-providers.php 3 years ago class-templates.php 3 years ago class-widget.php 3 years ago compat.php 3 years ago deprecated.php 3 years ago functions-list.php 3 years ago functions.php 3 years ago integrations.php 3 years ago
class-process.php
1114 lines
1 <?php
2
3 /**
4 * Process and validate form entries.
5 *
6 * @since 1.0.0
7 */
8 class WPForms_Process {
9
10 /**
11 * Store errors.
12 *
13 * @since 1.0.0
14 *
15 * @var array
16 */
17 public $errors;
18
19 /**
20 * Confirmation message.
21 *
22 * @var string
23 */
24 public $confirmation_message;
25
26 /**
27 * Current confirmation.
28 *
29 * @since 1.6.9
30 *
31 * @var array
32 */
33 private $confirmation;
34
35 /**
36 * Store formatted fields.
37 *
38 * @since 1.0.0
39 *
40 * @var array
41 */
42 public $fields;
43
44 /**
45 * Store the ID of a successful entry.
46 *
47 * @since 1.2.3
48 *
49 * @var int
50 */
51 public $entry_id = 0;
52
53 /**
54 * Form data and settings.
55 *
56 * @since 1.4.5
57 *
58 * @var array
59 */
60 public $form_data;
61
62 /**
63 * If a valid return has was processed.
64 *
65 * @since 1.4.5
66 *
67 * @var bool
68 */
69 public $valid_hash = false;
70
71 /**
72 * Primary class constructor.
73 *
74 * @since 1.0.0
75 */
76 public function __construct() {
77
78 add_action( 'wp', array( $this, 'listen' ) );
79
80 add_action( 'wp_ajax_wpforms_submit', array( $this, 'ajax_submit' ) );
81 add_action( 'wp_ajax_nopriv_wpforms_submit', array( $this, 'ajax_submit' ) );
82 }
83
84 /**
85 * Listen to see if this is a return callback or a posted form entry.
86 *
87 * @since 1.0.0
88 */
89 public function listen() {
90
91 // Catch the post_max_size overflow.
92 if ( $this->post_max_size_overflow() ) {
93 return;
94 }
95
96 // phpcs:disable WordPress.Security.NonceVerification
97 if ( ! empty( $_GET['wpforms_return'] ) ) {
98 // Additional redirect trigger for addons.
99 $this->entry_confirmation_redirect( '', sanitize_text_field( wp_unslash( $_GET['wpforms_return'] ) ) );
100 }
101
102 $form_id = ! empty( $_POST['wpforms']['id'] ) ? absint( $_POST['wpforms']['id'] ) : 0;
103
104 if ( ! $form_id ) {
105 return;
106 }
107
108 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
109 $this->process( wp_unslash( $_POST['wpforms'] ) );
110 // phpcs:enable WordPress.Security.NonceVerification
111
112 if ( ! wpforms_is_amp() ) {
113 return;
114 }
115
116 // Send 400 Bad Request when there are errors.
117 if ( empty( $this->errors[ $form_id ] ) ) {
118 wp_send_json(
119 [
120 'message' => $this->get_confirmation_message( $this->form_data, $this->fields, $this->entry_id ),
121 ],
122 200
123 );
124
125 return;
126 }
127
128 $message = $this->errors[ $form_id ]['header'];
129
130 if ( ! empty( $this->errors[ $form_id ]['footer'] ) ) {
131 $message .= ' ' . $this->errors[ $form_id ]['footer'];
132 }
133
134 wp_send_json(
135 [
136 'message' => $message,
137 ],
138 400
139 );
140 }
141
142 /**
143 * Process the form entry.
144 *
145 * @since 1.0.0
146 * @since 1.6.4 Added hCaptcha support.
147 *
148 * @param array $entry Form submission raw data ($_POST).
149 */
150 public function process( $entry ) {
151
152 $this->errors = array();
153 $this->fields = array();
154 $form_id = absint( $entry['id'] );
155 $form = wpforms()->form->get( $form_id );
156
157 // Validate form is real and active (published).
158 if ( ! $form || 'publish' !== $form->post_status ) {
159 $this->errors[ $form_id ]['header'] = esc_html__( 'Invalid form.', 'wpforms-lite' );
160 return;
161 }
162
163 /**
164 * Filter form data obtained during form process.
165 *
166 * @since 1.5.3
167 *
168 * @param array $form_data Form data.
169 * @param array $entry Form entry.
170 */
171 $this->form_data = (array) apply_filters( 'wpforms_process_before_form_data', wpforms_decode( $form->post_content ), $entry );
172
173 if ( ! isset( $this->form_data['fields'], $this->form_data['id'] ) ) {
174 $error_id = uniqid();
175
176 // Logs missing form data.
177 wpforms_log(
178 /* translators: %s - error unique ID. */
179 sprintf( esc_html__( 'Missing form data on form submission process %s', 'wpforms-lite' ), $error_id ),
180 esc_html__( 'Form data is not an array in `\WPForms_Process::process()`. It might be caused by incorrect data returned by `wpforms_process_before_form_data` filter. Verify whether you have a custom code using this filter and debug value it is returning.', 'wpforms-lite' ),
181 [
182 'type' => [ 'error', 'entry' ],
183 'form_id' => $form_id,
184 ]
185 );
186
187 $error_messages[] = esc_html__( 'Your form has not been submitted because data is missing from the entry.', 'wpforms-lite' );
188
189 if ( wpforms_setting( 'logs-enable' ) && wpforms_current_user_can( wpforms_get_capability_manage_options() ) ) {
190 $error_messages[] = sprintf(
191 wp_kses( /* translators: %s - URL to the WForms Logs admin page. */
192 __( 'Check the WPForms &raquo; Tools &raquo; <a href="%s">Logs</a> for more details.', 'wpforms-lite' ),
193 [ 'a' => [ 'href' => [] ] ]
194 ),
195 esc_url(
196 add_query_arg(
197 [
198 'page' => 'wpforms-tool',
199 'view' => 'logs',
200 ],
201 admin_url( 'admin.php' )
202 )
203 )
204 );
205
206 /* translators: %s - error unique ID. */
207 $error_messages[] = sprintf( esc_html__( 'Error ID: %s.', 'wpforms-lite' ), $error_id );
208 }
209
210 $errors[ $form_id ]['header'] = implode( '<br>', $error_messages );
211 $this->errors = $errors;
212
213 return;
214 }
215
216 // Pre-process/validate hooks and filter.
217 // Data is not validated or cleaned yet so use with caution.
218 $entry = apply_filters( 'wpforms_process_before_filter', $entry, $this->form_data );
219
220 do_action( 'wpforms_process_before', $entry, $this->form_data );
221 do_action( "wpforms_process_before_{$form_id}", $entry, $this->form_data );
222
223 // Validate fields.
224 foreach ( $this->form_data['fields'] as $field_properties ) {
225
226 $field_id = $field_properties['id'];
227 $field_type = $field_properties['type'];
228 $field_submit = isset( $entry['fields'][ $field_id ] ) ? $entry['fields'][ $field_id ] : '';
229
230 do_action( "wpforms_process_validate_{$field_type}", $field_id, $field_submit, $this->form_data );
231 }
232
233 // CAPTCHA check.
234 $captcha_settings = wpforms_get_captcha_settings();
235 $bypass_captcha = apply_filters( 'wpforms_process_bypass_captcha', false, $entry, $this->form_data );
236
237 if (
238 ! empty( $captcha_settings['provider'] ) &&
239 $captcha_settings['provider'] !== 'none' &&
240 ! empty( $captcha_settings['site_key'] ) &&
241 ! empty( $captcha_settings['secret_key'] ) &&
242 isset( $this->form_data['settings']['recaptcha'] ) &&
243 (int) $this->form_data['settings']['recaptcha'] === 1 &&
244 empty( $bypass_captcha ) &&
245 ! isset( $_POST['__amp_form_verify'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- No need to check CAPTCHA until form is submitted.
246 &&
247 ( ( $captcha_settings['provider'] === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3' ) || ! wpforms_is_amp() ) // AMP requires Google reCAPTCHA v3.
248 ) {
249
250 if ( $captcha_settings['provider'] === 'hcaptcha' ) {
251 $verify_url_raw = 'https://hcaptcha.com/siteverify';
252 $captcha_provider = esc_html__( 'hCaptcha', 'wpforms-lite' );
253 $post_key = 'h-captcha-response';
254 } else {
255 $verify_url_raw = 'https://www.google.com/recaptcha/api/siteverify';
256 $captcha_provider = esc_html__( 'Google reCAPTCHA', 'wpforms-lite' );
257 $post_key = 'g-recaptcha-response';
258 }
259
260 /* translators: %s - The CAPTCHA provider name. */
261 $error = wpforms_setting( "{$captcha_settings['provider']}-fail-msg", sprintf( esc_html__( '%s verification failed, please try again later.', 'wpforms-lite' ), $captcha_provider ) );
262 $token = ! empty( $_POST[ $post_key ] ) ? $_POST[ $post_key ] : false; // phpcs:ignore
263 $is_recaptcha_v3 = $captcha_settings['provider'] === 'recaptcha' && $captcha_settings['recaptcha_type'] === 'v3';
264
265 if ( $is_recaptcha_v3 ) {
266 $token = ! empty( $_POST['wpforms']['recaptcha'] ) ? $_POST['wpforms']['recaptcha'] : false; // phpcs:ignore
267 }
268
269 $verify_query_arg = [
270 'secret' => $captcha_settings['secret_key'],
271 'response' => $token,
272 'remoteip' => wpforms_get_ip(),
273 ];
274
275 /*
276 * hCaptcha uses user IP to better detect bots and their attacks on a form.
277 * Majority of our users have GDPR disabled.
278 * So we remove this data from the request only when it's not needed, depending on wpforms_is_collecting_ip_allowed($this->form_data) check.
279 */
280 if ( ! wpforms_is_collecting_ip_allowed( $this->form_data ) ) {
281 unset( $verify_query_arg['remoteip'] );
282 }
283
284 $verify_url = add_query_arg( $verify_query_arg, $verify_url_raw );
285
286 /**
287 * Filter the CAPTCHA verify URL.
288 *
289 * @since 1.6.4
290 *
291 * @param string $verify_url The full CAPTCHA verify URL.
292 * @param string $verify_url_raw The CAPTCHA verify URL without query.
293 * @param string $verify_query_arg The query arguments for verify URL.
294 */
295 $verify_url = apply_filters( 'wpforms_process_captcha_verify_url', $verify_url, $verify_url_raw, $verify_query_arg );
296
297 // API call.
298 $response = json_decode( wp_remote_retrieve_body( wp_remote_get( $verify_url ) ) );
299
300 if (
301 empty( $response->success ) ||
302 ( $is_recaptcha_v3 && $response->score <= wpforms_setting( 'recaptcha-v3-threshold', '0.4' ) )
303 ) {
304 if ( $is_recaptcha_v3 ) {
305 if ( isset( $response->score ) ) {
306 $error .= ' (' . esc_html( $response->score ) . ')';
307 }
308 $this->errors[ $form_id ]['footer'] = $error;
309 } else {
310 $this->errors[ $form_id ]['recaptcha'] = $error;
311 }
312 }
313 }
314
315 // Check if combined upload size exceeds allowed maximum.
316 $this->validate_combined_upload_size( $form );
317
318 // Initial error check.
319 // Don't proceed if there are any errors thus far. We provide a filter
320 // so that other features, such as conditional logic, have the ability
321 // to adjust blocking errors.
322 $errors = apply_filters( 'wpforms_process_initial_errors', $this->errors, $this->form_data );
323
324 if ( isset( $_POST['__amp_form_verify'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
325 if ( empty( $errors[ $form_id ] ) ) {
326 wp_send_json( array(), 200 );
327 } else {
328 $verify_errors = array();
329
330 foreach ( $errors[ $form_id ] as $field_id => $error_fields ) {
331 $field = $this->form_data['fields'][ $field_id ];
332 $field_properties = wpforms()->frontend->get_field_properties( $field, $this->form_data );
333
334 if ( is_string( $error_fields ) ) {
335
336 if ( 'checkbox' === $field['type'] || 'radio' === $field['type'] || 'select' === $field['type'] ) {
337 $first = current( $field_properties['inputs'] );
338 $name = $first['attr']['name'];
339 } elseif ( isset( $field_properties['inputs']['primary']['attr']['name'] ) ) {
340 $name = $field_properties['inputs']['primary']['attr']['name'];
341 }
342
343 $verify_errors[] = array(
344 'name' => $name,
345 'message' => $error_fields,
346 );
347 } else {
348 foreach ( $error_fields as $error_field => $error_message ) {
349
350 if ( isset( $field_properties['inputs'][ $error_field ]['attr']['name'] ) ) {
351 $name = $field_properties['inputs'][ $error_field ]['attr']['name'];
352 }
353
354 $verify_errors[] = array(
355 'name' => $name,
356 'message' => $error_message,
357 );
358 }
359 }
360 }
361
362 wp_send_json(
363 array(
364 'verifyErrors' => $verify_errors,
365 ),
366 400
367 );
368 }
369 return;
370 }
371
372 if ( ! empty( $errors[ $form_id ] ) ) {
373 if ( empty( $errors[ $form_id ]['header'] ) ) {
374 $errors[ $form_id ]['header'] = esc_html__( 'Form has not been submitted, please see the errors below.', 'wpforms-lite' );
375 }
376 $this->errors = $errors;
377 return;
378 }
379
380 // If a logged-in user fails the nonce check, we want to log the entry, disable the errors and fail silently.
381 // Please note that logs may be disabled and in this case nothing will be logged or reported.
382 if (
383 is_user_logged_in() &&
384 ( empty( $entry['nonce'] ) || ! wp_verify_nonce( $entry['nonce'], "wpforms::form_{$form_id}" ) )
385 ) {
386 // Logs XSS attempt depending on log levels set.
387 wpforms_log(
388 'Cross-site scripting attempt ' . uniqid( '', true ),
389 [ true, $entry ],
390 [
391 'type' => [ 'security' ],
392 'form_id' => $this->form_data['id'],
393 ]
394 );
395
396 // Fail silently.
397 return;
398 }
399
400 $honeypot = wpforms()->get( 'honeypot' )->validate( $this->form_data, $this->fields, $entry );
401
402 // If we trigger the honey pot, we want to log the entry, disable the errors, and fail silently.
403 if ( $honeypot ) {
404
405 // Logs spam entry depending on log levels set.
406 wpforms_log(
407 'Spam Entry ' . uniqid(),
408 array( $honeypot, $entry ),
409 array(
410 'type' => array( 'spam' ),
411 'form_id' => $this->form_data['id'],
412 )
413 );
414
415 // Fail silently.
416 return;
417 }
418
419 $antispam = wpforms()->get( 'token' )->validate( $this->form_data, $this->fields, $entry );
420
421 // If spam - return early.
422 // For antispam, we want to make sure that we have a value, we are not using AMP, and the value is an error string.
423 if ( $antispam && ! wpforms_is_amp() && is_string( $antispam ) ) {
424
425 if ( $antispam ) {
426 $this->errors[ $form_id ]['header'] = $antispam;
427 }
428
429 // Logs spam entry depending on log levels set.
430 wpforms_log(
431 esc_html__( 'Spam Entry ' ) . uniqid(),
432 array( $antispam, $entry ),
433 array(
434 'type' => array( 'spam' ),
435 'form_id' => $this->form_data['id'],
436 )
437 );
438
439 return;
440 }
441
442 $akismet = wpforms()->get( 'akismet' )->validate( $this->form_data, $entry );
443
444 // If Akismet marks the entry as spam, we want to log the entry and fail silently.
445 if ( $akismet ) {
446
447 $this->errors[ $form_id ]['header'] = $akismet;
448
449 // Log the spam entry depending on log levels set.
450 wpforms_log(
451 'Spam Entry ' . uniqid(),
452 [ $akismet, $entry ],
453 [
454 'type' => [ 'spam' ],
455 'form_id' => $this->form_data['id'],
456 ]
457 );
458
459 // Fail silently.
460 return;
461 }
462
463 // Pass the form created date into the form data.
464 $this->form_data['created'] = $form->post_date;
465
466 // Format fields.
467 foreach ( (array) $this->form_data['fields'] as $field_properties ) {
468
469 $field_id = $field_properties['id'];
470 $field_type = $field_properties['type'];
471 $field_submit = isset( $entry['fields'][ $field_id ] ) ? $entry['fields'][ $field_id ] : '';
472
473 do_action( "wpforms_process_format_{$field_type}", $field_id, $field_submit, $this->form_data );
474 }
475
476 // This hook is for internal purposes and should not be leveraged.
477 do_action( 'wpforms_process_format_after', $this->form_data );
478
479 // Process hooks/filter - this is where most addons should hook
480 // because at this point we have completed all field validation and
481 // formatted the data.
482 $this->fields = apply_filters( 'wpforms_process_filter', $this->fields, $entry, $this->form_data );
483
484 do_action( 'wpforms_process', $this->fields, $entry, $this->form_data );
485 do_action( "wpforms_process_{$form_id}", $this->fields, $entry, $this->form_data );
486
487 $this->fields = apply_filters( 'wpforms_process_after_filter', $this->fields, $entry, $this->form_data );
488
489 // One last error check - don't proceed if there are any errors.
490 if ( ! empty( $this->errors[ $form_id ] ) ) {
491 if ( empty( $this->errors[ $form_id ]['header'] ) ) {
492 $this->errors[ $form_id ]['header'] = esc_html__( 'Form has not been submitted, please see the errors below.', 'wpforms-lite' );
493 }
494 return;
495 }
496
497 // Success - add entry to database.
498 $this->entry_id = $this->entry_save( $this->fields, $entry, $this->form_data['id'], $this->form_data );
499
500 /**
501 * Runs right after adding entry to the database.
502 *
503 * @since 1.7.7
504 *
505 * @param array $fields Fields data.
506 * @param array $entry User submitted data.
507 * @param array $form_data Form data.
508 * @param int $entry_id Entry ID.
509 */
510 do_action( 'wpforms_process_entry_saved', $this->fields, $entry, $this->form_data, $this->entry_id );
511
512 // Fire the logic to send notification emails.
513 $this->entry_email( $this->fields, $entry, $this->form_data, $this->entry_id, 'entry' );
514
515 // Pass completed and formatted fields in POST.
516 $_POST['wpforms']['complete'] = $this->fields;
517
518 // Pass entry ID in POST.
519 $_POST['wpforms']['entry_id'] = $this->entry_id;
520
521 // Logs entry depending on log levels set.
522 if ( wpforms()->is_pro() ) {
523 wpforms_log(
524 $this->entry_id ? "Entry {$this->entry_id}" : 'Entry',
525 $this->fields,
526 [
527 'type' => [ 'entry' ],
528 'parent' => $this->entry_id,
529 'form_id' => $this->form_data['id'],
530 ]
531 );
532 }
533
534 // Post-process hooks.
535 do_action( 'wpforms_process_complete', $this->fields, $entry, $this->form_data, $this->entry_id );
536 do_action( "wpforms_process_complete_{$form_id}", $this->fields, $entry, $this->form_data, $this->entry_id );
537
538 $this->entry_confirmation_redirect( $this->form_data );
539 }
540
541 /**
542 * Check if combined upload size exceeds allowed maximum.
543 *
544 * @since 1.6.0
545 *
546 * @param \WP_Post $form Form post object.
547 */
548 public function validate_combined_upload_size( $form ) {
549
550 $form_id = (int) $form->ID;
551 $upload_fields = wpforms_get_form_fields( $form, array( 'file-upload' ) );
552
553 if ( ! empty( $upload_fields ) && ! empty( $_FILES ) ) {
554
555 // Get $_FILES keys generated by WPForms only.
556 $files_keys = preg_filter( '/^/', 'wpforms_' . $form_id . '_', array_keys( $upload_fields ) );
557
558 // Filter uploads without errors. Individual errors are handled by WPForms_Field_File_Upload class.
559 $files = wp_list_filter( wp_array_slice_assoc( $_FILES, $files_keys ), array( 'error' => 0 ) );
560 $files_size = array_sum( wp_list_pluck( $files, 'size' ) );
561 $files_size_max = wpforms_max_upload( true );
562
563 if ( $files_size > $files_size_max ) {
564
565 // Add new header error preserving previous ones.
566 $this->errors[ $form_id ]['header'] = ! empty( $this->errors[ $form_id ]['header'] ) ? $this->errors[ $form_id ]['header'] . '<br>' : '';
567 $this->errors[ $form_id ]['header'] .= esc_html__( 'Uploaded files combined size exceeds allowed maximum.', 'wpforms-lite' );
568 }
569 }
570 }
571
572 /**
573 * Validate the form return hash.
574 *
575 * @since 1.0.0
576 *
577 * @param string $hash Base64-encoded hash of form and entry IDs.
578 *
579 * @return array|false False for invalid or form id.
580 */
581 public function validate_return_hash( $hash = '' ) {
582
583 $query_args = base64_decode( $hash );
584
585 parse_str( $query_args, $output );
586
587 // Verify hash matches.
588 if ( wp_hash( $output['form_id'] . ',' . $output['entry_id'] ) !== $output['hash'] ) {
589 return false;
590 }
591
592 // Get lead and verify it is attached to the form we received with it.
593 $entry = wpforms()->entry->get( $output['entry_id'], [ 'cap' => false ] );
594
595 if ( empty( $entry->form_id ) ) {
596 return false;
597 }
598
599 if ( $output['form_id'] !== $entry->form_id ) {
600 return false;
601 }
602
603 return array(
604 'form_id' => absint( $output['form_id'] ),
605 'entry_id' => absint( $output['form_id'] ),
606 'fields' => null !== $entry && isset( $entry->fields ) ? $entry->fields : array(),
607 );
608 }
609
610 /**
611 * Check if the confirmation data are valid.
612 *
613 * @since 1.6.4
614 *
615 * @param array $data The confirmation data.
616 *
617 * @return bool
618 */
619 protected function is_valid_confirmation( $data ) {
620
621 if ( empty( $data['type'] ) ) {
622 return false;
623 }
624
625 // Confirmation type: redirect, page or message.
626 $type = $data['type'];
627
628 return isset( $data[ $type ] ) && ! wpforms_is_empty_string( $data[ $type ] );
629 }
630
631 /**
632 * Redirect user to a page or URL specified in the form confirmation settings.
633 *
634 * @since 1.0.0
635 *
636 * @param array $form_data Form data and settings.
637 * @param string $hash Base64-encoded hash of form and entry IDs.
638 */
639 public function entry_confirmation_redirect( $form_data = array(), $hash = '' ) {
640
641 // Maybe process return hash.
642 if ( ! empty( $hash ) ) {
643
644 $hash_data = $this->validate_return_hash( $hash );
645
646 if ( ! $hash_data || ! is_array( $hash_data ) ) {
647 return;
648 }
649
650 $this->valid_hash = true;
651 $this->entry_id = absint( $hash_data['entry_id'] );
652 $this->fields = json_decode( $hash_data['fields'], true );
653 $this->form_data = wpforms()->form->get(
654 absint( $hash_data['form_id'] ),
655 array(
656 'content_only' => true,
657 )
658 );
659
660 } else {
661
662 $this->form_data = $form_data;
663 }
664
665 // Backward compatibility.
666 if ( empty( $this->form_data['settings']['confirmations'] ) ) {
667 $this->form_data['settings']['confirmations'][1]['type'] = ! empty( $this->form_data['settings']['confirmation_type'] ) ? $this->form_data['settings']['confirmation_type'] : 'message';
668 $this->form_data['settings']['confirmations'][1]['message'] = ! empty( $this->form_data['settings']['confirmation_message'] ) ? $this->form_data['settings']['confirmation_message'] : esc_html__( 'Thanks for contacting us! We will be in touch with you shortly.', 'wpforms-lite' );
669 $this->form_data['settings']['confirmations'][1]['message_scroll'] = ! empty( $this->form_data['settings']['confirmation_message_scroll'] ) ? $this->form_data['settings']['confirmation_message_scroll'] : 1;
670 $this->form_data['settings']['confirmations'][1]['page'] = ! empty( $this->form_data['settings']['confirmation_page'] ) ? $this->form_data['settings']['confirmation_page'] : '';
671 $this->form_data['settings']['confirmations'][1]['redirect'] = ! empty( $this->form_data['settings']['confirmation_redirect'] ) ? $this->form_data['settings']['confirmation_redirect'] : '';
672 }
673
674 if ( empty( $this->form_data['settings']['confirmations'] ) || ! is_array( $this->form_data['settings']['confirmations'] ) ) {
675 return;
676 }
677
678 $confirmations = $this->form_data['settings']['confirmations'];
679
680 // Reverse sort confirmations by id to process newer ones first.
681 krsort( $confirmations );
682
683 $default_confirmation_key = min( array_keys( $confirmations ) );
684
685 foreach ( $confirmations as $confirmation_id => $confirmation ) {
686 // Last confirmation should execute in any case.
687 if ( $default_confirmation_key === $confirmation_id ) {
688 break;
689 }
690
691 if ( ! $this->is_valid_confirmation( $confirmation ) ) {
692 continue;
693 }
694
695 // phpcs:disable WPForms.PHP.ValidateHooks.InvalidHookName
696
697 /**
698 * Process confirmation filter.
699 *
700 * @since 1.4.8
701 *
702 * @param bool $process Whether to process the logic or not.
703 * @param array $fields List of submitted fields.
704 * @param array $form_data Form data and settings.
705 * @param int $id Confirmation ID.
706 */
707 $process_confirmation = apply_filters( 'wpforms_entry_confirmation_process', true, $this->fields, $this->form_data, $confirmation_id );
708 // phpcs:enable WPForms.PHP.ValidateHooks.InvalidHookName
709
710 if ( $process_confirmation ) {
711 break;
712 }
713 }
714
715 $url = '';
716 // Redirect if needed, to either a page or URL, after form processing.
717 if ( ! empty( $confirmations[ $confirmation_id ]['type'] ) && 'message' !== $confirmations[ $confirmation_id ]['type'] ) {
718
719 if ( $confirmations[ $confirmation_id ]['type'] === 'redirect' ) {
720
721 $rawurlencode_callback = static function ( $value ) {
722 return $value === null ? null : rawurlencode( $value );
723 };
724
725 add_filter( 'wpforms_smarttags_process_field_id_value', $rawurlencode_callback );
726
727 $url = wpforms_process_smart_tags( $confirmations[ $confirmation_id ]['redirect'], $this->form_data, $this->fields, $this->entry_id );
728
729 remove_filter( 'wpforms_smarttags_process_field_id_value', $rawurlencode_callback );
730 }
731
732 if ( 'page' === $confirmations[ $confirmation_id ]['type'] ) {
733 $url = get_permalink( (int) $confirmations[ $confirmation_id ]['page'] );
734 }
735 }
736
737 if ( ! empty( $url ) ) {
738 $url = apply_filters( 'wpforms_process_redirect_url', $url, $this->form_data['id'], $this->fields, $this->form_data, $this->entry_id );
739 if ( wpforms_is_amp() ) {
740 /** This filter is documented in wp-includes/pluggable.php */
741 $url = apply_filters( 'wp_redirect', $url, 302 );
742 $url = wp_sanitize_redirect( $url );
743 header( sprintf( 'AMP-Redirect-To: %s', $url ) );
744 header( 'Access-Control-Expose-Headers: AMP-Redirect-To', false );
745 wp_send_json(
746 array(
747 'message' => __( 'Redirecting…', 'wpforms-lite' ),
748 'redirecting' => true,
749 ),
750 200
751 );
752 } else {
753 wp_redirect( esc_url_raw( $url ) ); // phpcs:ignore
754 }
755 do_action( 'wpforms_process_redirect', $this->form_data['id'] );
756 do_action( "wpforms_process_redirect_{$this->form_data['id']}", $this->form_data['id'] );
757 exit;
758 }
759
760 // Pass a message to a frontend if no redirection happened.
761 if ( ! empty( $confirmations[ $confirmation_id ]['type'] ) && 'message' === $confirmations[ $confirmation_id ]['type'] ) {
762 $this->confirmation = $confirmations[ $confirmation_id ];
763 $this->confirmation_message = $confirmations[ $confirmation_id ]['message'];
764
765 if ( ! empty( $confirmations[ $confirmation_id ]['message_scroll'] ) ) {
766 wpforms()->frontend->confirmation_message_scroll = true;
767 }
768 }
769 }
770
771 /**
772 * Get confirmation message.
773 *
774 * @since 1.5.3
775 *
776 * @param array $form_data Form data and settings.
777 * @param array $fields Sanitized field data.
778 * @param int $entry_id Entry id.
779 *
780 * @return string Confirmation message.
781 */
782 public function get_confirmation_message( $form_data, $fields, $entry_id ) {
783
784 if ( empty( $this->confirmation_message ) ) {
785 return '';
786 }
787
788 $confirmation_message = wpforms_process_smart_tags( $this->confirmation_message, $form_data, $fields, $entry_id );
789 $confirmation_message = apply_filters( 'wpforms_frontend_confirmation_message', wpautop( $confirmation_message ), $form_data, $fields, $entry_id );
790
791 return $confirmation_message;
792 }
793
794 /**
795 * Get current confirmation.
796 *
797 * @since 1.6.9
798 *
799 * @return array
800 */
801 public function get_current_confirmation() {
802
803 return ! empty( $this->confirmation ) ? $this->confirmation : [];
804 }
805
806 /**
807 * Catch the post_max_size overflow.
808 *
809 * @since 1.5.2
810 *
811 * @return bool
812 */
813 public function post_max_size_overflow() {
814
815 // phpcs:disable WordPress.Security.NonceVerification
816 if ( empty( $_SERVER['CONTENT_LENGTH'] ) || empty( $_GET['wpforms_form_id'] ) ) {
817 return false;
818 }
819
820 $form_id = (int) $_GET['wpforms_form_id'];
821 $total_size = (int) $_SERVER['CONTENT_LENGTH'];
822 $post_max_size = wpforms_size_to_bytes( ini_get( 'post_max_size' ) );
823
824 if ( ! ( $total_size > $post_max_size && empty( $_POST ) && $form_id > 0 ) ) {
825 return false;
826 }
827 // phpcs:enable WordPress.Security.NonceVerification
828
829 $error_msg = esc_html__( 'Form has not been submitted, please see the errors below.', 'wpforms-lite' );
830 $error_msg .= '<br>' . sprintf( /* translators: %1$.3f - the total size of the selected files in megabytes, %2$.3f - allowed file upload limit in megabytes.*/
831 esc_html__( 'The total size of the selected files %1$.3f MB exceeds the allowed limit %2$.3f MB.', 'wpforms-lite' ),
832 esc_html( $total_size / 1048576 ),
833 esc_html( $post_max_size / 1048576 )
834 );
835
836 $this->errors[ $form_id ]['header'] = $error_msg;
837
838 return true;
839 }
840
841 /**
842 * Send entry email notifications.
843 *
844 * @since 1.0.0
845 *
846 * @param array $fields List of fields.
847 * @param array $entry Submitted form entry.
848 * @param array $form_data Form data and settings.
849 * @param int $entry_id Saved entry id.
850 * @param string $context In which context this email is sent.
851 */
852 public function entry_email( $fields, $entry, $form_data, $entry_id, $context = '' ) {
853
854 // Check that the form was configured for email notifications.
855 if ( empty( $form_data['settings']['notification_enable'] ) ) {
856 return;
857 }
858
859 // Provide the opportunity to override via a filter.
860 if ( ! apply_filters( 'wpforms_entry_email', true, $fields, $entry, $form_data ) ) {
861 return;
862 }
863
864 // Make sure we have and entry id.
865 if ( empty( $this->entry_id ) ) {
866 $this->entry_id = (int) $entry_id;
867 }
868
869 $fields = apply_filters( 'wpforms_entry_email_data', $fields, $entry, $form_data );
870
871 // Backwards compatibility for notifications before v1.4.3.
872 if ( empty( $form_data['settings']['notifications'] ) && ! empty( $form_data['settings']['notification_email'] ) ) {
873 $notifications[1] = array(
874 'email' => $form_data['settings']['notification_email'],
875 'subject' => $form_data['settings']['notification_subject'],
876 'sender_name' => $form_data['settings']['notification_fromname'],
877 'sender_address' => $form_data['settings']['notification_fromaddress'],
878 'replyto' => $form_data['settings']['notification_replyto'],
879 'message' => '{all_fields}',
880 );
881 } else {
882 $notifications = $form_data['settings']['notifications'];
883 }
884
885 foreach ( $notifications as $notification_id => $notification ) :
886
887 if ( empty( $notification['email'] ) ) {
888 continue;
889 }
890
891 $process_email = apply_filters( 'wpforms_entry_email_process', true, $fields, $form_data, $notification_id, $context );
892
893 if ( ! $process_email ) {
894 continue;
895 }
896
897 $email = array();
898
899 // Setup email properties.
900 /* translators: %s - form name. */
901 $email['subject'] = ! empty( $notification['subject'] ) ? $notification['subject'] : sprintf( esc_html__( 'New %s Entry', 'wpforms-lite' ), $form_data['settings']['form_title'] );
902 $email['address'] = explode( ',', wpforms_process_smart_tags( $notification['email'], $form_data, $fields, $this->entry_id ) );
903 $email['address'] = array_map( 'sanitize_email', $email['address'] );
904 $email['sender_address'] = ! empty( $notification['sender_address'] ) ? $notification['sender_address'] : get_option( 'admin_email' );
905 $email['sender_name'] = ! empty( $notification['sender_name'] ) ? $notification['sender_name'] : get_bloginfo( 'name' );
906 $email['replyto'] = ! empty( $notification['replyto'] ) ? $notification['replyto'] : false;
907 $email['message'] = ! empty( $notification['message'] ) ? $notification['message'] : '{all_fields}';
908 $email = apply_filters( 'wpforms_entry_email_atts', $email, $fields, $entry, $form_data, $notification_id );
909
910 // Create new email.
911 $emails = new WPForms_WP_Emails();
912 $emails->__set( 'form_data', $form_data );
913 $emails->__set( 'fields', $fields );
914 $emails->__set( 'notification_id', $notification_id );
915 $emails->__set( 'entry_id', $this->entry_id );
916 $emails->__set( 'from_name', $email['sender_name'] );
917 $emails->__set( 'from_address', $email['sender_address'] );
918 $emails->__set( 'reply_to', $email['replyto'] );
919
920 // Maybe include CC.
921 if ( ! empty( $notification['carboncopy'] ) && wpforms_setting( 'email-carbon-copy', false ) ) {
922 $emails->__set( 'cc', $notification['carboncopy'] );
923 }
924
925 $emails = apply_filters( 'wpforms_entry_email_before_send', $emails );
926
927 // Go.
928 foreach ( $email['address'] as $address ) {
929 $emails->send( trim( $address ), $email['subject'], $email['message'] );
930 }
931 endforeach;
932 }
933
934 /**
935 * Save entry to database.
936 *
937 * @since 1.0.0
938 *
939 * @param array $fields List of form fields.
940 * @param array $entry User submitted data.
941 * @param int $form_id Form ID.
942 * @param array $form_data Prepared form settings.
943 *
944 * @return int
945 */
946 public function entry_save( $fields, $entry, $form_id, $form_data = array() ) {
947
948 do_action( 'wpforms_process_entry_save', $fields, $entry, $form_id, $form_data );
949
950 return $this->entry_id;
951 }
952
953 /**
954 * Process AJAX form submit.
955 *
956 * @since 1.5.3
957 */
958 public function ajax_submit() {
959
960 // phpcs:disable WordPress.Security.NonceVerification.Missing
961 $form_id = isset( $_POST['wpforms']['id'] ) ? absint( $_POST['wpforms']['id'] ) : 0;
962
963 if ( empty( $form_id ) ) {
964 wp_send_json_error();
965 }
966
967 if ( isset( $_POST['wpforms']['post_id'] ) ) {
968 // We don't have a global $post when processing ajax requests.
969 // Therefore, it's needed to set a global $post manually for compatibility with functions used in smart tag processing.
970 global $post;
971 // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
972 $post = WP_Post::get_instance( absint( $_POST['wpforms']['post_id'] ) );
973 }
974 // phpcs:enable WordPress.Security.NonceVerification.Missing
975
976 add_filter( 'wp_redirect', [ $this, 'ajax_process_redirect' ], 999 );
977
978 do_action( 'wpforms_ajax_submit_before_processing', $form_id );
979
980 // If redirect happens in listen(), ajax_process_redirect() gets executed because of the filter on `wp_redirect`.
981 // The code, that is below listen(), runs only if no redirect happened.
982 $this->listen();
983
984 $form_data = $this->form_data;
985
986 if ( empty( $form_data ) ) {
987 $form_data = wpforms()->form->get( $form_id, [ 'content_only' => true ] );
988 $form_data = apply_filters( 'wpforms_frontend_form_data', $form_data );
989 }
990
991 if ( ! empty( $this->errors[ $form_id ] ) ) {
992 $this->ajax_process_errors( $form_id, $form_data );
993 wp_send_json_error();
994 }
995
996 ob_start();
997
998 wpforms()->frontend->confirmation( $form_data );
999
1000 $response = apply_filters( 'wpforms_ajax_submit_success_response', [ 'confirmation' => ob_get_clean() ], $form_id, $form_data );
1001
1002 do_action( 'wpforms_ajax_submit_completed', $form_id, $response );
1003
1004 wp_send_json_success( $response );
1005 }
1006
1007 /**
1008 * Process AJAX errors.
1009 *
1010 * @since 1.5.3
1011 * @todo This should be re-used/combined for AMP verify-xhr requests.
1012 *
1013 * @param int $form_id Form ID.
1014 * @param array $form_data Form data and settings.
1015 */
1016 protected function ajax_process_errors( $form_id, $form_data ) {
1017
1018 $errors = isset( $this->errors[ $form_id ] ) ? $this->errors[ $form_id ] : array();
1019
1020 $errors = apply_filters( 'wpforms_ajax_submit_errors', $errors, $form_id, $form_data );
1021
1022 if ( empty( $errors ) ) {
1023 wp_send_json_error();
1024 }
1025
1026 // General errors are errors that cannot be populated with jQuery Validate plugin.
1027 $general_errors = array_intersect_key( $errors, array_flip( array( 'header', 'footer', 'recaptcha' ) ) );
1028
1029 foreach ( $general_errors as $key => $error ) {
1030 ob_start();
1031 wpforms()->frontend->form_error( $key, $error );
1032 $general_errors[ $key ] = ob_get_clean();
1033 }
1034
1035 $fields = isset( $form_data['fields'] ) ? $form_data['fields'] : array();
1036
1037 // Get registered fields errors only.
1038 $field_errors = array_intersect_key( $errors, $fields );
1039
1040 // Transform field ids to field names for jQuery Validate plugin.
1041 foreach ( $field_errors as $key => $error ) {
1042
1043 $name = $this->ajax_error_field_name( $fields[ $key ], $form_data, $error );
1044 if ( $name ) {
1045 $field_errors[ $name ] = $error;
1046 }
1047
1048 unset( $field_errors[ $key ] );
1049 }
1050
1051 $response = array();
1052
1053 if ( $general_errors ) {
1054 $response['errors']['general'] = $general_errors;
1055 }
1056
1057 if ( $field_errors ) {
1058 $response['errors']['field'] = $field_errors;
1059 }
1060
1061 $response = apply_filters( 'wpforms_ajax_submit_errors_response', $response, $form_id, $form_data );
1062
1063 do_action( 'wpforms_ajax_submit_completed', $form_id, $response );
1064
1065 wp_send_json_error( $response );
1066 }
1067
1068 /**
1069 * Get field name for ajax error message.
1070 *
1071 * @since 1.6.3
1072 *
1073 * @param array $field Field settings.
1074 * @param array $form_data Form data and settings.
1075 * @param string $error Error message.
1076 *
1077 * @return string
1078 */
1079 private function ajax_error_field_name( $field, $form_data, $error ) {
1080
1081 $props = wpforms()->frontend->get_field_properties( $field, $form_data );
1082
1083 return apply_filters( 'wpforms_process_ajax_error_field_name', '', $field, $props, $error );
1084 }
1085
1086 /**
1087 * Process AJAX redirect.
1088 *
1089 * @since 1.5.3
1090 *
1091 * @param string $url Redirect URL.
1092 */
1093 public function ajax_process_redirect( $url ) {
1094
1095 // phpcs:ignore WordPress.Security.NonceVerification.Missing
1096 $form_id = isset( $_POST['wpforms']['id'] ) ? absint( $_POST['wpforms']['id'] ) : 0;
1097
1098 if ( empty( $form_id ) ) {
1099 wp_send_json_error();
1100 }
1101
1102 $response = array(
1103 'form_id' => $form_id,
1104 'redirect_url' => $url,
1105 );
1106
1107 $response = apply_filters( 'wpforms_ajax_submit_redirect', $response, $form_id, $url );
1108
1109 do_action( 'wpforms_ajax_submit_completed', $form_id, $response );
1110
1111 wp_send_json_success( $response );
1112 }
1113 }
1114