PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.7.2
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.7.2
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / inc / form-submit.php

form-submit.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 1.7.2, at inc/form-submit.php

1,095 lines 38.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Sureforms Submit Class file.
4 *
5 * @package sureforms.
6 * @since 0.0.1
7 */
8
9 namespace SRFM\Inc;
10
11 use SRFM\Inc\Database\Tables\Entries;
12 use SRFM\Inc\Email\Email_Template;
13 use SRFM\Inc\Lib\Browser\Browser;
14 use SRFM\Inc\Traits\Get_Instance;
15 use WP_Error;
16 use WP_REST_Server;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit; // Exit if accessed directly.
20 }
21
22 if ( ! function_exists( 'wp_handle_upload' ) ) {
23 require_once ABSPATH . 'wp-admin/includes/file.php';
24 }
25
26 /**
27 * Sureforms Submit Class.
28 *
29 * @since 0.0.1
30 */
31 class Form_Submit {
32 use Get_Instance;
33
34 /**
35 * Namespace.
36 *
37 * @var string
38 */
39 protected $namespace = 'sureforms/v1';
40
41 /**
42 * Addresses.
43 *
44 * @var string
45 * @since 1.6.1
46 */
47 private $addresses = '';
48
49 /**
50 * Constructor
51 *
52 * @since 0.0.1
53 */
54 public function __construct() {
55 add_action( 'rest_api_init', [ $this, 'register_custom_endpoint' ] );
56 add_action( 'wp_ajax_validation_ajax_action', [ $this, 'field_unique_validation' ] );
57 add_action( 'wp_ajax_nopriv_validation_ajax_action', [ $this, 'field_unique_validation' ] );
58 // for quick action bar.
59 add_action( 'wp_ajax_srfm_global_update_allowed_block', [ $this, 'srfm_global_update_allowed_block' ] );
60 add_action( 'wp_ajax_srfm_global_sidebar_enabled', [ $this, 'srfm_global_sidebar_enabled' ] );
61 }
62
63 /**
64 * Add custom API Route submit-form
65 *
66 * @return void
67 * @since 0.0.1
68 */
69 public function register_custom_endpoint() {
70 register_rest_route(
71 $this->namespace,
72 '/submit-form',
73 [
74 'methods' => WP_REST_Server::EDITABLE,
75 'callback' => [ $this, 'handle_form_submission' ],
76 'permission_callback' => '__return_true',
77 ]
78 );
79 }
80
81 /**
82 * Check whether a given request has permission access route.
83 *
84 * @since 0.0.1
85 * @return WP_Error|bool
86 */
87 public function permissions_check() {
88 if ( ! current_user_can( 'manage_options' ) ) {
89 return new WP_Error( 'rest_forbidden', __( 'Sorry, you cannot access this route', 'sureforms' ), [ 'status' => rest_authorization_required_code() ] );
90 }
91 return true;
92 }
93
94 /**
95 * Validate Turnstile token
96 *
97 * @param string $secret_key Turnstile token.
98 * @param string|false $response Response.
99 * @param string|false $remote_ip Remote IP.
100 * @return array<mixed>|mixed Result of the validation.
101 */
102 public static function validate_turnstile_token( $secret_key, $response, $remote_ip ) {
103
104 if ( empty( $secret_key ) || ! is_string( $secret_key ) ) {
105 return [
106 'success' => false,
107 'error' => __( 'Cloudflare Turnstile secret key is invalid.', 'sureforms' ),
108 ];
109 }
110
111 if ( empty( $response ) ) {
112 return [
113 'success' => false,
114 'error' => __( 'Cloudflare Turnstile response is missing.', 'sureforms' ),
115 ];
116 }
117
118 $body = [
119 'secret' => $secret_key,
120 'response' => $response,
121 'remoteip' => $remote_ip,
122 ];
123
124 $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
125
126 $args = [
127 'body' => $body,
128 'timeout' => 15,
129 ];
130
131 $response = wp_remote_post( $url, $args );
132
133 if ( is_wp_error( $response ) ) {
134 $error_message = $response->get_error_message();
135 return [
136 'success' => false,
137 'error' => $error_message,
138 ];
139 }
140
141 return json_decode( wp_remote_retrieve_body( $response ), true );
142 }
143
144 /**
145 * Validate hCaptcha token
146 *
147 * @param string $secret_key hCaptcha token.
148 * @param string|false $response Response.
149 * @param string|false $remote_ip Remote IP.
150 * @since 0.0.5
151 * @return array<mixed>|mixed Result of the validation.
152 */
153 public static function validate_hcaptcha_token( $secret_key, $response, $remote_ip ) {
154
155 if ( empty( $secret_key ) || ! is_string( $secret_key ) ) {
156 return [
157 'success' => false,
158 'error' => __( 'hCaptcha secret key is invalid.', 'sureforms' ),
159 ];
160 }
161
162 if ( empty( $response ) ) {
163 return [
164 'success' => false,
165 'error' => __( 'hCaptcha response is missing.', 'sureforms' ),
166 ];
167 }
168
169 $body = [
170 'secret' => $secret_key,
171 'response' => $response,
172 'remoteip' => $remote_ip,
173 ];
174
175 $url = 'https://api.hcaptcha.com/siteverify';
176
177 $args = [
178 'body' => $body,
179 'timeout' => 15,
180 ];
181
182 $response = wp_remote_post( $url, $args );
183
184 if ( is_wp_error( $response ) ) {
185 $error_message = $response->get_error_message();
186 return [
187 'success' => false,
188 'error' => $error_message,
189 ];
190 }
191
192 return json_decode( wp_remote_retrieve_body( $response ), true );
193 }
194
195 /**
196 * Handle Form Submission
197 *
198 * @param \WP_REST_Request $request Request object or array containing form data.
199 * @since 0.0.1
200 * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
201 */
202 public function handle_form_submission( $request ) {
203 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Nonce' ) );
204
205 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'wp_rest' ) ) {
206 wp_send_json_error(
207 [
208 'message' => __( 'Nonce verification failed.', 'sureforms' ),
209 ]
210 );
211 }
212
213 $form_data = Helper::sanitize_by_field_type( $request->get_params() );
214
215 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
216 wp_send_json_error(
217 [
218 'message' => __( 'Form data is not found.', 'sureforms' ),
219 ]
220 );
221 }
222
223 if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && ! empty( $_FILES ) ) {
224 add_filter( 'upload_dir', [ $this, 'change_upload_dir' ] );
225
226 foreach ( $_FILES as $field => $file ) {
227 if ( is_array( $file['name'] ) ) {
228 foreach ( $file['name'] as $key => $filename ) {
229 $temp_path = $file['tmp_name'][ $key ];
230 $file_size = $file['size'][ $key ];
231 $file_type = $file['type'][ $key ];
232 $file_error = $file['error'][ $key ];
233
234 if ( ! $filename && ! $temp_path && ! $file_size && ! $file_type ) {
235 $form_data[ $field ][] = '';
236 continue;
237 }
238
239 $uploaded_file = [
240 'name' => $filename,
241 'type' => $file_type,
242 'tmp_name' => $temp_path,
243 'error' => $file_error,
244 'size' => $file_size,
245 ];
246
247 $upload_overrides = [
248 'test_form' => false,
249 ];
250 $move_file = wp_handle_upload( $uploaded_file, $upload_overrides );
251 remove_filter( 'upload_dir', [ $this, 'change_upload_dir' ] );
252
253 if ( $move_file && ! isset( $move_file['error'] ) ) {
254 $form_data[ $field ][] = $move_file['url'];
255 } else {
256 wp_send_json_error(
257 [
258 'message' => __( 'File is not uploaded', 'sureforms' ),
259 ]
260 );
261 }
262 }
263 } else {
264 $form_data[ $field ][] = '';
265 }
266 }
267 }
268
269 if ( ! $form_data['form-id'] ) {
270 wp_send_json_error(
271 [
272 'message' => __( 'Form Id is missing.', 'sureforms' ),
273 'position' => 'header',
274 ]
275 );
276 }
277 $current_form_id = $form_data['form-id'];
278 $security_type = Helper::get_meta_value( Helper::get_integer_value( $current_form_id ), '_srfm_captcha_security_type' );
279 $selected_captcha_type = get_post_meta( Helper::get_integer_value( $current_form_id ), '_srfm_form_recaptcha', true ) ? Helper::get_string_value( get_post_meta( Helper::get_integer_value( $current_form_id ), '_srfm_form_recaptcha', true ) ) : '';
280
281 if ( 'none' !== $security_type ) {
282 $global_setting_options = get_option( 'srfm_security_settings_options' );
283 } else {
284 $global_setting_options = [];
285 }
286
287 if ( 'g-recaptcha' === $security_type ) {
288 switch ( $selected_captcha_type ) {
289 case 'v2-checkbox':
290 $key = 'srfm_v2_checkbox_secret_key';
291 break;
292 case 'v2-invisible':
293 $key = 'srfm_v2_invisible_secret_key';
294 break;
295 case 'v3-reCAPTCHA':
296 $key = 'srfm_v3_secret_key';
297 break;
298 default:
299 $key = '';
300 break;
301 }
302
303 $google_captcha_secret_key = is_array( $global_setting_options ) && isset( $global_setting_options[ $key ] ) ? $global_setting_options[ $key ] : '';
304 }
305
306 if ( 'cf-turnstile' === $security_type ) {
307 // Turnstile validation.
308 $srfm_cf_turnstile_secret_key = is_array( $global_setting_options ) && isset( $global_setting_options['srfm_cf_turnstile_secret_key'] ) ? Helper::get_string_value( $global_setting_options['srfm_cf_turnstile_secret_key'] ) : '';
309 $cf_response = ! empty( $form_data['cf-turnstile-response'] ) ? $form_data['cf-turnstile-response'] : false;
310
311 // if gdpr is enabled then set remote ip to empty.
312 $compliance = get_post_meta( Helper::get_integer_value( $current_form_id ), '_srfm_compliance', true );
313 $gdpr = false;
314
315 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
316 $gdpr = ! empty( $compliance[0]['gdpr'] ) ? $compliance[0]['gdpr'] : false;
317 }
318
319 // check if ip logging is disabled in global settings then set remote ip to empty.
320 $gb_general_settinionsgs_opt = get_option( 'srfm_general_settings_options' );
321 $srfm_ip_log = is_array( $gb_general_settinionsgs_opt ) && isset( $gb_general_settinionsgs_opt['srfm_ip_log'] ) ? $gb_general_settinionsgs_opt['srfm_ip_log'] : '';
322
323 $remote_ip = $gdpr || ( ! $srfm_ip_log ) ? '' : ( isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '' );
324
325 $turnstile_validation_result = self::validate_turnstile_token( $srfm_cf_turnstile_secret_key, $cf_response, $remote_ip );
326
327 // If the cloudflare validation fails, return an error.
328 if ( is_array( $turnstile_validation_result ) && isset( $turnstile_validation_result['success'] ) && false === $turnstile_validation_result['success'] ) {
329 $this->recaptcha_error_response( 'cf-turnstile', $turnstile_validation_result );
330 }
331 }
332
333 if ( 'hcaptcha' === $security_type ) {
334 $srfm_hcaptcha_secret_key = is_array( $global_setting_options ) && isset( $global_setting_options['srfm_hcaptcha_secret_key'] ) ? Helper::get_string_value( $global_setting_options['srfm_hcaptcha_secret_key'] ) : '';
335 $hcaptcha_response = ! empty( $form_data['h-captcha-response'] ) ? $form_data['h-captcha-response'] : false;
336
337 // if gdpr is enabled then set remote ip to empty.
338 $compliance = get_post_meta( Helper::get_integer_value( $current_form_id ), '_srfm_compliance', true );
339 $gdpr = false;
340
341 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
342 $gdpr = ! empty( $compliance[0]['gdpr'] ) ? $compliance[0]['gdpr'] : false;
343 }
344
345 // check if ip logging is disabled in global settings then set remote ip to empty.
346 $gb_general_settings_options = get_option( 'srfm_general_settings_options' );
347 $srfm_ip_log = is_array( $gb_general_settings_options ) && isset( $gb_general_settings_options['srfm_ip_log'] ) ? $gb_general_settings_options['srfm_ip_log'] : '';
348
349 $remote_ip = $gdpr || ( ! $srfm_ip_log ) ? '' : ( isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '' );
350 $hcaptcha_validation_result = self::validate_hcaptcha_token( $srfm_hcaptcha_secret_key, $hcaptcha_response, $remote_ip );
351
352 // If the hcaptcha validation fails, return an error.
353 if ( is_array( $hcaptcha_validation_result ) && isset( $hcaptcha_validation_result['success'] ) && false === $hcaptcha_validation_result['success'] ) {
354 $this->recaptcha_error_response( 'hcaptcha', $hcaptcha_validation_result );
355 }
356 }
357
358 if ( isset( $form_data['srfm-honeypot-field'] ) && empty( $form_data['srfm-honeypot-field'] ) ) {
359 if ( ! empty( $google_captcha_secret_key ) ) {
360 if ( isset( $form_data['sureforms_form_submit'] ) ) {
361 $secret_key = $google_captcha_secret_key;
362 $ipaddress = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
363 $captcha_response = $form_data['g-recaptcha-response'];
364 $url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $captcha_response . '&ip=' . $ipaddress;
365
366 $response = wp_remote_get( $url );
367
368 if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
369 $json_string = wp_remote_retrieve_body( $response );
370 $data = (array) json_decode( $json_string, true );
371 } else {
372 $data = [];
373 }
374 $sureforms_captcha_data = $data;
375
376 } else {
377 wp_send_json_error(
378 [
379 'message' => __( 'reCAPTCHA error: Submit nonce is not available.', 'sureforms' ),
380 ]
381 );
382 }
383 if ( isset( $sureforms_captcha_data['success'] ) && true === $sureforms_captcha_data['success'] ) {
384 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
385 }
386
387 $this->recaptcha_error_response( 'g-recaptcha', $sureforms_captcha_data );
388 }
389
390 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
391 }
392
393 if ( ! isset( $form_data['srfm-honeypot-field'] ) ) {
394 if ( ! empty( $google_captcha_secret_key ) ) {
395 if ( isset( $form_data['sureforms_form_submit'] ) ) {
396 $secret_key = $google_captcha_secret_key;
397 $ipaddress = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
398 $captcha_response = $form_data['g-recaptcha-response'];
399 $url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $captcha_response . '&ip=' . $ipaddress;
400
401 $response = wp_remote_get( $url );
402
403 if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
404 $json_string = wp_remote_retrieve_body( $response );
405 $data = (array) json_decode( $json_string, true );
406 } else {
407 $data = [];
408 }
409 $sureforms_captcha_data = $data;
410
411 } else {
412 wp_send_json_error(
413 [
414 'message' => __( 'reCAPTCHA error: Submit nonce is not available.', 'sureforms' ),
415 ]
416 );
417 }
418 if ( true === $sureforms_captcha_data['success'] ) {
419 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
420 }
421
422 $this->recaptcha_error_response( 'g-recaptcha', $sureforms_captcha_data );
423 }
424
425 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
426 }
427
428 wp_send_json_error(
429 [
430 'message' => __( 'Spam Detected', 'sureforms' ),
431 ]
432 );
433 }
434
435 /**
436 * Change the upload directory
437 *
438 * @param array<mixed> $dirs upload directory.
439 * @return array<mixed>
440 * @since 0.0.1
441 */
442 public function change_upload_dir( $dirs ) {
443 $dirs['subdir'] = '/sureforms';
444 $dirs['path'] = $dirs['basedir'] . $dirs['subdir'];
445 $dirs['url'] = $dirs['baseurl'] . $dirs['subdir'];
446 return $dirs;
447 }
448
449 /**
450 * Send Email and Create Entry.
451 *
452 * @param array<string> $form_data Request object or array containing form data.
453 * @since 0.0.1
454 * @return array<mixed> Array containing the response data.
455 */
456 public function handle_form_entry( $form_data ) {
457
458 $id = sanitize_text_field( $form_data['form-id'] );
459
460 // Get the compliance settings.
461 $compliance = get_post_meta( Helper::get_integer_value( $id ), '_srfm_compliance', true );
462 $gdpr = '';
463 $do_not_store_entries = '';
464
465 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
466 $gdpr = $compliance[0]['gdpr'] ?? '';
467 $do_not_store_entries = $compliance[0]['do_not_store_entries'] ?? '';
468 }
469
470 // Check if the form data contains 'srfm_addresses' and is not empty.
471 if ( ! empty( $form_data['srfm_addresses'] ) ) {
472 // Assign the addresses to the class property for further processing.
473 $this->addresses = $form_data['srfm_addresses'];
474 // Remove the address data from the form data to avoid redundancy.
475 unset( $form_data['srfm_addresses'] );
476 }
477
478 $submission_data = [];
479
480 $form_data_keys = array_keys( $form_data );
481 $form_data_count = count( $form_data );
482
483 for ( $i = 0; $i < $form_data_count; $i++ ) {
484 $key = strval( $form_data_keys[ $i ] );
485
486 /**
487 * This will allow to pass only sureforms fields
488 * checking -lbl- as thats mandatory for in key of sureforms fields.
489 */
490 if ( false === str_contains( $key, '-lbl-' ) ) {
491 continue;
492 }
493
494 $value = $form_data[ $key ];
495
496 $field_name = htmlspecialchars( str_replace( '_', ' ', $key ) );
497
498 // If the field is an array, encode the values. This is to add support for multi-upload field.
499 if ( is_array( $value ) ) {
500 $submission_data[ $field_name ] =
501 array_map(
502 static function ( $val ) {
503 return rawurlencode( $val );
504 },
505 $value
506 );
507 } else {
508 $submission_data[ $field_name ] = htmlspecialchars( $value );
509 }
510 }
511
512 $submission_data = apply_filters( 'srfm_before_prepare_submission_data', $submission_data );
513
514 $modified_message = $this->prepare_submission_data( $submission_data );
515
516 $form_before_submission_data = [
517 'form_id' => $id ? intval( $id ) : '',
518 'data' => $modified_message,
519 ];
520
521 /**
522 * Fires before submission process starts.
523 */
524 do_action( 'srfm_before_submission', $form_before_submission_data );
525
526 $name = sanitize_text_field( get_the_title( intval( $id ) ) );
527 $send_email = $this->send_email( $id, $submission_data, $form_data );
528 $emails = [];
529
530 if ( $send_email ) {
531 $emails = $send_email['emails'];
532 }
533
534 // Check if GDPR is enabled and do not store entries is enabled.
535 // If so, send email and do not store entries.
536 if ( $gdpr && $do_not_store_entries ) {
537
538 $form_submit_response = [
539 'success' => true,
540 'form_id' => $id ? intval( $id ) : '',
541 'to_emails' => $emails,
542 'form_name' => $name ? esc_attr( $name ) : '',
543 'message' => Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data ),
544 'data' => $modified_message,
545 ];
546
547 do_action( 'srfm_form_submit', $form_submit_response );
548
549 /**
550 * Hook for enabling background processes.
551 *
552 * @param array $form_data form data related to submission.
553 */
554 $form_data['form_id'] = $id ? intval( $id ) : '';
555 do_action( 'srfm_after_submission_process', $form_data );
556
557 return [
558 'success' => true,
559 'message' => Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data ),
560 'data' => [
561 'name' => $name,
562 'after_submit' => false,
563 ],
564 'redirect_url' => Generate_Form_Markup::get_redirect_url( $form_data, $submission_data ),
565 ];
566
567 }
568
569 $global_setting_options = get_option( 'srfm_general_settings_options' );
570
571 // If GDPR is enabled, do not store IP, browser, and device info.
572 // If not, store IP, browser, and device info.
573 $user_ip = '';
574 $browser_name = '';
575 $device_name = '';
576 if ( ! $gdpr ) {
577 $srfm_ip_log = is_array( $global_setting_options ) && isset( $global_setting_options['srfm_ip_log'] ) ? $global_setting_options['srfm_ip_log'] : '';
578
579 $user_ip = $srfm_ip_log && isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
580 $browser = new Browser();
581 $browser_name = sanitize_text_field( $browser->getBrowser() );
582 $device_name = sanitize_text_field( $browser->getPlatform() );
583 }
584
585 $form_markup = get_the_content( null, false, Helper::get_integer_value( $form_data['form-id'] ) );
586 $pattern = '/"label":"(.*?)"/';
587 preg_match_all( $pattern, $form_markup, $matches );
588 $submission_info = [
589 'user_ip' => $user_ip,
590 'browser_name' => $browser_name,
591 'device_name' => $device_name,
592 ];
593 $entries_data = [
594 'form_id' => $id,
595 'form_data' => $submission_data,
596 'submission_info' => $submission_info,
597 ];
598 if ( is_user_logged_in() ) {
599 // If user is logged in then save their user id.
600 $entries_data['user_id'] = get_current_user_id();
601 }
602 $entry_id = Entries::add( $entries_data );
603 if ( $entry_id ) {
604
605 $response = [
606 'success' => true,
607 'message' => Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data ),
608 'data' => [
609 'name' => $name,
610 'submission_id' => $entry_id,
611 'after_submit' => true,
612 ],
613 'redirect_url' => Generate_Form_Markup::get_redirect_url( $form_data, $submission_data ),
614 ];
615
616 $form_submit_response = apply_filters(
617 'srfm_form_submit_response',
618 [
619 'success' => true,
620 'form_id' => $id ? intval( $id ) : '',
621 'entry_id' => intval( $entry_id ),
622 'to_emails' => $emails,
623 'form_name' => $name ? esc_attr( $name ) : '',
624 'message' => Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data ),
625 'data' => $modified_message,
626 ]
627 );
628
629 do_action( 'srfm_form_submit', $form_submit_response );
630 } else {
631 $response = [
632 'success' => false,
633 'message' => __( 'Error submitting form', 'sureforms' ),
634 ];
635 }
636
637 return $response;
638 }
639
640 /**
641 * Prepare submission data.
642 *
643 * @param array<mixed> $submission_data Submission data.
644 * @since 0.0.7
645 * @return array<mixed> Modified submission data.
646 */
647 public function prepare_submission_data( $submission_data ) {
648 $modified_message = [];
649 foreach ( $submission_data as $key => $value ) {
650 $parts = explode( '-lbl-', $key );
651 $label = '';
652
653 if ( ! empty( $parts[1] ) ) {
654 $tokens = explode( '-', $parts[1] );
655 if ( count( $tokens ) > 1 ) {
656 $label = implode( '-', array_slice( $tokens, 1 ) );
657 }
658
659 $fields = explode( '-', $parts[0] );
660
661 // Since the upload field returns an array of file URLs, we need to implode them with a comma.
662 if ( 'upload' === $fields[1] && ! empty( $value ) && is_array( $value ) ) {
663 $modified_message[ $label ] = urldecode( implode( ', ', $value ) );
664 } else {
665 $modified_message[ $label ] = html_entity_decode( esc_attr( Helper::get_string_value( $value ) ) );
666 }
667 }
668 }
669
670 // If the address is not empty, add it to the submission data.
671 // We are providing this for third-party integrations like Ottokit.
672 // They can use compact addresses such as permanent address, temporary address, etc.
673 // The address will be structured as field 1, field 2, and so on.
674 if ( ! empty( $this->addresses ) ) {
675 // Address will be JSON stringified, so decode it.
676 $address = json_decode( wp_unslash( $this->addresses ), true );
677 if ( ! empty( $address ) && is_array( $address ) ) {
678 $modified_message = array_merge( $modified_message, $address );
679 }
680 }
681
682 return $modified_message;
683 }
684
685 /**
686 * Parse an email notification template and generate the necessary components for sending an email.
687 *
688 * @param array<mixed> $submission_data An associative array containing submission data to be used in the email template.
689 * @param array<string,string> $item An associative array containing email settings, such as 'email_to', 'subject', 'email_body', and optional headers like 'email_reply_to', 'email_cc', and 'email_bcc'.
690 * @param array<string> $form_data Request object or array containing form data.
691 * @since 1.3.0
692 * @return array<string,string> An associative array containing 'to', 'subject', 'message', and 'headers' for the email.
693 */
694 public static function parse_email_notification_template( $submission_data, $item, $form_data = [] ) {
695 $smart_tags = Smart_Tags::get_instance();
696
697 $to = $smart_tags->process_smart_tags( $item['email_to'], $submission_data );
698 $subject = $smart_tags->process_smart_tags( $item['subject'], $submission_data, $form_data );
699 $email_body = $smart_tags->process_smart_tags( $item['email_body'], $submission_data, $form_data );
700 $email_template = new Email_Template();
701 $message = $email_template->render( $submission_data, $email_body );
702 $headers = 'X-Mailer: PHP/' . phpversion() . "\r\n";
703 $headers .= "Content-Type: text/html; charset=utf-8\r\n";
704
705 // Add the From: to the headers.
706 $headers .= self::add_from_data_in_header( $submission_data, $item, $smart_tags );
707
708 if ( isset( $item['email_reply_to'] ) && ! empty( $item['email_reply_to'] ) ) {
709 $headers .= 'Reply-To:' . $smart_tags->process_smart_tags( $item['email_reply_to'], $submission_data ) . "\r\n";
710 }
711 if ( isset( $item['email_cc'] ) && ! empty( $item['email_cc'] ) ) {
712 $headers .= 'Cc:' . $smart_tags->process_smart_tags( $item['email_cc'], $submission_data ) . "\r\n";
713 }
714 if ( isset( $item['email_bcc'] ) && ! empty( $item['email_bcc'] ) ) {
715 $headers .= 'Bcc:' . $smart_tags->process_smart_tags( $item['email_bcc'], $submission_data ) . "\r\n";
716 }
717
718 return compact( 'to', 'subject', 'message', 'headers' );
719 }
720
721 /**
722 * Send Email.
723 *
724 * @param string $id Form ID.
725 * @param array<mixed> $submission_data Submission data.
726 * @param array<string> $form_data Request object or array containing form data.
727 * @since 0.0.1
728 * @return array<mixed> Array containing the response data.
729 */
730 public static function send_email( $id, $submission_data, $form_data = [] ) {
731 $email_notification = get_post_meta( intval( $id ), '_srfm_email_notification' );
732 $is_mail_sent = false;
733 $emails = [];
734
735 // Filter to determine whether the email notification should be sent.
736 $email_notification = apply_filters( 'srfm_email_notification_should_send', $email_notification, $submission_data, $form_data );
737
738 if ( is_iterable( $email_notification ) ) {
739 $entries_db_instance = Entries::get_instance();
740 $log_key = $entries_db_instance->add_log( __( 'Email notification passed to the sending server', 'sureforms' ) );
741
742 foreach ( $email_notification as $notification ) {
743 foreach ( $notification as $item ) {
744 if ( true === $item['status'] ) {
745
746 $parsed = self::parse_email_notification_template( $submission_data, $item, $form_data );
747
748 // Allow filtering of the email data before it is sent.
749 $parsed = apply_filters( 'srfm_email_notification', $parsed, $submission_data, $item, $form_data );
750
751 // Trigger an action before sending the email, allowing additional processing or logging.
752 do_action( 'srfm_before_email_send', $parsed, $submission_data, $item, $form_data );
753
754 /**
755 * Temporary override the content type for wp_mail.
756 * This helps us from breaking of content type from other plugins.
757 *
758 * @since 1.2.2
759 */
760 add_filter(
761 'wp_mail_content_type',
762 static function() {
763 return 'text/html'; // We need "text/html" content type to render our emails.
764 },
765 99
766 );
767
768 /**
769 * Start sending email.
770 * Wrapping it in the buffer because when some plugin such as zoho mail, overrides the wp_mail
771 * function and any exception is thrown ( Or printed ) from that plugin side, it affects the JSON response.
772 * So, to make sure such exceptions doesn't affect our JSON response, we are wrapping it inside buffer.
773 *
774 * Try-Catch does not work because the notice or errors might be echoed by other plugins rather than thrown as an exception.
775 *
776 * @since 1.2.2
777 */
778 $sent = false;
779 ob_start();
780 $sent = wp_mail( $parsed['to'], $parsed['subject'], $parsed['message'], $parsed['headers'] );
781 if ( ! $sent ) {
782 // Fallback to default PHP mail if for some reasons wp_mail fails.
783 $sent = mail( $parsed['to'], $parsed['subject'], $parsed['message'], $parsed['headers'] );
784 }
785 $email_report = ob_get_clean(); // Catch any printed notice/errors/message for reports.
786
787 if ( is_int( $log_key ) ) {
788 if ( true === $sent ) {
789 $entries_db_instance->update_log(
790 $log_key,
791 null,
792 [
793 /* translators: Here, %s is the comma separated emails list. */
794 sprintf( __( 'Email notification recipient: %s', 'sureforms' ), esc_html( $parsed['to'] ) ),
795 ]
796 );
797 } else {
798 $entries_db_instance->update_log(
799 $log_key,
800 null,
801 [
802 sprintf(
803 /* translators: Here, %1$s is the comma separated emails list and %2$s is error report ( if any ). */
804 __( 'Email server was unable to send the email notification. Recipient: %1$s. Reason: %2$s', 'sureforms' ),
805 esc_html( $parsed['to'] ),
806 ! empty( $email_report ) ? esc_html( $email_report ) : esc_html__( 'Unknown', 'sureforms' )
807 ),
808 ]
809 );
810 }
811 }
812
813 // Trigger an action after the email is sent, allowing additional processing or logging.
814 do_action(
815 'srfm_after_email_send',
816 $parsed,
817 $submission_data,
818 $item,
819 $form_data
820 );
821
822 $is_mail_sent = $sent;
823 $emails[] = $parsed['to'];
824 }
825 }
826 }
827
828 if ( empty( $emails ) ) {
829 $entries_db_instance->reset_logs();
830 $entries_db_instance->add_log( __( 'No emails were sent', 'sureforms' ) );
831 }
832 }
833
834 return [
835 'success' => $is_mail_sent,
836 'emails' => $emails,
837 ];
838 }
839
840 /**
841 * Retrieve all entries data for a specific form ID to check for unique values.
842 *
843 * @since 0.0.1
844 * @return void
845 */
846 public function field_unique_validation() {
847 if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'unique_validation_nonce' ) ) {
848 $error_message = __( 'Nonce verification failed.', 'sureforms' );
849 $error_data = [
850 'error' => $error_message,
851 ];
852 wp_send_json_error( $error_data );
853 }
854
855 global $wpdb;
856 $id = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : 0;
857 $meta_value = $id;
858
859 if ( ! $meta_value ) {
860 $error_message = __( 'Invalid form ID.', 'sureforms' );
861 $error_data = [
862 'error' => $error_message,
863 ];
864 wp_send_json_error( $error_data );
865 }
866
867 $_POST = array_map( 'wp_unslash', $_POST );
868
869 // Get the entry IDs for the particualr form to perform unique field validation.
870 $entry_ids = Entries::get_all_entry_ids_for_form( $id );
871
872 $all_form_entries = [];
873 $keys = array_keys( $_POST );
874 $length = count( $keys );
875
876 for ( $i = 3; $i < $length; $i++ ) {
877 $key = $keys[ $i ];
878 $value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
879 $key = str_replace( '_', ' ', $keys[ $i ] );
880
881 foreach ( $entry_ids as $entry_id ) {
882 $entry_id = is_array( $entry_id ) ? Helper::get_integer_value( $entry_id['ID'] ) : 0;
883 $form_data = Entries::get_form_data( $entry_id );
884 if ( is_array( $form_data ) && isset( $form_data[ $key ] ) && $form_data[ $key ] === $value ) {
885 $obj = [ $key => 'not unique' ];
886 array_push( $all_form_entries, $obj );
887 break;
888 }
889 }
890 }
891
892 $results = [
893 'data' => $all_form_entries,
894 ];
895
896 wp_send_json( $results );
897 }
898
899 /**
900 * Function to save allowed block data.
901 *
902 * @since 0.0.1
903 * @return void
904 */
905 public function srfm_global_update_allowed_block() {
906 if ( ! current_user_can( 'manage_options' ) ) {
907 wp_send_json_error();
908 }
909
910 if ( ! check_ajax_referer( 'srfm_ajax_nonce', 'security', false ) ) {
911 wp_send_json_error();
912 }
913
914 if ( ! empty( $_POST['defaultAllowedQuickSidebarBlocks'] ) ) {
915 $srfm_default_allowed_quick_sidebar_blocks = json_decode( sanitize_text_field( wp_unslash( $_POST['defaultAllowedQuickSidebarBlocks'] ) ), true );
916 Helper::update_admin_settings_option( 'srfm_quick_sidebar_allowed_blocks', $srfm_default_allowed_quick_sidebar_blocks );
917 wp_send_json_success();
918 }
919 wp_send_json_error();
920 }
921
922 /**
923 * Function to save enable/disable data.
924 *
925 * @since 0.0.1
926 * @return void
927 */
928 public function srfm_global_sidebar_enabled() {
929 if ( ! current_user_can( 'manage_options' ) ) {
930 wp_send_json_error();
931 }
932
933 if ( ! check_ajax_referer( 'srfm_ajax_nonce', 'security', false ) ) {
934 wp_send_json_error();
935 }
936
937 if ( ! empty( $_POST['enableQuickActionSidebar'] ) ) {
938 $srfm_enable_quick_action_sidebar = ( 'enabled' === $_POST['enableQuickActionSidebar'] ? 'enabled' : 'disabled' );
939 Helper::update_admin_settings_option( 'srfm_enable_quick_action_sidebar', $srfm_enable_quick_action_sidebar );
940 wp_send_json_success();
941 }
942 wp_send_json_error();
943 }
944
945 /**
946 * Send error response for reCAPTCHA validation failure.
947 *
948 * @param string $type The type of CAPTCHA used. Accepted values: 'g-recaptcha', 'hcaptcha', 'cf-turnstile'.
949 * @param array<mixed> $api_response The response returned from the CAPTCHA validation API.
950 * @since 1.7.0
951 * @return void
952 */
953 public function recaptcha_error_response( $type, $api_response ) {
954 $error_message = $this->recaptcha_error_message( $type, $api_response );
955 $response = array_merge(
956 [
957 'api_response' => $api_response,
958 ],
959 $error_message
960 );
961
962 wp_send_json_error( $response );
963 }
964
965 /**
966 * Get the error message for a CAPTCHA validation failure based on the service type and API response.
967 *
968 * @param string $type The type of CAPTCHA used. Accepted values: 'g-recaptcha', 'hcaptcha', 'cf-turnstile'.
969 * @param array<mixed> $api_response The response returned from the CAPTCHA validation API.
970 * @since 1.7.0
971 * @return array<string,string> An associative array containing the error message and a detailed message.
972 */
973 public function recaptcha_error_message( $type, $api_response ) {
974
975 if ( empty( $api_response['error-codes'] ) || ! is_array( $api_response['error-codes'] ) ) {
976 return [
977 'detail_message' => __( 'Captcha validation failed. No error code provided.', 'sureforms' ),
978 'message' => __( 'Captcha validation failed.', 'sureforms' ),
979 ];
980 }
981
982 /**
983 * Note: The error codes are not translated because these messages are intended for debugging purposes.
984 * Translating them would make debugging difficult. These error messages are primarily for developers or administrators.
985 * A generic message will be displayed to the user, while detailed error information will be logged or shown in the console.
986 */
987
988 // Google reCAPTCHA error codes.
989 // Reference: (https://developers.google.com/recaptcha/docs/verify#error-code-reference).
990 $google_recaptcha_error = [
991 'missing-input-secret' => 'The secret parameter is missing.',
992 'invalid-input-secret' => 'The secret parameter is invalid or malformed.',
993 'missing-input-response' => 'The response parameter is missing.',
994 'invalid-input-response' => 'The response parameter is invalid or malformed.',
995 'bad-request' => 'The request is invalid or malformed.',
996 'timeout-or-duplicate' => 'The response is no longer valid: either is too old or has been used previously.',
997 ];
998
999 // hCaptcha error codes.
1000 // Reference: (https://docs.hcaptcha.com/#siteverify-error-codes).
1001 $hcaptcha_errors = [
1002 'missing-input-secret' => 'Your secret key is missing.',
1003 'invalid-input-secret' => 'Your secret key is invalid or malformed.',
1004 'missing-input-response' => 'The response parameter (verification token) is missing.',
1005 'invalid-input-response' => 'The response parameter (verification token) is invalid or malformed.',
1006 'expired-input-response' => 'The response parameter (verification token) is expired. (120s default)',
1007 'already-seen-response' => 'The response parameter (verification token) was already verified once.',
1008 'bad-request' => 'The request is invalid or malformed.',
1009 'missing-remoteip' => 'The remoteip parameter is missing.',
1010 'invalid-remoteip' => 'The remoteip parameter is not a valid IP address or blinded value.',
1011 'not-using-dummy-passcode' => 'You have used a testing sitekey but have not used its matching secret.',
1012 'sitekey-secret-mismatch' => 'The sitekey is not registered with the provided secret.',
1013 ];
1014
1015 // Cloudflare Turnstile error codes.
1016 // Reference: (https://developers.cloudflare.com/turnstile/get-started/server-side-validation/).
1017 $cf_turnstile_errors = [
1018 'missing-input-secret' => 'The secret parameter was not passed.',
1019 'invalid-input-secret' => 'The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.',
1020 'missing-input-response' => 'The response parameter (token) was not passed.',
1021 'invalid-input-response' => 'The response parameter (token) is invalid or has expired. Most of the time, this means a fake token has been used. If the error persists, contact customer support.',
1022 'bad-request' => 'The request was rejected because it was malformed.',
1023 'timeout-or-duplicate' => 'The response parameter (token) has already been validated before. This means that the token was issued five minutes ago and is no longer valid, or it was already redeemed.',
1024 'internal-error' => 'An internal error happened while validating the response. The request can be retried.',
1025 ];
1026
1027 $error_code = $api_response['error-codes'][0] ?? 'no-error-code';
1028
1029 $captcha_title = '';
1030 $captcha_message = '';
1031 switch ( $type ) {
1032 case 'g-recaptcha':
1033 $captcha_title = __( 'Google reCAPTCHA', 'sureforms' );
1034 $captcha_message = $google_recaptcha_error[ $error_code ];
1035 break;
1036 case 'hcaptcha':
1037 $captcha_title = __( 'hCaptcha', 'sureforms' );
1038 $captcha_message = $hcaptcha_errors[ $error_code ];
1039 break;
1040 case 'cf-turnstile':
1041 $captcha_title = __( 'Cloudflare Turnstile', 'sureforms' );
1042 $captcha_message = $cf_turnstile_errors[ $error_code ];
1043 break;
1044 default:
1045 $captcha_title = __( 'Unknown Captcha', 'sureforms' );
1046 $captcha_message = __( 'Invalid captcha type.', 'sureforms' );
1047 break;
1048 }
1049
1050 $detail_message = sprintf(
1051 '%s: %s <br> Error Code: %s',
1052 $captcha_title,
1053 $captcha_message ?? 'Unknown error occurred.',
1054 $error_code
1055 );
1056
1057 $message = sprintf(
1058 /* translators: %s is the captcha title. */
1059 __( '%s verification failed. Please contact your site administrator.', 'sureforms' ),
1060 $captcha_title
1061 );
1062
1063 return [
1064 'log_message' => $detail_message, // This variable is used for logging purposes, such as displaying detailed error information in the console on the front end.
1065 'message' => $message,
1066 ];
1067 }
1068
1069 /**
1070 * Add From email and name in the header.
1071 *
1072 * @param array<mixed> $submission_data Submission data.
1073 * @param array<string> $item An associative array containing email settings, such as 'email_to', 'subject', 'email_body', and optional headers like 'email_reply_to', 'email_cc', and 'email_bcc'.
1074 * @param Smart_Tags $smart_tags Smart Tags instance.
1075 * @since 1.6.1
1076 * @return string The formatted "From" email header.
1077 */
1078 private static function add_from_data_in_header( $submission_data, $item, $smart_tags ) {
1079 $from_name = is_array( $item ) && ! empty( $item['from_name'] ) ? sanitize_text_field( Helper::get_string_value( $item['from_name'] ) ) : '{site_title}';
1080 $from_email = is_array( $item ) && ! empty( $item['from_email'] ) ? Helper::get_string_value( $item['from_email'] ) : '{admin_email}';
1081
1082 // Check if the email contains smart tags. If not, validate the email.
1083 $is_valid_email = true;
1084 if ( ! str_contains( $from_email, '{' ) && ! str_contains( $from_email, '}' ) ) {
1085 $is_valid_email = filter_var( $from_email, FILTER_VALIDATE_EMAIL );
1086 }
1087 // if the email is not valid, set it to the admin email.
1088 if ( ! $is_valid_email ) {
1089 $from_email = Helper::get_string_value( get_option( 'admin_email' ) );
1090 }
1091
1092 return 'From: ' . esc_html( $smart_tags->process_smart_tags( $from_name, $submission_data ) ) . ' <' . esc_html( $smart_tags->process_smart_tags( $from_email, $submission_data ) ) . '>' . "\r\n";
1093 }
1094 }
1095