PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.7.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.7.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
1,363 lines 49.1 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
618 $confirmation_message = Generate_Form_Markup::get_confirmation_markup( $form_data, $submission_data );
619
620 $response = [
621 'success' => true,
622 'message' => $confirmation_message,
623 'data' => [
624 'name' => $name,
625 'submission_id' => $entry_id,
626 'after_submit' => true,
627 'after_submit_nonce' => wp_create_nonce( 'srfm_after_submission_' . Helper::get_string_value( $entry_id ) ),
628 ],
629 'redirect_url' => Generate_Form_Markup::get_redirect_url( $form_data, $submission_data ),
630 ];
631
632 $form_submit_response = apply_filters(
633 'srfm_form_submit_response',
634 [
635 'success' => true,
636 'form_id' => $id ? intval( $id ) : '',
637 'entry_id' => intval( $entry_id ),
638 'to_emails' => $emails,
639 'form_name' => $name ? esc_attr( $name ) : '',
640 'message' => $confirmation_message,
641 'data' => $modified_message,
642 ]
643 );
644
645 do_action( 'srfm_form_submit', $form_submit_response );
646 } else {
647 $response = [
648 'success' => false,
649 'message' => __( 'Unable to submit form. Please try again.', 'sureforms' ),
650 ];
651 }
652
653 /**
654 * Filter the form submission response.
655 *
656 * @param array<mixed> $response The response data.
657 * @param array<string> $form_data The original form data.
658 * @param array<mixed> $submission_data The processed submission data.
659 * @since 2.4.0
660 */
661 return apply_filters( 'srfm_form_submission_response', $response, $form_data, $submission_data );
662 }
663
664 /**
665 * Prepare submission data.
666 *
667 * @param array<mixed> $submission_data Submission data.
668 * @since 0.0.7
669 * @return array<mixed> Modified submission data.
670 */
671 public function prepare_submission_data( $submission_data ) {
672 $modified_message = [];
673 foreach ( $submission_data as $key => $value ) {
674 $parts = explode( '-lbl-', $key );
675 $label = '';
676
677 /**
678 * Filters submission data for field processing.
679 *
680 * This filter allows customization of how individual fields are processed
681 * during submission data preparation. Plugins can modify field values,
682 * labels, or exclude specific fields from the final submission data.
683 *
684 * @since 1.11.0
685 *
686 * @param array $field_data {
687 * Field data for processing.
688 *
689 * @type array $block_parts The field key split by '-lbl-' delimiter.
690 * @type string $field_key The original field key from submission data.
691 * @type mixed $field_value The field value from submission data.
692 * }
693 */
694 $should_add_field_row = apply_filters(
695 'srfm_prepare_submission_data',
696 [
697 'block_parts' => $parts,
698 'field_key' => $key,
699 'field_value' => $value,
700 ]
701 );
702
703 // If we get the label and value from the filter, then use it.
704 if ( ! empty( $should_add_field_row['label'] ) && ! empty( $should_add_field_row['value'] ) ) {
705 $modified_message[ $should_add_field_row['label'] ] = $should_add_field_row['value'];
706 continue;
707 }
708
709 if ( ! empty( $parts[1] ) ) {
710 $tokens = explode( '-', $parts[1] );
711 if ( count( $tokens ) > 1 ) {
712 $label = implode( '-', array_slice( $tokens, 1 ) );
713 }
714
715 $fields = explode( '-', $parts[0] );
716
717 // Since the upload field returns an array of file URLs, we need to implode them with a comma.
718 if ( 'upload' === $fields[1] && ! empty( $value ) && is_array( $value ) ) {
719 $modified_message[ $label ] = implode( ', ', array_map( 'rawurldecode', $value ) );
720 } else {
721 $modified_message[ $label ] = html_entity_decode( esc_attr( Helper::get_string_value( $value ) ) );
722 }
723 }
724 }
725
726 // If the address is not empty, add it to the submission data.
727 // We are providing this for third-party integrations like Ottokit.
728 // They can use compact addresses such as permanent address, temporary address, etc.
729 // The address will be structured as field 1, field 2, and so on.
730 if ( ! empty( $this->addresses ) ) {
731 // Address will be JSON stringified, so decode it.
732 $address = json_decode( wp_unslash( $this->addresses ), true );
733 if ( ! empty( $address ) && is_array( $address ) ) {
734 $modified_message = array_merge( $modified_message, $address );
735 }
736 }
737
738 return apply_filters( 'srfm_update_prepared_submission_data', $modified_message );
739 }
740
741 /**
742 * Parse an email notification template and generate the necessary components for sending an email.
743 *
744 * @param array<mixed> $submission_data An associative array containing submission data to be used in the email template.
745 * @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'.
746 * @param array<string> $form_data Request object or array containing form data.
747 * @since 1.3.0
748 * @return array<string,string> An associative array containing 'to', 'subject', 'message', and 'headers' for the email.
749 */
750 public static function parse_email_notification_template( $submission_data, $item, $form_data = [] ) {
751 $smart_tags = Smart_Tags::get_instance();
752
753 $to = Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_to'], $submission_data ) );
754 $subject = Helper::get_string_value( $smart_tags->process_smart_tags( $item['subject'], $submission_data, $form_data ) );
755 $email_body = Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_body'], $submission_data, $form_data ) );
756 $is_raw_format = isset( $item['is_raw_format'] ) && true === $item['is_raw_format'];
757
758 /**
759 * Sanitize the email body after smart tag substitution to prevent XSS.
760 *
761 * After process_smart_tags() resolves {form:slug} placeholders, the body may contain
762 * raw user-submitted values that must not render as executable HTML in email clients.
763 * wp_kses_post() strips dangerous markup (script, on* handlers, javascript: URIs)
764 * while preserving all legitimate email formatting (tables, links, bold, etc.).
765 *
766 * Note: {all_data} is not a recognised smart tag and remains a literal placeholder
767 * at this point; it is substituted later by process_all_data_tag() which applies
768 * its own per-field escaping, so this call does not interfere with that path.
769 *
770 * @since 2.5.2
771 */
772 $email_body = wp_kses_post( $email_body );
773
774 $email_template = new Email_Template();
775 $message = $is_raw_format
776 ? $email_template->render_raw( $submission_data, $email_body )
777 : $email_template->render( $submission_data, $email_body );
778 $headers = 'X-Mailer: PHP/' . phpversion() . "\r\n";
779 $headers .= "Content-Type: text/html; charset=utf-8\r\n";
780
781 // Add the From: to the headers.
782 $headers .= self::add_from_data_in_header( $submission_data, $item, $smart_tags );
783
784 // Handle Reply-To with proper sanitization.
785 if ( isset( $item['email_reply_to'] ) && ! empty( $item['email_reply_to'] ) ) {
786 $headers .= 'Reply-To: ' . Helper::sanitize_email_header( Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_reply_to'], $submission_data ) ) ) . "\r\n";
787 }
788
789 // Handle CC with proper sanitization.
790 if ( isset( $item['email_cc'] ) && ! empty( $item['email_cc'] ) ) {
791 $headers .= 'Cc: ' . Helper::sanitize_email_header( Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_cc'], $submission_data ) ) ) . "\r\n";
792 }
793
794 // Handle BCC with proper sanitization.
795 if ( isset( $item['email_bcc'] ) && ! empty( $item['email_bcc'] ) ) {
796 $headers .= 'Bcc: ' . Helper::sanitize_email_header( Helper::get_string_value( $smart_tags->process_smart_tags( $item['email_bcc'], $submission_data ) ) ) . "\r\n";
797 }
798
799 return compact( 'to', 'subject', 'message', 'headers' );
800 }
801
802 /**
803 * Send Email.
804 *
805 * @param string $id Form ID.
806 * @param array<mixed> $submission_data Submission data.
807 * @param array<string> $form_data Request object or array containing form data.
808 * @since 0.0.1
809 * @return array<mixed> Array containing the response data.
810 */
811 public static function send_email( $id, $submission_data, $form_data = [] ) {
812 $email_notification = get_post_meta( intval( $id ), '_srfm_email_notification' );
813 $is_mail_sent = false;
814 $emails = [];
815
816 // Filter to determine whether the email notification should be sent.
817 $email_notification = apply_filters( 'srfm_email_notification_should_send', $email_notification, $submission_data, $form_data );
818
819 if ( is_iterable( $email_notification ) ) {
820 $entries_db_instance = Entries::get_instance();
821 $log_key = $entries_db_instance->add_log( __( 'Email notification passed to the sending server', 'sureforms' ) );
822
823 foreach ( $email_notification as $notification ) {
824 foreach ( $notification as $item ) {
825 if ( true === $item['status'] ) {
826
827 $parsed = self::parse_email_notification_template( $submission_data, $item, $form_data );
828
829 // Allow filtering of the email data before it is sent.
830 $parsed = apply_filters( 'srfm_email_notification', $parsed, $submission_data, $item, $form_data );
831
832 // Trigger an action before sending the email, allowing additional processing or logging.
833 do_action( 'srfm_before_email_send', $parsed, $submission_data, $item, $form_data );
834
835 $notification_id = isset( $item['id'] ) ? intval( $item['id'] ) : 0;
836
837 /**
838 * Filter to determine whether the email should be sent.
839 *
840 * @since 1.10.1
841 */
842 $should_send_email = apply_filters(
843 'srfm_should_send_email',
844 true,
845 $notification_id,
846 $id,
847 $form_data,
848 );
849
850 if ( ! wp_validate_boolean( $should_send_email ) ) {
851 continue;
852 }
853
854 /**
855 * Temporary override the content type for wp_mail.
856 * This helps us from breaking of content type from other plugins.
857 *
858 * @since 1.2.2
859 */
860 add_filter(
861 'wp_mail_content_type',
862 static function() {
863 return 'text/html'; // We need "text/html" content type to render our emails.
864 },
865 99
866 );
867
868 /**
869 * Start sending email.
870 * Wrapping it in the buffer because when some plugin such as zoho mail, overrides the wp_mail
871 * function and any exception is thrown ( Or printed ) from that plugin side, it affects the JSON response.
872 * So, to make sure such exceptions doesn't affect our JSON response, we are wrapping it inside buffer.
873 *
874 * Try-Catch does not work because the notice or errors might be echoed by other plugins rather than thrown as an exception.
875 *
876 * @since 1.2.2
877 */
878 $sent = false;
879 ob_start();
880 $sent = wp_mail( $parsed['to'], $parsed['subject'], $parsed['message'], $parsed['headers'] );
881 if ( ! $sent ) {
882 // Fallback to default PHP mail if for some reasons wp_mail fails.
883 $sent = mail( $parsed['to'], $parsed['subject'], $parsed['message'], $parsed['headers'] );
884 }
885 $email_report = ob_get_clean(); // Catch any printed notice/errors/message for reports.
886
887 if ( is_int( $log_key ) ) {
888 if ( true === $sent ) {
889 $entries_db_instance->update_log(
890 $log_key,
891 null,
892 [
893 /* translators: Here, %s is the comma separated emails list. */
894 sprintf( __( 'Email notification recipient: %s', 'sureforms' ), esc_html( $parsed['to'] ) ),
895 ]
896 );
897 } else {
898 $reason = ! empty( $email_report )
899 ? esc_html( $email_report )
900 : ( ! Helper::is_any_smtp_plugin_active()
901 ? esc_html__( 'No SMTP plugin detected. Please configure an SMTP plugin to enable email sending.', 'sureforms' )
902 : esc_html__( 'Email sending failed for an unknown reason.', 'sureforms' )
903 );
904
905 $entries_db_instance->update_log(
906 $log_key,
907 null,
908 [
909 sprintf(
910 /* translators: Here, %1$s is the comma separated emails list and %2$s is error report ( if any ). */
911 __(
912 'Email server was unable to send the email notification. Recipient: %1$s. Reason: %2$s',
913 'sureforms'
914 ),
915 esc_html( $parsed['to'] ),
916 $reason
917 ),
918 ]
919 );
920
921 }
922 }
923
924 // Trigger an action after the email is sent, allowing additional processing or logging.
925 do_action(
926 'srfm_after_email_send',
927 $parsed,
928 $submission_data,
929 $item,
930 $form_data
931 );
932
933 $is_mail_sent = $sent;
934 $emails[] = $parsed['to'];
935 }
936 }
937 }
938
939 if ( empty( $emails ) ) {
940 $entries_db_instance->reset_logs();
941 $entries_db_instance->add_log( __( 'No emails were sent.', 'sureforms' ) );
942 }
943 }
944
945 return [
946 'success' => $is_mail_sent,
947 'emails' => $emails,
948 ];
949 }
950
951 /**
952 * Validate unique field values for a specific form via AJAX.
953 *
954 * Checks submitted field values against existing entries to determine
955 * if duplicates exist. Rate-limited to prevent data enumeration.
956 *
957 * @since 0.0.1
958 * @since 2.7.0 Added rate limiting, form validation, and optimized query.
959 * @return void
960 */
961 public function field_unique_validation() {
962 $token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verification replaces nonce.
963 $form_id = isset( $_POST['id'] ) ? absint( wp_unslash( $_POST['id'] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing
964
965 if ( ! Submit_Token::verify( $token, $form_id ) ) {
966 wp_send_json_error( [ 'error' => __( 'Security verification failed. Please refresh the page and try again.', 'sureforms' ) ] );
967 }
968
969 if ( ! $form_id ) {
970 wp_send_json_error( [ 'error' => __( 'Invalid form ID.', 'sureforms' ) ] );
971 }
972
973 // Validate the form exists and is published to prevent cross-form probing.
974 if ( 'publish' !== get_post_status( $form_id ) || 'sureforms_form' !== get_post_type( $form_id ) ) {
975 wp_send_json_error( [ 'error' => __( 'Invalid form.', 'sureforms' ) ] );
976 }
977
978 // Rate limit: 10 requests per minute per IP per form.
979 if ( $this->is_unique_validation_rate_limited( $form_id ) ) {
980 wp_send_json_error( [ 'error' => __( 'Too many requests. Please try again shortly.', 'sureforms' ) ], 429 );
981 }
982
983 // Extract and validate field values from POST data.
984 $skip_keys = [ 'action', 'token', 'id' ];
985 $duplicates = [];
986
987 foreach ( $_POST as $raw_key => $raw_value ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verified above.
988 if ( in_array( $raw_key, $skip_keys, true ) ) {
989 continue;
990 }
991
992 $field_key = str_replace( '_', ' ', sanitize_text_field( $raw_key ) );
993 $value = sanitize_text_field( wp_unslash( $raw_value ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- HMAC token verified above.
994
995 // Only process SureForms field keys (they contain -lbl- in the name).
996 if ( false === strpos( $field_key, '-lbl-' ) ) {
997 continue;
998 }
999
1000 if ( '' === $value ) {
1001 continue;
1002 }
1003
1004 // Single optimized query per field instead of loading all entries.
1005 if ( Entries::has_duplicate_field_value( $form_id, $field_key, $value ) ) {
1006 $duplicates[] = [ $field_key => 'not unique' ];
1007 }
1008 }
1009
1010 wp_send_json( [ 'data' => $duplicates ] );
1011 }
1012
1013 /**
1014 * Function to save allowed block data.
1015 *
1016 * @since 0.0.1
1017 * @return void
1018 */
1019 public function srfm_global_update_allowed_block() {
1020 if ( ! Helper::current_user_can() ) {
1021 wp_send_json_error();
1022 }
1023
1024 if ( ! check_ajax_referer( 'srfm_ajax_nonce', 'security', false ) ) {
1025 wp_send_json_error();
1026 }
1027
1028 if ( ! empty( $_POST['defaultAllowedQuickSidebarBlocks'] ) ) {
1029 $srfm_default_allowed_quick_sidebar_blocks = json_decode( sanitize_text_field( wp_unslash( $_POST['defaultAllowedQuickSidebarBlocks'] ) ), true );
1030 Helper::update_admin_settings_option( 'srfm_quick_sidebar_allowed_blocks', $srfm_default_allowed_quick_sidebar_blocks );
1031 wp_send_json_success();
1032 }
1033 wp_send_json_error();
1034 }
1035
1036 /**
1037 * Function to save enable/disable data.
1038 *
1039 * @since 0.0.1
1040 * @return void
1041 */
1042 public function srfm_global_sidebar_enabled() {
1043 if ( ! Helper::current_user_can() ) {
1044 wp_send_json_error();
1045 }
1046
1047 if ( ! check_ajax_referer( 'srfm_ajax_nonce', 'security', false ) ) {
1048 wp_send_json_error();
1049 }
1050
1051 if ( ! empty( $_POST['enableQuickActionSidebar'] ) ) {
1052 $srfm_enable_quick_action_sidebar = ( 'enabled' === $_POST['enableQuickActionSidebar'] ? 'enabled' : 'disabled' );
1053 Helper::update_admin_settings_option( 'srfm_enable_quick_action_sidebar', $srfm_enable_quick_action_sidebar );
1054 wp_send_json_success();
1055 }
1056 wp_send_json_error();
1057 }
1058
1059 /**
1060 * Send error response for reCAPTCHA validation failure.
1061 *
1062 * @param string $type The type of CAPTCHA used. Accepted values: 'g-recaptcha', 'hcaptcha', 'cf-turnstile'.
1063 * @param array<mixed> $api_response The response returned from the CAPTCHA validation API.
1064 * @since 1.7.0
1065 * @return void
1066 */
1067 public function recaptcha_error_response( $type, $api_response ) {
1068 $error_message = $this->recaptcha_error_message( $type, $api_response );
1069 $response = array_merge(
1070 [
1071 'api_response' => $api_response,
1072 ],
1073 $error_message
1074 );
1075
1076 wp_send_json_error( $response );
1077 }
1078
1079 /**
1080 * Get the error message for a CAPTCHA validation failure based on the service type and API response.
1081 *
1082 * @param string $type The type of CAPTCHA used. Accepted values: 'g-recaptcha', 'hcaptcha', 'cf-turnstile'.
1083 * @param array<mixed> $api_response The response returned from the CAPTCHA validation API.
1084 * @since 1.7.0
1085 * @return array<string,string> An associative array containing the error message and a detailed message.
1086 */
1087 public function recaptcha_error_message( $type, $api_response ) {
1088
1089 if ( empty( $api_response['error-codes'] ) || ! is_array( $api_response['error-codes'] ) ) {
1090 return [
1091 'detail_message' => __( 'Captcha validation failed. No error code provided.', 'sureforms' ),
1092 'message' => __( 'Captcha validation failed.', 'sureforms' ),
1093 ];
1094 }
1095
1096 /**
1097 * Note: The error codes are not translated because these messages are intended for debugging purposes.
1098 * Translating them would make debugging difficult. These error messages are primarily for developers or administrators.
1099 * A generic message will be displayed to the user, while detailed error information will be logged or shown in the console.
1100 */
1101
1102 // Google reCAPTCHA error codes.
1103 // Reference: (https://developers.google.com/recaptcha/docs/verify#error-code-reference).
1104 $google_recaptcha_error = [
1105 'missing-input-secret' => 'The secret parameter is missing.',
1106 'invalid-input-secret' => 'The secret parameter is invalid or malformed.',
1107 'missing-input-response' => 'The response parameter is missing.',
1108 'invalid-input-response' => 'The response parameter is invalid or malformed.',
1109 'bad-request' => 'The request is invalid or malformed.',
1110 'timeout-or-duplicate' => 'The response is no longer valid: either is too old or has been used previously.',
1111 ];
1112
1113 // hCaptcha error codes.
1114 // Reference: (https://docs.hcaptcha.com/#siteverify-error-codes).
1115 $hcaptcha_errors = [
1116 'missing-input-secret' => 'Your secret key is missing.',
1117 'invalid-input-secret' => 'Your secret key is invalid or malformed.',
1118 'missing-input-response' => 'The response parameter (verification token) is missing.',
1119 'invalid-input-response' => 'The response parameter (verification token) is invalid or malformed.',
1120 'expired-input-response' => 'The response parameter (verification token) is expired. (120s default)',
1121 'already-seen-response' => 'The response parameter (verification token) was already verified once.',
1122 'bad-request' => 'The request is invalid or malformed.',
1123 'missing-remoteip' => 'The remoteip parameter is missing.',
1124 'invalid-remoteip' => 'The remoteip parameter is not a valid IP address or blinded value.',
1125 'not-using-dummy-passcode' => 'You have used a testing sitekey but have not used its matching secret.',
1126 'sitekey-secret-mismatch' => 'The sitekey is not registered with the provided secret.',
1127 ];
1128
1129 // Cloudflare Turnstile error codes.
1130 // Reference: (https://developers.cloudflare.com/turnstile/get-started/server-side-validation/).
1131 $cf_turnstile_errors = [
1132 'missing-input-secret' => 'The secret parameter was not passed.',
1133 'invalid-input-secret' => 'The secret parameter was invalid, did not exist, or is a testing secret key with a non-testing response.',
1134 'missing-input-response' => 'The response parameter (token) was not passed.',
1135 '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.',
1136 'bad-request' => 'The request was rejected because it was malformed.',
1137 '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.',
1138 'internal-error' => 'An internal error happened while validating the response. The request can be retried.',
1139 ];
1140
1141 $error_code = $api_response['error-codes'][0] ?? 'no-error-code';
1142
1143 $captcha_title = '';
1144 $captcha_message = '';
1145 switch ( $type ) {
1146 case 'g-recaptcha':
1147 $captcha_title = __( 'Google reCAPTCHA', 'sureforms' );
1148 $captcha_message = $google_recaptcha_error[ $error_code ];
1149 break;
1150 case 'hcaptcha':
1151 $captcha_title = __( 'hCaptcha', 'sureforms' );
1152 $captcha_message = $hcaptcha_errors[ $error_code ];
1153 break;
1154 case 'cf-turnstile':
1155 $captcha_title = __( 'Cloudflare Turnstile', 'sureforms' );
1156 $captcha_message = $cf_turnstile_errors[ $error_code ];
1157 break;
1158 default:
1159 $captcha_title = __( 'Unknown Captcha', 'sureforms' );
1160 $captcha_message = __( 'Invalid captcha type.', 'sureforms' );
1161 break;
1162 }
1163
1164 $detail_message = sprintf(
1165 '%s: %s <br> Error Code: %s',
1166 $captcha_title,
1167 $captcha_message ?? 'Unknown error occurred.',
1168 $error_code
1169 );
1170
1171 $message = sprintf(
1172 /* translators: %s is the captcha title. */
1173 __( '%s verification failed. Please contact your site administrator.', 'sureforms' ),
1174 $captcha_title
1175 );
1176
1177 return [
1178 'log_message' => $detail_message, // This variable is used for logging purposes, such as displaying detailed error information in the console on the front end.
1179 'message' => $message,
1180 ];
1181 }
1182
1183 /**
1184 * Check if the current request is rate-limited for unique validation.
1185 *
1186 * Uses transients keyed by IP + form ID to throttle requests.
1187 * Allows 10 requests per 60-second window per IP per form.
1188 *
1189 * @param int $form_id The form ID being validated.
1190 * @since 2.7.0
1191 * @return bool True if rate-limited (should block), false if allowed.
1192 */
1193 private function is_unique_validation_rate_limited( $form_id ) {
1194 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
1195
1196 if ( empty( $ip ) || ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
1197 return true; // Fail closed if IP cannot be determined.
1198 }
1199
1200 $transient_key = 'srfm_uv_' . md5( $ip . '_' . $form_id );
1201 $attempts = get_transient( $transient_key );
1202
1203 if ( false === $attempts ) {
1204 set_transient( $transient_key, 1, MINUTE_IN_SECONDS );
1205 return false;
1206 }
1207
1208 $attempts_count = Helper::get_integer_value( $attempts );
1209
1210 if ( $attempts_count >= 10 ) {
1211 return true;
1212 }
1213
1214 set_transient( $transient_key, $attempts_count + 1, MINUTE_IN_SECONDS );
1215 return false;
1216 }
1217
1218 /**
1219 * Process and sanitize SureForms field data from submitted form data.
1220 *
1221 * @param array<mixed> $form_data Raw form data from submission.
1222 *
1223 * @since 1.11.0
1224 * @return array Processed and sanitized submission data.
1225 */
1226 private function process_form_fields( $form_data ) {
1227 $form_id = isset( $form_data['form-id'] ) && is_numeric( $form_data['form-id'] ) ? absint( $form_data['form-id'] ) : 0;
1228
1229 $submission_data = [];
1230
1231 $form_data_keys = array_keys( $form_data );
1232 $form_data_count = count( $form_data );
1233
1234 for ( $i = 0; $i < $form_data_count; $i++ ) {
1235 $key = strval( $form_data_keys[ $i ] );
1236
1237 /**
1238 * This will allow to pass only sureforms fields
1239 * checking -lbl- as thats mandatory for in key of sureforms fields.
1240 */
1241 if ( false === str_contains( $key, '-lbl-' ) ) {
1242 continue;
1243 }
1244
1245 $value = $form_data[ $key ];
1246
1247 $field_name = htmlspecialchars( str_replace( '_', ' ', $key ) );
1248
1249 $field_block_name = Helper::get_block_name_from_field( $field_name );
1250
1251 /**
1252 * Filters the field value during form submission processing.
1253 *
1254 * This filter allows the Pro plugin to process and modify field values before they are saved.
1255 * The Pro plugin can implement custom sanitization, validation and escaping logic for its
1256 * specialized field types. When this filter is used by Pro, the core plugin will skip its
1257 * default validation.
1258 *
1259 * @since 1.11.0
1260 *
1261 * @param mixed $value The raw field value from form submission.
1262 * @param array $field_data Field information array containing:
1263 * - 'field_name': The field name/key
1264 * - 'field_block_name': The block type identifier
1265 * @return array {
1266 * Processed field value data
1267 *
1268 * @type bool $is_processed Whether the value was processed by Pro plugin
1269 * @type mixed $value The processed and sanitized field value
1270 * }
1271 */
1272 $process_field_value = apply_filters(
1273 'srfm_process_field_value',
1274 $value,
1275 [
1276 'field_name' => $field_name,
1277 'field_block_name' => $field_block_name,
1278 ]
1279 );
1280
1281 if ( is_array( $process_field_value ) && ! empty( $process_field_value['is_processed'] ) && ! empty( $process_field_value['value'] ) ) {
1282 $submission_data[ $field_name ] = $process_field_value['value'];
1283 continue;
1284 }
1285
1286 /**
1287 * Need to remove this refactor array value handling.
1288 *
1289 * The current array-based value handling needs to be replaced with:
1290 * 1. Block-specific value processing based on block type.
1291 * 2. Move premium features to pro version.
1292 * 3. Implement value processing through filters for extensibility.
1293 *
1294 * This will improve code organization and maintainability while properly
1295 * separating free/pro functionality.
1296 */
1297
1298 // If the field is an array, encode the values. This is to add support for multi-upload field.
1299 if ( is_array( $value ) ) {
1300 $submission_data[ $field_name ] =
1301 array_map(
1302 static function ( $val ) {
1303 return rawurlencode( $val );
1304 },
1305 $value
1306 );
1307 } else {
1308 $submission_data[ $field_name ] = is_string( $value ) ? htmlspecialchars( $value ) : $value;
1309 }
1310 }
1311
1312 /**
1313 * Filters the submission data before preparing it for storage.
1314 *
1315 * The second parameter is a context array containing additional metadata
1316 * about the submission. This array is extensible — new keys may be added
1317 * in future versions without changing the filter signature.
1318 *
1319 * @since 2.6.0
1320 *
1321 * @param array<string,mixed> $submission_data Processed form submission data.
1322 * @param array<string,mixed> $context {
1323 * Additional context for the submission.
1324 *
1325 * @type int $form_id The ID of the form being submitted.
1326 * }
1327 */
1328 return apply_filters(
1329 'srfm_before_prepare_submission_data',
1330 $submission_data,
1331 [
1332 'form_id' => $form_id,
1333 ]
1334 );
1335 }
1336
1337 /**
1338 * Add From email and name in the header.
1339 *
1340 * @param array<mixed> $submission_data Submission data.
1341 * @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'.
1342 * @param Smart_Tags $smart_tags Smart Tags instance.
1343 * @since 1.6.1
1344 * @return string The formatted "From" email header.
1345 */
1346 private static function add_from_data_in_header( $submission_data, $item, $smart_tags ) {
1347 $from_name = is_array( $item ) && ! empty( $item['from_name'] ) ? sanitize_text_field( Helper::get_string_value( $item['from_name'] ) ) : '{site_title}';
1348 $from_email = is_array( $item ) && ! empty( $item['from_email'] ) ? Helper::get_string_value( $item['from_email'] ) : '{admin_email}';
1349
1350 // Check if the email contains smart tags. If not, validate the email.
1351 $is_valid_email = true;
1352 if ( ! str_contains( $from_email, '{' ) && ! str_contains( $from_email, '}' ) ) {
1353 $is_valid_email = filter_var( $from_email, FILTER_VALIDATE_EMAIL );
1354 }
1355 // if the email is not valid, set it to the admin email.
1356 if ( ! $is_valid_email ) {
1357 $from_email = Helper::get_string_value( get_option( 'admin_email' ) );
1358 }
1359
1360 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";
1361 }
1362 }
1363