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