PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.6.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.6.0
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 2.6.0, at inc/form-submit.php

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