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