PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.5.2
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.5.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
1,349 lines 47.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' => [ $this, 'submit_form_permissions_check' ],
77 ]
78 );
79
80 register_rest_route(
81 $this->namespace,
82 '/refresh-nonces',
83 [
84 'methods' => 'GET',
85 'callback' => [ $this, 'refresh_nonces' ],
86 'permission_callback' => '__return_true',
87 ]
88 );
89 }
90
91 /**
92 * Refresh frontend nonces for form submission
93 *
94 * @return \WP_REST_Response Response with fresh nonces.
95 * @since 2.5.1
96 */
97 public function refresh_nonces() {
98 nocache_headers();
99
100 // Check if nonce refresh is allowed.
101 if ( ! Helper::should_update_form_markup_nonce() ) {
102 return rest_ensure_response(
103 [
104 'success' => false,
105 'message' => __( 'Nonce refresh is disabled.', 'sureforms' ),
106 ]
107 );
108 }
109
110 // Get fresh nonces from Helper.
111 $nonces = Helper::get_frontend_nonces();
112
113 return rest_ensure_response(
114 [
115 'success' => true,
116 'nonces' => $nonces,
117 ]
118 );
119 }
120
121 /**
122 * Check whether a given request has permission access route.
123 *
124 * @param \WP_REST_Request $request Request object or array containing form data.
125 * @since 1.8.0
126 * @return WP_Error|bool
127 */
128 public function submit_form_permissions_check( $request ) {
129 $nonce = Helper::get_string_value( $request->get_header( 'X-WP-Submit-Nonce' ) );
130 if ( ! wp_verify_nonce( sanitize_text_field( $nonce ), 'srfm_form_submit' ) ) {
131 wp_send_json_error(
132 [
133 'message' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
134 ]
135 );
136 }
137
138 $form_data = Helper::sanitize_by_field_type( $request->get_params() );
139
140 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
141 wp_send_json_error(
142 [
143 'message' => __( 'Form data was not found.', 'sureforms' ),
144 ]
145 );
146 }
147
148 if ( ! $form_data['form-id'] ) {
149 wp_send_json_error(
150 [
151 'message' => __( 'Form ID is missing.', 'sureforms' ),
152 'position' => 'header',
153 ]
154 );
155 }
156
157 return true;
158 }
159
160 /**
161 * Check whether a given request has permission access route.
162 *
163 * @since 0.0.1
164 * @return WP_Error|bool
165 */
166 public function permissions_check() {
167 if ( ! Helper::current_user_can() ) {
168 return new WP_Error( 'rest_forbidden', __( 'Sorry, you do not have permission to access this resource.', 'sureforms' ), [ 'status' => rest_authorization_required_code() ] );
169 }
170 return true;
171 }
172
173 /**
174 * Validate Turnstile token
175 *
176 * @param string $secret_key Turnstile token.
177 * @param string|false $response Response.
178 * @param string|false $remote_ip Remote IP.
179 * @return array<mixed>|mixed Result of the validation.
180 */
181 public static function validate_turnstile_token( $secret_key, $response, $remote_ip ) {
182
183 if ( empty( $secret_key ) || ! is_string( $secret_key ) ) {
184 return [
185 'success' => false,
186 'error' => __( 'Cloudflare Turnstile secret key is invalid.', 'sureforms' ),
187 ];
188 }
189
190 if ( empty( $response ) ) {
191 return [
192 'success' => false,
193 'error' => __( 'Cloudflare Turnstile response is missing.', 'sureforms' ),
194 ];
195 }
196
197 $body = [
198 'secret' => $secret_key,
199 'response' => $response,
200 'remoteip' => $remote_ip,
201 ];
202
203 $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
204
205 $args = [
206 'body' => $body,
207 'timeout' => 15,
208 ];
209
210 $response = wp_remote_post( $url, $args );
211
212 if ( is_wp_error( $response ) ) {
213 $error_message = $response->get_error_message();
214 return [
215 'success' => false,
216 'error' => $error_message,
217 ];
218 }
219
220 return json_decode( wp_remote_retrieve_body( $response ), true );
221 }
222
223 /**
224 * Validate hCaptcha token
225 *
226 * @param string $secret_key hCaptcha token.
227 * @param string|false $response Response.
228 * @param string|false $remote_ip Remote IP.
229 * @since 0.0.5
230 * @return array<mixed>|mixed Result of the validation.
231 */
232 public static function validate_hcaptcha_token( $secret_key, $response, $remote_ip ) {
233
234 if ( empty( $secret_key ) || ! is_string( $secret_key ) ) {
235 return [
236 'success' => false,
237 'error' => __( 'hCaptcha secret key is invalid.', 'sureforms' ),
238 ];
239 }
240
241 if ( empty( $response ) ) {
242 return [
243 'success' => false,
244 'error' => __( 'hCaptcha response is missing.', 'sureforms' ),
245 ];
246 }
247
248 $body = [
249 'secret' => $secret_key,
250 'response' => $response,
251 'remoteip' => $remote_ip,
252 ];
253
254 $url = 'https://api.hcaptcha.com/siteverify';
255
256 $args = [
257 'body' => $body,
258 'timeout' => 15,
259 ];
260
261 $response = wp_remote_post( $url, $args );
262
263 if ( is_wp_error( $response ) ) {
264 $error_message = $response->get_error_message();
265 return [
266 'success' => false,
267 'error' => $error_message,
268 ];
269 }
270
271 return json_decode( wp_remote_retrieve_body( $response ), true );
272 }
273
274 /**
275 * Handle Form Submission
276 *
277 * @param \WP_REST_Request $request Request object or array containing form data.
278 * @since 0.0.1
279 * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
280 */
281 public function handle_form_submission( $request ) {
282 /**
283 * All checks are done in submit_form_permissions_check method:
284 * - Nonce verification
285 * - Form data validation
286 * - Form ID validation
287 *
288 * @since 1.8.0
289 */
290 $form_data = Helper::sanitize_by_field_type( $request->get_params() );
291
292 $current_form_id = $form_data['form-id'];
293
294 /**
295 * If someone tries to access the form submit endpoint directly, we need to check if the form is restricted.
296 * If a form is loaded in a browser window and the limit exceeds then the form will not be submitted.
297 */
298 $form_id = Helper::get_integer_value( $current_form_id );
299 if ( Form_Restriction::is_form_restricted( $form_id ) ) {
300 $form_restriction = Form_Restriction::get_form_restriction_setting( $form_id );
301
302 // Get the scheduling state and appropriate message.
303 $scheduling_state = Form_Restriction::get_form_scheduling_state( $form_restriction );
304 $form_restriction_message = Form_Restriction::get_restriction_message_by_state( $scheduling_state, $form_restriction );
305
306 $form_restriction_message = apply_filters( 'srfm_form_restriction_message', $form_restriction_message, $form_id, $form_restriction );
307
308 wp_send_json_error(
309 [
310 'message' => $form_restriction_message,
311 ]
312 );
313 }
314
315 if ( apply_filters( 'srfm_additional_restriction_check', false, $form_id, $form_data ) ) {
316 wp_send_json_error(
317 [
318 'message' => apply_filters( 'srfm_additional_restriction_message', __( 'You do not have permission to submit this form.', 'sureforms' ), $form_id, $form_data ),
319 ]
320 );
321 }
322
323 // Check whether the form is valid.
324 if ( ! Helper::is_valid_form( $current_form_id ) ) {
325 wp_send_json_error(
326 [
327 'code' => 'srfm_invalid_form_id',
328 'message' => __( 'This form is no longer available.', 'sureforms' ),
329 ]
330 );
331 }
332
333 $validated_form_data = Field_Validation::validate_form_data( $form_data, $current_form_id );
334
335 if ( ! empty( $validated_form_data ) ) {
336 // Get the first error message to display as the main message.
337 $first_error = reset( $validated_form_data );
338
339 wp_send_json_error(
340 [
341 'message' => $first_error ?? __( 'Please check the form for errors.', 'sureforms' ),
342 'field_errors' => $validated_form_data,
343 ]
344 );
345 }
346
347 $security_type = Helper::get_meta_value( Helper::get_integer_value( $current_form_id ), '_srfm_captcha_security_type' );
348 $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 ) ) : '';
349
350 if ( 'none' !== $security_type ) {
351 $global_setting_options = get_option( 'srfm_security_settings_options' );
352 } else {
353 $global_setting_options = [];
354 }
355
356 if ( 'g-recaptcha' === $security_type ) {
357 switch ( $selected_captcha_type ) {
358 case 'v2-checkbox':
359 $key = 'srfm_v2_checkbox_secret_key';
360 break;
361 case 'v2-invisible':
362 $key = 'srfm_v2_invisible_secret_key';
363 break;
364 case 'v3-reCAPTCHA':
365 $key = 'srfm_v3_secret_key';
366 break;
367 default:
368 $key = '';
369 break;
370 }
371
372 $google_captcha_secret_key = is_array( $global_setting_options ) && isset( $global_setting_options[ $key ] ) ? $global_setting_options[ $key ] : '';
373 }
374
375 if ( 'cf-turnstile' === $security_type ) {
376 // Turnstile validation.
377 $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'] ) : '';
378 $cf_response = ! empty( $form_data['cf-turnstile-response'] ) && is_string( $form_data['cf-turnstile-response'] ) ? $form_data['cf-turnstile-response'] : '';
379
380 // if gdpr is enabled then set remote ip to empty.
381 $compliance = get_post_meta( Helper::get_integer_value( $current_form_id ), '_srfm_compliance', true );
382 $gdpr = false;
383
384 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
385 $gdpr = ! empty( $compliance[0]['gdpr'] ) ? $compliance[0]['gdpr'] : false;
386 }
387
388 // check if ip logging is disabled in global settings then set remote ip to empty.
389 $gb_general_settinionsgs_opt = get_option( 'srfm_general_settings_options' );
390 $srfm_ip_log = is_array( $gb_general_settinionsgs_opt ) && isset( $gb_general_settinionsgs_opt['srfm_ip_log'] ) ? $gb_general_settinionsgs_opt['srfm_ip_log'] : '';
391
392 $remote_ip = $gdpr || ( ! $srfm_ip_log ) ? '' : ( isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '' );
393
394 $turnstile_validation_result = self::validate_turnstile_token( $srfm_cf_turnstile_secret_key, $cf_response, $remote_ip );
395
396 // If the cloudflare validation fails, return an error.
397 if ( is_array( $turnstile_validation_result ) && isset( $turnstile_validation_result['success'] ) && false === $turnstile_validation_result['success'] ) {
398 $this->recaptcha_error_response( 'cf-turnstile', $turnstile_validation_result );
399 }
400 }
401
402 if ( 'hcaptcha' === $security_type ) {
403 $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'] ) : '';
404 $hcaptcha_response = ! empty( $form_data['h-captcha-response'] ) && is_string( $form_data['h-captcha-response'] ) ? $form_data['h-captcha-response'] : '';
405
406 // if gdpr is enabled then set remote ip to empty.
407 $compliance = get_post_meta( Helper::get_integer_value( $current_form_id ), '_srfm_compliance', true );
408 $gdpr = false;
409
410 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
411 $gdpr = ! empty( $compliance[0]['gdpr'] ) ? $compliance[0]['gdpr'] : false;
412 }
413
414 // check if ip logging is disabled in global settings then set remote ip to empty.
415 $gb_general_settings_options = get_option( 'srfm_general_settings_options' );
416 $srfm_ip_log = is_array( $gb_general_settings_options ) && isset( $gb_general_settings_options['srfm_ip_log'] ) ? $gb_general_settings_options['srfm_ip_log'] : '';
417
418 $remote_ip = $gdpr || ( ! $srfm_ip_log ) ? '' : ( isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '' );
419 $hcaptcha_validation_result = self::validate_hcaptcha_token( $srfm_hcaptcha_secret_key, $hcaptcha_response, $remote_ip );
420
421 // If the hcaptcha validation fails, return an error.
422 if ( is_array( $hcaptcha_validation_result ) && isset( $hcaptcha_validation_result['success'] ) && false === $hcaptcha_validation_result['success'] ) {
423 $this->recaptcha_error_response( 'hcaptcha', $hcaptcha_validation_result );
424 }
425 }
426
427 if ( isset( $form_data['srfm-honeypot-field'] ) && empty( $form_data['srfm-honeypot-field'] ) ) {
428 if ( ! empty( $google_captcha_secret_key ) ) {
429 if ( isset( $form_data['sureforms_form_submit'] ) ) {
430 $secret_key = $google_captcha_secret_key;
431 $ipaddress = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
432 $captcha_response = $form_data['g-recaptcha-response'];
433 $url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $captcha_response . '&ip=' . $ipaddress;
434
435 $response = wp_remote_get( $url );
436
437 if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
438 $json_string = wp_remote_retrieve_body( $response );
439 $data = (array) json_decode( $json_string, true );
440 } else {
441 $data = [];
442 }
443 $sureforms_captcha_data = $data;
444
445 } else {
446 wp_send_json_error(
447 [
448 'message' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
449 ]
450 );
451 }
452 if ( isset( $sureforms_captcha_data['success'] ) && true === $sureforms_captcha_data['success'] ) {
453 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
454 }
455
456 $this->recaptcha_error_response( 'g-recaptcha', $sureforms_captcha_data );
457 }
458
459 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
460 }
461
462 if ( ! isset( $form_data['srfm-honeypot-field'] ) ) {
463 // If honeypot is enabled globally, the missing field means a bot stripped it.
464 $srfm_security_options = get_option( 'srfm_security_settings_options' );
465 if ( is_array( $srfm_security_options ) && ! empty( $srfm_security_options['srfm_honeypot'] ) ) {
466 wp_send_json_error(
467 [
468 'message' => __( 'Your submission was flagged as spam. Please try again.', 'sureforms' ),
469 ]
470 );
471 }
472
473 if ( ! empty( $google_captcha_secret_key ) ) {
474 if ( isset( $form_data['sureforms_form_submit'] ) ) {
475 $secret_key = $google_captcha_secret_key;
476 $ipaddress = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
477 $captcha_response = $form_data['g-recaptcha-response'];
478 $url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $captcha_response . '&ip=' . $ipaddress;
479
480 $response = wp_remote_get( $url );
481
482 if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
483 $json_string = wp_remote_retrieve_body( $response );
484 $data = (array) json_decode( $json_string, true );
485 } else {
486 $data = [];
487 }
488 $sureforms_captcha_data = $data;
489
490 } else {
491 wp_send_json_error(
492 [
493 'message' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
494 ]
495 );
496 }
497 if ( true === $sureforms_captcha_data['success'] ) {
498 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
499 }
500
501 $this->recaptcha_error_response( 'g-recaptcha', $sureforms_captcha_data );
502 }
503
504 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
505 }
506
507 wp_send_json_error(
508 [
509 'message' => __( 'Your submission was flagged as spam. Please try again.', 'sureforms' ),
510 ]
511 );
512 }
513
514 /**
515 * Send Email and Create Entry.
516 *
517 * @param array<string> $form_data Request object or array containing form data.
518 * @since 0.0.1
519 * @return array<mixed> Array containing the response data.
520 */
521 public function handle_form_entry( $form_data ) {
522 // Filter the form data.
523 $form_data = apply_filters( 'srfm_form_submit_data', $form_data );
524 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
525 wp_send_json_error(
526 [
527 'message' => __( 'Form data was not found.', 'sureforms' ),
528 'position' => 'header',
529 ]
530 );
531 } elseif ( isset( $form_data['error'] ) ) {
532 wp_send_json_error(
533 [
534 'message' => is_string( $form_data['error'] ) ? $form_data['error'] : __( 'Form data is not found.', 'sureforms' ),
535 'position' => 'header',
536 ]
537 );
538 }
539
540 $id = sanitize_text_field( $form_data['form-id'] );
541
542 // Get the compliance settings.
543 $compliance = get_post_meta( Helper::get_integer_value( $id ), '_srfm_compliance', true );
544 $gdpr = '';
545 $do_not_store_entries = '';
546
547 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
548 $gdpr = $compliance[0]['gdpr'] ?? '';
549 $do_not_store_entries = $compliance[0]['do_not_store_entries'] ?? '';
550 }
551
552 // Check if the form data contains 'srfm_addresses' and is not empty.
553 if ( ! empty( $form_data['srfm_addresses'] ) ) {
554 // Assign the addresses to the class property for further processing.
555 $this->addresses = $form_data['srfm_addresses'];
556 // Remove the address data from the form data to avoid redundancy.
557 unset( $form_data['srfm_addresses'] );
558 }
559
560 $form_data = apply_filters( 'srfm_before_fields_processing', $form_data );
561
562 $submission_data = $this->process_form_fields( $form_data );
563
564 $modified_message = $this->prepare_submission_data( $submission_data );
565
566 $form_before_submission_data = [
567 'form_id' => $id ? intval( $id ) : '',
568 'data' => $modified_message,
569 ];
570
571 /**
572 * Fires before submission process starts.
573 */
574 do_action( 'srfm_before_submission', $form_before_submission_data );
575
576 $name = sanitize_text_field( get_the_title( intval( $id ) ) );
577 $send_email = $this->send_email( $id, $submission_data, $form_data );
578 $emails = [];
579
580 if ( $send_email ) {
581 $emails = $send_email['emails'];
582 }
583
584 // Check if GDPR is enabled and do not store entries is enabled.
585 // If so, send email and do not store entries.
586 if ( $gdpr && $do_not_store_entries ) {
587
588 $form_submit_response = [
589 'success' => true,
590 'form_id' => $id ? intval( $id ) : '',
591 'to_emails' => $emails,
592 'form_name' => $name ? esc_attr( $name ) : '',
593 'message' => Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data ),
594 'data' => $modified_message,
595 ];
596
597 do_action( 'srfm_form_submit', $form_submit_response );
598
599 /**
600 * Hook for enabling background processes.
601 *
602 * @param array $form_data form data related to submission.
603 */
604 $form_data['form_id'] = $id ? intval( $id ) : '';
605 do_action( 'srfm_after_submission_process', $form_data );
606
607 return [
608 'success' => true,
609 'message' => Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data ),
610 'data' => [
611 'name' => $name,
612 'after_submit' => false,
613 ],
614 'redirect_url' => Generate_Form_Markup::get_redirect_url( $form_data, $submission_data ),
615 ];
616
617 }
618
619 $global_setting_options = get_option( 'srfm_general_settings_options' );
620
621 // If GDPR is enabled, do not store IP, browser, and device info.
622 // If not, store IP, browser, and device info.
623 $user_ip = '';
624 $browser_name = '';
625 $device_name = '';
626 if ( ! $gdpr ) {
627 $srfm_ip_log = is_array( $global_setting_options ) && isset( $global_setting_options['srfm_ip_log'] ) ? $global_setting_options['srfm_ip_log'] : '';
628
629 $user_ip = $srfm_ip_log && isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
630 $browser = new Browser();
631 $browser_name = sanitize_text_field( $browser->getBrowser() );
632 $device_name = sanitize_text_field( $browser->getPlatform() );
633 }
634
635 $form_markup = get_the_content( null, false, Helper::get_integer_value( $form_data['form-id'] ) );
636 $pattern = '/"label":"(.*?)"/';
637 preg_match_all( $pattern, $form_markup, $matches );
638 $submission_info = [
639 'user_ip' => $user_ip,
640 'browser_name' => $browser_name,
641 'device_name' => $device_name,
642 ];
643 $entries_data = [
644 'form_id' => $id,
645 'form_data' => $submission_data,
646 'submission_info' => $submission_info,
647 'created_at' => current_time( 'mysql' ),
648 ];
649 if ( is_user_logged_in() ) {
650 // If user is logged in then save their user id.
651 $entries_data['user_id'] = get_current_user_id();
652 }
653
654 $entries_data = apply_filters(
655 'srfm_before_entry_data',
656 $entries_data,
657 [
658 'form_data' => $form_data,
659 'submission_data' => $submission_data,
660 ]
661 );
662
663 $entry_id = Entries::add( $entries_data );
664 if ( $entry_id ) {
665
666 $confirmation_message = Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data );
667
668 $response = [
669 'success' => true,
670 'message' => $confirmation_message,
671 'data' => [
672 'name' => $name,
673 'submission_id' => $entry_id,
674 'after_submit' => true,
675 'after_submit_nonce' => wp_create_nonce( 'srfm_after_submission_' . Helper::get_string_value( $entry_id ) ),
676 ],
677 'redirect_url' => Generate_Form_Markup::get_redirect_url( $form_data, $submission_data ),
678 ];
679
680 $form_submit_response = apply_filters(
681 'srfm_form_submit_response',
682 [
683 'success' => true,
684 'form_id' => $id ? intval( $id ) : '',
685 'entry_id' => intval( $entry_id ),
686 'to_emails' => $emails,
687 'form_name' => $name ? esc_attr( $name ) : '',
688 'message' => $confirmation_message,
689 'data' => $modified_message,
690 ]
691 );
692
693 do_action( 'srfm_form_submit', $form_submit_response );
694 } else {
695 $response = [
696 'success' => false,
697 'message' => __( 'Unable to submit form. Please try again.', 'sureforms' ),
698 ];
699 }
700
701 /**
702 * Filter the form submission response.
703 *
704 * @param array<mixed> $response The response data.
705 * @param array<string> $form_data The original form data.
706 * @param array<mixed> $submission_data The processed submission data.
707 * @since 2.4.0
708 */
709 return apply_filters( 'srfm_form_submission_response', $response, $form_data, $submission_data );
710 }
711
712 /**
713 * Prepare submission data.
714 *
715 * @param array<mixed> $submission_data Submission data.
716 * @since 0.0.7
717 * @return array<mixed> Modified submission data.
718 */
719 public function prepare_submission_data( $submission_data ) {
720 $modified_message = [];
721 foreach ( $submission_data as $key => $value ) {
722 $parts = explode( '-lbl-', $key );
723 $label = '';
724
725 /**
726 * Filters submission data for field processing.
727 *
728 * This filter allows customization of how individual fields are processed
729 * during submission data preparation. Plugins can modify field values,
730 * labels, or exclude specific fields from the final submission data.
731 *
732 * @since 1.11.0
733 *
734 * @param array $field_data {
735 * Field data for processing.
736 *
737 * @type array $block_parts The field key split by '-lbl-' delimiter.
738 * @type string $field_key The original field key from submission data.
739 * @type mixed $field_value The field value from submission data.
740 * }
741 */
742 $should_add_field_row = apply_filters(
743 'srfm_prepare_submission_data',
744 [
745 'block_parts' => $parts,
746 'field_key' => $key,
747 'field_value' => $value,
748 ]
749 );
750
751 // If we get the label and value from the filter, then use it.
752 if ( ! empty( $should_add_field_row['label'] ) && ! empty( $should_add_field_row['value'] ) ) {
753 $modified_message[ $should_add_field_row['label'] ] = $should_add_field_row['value'];
754 continue;
755 }
756
757 if ( ! empty( $parts[1] ) ) {
758 $tokens = explode( '-', $parts[1] );
759 if ( count( $tokens ) > 1 ) {
760 $label = implode( '-', array_slice( $tokens, 1 ) );
761 }
762
763 $fields = explode( '-', $parts[0] );
764
765 // Since the upload field returns an array of file URLs, we need to implode them with a comma.
766 if ( 'upload' === $fields[1] && ! empty( $value ) && is_array( $value ) ) {
767 $modified_message[ $label ] = implode( ', ', array_map( 'rawurldecode', $value ) );
768 } else {
769 $modified_message[ $label ] = html_entity_decode( esc_attr( Helper::get_string_value( $value ) ) );
770 }
771 }
772 }
773
774 // If the address is not empty, add it to the submission data.
775 // We are providing this for third-party integrations like Ottokit.
776 // They can use compact addresses such as permanent address, temporary address, etc.
777 // The address will be structured as field 1, field 2, and so on.
778 if ( ! empty( $this->addresses ) ) {
779 // Address will be JSON stringified, so decode it.
780 $address = json_decode( wp_unslash( $this->addresses ), true );
781 if ( ! empty( $address ) && is_array( $address ) ) {
782 $modified_message = array_merge( $modified_message, $address );
783 }
784 }
785
786 return apply_filters( 'srfm_update_prepared_submission_data', $modified_message );
787 }
788
789 /**
790 * Parse an email notification template and generate the necessary components for sending an email.
791 *
792 * @param array<mixed> $submission_data An associative array containing submission data to be used in the email template.
793 * @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'.
794 * @param array<string> $form_data Request object or array containing form data.
795 * @since 1.3.0
796 * @return array<string,string> An associative array containing 'to', 'subject', 'message', and 'headers' for the email.
797 */
798 public static function parse_email_notification_template( $submission_data, $item, $form_data = [] ) {
799 $smart_tags = Smart_Tags::get_instance();
800
801 $to = Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_to'], $submission_data ) );
802 $subject = Helper::get_string_value( $smart_tags->process_smart_tags( $item['subject'], $submission_data, $form_data ) );
803 $email_body = Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_body'], $submission_data, $form_data ) );
804 $is_raw_format = isset( $item['is_raw_format'] ) && true === $item['is_raw_format'];
805
806 /**
807 * Sanitize the email body after smart tag substitution to prevent XSS.
808 *
809 * After process_smart_tags() resolves {form:slug} placeholders, the body may contain
810 * raw user-submitted values that must not render as executable HTML in email clients.
811 * wp_kses_post() strips dangerous markup (script, on* handlers, javascript: URIs)
812 * while preserving all legitimate email formatting (tables, links, bold, etc.).
813 *
814 * Note: {all_data} is not a recognised smart tag and remains a literal placeholder
815 * at this point; it is substituted later by process_all_data_tag() which applies
816 * its own per-field escaping, so this call does not interfere with that path.
817 *
818 * @since 2.5.2
819 */
820 $email_body = wp_kses_post( $email_body );
821
822 $email_template = new Email_Template();
823 $message = $is_raw_format
824 ? $email_template->render_raw( $submission_data, $email_body )
825 : $email_template->render( $submission_data, $email_body );
826 $headers = 'X-Mailer: PHP/' . phpversion() . "\r\n";
827 $headers .= "Content-Type: text/html; charset=utf-8\r\n";
828
829 // Add the From: to the headers.
830 $headers .= self::add_from_data_in_header( $submission_data, $item, $smart_tags );
831
832 // Handle Reply-To with proper sanitization.
833 if ( isset( $item['email_reply_to'] ) && ! empty( $item['email_reply_to'] ) ) {
834 $headers .= 'Reply-To: ' . Helper::sanitize_email_header( Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_reply_to'], $submission_data ) ) ) . "\r\n";
835 }
836
837 // Handle CC with proper sanitization.
838 if ( isset( $item['email_cc'] ) && ! empty( $item['email_cc'] ) ) {
839 $headers .= 'Cc: ' . Helper::sanitize_email_header( Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_cc'], $submission_data ) ) ) . "\r\n";
840 }
841
842 // Handle BCC with proper sanitization.
843 if ( isset( $item['email_bcc'] ) && ! empty( $item['email_bcc'] ) ) {
844 $headers .= 'Bcc: ' . Helper::sanitize_email_header( Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_bcc'], $submission_data ) ) ) . "\r\n";
845 }
846
847 return compact( 'to', 'subject', 'message', 'headers' );
848 }
849
850 /**
851 * Send Email.
852 *
853 * @param string $id Form ID.
854 * @param array<mixed> $submission_data Submission data.
855 * @param array<string> $form_data Request object or array containing form data.
856 * @since 0.0.1
857 * @return array<mixed> Array containing the response data.
858 */
859 public static function send_email( $id, $submission_data, $form_data = [] ) {
860 $email_notification = get_post_meta( intval( $id ), '_srfm_email_notification' );
861 $is_mail_sent = false;
862 $emails = [];
863
864 // Filter to determine whether the email notification should be sent.
865 $email_notification = apply_filters( 'srfm_email_notification_should_send', $email_notification, $submission_data, $form_data );
866
867 if ( is_iterable( $email_notification ) ) {
868 $entries_db_instance = Entries::get_instance();
869 $log_key = $entries_db_instance->add_log( __( 'Email notification passed to the sending server', 'sureforms' ) );
870
871 foreach ( $email_notification as $notification ) {
872 foreach ( $notification as $item ) {
873 if ( true === $item['status'] ) {
874
875 $parsed = self::parse_email_notification_template( $submission_data, $item, $form_data );
876
877 // Allow filtering of the email data before it is sent.
878 $parsed = apply_filters( 'srfm_email_notification', $parsed, $submission_data, $item, $form_data );
879
880 // Trigger an action before sending the email, allowing additional processing or logging.
881 do_action( 'srfm_before_email_send', $parsed, $submission_data, $item, $form_data );
882
883 $notification_id = isset( $item['id'] ) ? intval( $item['id'] ) : 0;
884
885 /**
886 * Filter to determine whether the email should be sent.
887 *
888 * @since 1.10.1
889 */
890 $should_send_email = apply_filters(
891 'srfm_should_send_email',
892 true,
893 $notification_id,
894 $id,
895 $form_data,
896 );
897
898 if ( ! wp_validate_boolean( $should_send_email ) ) {
899 continue;
900 }
901
902 /**
903 * Temporary override the content type for wp_mail.
904 * This helps us from breaking of content type from other plugins.
905 *
906 * @since 1.2.2
907 */
908 add_filter(
909 'wp_mail_content_type',
910 static function() {
911 return 'text/html'; // We need "text/html" content type to render our emails.
912 },
913 99
914 );
915
916 /**
917 * Start sending email.
918 * Wrapping it in the buffer because when some plugin such as zoho mail, overrides the wp_mail
919 * function and any exception is thrown ( Or printed ) from that plugin side, it affects the JSON response.
920 * So, to make sure such exceptions doesn't affect our JSON response, we are wrapping it inside buffer.
921 *
922 * Try-Catch does not work because the notice or errors might be echoed by other plugins rather than thrown as an exception.
923 *
924 * @since 1.2.2
925 */
926 $sent = false;
927 ob_start();
928 $sent = wp_mail( $parsed['to'], $parsed['subject'], $parsed['message'], $parsed['headers'] );
929 if ( ! $sent ) {
930 // Fallback to default PHP mail if for some reasons wp_mail fails.
931 $sent = mail( $parsed['to'], $parsed['subject'], $parsed['message'], $parsed['headers'] );
932 }
933 $email_report = ob_get_clean(); // Catch any printed notice/errors/message for reports.
934
935 if ( is_int( $log_key ) ) {
936 if ( true === $sent ) {
937 $entries_db_instance->update_log(
938 $log_key,
939 null,
940 [
941 /* translators: Here, %s is the comma separated emails list. */
942 sprintf( __( 'Email notification recipient: %s', 'sureforms' ), esc_html( $parsed['to'] ) ),
943 ]
944 );
945 } else {
946 $reason = ! empty( $email_report )
947 ? esc_html( $email_report )
948 : ( ! Helper::is_any_smtp_plugin_active()
949 ? esc_html__( 'No SMTP plugin detected. Please configure an SMTP plugin to enable email sending.', 'sureforms' )
950 : esc_html__( 'Email sending failed for an unknown reason.', 'sureforms' )
951 );
952
953 $entries_db_instance->update_log(
954 $log_key,
955 null,
956 [
957 sprintf(
958 /* translators: Here, %1$s is the comma separated emails list and %2$s is error report ( if any ). */
959 __(
960 'Email server was unable to send the email notification. Recipient: %1$s. Reason: %2$s',
961 'sureforms'
962 ),
963 esc_html( $parsed['to'] ),
964 $reason
965 ),
966 ]
967 );
968
969 }
970 }
971
972 // Trigger an action after the email is sent, allowing additional processing or logging.
973 do_action(
974 'srfm_after_email_send',
975 $parsed,
976 $submission_data,
977 $item,
978 $form_data
979 );
980
981 $is_mail_sent = $sent;
982 $emails[] = $parsed['to'];
983 }
984 }
985 }
986
987 if ( empty( $emails ) ) {
988 $entries_db_instance->reset_logs();
989 $entries_db_instance->add_log( __( 'No emails were sent.', 'sureforms' ) );
990 }
991 }
992
993 return [
994 'success' => $is_mail_sent,
995 'emails' => $emails,
996 ];
997 }
998
999 /**
1000 * Retrieve all entries data for a specific form ID to check for unique values.
1001 *
1002 * @since 0.0.1
1003 * @return void
1004 */
1005 public function field_unique_validation() {
1006 if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'unique_validation_nonce' ) ) {
1007 $error_message = __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' );
1008 $error_data = [
1009 'error' => $error_message,
1010 ];
1011 wp_send_json_error( $error_data );
1012 }
1013
1014 global $wpdb;
1015 $id = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : 0;
1016 $meta_value = $id;
1017
1018 if ( ! $meta_value ) {
1019 $error_message = __( 'Invalid form ID.', 'sureforms' );
1020 $error_data = [
1021 'error' => $error_message,
1022 ];
1023 wp_send_json_error( $error_data );
1024 }
1025
1026 $_POST = array_map( 'wp_unslash', $_POST );
1027
1028 // Get the entry IDs for the particualr form to perform unique field validation.
1029 $entry_ids = Entries::get_all_entry_ids_for_form( $id );
1030
1031 $all_form_entries = [];
1032 $keys = array_keys( $_POST );
1033 $length = count( $keys );
1034
1035 for ( $i = 3; $i < $length; $i++ ) {
1036 $key = $keys[ $i ];
1037 $value = isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : '';
1038 $key = str_replace( '_', ' ', $keys[ $i ] );
1039
1040 foreach ( $entry_ids as $entry_id ) {
1041 $entry_id = is_array( $entry_id ) ? Helper::get_integer_value( $entry_id['ID'] ) : 0;
1042 $form_data = Entries::get_form_data( $entry_id );
1043 if ( is_array( $form_data ) && isset( $form_data[ $key ] ) && $form_data[ $key ] === $value ) {
1044 $obj = [ $key => 'not unique' ];
1045 array_push( $all_form_entries, $obj );
1046 break;
1047 }
1048 }
1049 }
1050
1051 $results = [
1052 'data' => $all_form_entries,
1053 ];
1054
1055 wp_send_json( $results );
1056 }
1057
1058 /**
1059 * Function to save allowed block data.
1060 *
1061 * @since 0.0.1
1062 * @return void
1063 */
1064 public function srfm_global_update_allowed_block() {
1065 if ( ! Helper::current_user_can() ) {
1066 wp_send_json_error();
1067 }
1068
1069 if ( ! check_ajax_referer( 'srfm_ajax_nonce', 'security', false ) ) {
1070 wp_send_json_error();
1071 }
1072
1073 if ( ! empty( $_POST['defaultAllowedQuickSidebarBlocks'] ) ) {
1074 $srfm_default_allowed_quick_sidebar_blocks = json_decode( sanitize_text_field( wp_unslash( $_POST['defaultAllowedQuickSidebarBlocks'] ) ), true );
1075 Helper::update_admin_settings_option( 'srfm_quick_sidebar_allowed_blocks', $srfm_default_allowed_quick_sidebar_blocks );
1076 wp_send_json_success();
1077 }
1078 wp_send_json_error();
1079 }
1080
1081 /**
1082 * Function to save enable/disable data.
1083 *
1084 * @since 0.0.1
1085 * @return void
1086 */
1087 public function srfm_global_sidebar_enabled() {
1088 if ( ! Helper::current_user_can() ) {
1089 wp_send_json_error();
1090 }
1091
1092 if ( ! check_ajax_referer( 'srfm_ajax_nonce', 'security', false ) ) {
1093 wp_send_json_error();
1094 }
1095
1096 if ( ! empty( $_POST['enableQuickActionSidebar'] ) ) {
1097 $srfm_enable_quick_action_sidebar = ( 'enabled' === $_POST['enableQuickActionSidebar'] ? 'enabled' : 'disabled' );
1098 Helper::update_admin_settings_option( 'srfm_enable_quick_action_sidebar', $srfm_enable_quick_action_sidebar );
1099 wp_send_json_success();
1100 }
1101 wp_send_json_error();
1102 }
1103
1104 /**
1105 * Send error response for reCAPTCHA validation failure.
1106 *
1107 * @param string $type The type of CAPTCHA used. Accepted values: 'g-recaptcha', 'hcaptcha', 'cf-turnstile'.
1108 * @param array<mixed> $api_response The response returned from the CAPTCHA validation API.
1109 * @since 1.7.0
1110 * @return void
1111 */
1112 public function recaptcha_error_response( $type, $api_response ) {
1113 $error_message = $this->recaptcha_error_message( $type, $api_response );
1114 $response = array_merge(
1115 [
1116 'api_response' => $api_response,
1117 ],
1118 $error_message
1119 );
1120
1121 wp_send_json_error( $response );
1122 }
1123
1124 /**
1125 * Get the error message for a CAPTCHA validation failure based on the service type and API response.
1126 *
1127 * @param string $type The type of CAPTCHA used. Accepted values: 'g-recaptcha', 'hcaptcha', 'cf-turnstile'.
1128 * @param array<mixed> $api_response The response returned from the CAPTCHA validation API.
1129 * @since 1.7.0
1130 * @return array<string,string> An associative array containing the error message and a detailed message.
1131 */
1132 public function recaptcha_error_message( $type, $api_response ) {
1133
1134 if ( empty( $api_response['error-codes'] ) || ! is_array( $api_response['error-codes'] ) ) {
1135 return [
1136 'detail_message' => __( 'Captcha validation failed. No error code provided.', 'sureforms' ),
1137 'message' => __( 'Captcha validation failed.', 'sureforms' ),
1138 ];
1139 }
1140
1141 /**
1142 * Note: The error codes are not translated because these messages are intended for debugging purposes.
1143 * Translating them would make debugging difficult. These error messages are primarily for developers or administrators.
1144 * A generic message will be displayed to the user, while detailed error information will be logged or shown in the console.
1145 */
1146
1147 // Google reCAPTCHA error codes.
1148 // Reference: (https://developers.google.com/recaptcha/docs/verify#error-code-reference).
1149 $google_recaptcha_error = [
1150 'missing-input-secret' => 'The secret parameter is missing.',
1151 'invalid-input-secret' => 'The secret parameter is invalid or malformed.',
1152 'missing-input-response' => 'The response parameter is missing.',
1153 'invalid-input-response' => 'The response parameter is invalid or malformed.',
1154 'bad-request' => 'The request is invalid or malformed.',
1155 'timeout-or-duplicate' => 'The response is no longer valid: either is too old or has been used previously.',
1156 ];
1157
1158 // hCaptcha error codes.
1159 // Reference: (https://docs.hcaptcha.com/#siteverify-error-codes).
1160 $hcaptcha_errors = [
1161 'missing-input-secret' => 'Your secret key is missing.',
1162 'invalid-input-secret' => 'Your secret key is invalid or malformed.',
1163 'missing-input-response' => 'The response parameter (verification token) is missing.',
1164 'invalid-input-response' => 'The response parameter (verification token) is invalid or malformed.',
1165 'expired-input-response' => 'The response parameter (verification token) is expired. (120s default)',
1166 'already-seen-response' => 'The response parameter (verification token) was already verified once.',
1167 'bad-request' => 'The request is invalid or malformed.',
1168 'missing-remoteip' => 'The remoteip parameter is missing.',
1169 'invalid-remoteip' => 'The remoteip parameter is not a valid IP address or blinded value.',
1170 'not-using-dummy-passcode' => 'You have used a testing sitekey but have not used its matching secret.',
1171 'sitekey-secret-mismatch' => 'The sitekey is not registered with the provided secret.',
1172 ];
1173
1174 // Cloudflare Turnstile error codes.
1175 // Reference: (https://developers.cloudflare.com/turnstile/get-started/server-side-validation/).
1176 $cf_turnstile_errors = [
1177 'missing-input-secret' => 'The secret parameter was not passed.',
1178 'invalid-input-secret' => 'The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.',
1179 'missing-input-response' => 'The response parameter (token) was not passed.',
1180 '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.',
1181 'bad-request' => 'The request was rejected because it was malformed.',
1182 '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.',
1183 'internal-error' => 'An internal error happened while validating the response. The request can be retried.',
1184 ];
1185
1186 $error_code = $api_response['error-codes'][0] ?? 'no-error-code';
1187
1188 $captcha_title = '';
1189 $captcha_message = '';
1190 switch ( $type ) {
1191 case 'g-recaptcha':
1192 $captcha_title = __( 'Google reCAPTCHA', 'sureforms' );
1193 $captcha_message = $google_recaptcha_error[ $error_code ];
1194 break;
1195 case 'hcaptcha':
1196 $captcha_title = __( 'hCaptcha', 'sureforms' );
1197 $captcha_message = $hcaptcha_errors[ $error_code ];
1198 break;
1199 case 'cf-turnstile':
1200 $captcha_title = __( 'Cloudflare Turnstile', 'sureforms' );
1201 $captcha_message = $cf_turnstile_errors[ $error_code ];
1202 break;
1203 default:
1204 $captcha_title = __( 'Unknown Captcha', 'sureforms' );
1205 $captcha_message = __( 'Invalid captcha type.', 'sureforms' );
1206 break;
1207 }
1208
1209 $detail_message = sprintf(
1210 '%s: %s <br> Error Code: %s',
1211 $captcha_title,
1212 $captcha_message ?? 'Unknown error occurred.',
1213 $error_code
1214 );
1215
1216 $message = sprintf(
1217 /* translators: %s is the captcha title. */
1218 __( '%s verification failed. Please contact your site administrator.', 'sureforms' ),
1219 $captcha_title
1220 );
1221
1222 return [
1223 'log_message' => $detail_message, // This variable is used for logging purposes, such as displaying detailed error information in the console on the front end.
1224 'message' => $message,
1225 ];
1226 }
1227
1228 /**
1229 * Process and sanitize SureForms field data from submitted form data.
1230 *
1231 * @param array<mixed> $form_data Raw form data from submission.
1232 *
1233 * @since 1.11.0
1234 * @return array Processed and sanitized submission data.
1235 */
1236 private function process_form_fields( $form_data ) {
1237 $submission_data = [];
1238
1239 $form_data_keys = array_keys( $form_data );
1240 $form_data_count = count( $form_data );
1241
1242 for ( $i = 0; $i < $form_data_count; $i++ ) {
1243 $key = strval( $form_data_keys[ $i ] );
1244
1245 /**
1246 * This will allow to pass only sureforms fields
1247 * checking -lbl- as thats mandatory for in key of sureforms fields.
1248 */
1249 if ( false === str_contains( $key, '-lbl-' ) ) {
1250 continue;
1251 }
1252
1253 $value = $form_data[ $key ];
1254
1255 $field_name = htmlspecialchars( str_replace( '_', ' ', $key ) );
1256
1257 $field_block_name = Helper::get_block_name_from_field( $field_name );
1258
1259 /**
1260 * Filters the field value during form submission processing.
1261 *
1262 * This filter allows the Pro plugin to process and modify field values before they are saved.
1263 * The Pro plugin can implement custom sanitization, validation and escaping logic for its
1264 * specialized field types. When this filter is used by Pro, the core plugin will skip its
1265 * default validation.
1266 *
1267 * @since 1.11.0
1268 *
1269 * @param mixed $value The raw field value from form submission.
1270 * @param array $field_data Field information array containing:
1271 * - 'field_name': The field name/key
1272 * - 'field_block_name': The block type identifier
1273 * @return array {
1274 * Processed field value data
1275 *
1276 * @type bool $is_processed Whether the value was processed by Pro plugin
1277 * @type mixed $value The processed and sanitized field value
1278 * }
1279 */
1280 $process_field_value = apply_filters(
1281 'srfm_process_field_value',
1282 $value,
1283 [
1284 'field_name' => $field_name,
1285 'field_block_name' => $field_block_name,
1286 ]
1287 );
1288
1289 if ( is_array( $process_field_value ) && ! empty( $process_field_value['is_processed'] ) && ! empty( $process_field_value['value'] ) ) {
1290 $submission_data[ $field_name ] = $process_field_value['value'];
1291 continue;
1292 }
1293
1294 /**
1295 * Need to remove this refactor array value handling.
1296 *
1297 * The current array-based value handling needs to be replaced with:
1298 * 1. Block-specific value processing based on block type.
1299 * 2. Move premium features to pro version.
1300 * 3. Implement value processing through filters for extensibility.
1301 *
1302 * This will improve code organization and maintainability while properly
1303 * separating free/pro functionality.
1304 */
1305
1306 // If the field is an array, encode the values. This is to add support for multi-upload field.
1307 if ( is_array( $value ) ) {
1308 $submission_data[ $field_name ] =
1309 array_map(
1310 static function ( $val ) {
1311 return rawurlencode( $val );
1312 },
1313 $value
1314 );
1315 } else {
1316 $submission_data[ $field_name ] = is_string( $value ) ? htmlspecialchars( $value ) : $value;
1317 }
1318 }
1319
1320 return apply_filters( 'srfm_before_prepare_submission_data', $submission_data );
1321 }
1322
1323 /**
1324 * Add From email and name in the header.
1325 *
1326 * @param array<mixed> $submission_data Submission data.
1327 * @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'.
1328 * @param Smart_Tags $smart_tags Smart Tags instance.
1329 * @since 1.6.1
1330 * @return string The formatted "From" email header.
1331 */
1332 private static function add_from_data_in_header( $submission_data, $item, $smart_tags ) {
1333 $from_name = is_array( $item ) && ! empty( $item['from_name'] ) ? sanitize_text_field( Helper::get_string_value( $item['from_name'] ) ) : '{site_title}';
1334 $from_email = is_array( $item ) && ! empty( $item['from_email'] ) ? Helper::get_string_value( $item['from_email'] ) : '{admin_email}';
1335
1336 // Check if the email contains smart tags. If not, validate the email.
1337 $is_valid_email = true;
1338 if ( ! str_contains( $from_email, '{' ) && ! str_contains( $from_email, '}' ) ) {
1339 $is_valid_email = filter_var( $from_email, FILTER_VALIDATE_EMAIL );
1340 }
1341 // if the email is not valid, set it to the admin email.
1342 if ( ! $is_valid_email ) {
1343 $from_email = Helper::get_string_value( get_option( 'admin_email' ) );
1344 }
1345
1346 return 'From: ' . esc_html( Helper::get_string_value( $smart_tags->process_smart_tags( $from_name, $submission_data ) ) ) . ' <' . esc_html( Helper::get_string_value( $smart_tags->process_smart_tags( $from_email, $submission_data ) ) ) . '>' . "\r\n";
1347 }
1348 }
1349