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

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

1,365 lines 49.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Sureforms Submit Class file.
4 *
5 * @package sureforms.
6 * @since 0.0.1
7 */
8
9 namespace SRFM\Inc;
10
11 use SRFM\Inc\Database\Tables\Entries;
12 use SRFM\Inc\Email\Email_Template;
13 use SRFM\Inc\Lib\Browser\Browser;
14 use SRFM\Inc\Traits\Get_Instance;
15 use WP_Error;
16 use WP_REST_Server;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit; // Exit if accessed directly.
20 }
21
22 if ( ! function_exists( 'wp_handle_upload' ) ) {
23 require_once ABSPATH . 'wp-admin/includes/file.php';
24 }
25
26 /**
27 * Sureforms Submit Class.
28 *
29 * @since 0.0.1
30 */
31 class Form_Submit {
32 use Get_Instance;
33
34 /**
35 * Namespace.
36 *
37 * @var string
38 */
39 protected $namespace = 'sureforms/v1';
40
41 /**
42 * Addresses.
43 *
44 * @var string
45 * @since 1.6.1
46 */
47 private $addresses = '';
48
49 /**
50 * Constructor
51 *
52 * @since 0.0.1
53 */
54 public function __construct() {
55 add_action( 'rest_api_init', [ $this, 'register_custom_endpoint' ] );
56 add_action( 'wp_ajax_validation_ajax_action', [ $this, 'field_unique_validation' ] );
57 add_action( 'wp_ajax_nopriv_validation_ajax_action', [ $this, 'field_unique_validation' ] );
58 // for quick action bar.
59 add_action( 'wp_ajax_srfm_global_update_allowed_block', [ $this, 'srfm_global_update_allowed_block' ] );
60 add_action( 'wp_ajax_srfm_global_sidebar_enabled', [ $this, 'srfm_global_sidebar_enabled' ] );
61 }
62
63 /**
64 * Add custom API Route submit-form
65 *
66 * @return void
67 * @since 0.0.1
68 */
69 public function register_custom_endpoint() {
70 register_rest_route(
71 $this->namespace,
72 '/submit-form',
73 [
74 'methods' => WP_REST_Server::EDITABLE,
75 'callback' => [ $this, 'handle_form_submission' ],
76 'permission_callback' => [ $this, 'submit_form_permissions_check' ],
77 ]
78 );
79 }
80
81 /**
82 * Check whether a given request has permission to submit the form.
83 *
84 * Validates the HMAC-based submission token embedded in the page at render
85 * time. Tokens remain valid for up to 48 hours (four 12-hour windows), so
86 * they survive cached-page scenarios without any browser-side refresh call.
87 *
88 * @param \WP_REST_Request $request Incoming REST request.
89 * @since 2.6.0
90 * @return WP_Error|bool
91 */
92 public function submit_form_permissions_check( $request ) {
93 $token = Helper::get_string_value( $request->get_header( 'X-WP-Submit-Token' ) );
94 $form_id = absint( $request->get_param( 'form-id' ) );
95
96 if ( ! Submit_Token::verify( $token, $form_id ) ) {
97 return new WP_Error(
98 'srfm_token_invalid',
99 __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
100 [ 'status' => 403 ]
101 );
102 }
103
104 return true;
105 }
106
107 /**
108 * Check whether a given request has permission access route.
109 *
110 * @since 0.0.1
111 * @return WP_Error|bool
112 */
113 public function permissions_check() {
114 if ( ! Helper::current_user_can() ) {
115 return new WP_Error( 'rest_forbidden', __( 'Sorry, you do not have permission to access this resource.', 'sureforms' ), [ 'status' => rest_authorization_required_code() ] );
116 }
117 return true;
118 }
119
120 /**
121 * Validate Turnstile token
122 *
123 * @param string $secret_key Turnstile token.
124 * @param string|false $response Response.
125 * @param string|false $remote_ip Remote IP.
126 * @return array<mixed>|mixed Result of the validation.
127 */
128 public static function validate_turnstile_token( $secret_key, $response, $remote_ip ) {
129
130 if ( empty( $secret_key ) || ! is_string( $secret_key ) ) {
131 return [
132 'success' => false,
133 'error' => __( 'Cloudflare Turnstile secret key is invalid.', 'sureforms' ),
134 ];
135 }
136
137 if ( empty( $response ) ) {
138 return [
139 'success' => false,
140 'error' => __( 'Cloudflare Turnstile response is missing.', 'sureforms' ),
141 ];
142 }
143
144 $body = [
145 'secret' => $secret_key,
146 'response' => $response,
147 'remoteip' => $remote_ip,
148 ];
149
150 $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
151
152 $args = [
153 'body' => $body,
154 'timeout' => 15,
155 ];
156
157 $response = wp_remote_post( $url, $args );
158
159 if ( is_wp_error( $response ) ) {
160 $error_message = $response->get_error_message();
161 return [
162 'success' => false,
163 'error' => $error_message,
164 ];
165 }
166
167 return json_decode( wp_remote_retrieve_body( $response ), true );
168 }
169
170 /**
171 * Validate hCaptcha token
172 *
173 * @param string $secret_key hCaptcha token.
174 * @param string|false $response Response.
175 * @param string|false $remote_ip Remote IP.
176 * @since 0.0.5
177 * @return array<mixed>|mixed Result of the validation.
178 */
179 public static function validate_hcaptcha_token( $secret_key, $response, $remote_ip ) {
180
181 if ( empty( $secret_key ) || ! is_string( $secret_key ) ) {
182 return [
183 'success' => false,
184 'error' => __( 'hCaptcha secret key is invalid.', 'sureforms' ),
185 ];
186 }
187
188 if ( empty( $response ) ) {
189 return [
190 'success' => false,
191 'error' => __( 'hCaptcha response is missing.', 'sureforms' ),
192 ];
193 }
194
195 $body = [
196 'secret' => $secret_key,
197 'response' => $response,
198 'remoteip' => $remote_ip,
199 ];
200
201 $url = 'https://api.hcaptcha.com/siteverify';
202
203 $args = [
204 'body' => $body,
205 'timeout' => 15,
206 ];
207
208 $response = wp_remote_post( $url, $args );
209
210 if ( is_wp_error( $response ) ) {
211 $error_message = $response->get_error_message();
212 return [
213 'success' => false,
214 'error' => $error_message,
215 ];
216 }
217
218 return json_decode( wp_remote_retrieve_body( $response ), true );
219 }
220
221 /**
222 * Handle Form Submission
223 *
224 * @param \WP_REST_Request $request Request object or array containing form data.
225 * @since 0.0.1
226 * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
227 */
228 public function handle_form_submission( $request ) {
229 $form_data = Helper::sanitize_by_field_type( $request->get_params() );
230
231 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
232 wp_send_json_error( [ 'message' => __( 'Form data is not found.', 'sureforms' ) ] );
233 }
234
235 if ( empty( $form_data['form-id'] ) ) {
236 wp_send_json_error(
237 [
238 'message' => __( 'Form ID is missing.', 'sureforms' ),
239 'position' => 'header',
240 ]
241 );
242 }
243
244 $current_form_id = $form_data['form-id'];
245
246 /**
247 * If someone tries to access the form submit endpoint directly, we need to check if the form is restricted.
248 * If a form is loaded in a browser window and the limit exceeds then the form will not be submitted.
249 */
250 $form_id = Helper::get_integer_value( $current_form_id );
251 if ( Form_Restriction::is_form_restricted( $form_id ) ) {
252 $form_restriction = Form_Restriction::get_form_restriction_setting( $form_id );
253
254 // Get the scheduling state and appropriate message.
255 $scheduling_state = Form_Restriction::get_form_scheduling_state( $form_restriction );
256 $form_restriction_message = Form_Restriction::get_restriction_message_by_state( $scheduling_state, $form_restriction );
257
258 $form_restriction_message = apply_filters( 'srfm_form_restriction_message', $form_restriction_message, $form_id, $form_restriction );
259
260 wp_send_json_error(
261 [
262 'message' => $form_restriction_message,
263 ]
264 );
265 }
266
267 if ( apply_filters( 'srfm_additional_restriction_check', false, $form_id, $form_data ) ) {
268 wp_send_json_error(
269 [
270 'message' => apply_filters( 'srfm_additional_restriction_message', __( 'You do not have permission to submit this form.', 'sureforms' ), $form_id, $form_data ),
271 ]
272 );
273 }
274
275 // Check whether the form is valid.
276 if ( ! Helper::is_valid_form( $current_form_id ) ) {
277 wp_send_json_error(
278 [
279 'code' => 'srfm_invalid_form_id',
280 'message' => __( 'This form is no longer available.', 'sureforms' ),
281 ]
282 );
283 }
284
285 $validated_form_data = Field_Validation::validate_form_data( $form_data, $current_form_id );
286
287 if ( ! empty( $validated_form_data ) ) {
288 // Get the first error message to display as the main message.
289 $first_error = reset( $validated_form_data );
290
291 wp_send_json_error(
292 [
293 'message' => $first_error ?? __( 'Please check the form for errors.', 'sureforms' ),
294 'field_errors' => $validated_form_data,
295 ]
296 );
297 }
298
299 $security_type = Helper::get_meta_value( Helper::get_integer_value( $current_form_id ), '_srfm_captcha_security_type' );
300 $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 ) ) : '';
301
302 if ( 'none' !== $security_type ) {
303 $global_setting_options = get_option( 'srfm_security_settings_options' );
304 } else {
305 $global_setting_options = [];
306 }
307
308 if ( 'g-recaptcha' === $security_type ) {
309 switch ( $selected_captcha_type ) {
310 case 'v2-checkbox':
311 $key = 'srfm_v2_checkbox_secret_key';
312 break;
313 case 'v2-invisible':
314 $key = 'srfm_v2_invisible_secret_key';
315 break;
316 case 'v3-reCAPTCHA':
317 $key = 'srfm_v3_secret_key';
318 break;
319 default:
320 $key = '';
321 break;
322 }
323
324 $google_captcha_secret_key = is_array( $global_setting_options ) && isset( $global_setting_options[ $key ] ) ? $global_setting_options[ $key ] : '';
325 }
326
327 if ( 'cf-turnstile' === $security_type ) {
328 // Turnstile validation.
329 $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'] ) : '';
330 $cf_response = ! empty( $form_data['cf-turnstile-response'] ) && is_string( $form_data['cf-turnstile-response'] ) ? $form_data['cf-turnstile-response'] : '';
331
332 // if gdpr is enabled then set remote ip to empty.
333 $compliance = get_post_meta( Helper::get_integer_value( $current_form_id ), '_srfm_compliance', true );
334 $gdpr = false;
335
336 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
337 $gdpr = ! empty( $compliance[0]['gdpr'] ) ? $compliance[0]['gdpr'] : false;
338 }
339
340 // check if ip logging is disabled in global settings then set remote ip to empty.
341 $gb_general_settinionsgs_opt = get_option( 'srfm_general_settings_options' );
342 $srfm_ip_log = is_array( $gb_general_settinionsgs_opt ) && isset( $gb_general_settinionsgs_opt['srfm_ip_log'] ) ? $gb_general_settinionsgs_opt['srfm_ip_log'] : '';
343
344 $remote_ip = $gdpr || ( ! $srfm_ip_log ) ? '' : ( isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '' );
345
346 $turnstile_validation_result = self::validate_turnstile_token( $srfm_cf_turnstile_secret_key, $cf_response, $remote_ip );
347
348 // If the cloudflare validation fails, return an error.
349 if ( is_array( $turnstile_validation_result ) && isset( $turnstile_validation_result['success'] ) && false === $turnstile_validation_result['success'] ) {
350 $this->recaptcha_error_response( 'cf-turnstile', $turnstile_validation_result );
351 }
352 }
353
354 if ( 'hcaptcha' === $security_type ) {
355 $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'] ) : '';
356 $hcaptcha_response = ! empty( $form_data['h-captcha-response'] ) && is_string( $form_data['h-captcha-response'] ) ? $form_data['h-captcha-response'] : '';
357
358 // if gdpr is enabled then set remote ip to empty.
359 $compliance = get_post_meta( Helper::get_integer_value( $current_form_id ), '_srfm_compliance', true );
360 $gdpr = false;
361
362 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
363 $gdpr = ! empty( $compliance[0]['gdpr'] ) ? $compliance[0]['gdpr'] : false;
364 }
365
366 // check if ip logging is disabled in global settings then set remote ip to empty.
367 $gb_general_settings_options = get_option( 'srfm_general_settings_options' );
368 $srfm_ip_log = is_array( $gb_general_settings_options ) && isset( $gb_general_settings_options['srfm_ip_log'] ) ? $gb_general_settings_options['srfm_ip_log'] : '';
369
370 $remote_ip = $gdpr || ( ! $srfm_ip_log ) ? '' : ( isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '' );
371 $hcaptcha_validation_result = self::validate_hcaptcha_token( $srfm_hcaptcha_secret_key, $hcaptcha_response, $remote_ip );
372
373 // If the hcaptcha validation fails, return an error.
374 if ( is_array( $hcaptcha_validation_result ) && isset( $hcaptcha_validation_result['success'] ) && false === $hcaptcha_validation_result['success'] ) {
375 $this->recaptcha_error_response( 'hcaptcha', $hcaptcha_validation_result );
376 }
377 }
378
379 if ( isset( $form_data['srfm-honeypot-field'] ) && empty( $form_data['srfm-honeypot-field'] ) ) {
380 if ( ! empty( $google_captcha_secret_key ) ) {
381 if ( ! empty( $form_data['form-id'] ) ) {
382 $secret_key = $google_captcha_secret_key;
383 $ipaddress = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
384 $captcha_response = $form_data['g-recaptcha-response'];
385 $url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $captcha_response . '&ip=' . $ipaddress;
386
387 $response = wp_remote_get( $url );
388
389 if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
390 $json_string = wp_remote_retrieve_body( $response );
391 $data = (array) json_decode( $json_string, true );
392 } else {
393 $data = [];
394 }
395 $sureforms_captcha_data = $data;
396
397 } else {
398 wp_send_json_error(
399 [
400 'message' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
401 ]
402 );
403 }
404 if ( isset( $sureforms_captcha_data['success'] ) && true === $sureforms_captcha_data['success'] ) {
405 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
406 }
407
408 $this->recaptcha_error_response( 'g-recaptcha', $sureforms_captcha_data );
409 }
410
411 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
412 }
413
414 if ( ! isset( $form_data['srfm-honeypot-field'] ) ) {
415 // If honeypot is enabled globally, the missing field means a bot stripped it.
416 $srfm_security_options = get_option( 'srfm_security_settings_options' );
417 if ( is_array( $srfm_security_options ) && ! empty( $srfm_security_options['srfm_honeypot'] ) ) {
418 wp_send_json_error(
419 [
420 'message' => __( 'Your submission was flagged as spam. Please try again.', 'sureforms' ),
421 ]
422 );
423 }
424
425 if ( ! empty( $google_captcha_secret_key ) ) {
426 if ( ! empty( $form_data['form-id'] ) ) {
427 $secret_key = $google_captcha_secret_key;
428 $ipaddress = isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
429 $captcha_response = $form_data['g-recaptcha-response'];
430 $url = 'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $captcha_response . '&ip=' . $ipaddress;
431
432 $response = wp_remote_get( $url );
433
434 if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
435 $json_string = wp_remote_retrieve_body( $response );
436 $data = (array) json_decode( $json_string, true );
437 } else {
438 $data = [];
439 }
440 $sureforms_captcha_data = $data;
441
442 } else {
443 wp_send_json_error(
444 [
445 'message' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ),
446 ]
447 );
448 }
449 if ( true === $sureforms_captcha_data['success'] ) {
450 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
451 }
452
453 $this->recaptcha_error_response( 'g-recaptcha', $sureforms_captcha_data );
454 }
455
456 return rest_ensure_response( $this->handle_form_entry( $form_data ) );
457 }
458
459 wp_send_json_error(
460 [
461 'message' => __( 'Your submission was flagged as spam. Please try again.', 'sureforms' ),
462 ]
463 );
464 }
465
466 /**
467 * Send Email and Create Entry.
468 *
469 * @param array<string> $form_data Request object or array containing form data.
470 * @since 0.0.1
471 * @return array<mixed> Array containing the response data.
472 */
473 public function handle_form_entry( $form_data ) {
474 // Filter the form data.
475 $form_data = apply_filters( 'srfm_form_submit_data', $form_data );
476 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
477 wp_send_json_error(
478 [
479 'message' => __( 'Form data was not found.', 'sureforms' ),
480 'position' => 'header',
481 ]
482 );
483 } elseif ( isset( $form_data['error'] ) ) {
484 wp_send_json_error(
485 [
486 'message' => is_string( $form_data['error'] ) ? $form_data['error'] : __( 'Form data is not found.', 'sureforms' ),
487 'position' => 'header',
488 ]
489 );
490 }
491
492 $id = sanitize_text_field( $form_data['form-id'] );
493
494 // Get the compliance settings.
495 $compliance = get_post_meta( Helper::get_integer_value( $id ), '_srfm_compliance', true );
496 $gdpr = '';
497 $do_not_store_entries = '';
498
499 if ( is_array( $compliance ) && is_array( $compliance[0] ) ) {
500 $gdpr = $compliance[0]['gdpr'] ?? '';
501 $do_not_store_entries = $compliance[0]['do_not_store_entries'] ?? '';
502 }
503
504 // Check if the form data contains 'srfm_addresses' and is not empty.
505 if ( ! empty( $form_data['srfm_addresses'] ) ) {
506 // Assign the addresses to the class property for further processing.
507 $this->addresses = $form_data['srfm_addresses'];
508 // Remove the address data from the form data to avoid redundancy.
509 unset( $form_data['srfm_addresses'] );
510 }
511
512 $form_data = apply_filters( 'srfm_before_fields_processing', $form_data );
513
514 $submission_data = $this->process_form_fields( $form_data );
515
516 $modified_message = $this->prepare_submission_data( $submission_data );
517
518 $form_before_submission_data = [
519 'form_id' => $id ? intval( $id ) : '',
520 'data' => $modified_message,
521 ];
522
523 /**
524 * Fires before submission process starts.
525 */
526 do_action( 'srfm_before_submission', $form_before_submission_data );
527
528 $name = sanitize_text_field( get_the_title( intval( $id ) ) );
529 $send_email = $this->send_email( $id, $submission_data, $form_data );
530 $emails = [];
531
532 if ( $send_email ) {
533 $emails = $send_email['emails'];
534 }
535
536 // Check if GDPR is enabled and do not store entries is enabled.
537 // If so, send email and do not store entries.
538 if ( $gdpr && $do_not_store_entries ) {
539
540 $form_submit_response = [
541 'success' => true,
542 'form_id' => $id ? intval( $id ) : '',
543 'to_emails' => $emails,
544 'form_name' => $name ? esc_attr( $name ) : '',
545 'message' => Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data ),
546 'data' => $modified_message,
547 ];
548
549 do_action( 'srfm_form_submit', $form_submit_response );
550
551 /**
552 * Hook for enabling background processes.
553 *
554 * @param array $form_data form data related to submission.
555 */
556 $form_data['form_id'] = $id ? intval( $id ) : '';
557 do_action( 'srfm_after_submission_process', $form_data );
558
559 return [
560 'success' => true,
561 'message' => Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data ),
562 'data' => [
563 'name' => $name,
564 'after_submit' => false,
565 ],
566 'redirect_url' => Generate_Form_Markup::get_redirect_url( $form_data, $submission_data ),
567 ];
568
569 }
570
571 $global_setting_options = get_option( 'srfm_general_settings_options' );
572
573 // If GDPR is enabled, do not store IP, browser, and device info.
574 // If not, store IP, browser, and device info.
575 $user_ip = '';
576 $browser_name = '';
577 $device_name = '';
578 if ( ! $gdpr ) {
579 $srfm_ip_log = is_array( $global_setting_options ) && isset( $global_setting_options['srfm_ip_log'] ) ? $global_setting_options['srfm_ip_log'] : '';
580
581 $user_ip = $srfm_ip_log && isset( $_SERVER['REMOTE_ADDR'] ) ? filter_var( wp_unslash( $_SERVER['REMOTE_ADDR'] ), FILTER_VALIDATE_IP ) : '';
582 $browser = new Browser();
583 $browser_name = sanitize_text_field( $browser->getBrowser() );
584 $device_name = sanitize_text_field( $browser->getPlatform() );
585 }
586
587 $form_markup = get_the_content( null, false, Helper::get_integer_value( $form_data['form-id'] ) );
588 $pattern = '/"label":"(.*?)"/';
589 preg_match_all( $pattern, $form_markup, $matches );
590 $submission_info = [
591 'user_ip' => $user_ip,
592 'browser_name' => $browser_name,
593 'device_name' => $device_name,
594 ];
595 $entries_data = [
596 'form_id' => $id,
597 'form_data' => $submission_data,
598 'submission_info' => $submission_info,
599 'created_at' => current_time( 'mysql' ),
600 ];
601 if ( is_user_logged_in() ) {
602 // If user is logged in then save their user id.
603 $entries_data['user_id'] = get_current_user_id();
604 }
605
606 $entries_data = apply_filters(
607 'srfm_before_entry_data',
608 $entries_data,
609 [
610 'form_data' => $form_data,
611 'submission_data' => $submission_data,
612 ]
613 );
614
615 $entry_id = Entries::add( $entries_data );
616 if ( $entry_id ) {
617 // Inject entry_id so {entry_id} smart tag resolves in confirmation message, redirect URL, and downstream integrations.
618 $form_data['entry_id'] = intval( $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 * Validate unique field values for a specific form via AJAX.
955 *
956 * Checks submitted field values against existing entries to determine
957 * if duplicates exist. Rate-limited to prevent data enumeration.
958 *
959 * @since 0.0.1
960 * @since 2.7.0 Added rate limiting, form validation, and optimized query.
961 * @return void
962 */
963 public function field_unique_validation() {
964 $token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verification replaces nonce.
965 $form_id = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
966
967 if ( ! Submit_Token::verify( $token, $form_id ) ) {
968 wp_send_json_error( [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ] );
969 }
970
971 if ( ! $form_id ) {
972 wp_send_json_error( [ 'error' => __( 'Invalid form ID.', 'sureforms' ) ] );
973 }
974
975 // Validate the form exists and is published to prevent cross-form probing.
976 if ( 'publish' !== get_post_status( $form_id ) || 'sureforms_form' !== get_post_type( $form_id ) ) {
977 wp_send_json_error( [ 'error' => __( 'Invalid form.', 'sureforms' ) ] );
978 }
979
980 // Rate limit: 10 requests per minute per IP per form.
981 if ( $this->is_unique_validation_rate_limited( $form_id ) ) {
982 wp_send_json_error( [ 'error' => __( 'Too many requests. Please try again shortly.', 'sureforms' ) ], 429 );
983 }
984
985 // Extract and validate field values from POST data.
986 $skip_keys = [ 'action', 'token', 'id' ];
987 $duplicates = [];
988
989 foreach ( $_POST as $raw_key => $raw_value ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verified above.
990 if ( in_array( $raw_key, $skip_keys, true ) ) {
991 continue;
992 }
993
994 $field_key = str_replace( '_', ' ', sanitize_text_field( $raw_key ) );
995 $value = sanitize_text_field( wp_unslash( $raw_value ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verified above.
996
997 // Only process SureForms field keys (they contain -lbl- in the name).
998 if ( false === strpos( $field_key, '-lbl-' ) ) {
999 continue;
1000 }
1001
1002 if ( '' === $value ) {
1003 continue;
1004 }
1005
1006 // Single optimized query per field instead of loading all entries.
1007 if ( Entries::has_duplicate_field_value( $form_id, $field_key, $value ) ) {
1008 $duplicates[] = [ $field_key => 'not unique' ];
1009 }
1010 }
1011
1012 wp_send_json( [ 'data' => $duplicates ] );
1013 }
1014
1015 /**
1016 * Function to save allowed block data.
1017 *
1018 * @since 0.0.1
1019 * @return void
1020 */
1021 public function srfm_global_update_allowed_block() {
1022 if ( ! Helper::current_user_can() ) {
1023 wp_send_json_error();
1024 }
1025
1026 if ( ! check_ajax_referer( 'srfm_ajax_nonce', 'security', false ) ) {
1027 wp_send_json_error();
1028 }
1029
1030 if ( ! empty( $_POST['defaultAllowedQuickSidebarBlocks'] ) ) {
1031 $srfm_default_allowed_quick_sidebar_blocks = json_decode( sanitize_text_field( wp_unslash( $_POST['defaultAllowedQuickSidebarBlocks'] ) ), true );
1032 Helper::update_admin_settings_option( 'srfm_quick_sidebar_allowed_blocks', $srfm_default_allowed_quick_sidebar_blocks );
1033 wp_send_json_success();
1034 }
1035 wp_send_json_error();
1036 }
1037
1038 /**
1039 * Function to save enable/disable data.
1040 *
1041 * @since 0.0.1
1042 * @return void
1043 */
1044 public function srfm_global_sidebar_enabled() {
1045 if ( ! Helper::current_user_can() ) {
1046 wp_send_json_error();
1047 }
1048
1049 if ( ! check_ajax_referer( 'srfm_ajax_nonce', 'security', false ) ) {
1050 wp_send_json_error();
1051 }
1052
1053 if ( ! empty( $_POST['enableQuickActionSidebar'] ) ) {
1054 $srfm_enable_quick_action_sidebar = ( 'enabled' === $_POST['enableQuickActionSidebar'] ? 'enabled' : 'disabled' );
1055 Helper::update_admin_settings_option( 'srfm_enable_quick_action_sidebar', $srfm_enable_quick_action_sidebar );
1056 wp_send_json_success();
1057 }
1058 wp_send_json_error();
1059 }
1060
1061 /**
1062 * Send error response for reCAPTCHA validation failure.
1063 *
1064 * @param string $type The type of CAPTCHA used. Accepted values: 'g-recaptcha', 'hcaptcha', 'cf-turnstile'.
1065 * @param array<mixed> $api_response The response returned from the CAPTCHA validation API.
1066 * @since 1.7.0
1067 * @return void
1068 */
1069 public function recaptcha_error_response( $type, $api_response ) {
1070 $error_message = $this->recaptcha_error_message( $type, $api_response );
1071 $response = array_merge(
1072 [
1073 'api_response' => $api_response,
1074 ],
1075 $error_message
1076 );
1077
1078 wp_send_json_error( $response );
1079 }
1080
1081 /**
1082 * Get the error message for a CAPTCHA validation failure based on the service type and API response.
1083 *
1084 * @param string $type The type of CAPTCHA used. Accepted values: 'g-recaptcha', 'hcaptcha', 'cf-turnstile'.
1085 * @param array<mixed> $api_response The response returned from the CAPTCHA validation API.
1086 * @since 1.7.0
1087 * @return array<string,string> An associative array containing the error message and a detailed message.
1088 */
1089 public function recaptcha_error_message( $type, $api_response ) {
1090
1091 if ( empty( $api_response['error-codes'] ) || ! is_array( $api_response['error-codes'] ) ) {
1092 return [
1093 'detail_message' => __( 'Captcha validation failed. No error code provided.', 'sureforms' ),
1094 'message' => __( 'Captcha validation failed.', 'sureforms' ),
1095 ];
1096 }
1097
1098 /**
1099 * Note: The error codes are not translated because these messages are intended for debugging purposes.
1100 * Translating them would make debugging difficult. These error messages are primarily for developers or administrators.
1101 * A generic message will be displayed to the user, while detailed error information will be logged or shown in the console.
1102 */
1103
1104 // Google reCAPTCHA error codes.
1105 // Reference: (https://developers.google.com/recaptcha/docs/verify#error-code-reference).
1106 $google_recaptcha_error = [
1107 'missing-input-secret' => 'The secret parameter is missing.',
1108 'invalid-input-secret' => 'The secret parameter is invalid or malformed.',
1109 'missing-input-response' => 'The response parameter is missing.',
1110 'invalid-input-response' => 'The response parameter is invalid or malformed.',
1111 'bad-request' => 'The request is invalid or malformed.',
1112 'timeout-or-duplicate' => 'The response is no longer valid: either is too old or has been used previously.',
1113 ];
1114
1115 // hCaptcha error codes.
1116 // Reference: (https://docs.hcaptcha.com/#siteverify-error-codes).
1117 $hcaptcha_errors = [
1118 'missing-input-secret' => 'Your secret key is missing.',
1119 'invalid-input-secret' => 'Your secret key is invalid or malformed.',
1120 'missing-input-response' => 'The response parameter (verification token) is missing.',
1121 'invalid-input-response' => 'The response parameter (verification token) is invalid or malformed.',
1122 'expired-input-response' => 'The response parameter (verification token) is expired. (120s default)',
1123 'already-seen-response' => 'The response parameter (verification token) was already verified once.',
1124 'bad-request' => 'The request is invalid or malformed.',
1125 'missing-remoteip' => 'The remoteip parameter is missing.',
1126 'invalid-remoteip' => 'The remoteip parameter is not a valid IP address or blinded value.',
1127 'not-using-dummy-passcode' => 'You have used a testing sitekey but have not used its matching secret.',
1128 'sitekey-secret-mismatch' => 'The sitekey is not registered with the provided secret.',
1129 ];
1130
1131 // Cloudflare Turnstile error codes.
1132 // Reference: (https://developers.cloudflare.com/turnstile/get-started/server-side-validation/).
1133 $cf_turnstile_errors = [
1134 'missing-input-secret' => 'The secret parameter was not passed.',
1135 'invalid-input-secret' => 'The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.',
1136 'missing-input-response' => 'The response parameter (token) was not passed.',
1137 '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.',
1138 'bad-request' => 'The request was rejected because it was malformed.',
1139 '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.',
1140 'internal-error' => 'An internal error happened while validating the response. The request can be retried.',
1141 ];
1142
1143 $error_code = $api_response['error-codes'][0] ?? 'no-error-code';
1144
1145 $captcha_title = '';
1146 $captcha_message = '';
1147 switch ( $type ) {
1148 case 'g-recaptcha':
1149 $captcha_title = __( 'Google reCAPTCHA', 'sureforms' );
1150 $captcha_message = $google_recaptcha_error[ $error_code ];
1151 break;
1152 case 'hcaptcha':
1153 $captcha_title = __( 'hCaptcha', 'sureforms' );
1154 $captcha_message = $hcaptcha_errors[ $error_code ];
1155 break;
1156 case 'cf-turnstile':
1157 $captcha_title = __( 'Cloudflare Turnstile', 'sureforms' );
1158 $captcha_message = $cf_turnstile_errors[ $error_code ];
1159 break;
1160 default:
1161 $captcha_title = __( 'Unknown Captcha', 'sureforms' );
1162 $captcha_message = __( 'Invalid captcha type.', 'sureforms' );
1163 break;
1164 }
1165
1166 $detail_message = sprintf(
1167 '%s: %s <br> Error Code: %s',
1168 $captcha_title,
1169 $captcha_message ?? 'Unknown error occurred.',
1170 $error_code
1171 );
1172
1173 $message = sprintf(
1174 /* translators: %s is the captcha title. */
1175 __( '%s verification failed. Please contact your site administrator.', 'sureforms' ),
1176 $captcha_title
1177 );
1178
1179 return [
1180 'log_message' => $detail_message, // This variable is used for logging purposes, such as displaying detailed error information in the console on the front end.
1181 'message' => $message,
1182 ];
1183 }
1184
1185 /**
1186 * Check if the current request is rate-limited for unique validation.
1187 *
1188 * Uses transients keyed by IP + form ID to throttle requests.
1189 * Allows 10 requests per 60-second window per IP per form.
1190 *
1191 * @param int $form_id The form ID being validated.
1192 * @since 2.7.0
1193 * @return bool True if rate-limited (should block), false if allowed.
1194 */
1195 private function is_unique_validation_rate_limited( $form_id ) {
1196 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
1197
1198 if ( empty( $ip ) || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
1199 return true; // Fail closed if IP cannot be determined.
1200 }
1201
1202 $transient_key = 'srfm_uv_' . md5( $ip . '_' . $form_id );
1203 $attempts = get_transient( $transient_key );
1204
1205 if ( false === $attempts ) {
1206 set_transient( $transient_key, 1, MINUTE_IN_SECONDS );
1207 return false;
1208 }
1209
1210 $attempts_count = Helper::get_integer_value( $attempts );
1211
1212 if ( $attempts_count >= 10 ) {
1213 return true;
1214 }
1215
1216 set_transient( $transient_key, $attempts_count + 1, MINUTE_IN_SECONDS );
1217 return false;
1218 }
1219
1220 /**
1221 * Process and sanitize SureForms field data from submitted form data.
1222 *
1223 * @param array<mixed> $form_data Raw form data from submission.
1224 *
1225 * @since 1.11.0
1226 * @return array Processed and sanitized submission data.
1227 */
1228 private function process_form_fields( $form_data ) {
1229 $form_id = isset( $form_data['form-id'] ) && is_numeric( $form_data['form-id'] ) ? absint( $form_data['form-id'] ) : 0;
1230
1231 $submission_data = [];
1232
1233 $form_data_keys = array_keys( $form_data );
1234 $form_data_count = count( $form_data );
1235
1236 for ( $i = 0; $i < $form_data_count; $i++ ) {
1237 $key = strval( $form_data_keys[ $i ] );
1238
1239 /**
1240 * This will allow to pass only sureforms fields
1241 * checking -lbl- as thats mandatory for in key of sureforms fields.
1242 */
1243 if ( false === str_contains( $key, '-lbl-' ) ) {
1244 continue;
1245 }
1246
1247 $value = $form_data[ $key ];
1248
1249 $field_name = htmlspecialchars( str_replace( '_', ' ', $key ) );
1250
1251 $field_block_name = Helper::get_block_name_from_field( $field_name );
1252
1253 /**
1254 * Filters the field value during form submission processing.
1255 *
1256 * This filter allows the Pro plugin to process and modify field values before they are saved.
1257 * The Pro plugin can implement custom sanitization, validation and escaping logic for its
1258 * specialized field types. When this filter is used by Pro, the core plugin will skip its
1259 * default validation.
1260 *
1261 * @since 1.11.0
1262 *
1263 * @param mixed $value The raw field value from form submission.
1264 * @param array $field_data Field information array containing:
1265 * - 'field_name': The field name/key
1266 * - 'field_block_name': The block type identifier
1267 * @return array {
1268 * Processed field value data
1269 *
1270 * @type bool $is_processed Whether the value was processed by Pro plugin
1271 * @type mixed $value The processed and sanitized field value
1272 * }
1273 */
1274 $process_field_value = apply_filters(
1275 'srfm_process_field_value',
1276 $value,
1277 [
1278 'field_name' => $field_name,
1279 'field_block_name' => $field_block_name,
1280 ]
1281 );
1282
1283 if ( is_array( $process_field_value ) && ! empty( $process_field_value['is_processed'] ) && ! empty( $process_field_value['value'] ) ) {
1284 $submission_data[ $field_name ] = $process_field_value['value'];
1285 continue;
1286 }
1287
1288 /**
1289 * Need to remove this refactor array value handling.
1290 *
1291 * The current array-based value handling needs to be replaced with:
1292 * 1. Block-specific value processing based on block type.
1293 * 2. Move premium features to pro version.
1294 * 3. Implement value processing through filters for extensibility.
1295 *
1296 * This will improve code organization and maintainability while properly
1297 * separating free/pro functionality.
1298 */
1299
1300 // If the field is an array, encode the values. This is to add support for multi-upload field.
1301 if ( is_array( $value ) ) {
1302 $submission_data[ $field_name ] =
1303 array_map(
1304 static function ( $val ) {
1305 return rawurlencode( $val );
1306 },
1307 $value
1308 );
1309 } else {
1310 $submission_data[ $field_name ] = is_string( $value ) ? htmlspecialchars( $value ) : $value;
1311 }
1312 }
1313
1314 /**
1315 * Filters the submission data before preparing it for storage.
1316 *
1317 * The second parameter is a context array containing additional metadata
1318 * about the submission. This array is extensible — new keys may be added
1319 * in future versions without changing the filter signature.
1320 *
1321 * @since 2.6.0
1322 *
1323 * @param array<string,mixed> $submission_data Processed form submission data.
1324 * @param array<string,mixed> $context {
1325 * Additional context for the submission.
1326 *
1327 * @type int $form_id The ID of the form being submitted.
1328 * }
1329 */
1330 return apply_filters(
1331 'srfm_before_prepare_submission_data',
1332 $submission_data,
1333 [
1334 'form_id' => $form_id,
1335 ]
1336 );
1337 }
1338
1339 /**
1340 * Add From email and name in the header.
1341 *
1342 * @param array<mixed> $submission_data Submission data.
1343 * @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'.
1344 * @param Smart_Tags $smart_tags Smart Tags instance.
1345 * @since 1.6.1
1346 * @return string The formatted "From" email header.
1347 */
1348 private static function add_from_data_in_header( $submission_data, $item, $smart_tags ) {
1349 $from_name = is_array( $item ) && ! empty( $item['from_name'] ) ? sanitize_text_field( Helper::get_string_value( $item['from_name'] ) ) : '{site_title}';
1350 $from_email = is_array( $item ) && ! empty( $item['from_email'] ) ? Helper::get_string_value( $item['from_email'] ) : '{admin_email}';
1351
1352 // Check if the email contains smart tags. If not, validate the email.
1353 $is_valid_email = true;
1354 if ( ! str_contains( $from_email, '{' ) && ! str_contains( $from_email, '}' ) ) {
1355 $is_valid_email = filter_var( $from_email, FILTER_VALIDATE_EMAIL );
1356 }
1357 // if the email is not valid, set it to the admin email.
1358 if ( ! $is_valid_email ) {
1359 $from_email = Helper::get_string_value( get_option( 'admin_email' ) );
1360 }
1361
1362 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";
1363 }
1364 }
1365