PluginProbe
UiCore Elements – Free widgets and templates for Elementor / 1.3.18
UiCore Elements – Free widgets and templates for Elementor v1.3.18
1.3.18 1.3.17 1.3.16 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.2.0 1.2.1 1.2.2 1.2.3 All 42 releases
← All changes | includes/utils/form-service.php +939 -600 1.0.10 → 1.3.18 View file →
@@ -1,600 +1,939 @@
1 -<?php
2 -namespace UiCoreElements\Utils;
3 -
4 -class Email_Exception extends \Exception {}
5 -class Redirect_Exception extends \Exception {}
6 -class Submit_Exception extends \Exception {}
7 -class Mailchimp_Exception extends \Exception {}
8 -
9 -defined('ABSPATH') || exit();
10 -
11 -/**
12 - * Handles the form submissions and responses
13 - */
14 -
15 -class Contact_Form_Service {
16 -
17 - protected $form_data,
18 - $settings,
19 - $files;
20 -
21 - public function __construct($form_data, $settings, $files) {
22 - $this->form_data = $form_data;
23 - $this->settings = $settings;
24 - $this->files = $files;
25 - }
26 -
27 - public function handle() {
28 -
29 - $data = [];
30 - $responses = [];
31 -
32 - // Checks for reCAPTCHA validation
33 - if (isset($this->form_data['grecaptcha_token']) && !empty($this->form_data['grecaptcha_token'])) {
34 -
35 - $recaptcha = $this->validate_recaptcha($this->form_data['grecaptcha_token'], $this->form_data['grecaptcha_version']);
36 -
37 - if(!$recaptcha['success']){
38 - return [
39 - 'status' => 'error',
40 - 'data' => [
41 - 'message' => esc_html__('reCAPTCHA validation failed.', 'uicore-elements'),
42 - ]
43 - ];
44 - }
45 - }
46 -
47 - // Check for honeypot spam
48 - if(!$this->validate_spam()){
49 - return [
50 - 'status' => 'success',
51 - 'data' => [
52 - 'message' => $this->get_response_message('success') // Fakes a successfull submission
53 - ]
54 - ];
55 - }
56 -
57 - // Run all registered submit actions
58 - if (isset($this->settings['submit_actions']) && !empty($this->settings['submit_actions'])) {
59 - foreach ($this->settings['submit_actions'] as $action) {
60 - try {
61 - switch ($action) {
62 - case 'email':
63 - $data = $this->send_mail($action);
64 - $responses['email'] = $data['response'];
65 - break;
66 -
67 - case 'email_2' :
68 - $data = $this->send_mail($action, $data);
69 - $responses['email'] = $data['response'];
70 - break;
71 -
72 - case 'redirect':
73 - $responses['redirect'] = $this->redirect();
74 - break;
75 -
76 - case 'mailchimp':
77 - $responses['mailchimp'] = $this->mailchimp();
78 - break;
79 -
80 - default:
81 - throw new Submit_Exception(esc_html__('Unknown submit action: ', 'uicore-elements') . $action . esc_html__('. Check your settings.', 'uicore-elements'));
82 - }
83 - } catch (Email_Exception $e) {
84 - $responses['email'] = [
85 - 'status' => false,
86 - 'message' => $e->getMessage()
87 - ];
88 - } catch (Redirect_Exception $e) {
89 - $responses['redirect'] = [
90 - 'status' => 'error',
91 - 'message' => $e->getMessage()
92 - ];
93 - } catch (Mailchimp_Exception $e) {
94 - $responses['mailchimp'] = [
95 - 'status' => 'error',
96 - 'message' => $e->getMessage()
97 - ];
98 - } catch (Submit_Exception $e) {
99 - $responses['submit'] = [
100 - 'status' => 'error',
101 - 'message' => $e->getMessage()
102 - ];
103 - }
104 - // We're avoiding throwing exception for mailchimp because would require a specific validation function, and is to much for now
105 - }
106 -
107 - // There's no need to continue without a submit action enabled
108 - } else {
109 - return [
110 - 'status' => 'error',
111 - 'data' => [
112 - 'message' => esc_html__('No submit action enabled.', 'uicore-elements')
113 - ]
114 - ];
115 - }
116 -
117 - // Consider `current_user_can( 'manage_options' )` as filter to return more specific messages on frontend (not tested)
118 -
119 - // Since attachments may be used up to two times (both emails), they need to be deleted only after processing submits
120 - if ( isset($data['attachments']) && !empty($data['attachments']['files']) ) {
121 - register_shutdown_function('unlink', $data['attachments']['files']);
122 - }
123 -
124 - $output = $this->build_frontend_responses($responses);
125 -
126 - return [
127 - 'status' => $output['status'],
128 - 'data' => $output['data'],
129 - ];
130 - }
131 -
132 - /**
133 - * Mail submition
134 - */
135 - protected function send_mail(string $action, array $data = []){
136 -
137 - $attachments = isset($data['attachments']) ? $data['attachments'] : []; // Check if there's attachments from previous mail submit action
138 -
139 - $mail_data = $this->compose_mail_data($action, $attachments); // build mail data
140 -
141 - // Check if there's any attachment error before sending mail
142 - if (!empty($mail_data['attachments']['errors'])) {
143 - // throwing exceptions here will block proper data flow. Is best directly returning the error on email action
144 - return [
145 - 'response' => [
146 - 'status' => false,
147 - 'message' => $mail_data['attachments']['errors']
148 - ],
149 - ];
150 - }
151 -
152 - $email = wp_mail(
153 - $mail_data['email']['to'],
154 - $mail_data['email']['subject'],
155 - $mail_data['email']['message'],
156 - $mail_data['email']['headers'],
157 - $mail_data['email']['attachments']
158 - );
159 -
160 - return [
161 - 'response' => [
162 - 'status' => $email ? 'success' : 'error',
163 - 'message' => $email ? $this->get_response_message('success') : $this->get_response_message('mail_error')
164 - ],
165 - 'attachments' => $mail_data['attachments'] // Return attachments for deletion and error handling
166 - ];
167 - }
168 - protected function compose_mail_data(string $action, array $attachments = []) {
169 -
170 - // Set short vars for the data
171 - $settings = $this->settings;
172 - $data = $this->form_data;
173 - $files = $this->files;
174 -
175 - $slug = $action == 'email_2' ? '_2' : ''; // Update controls slugs based on the mail submit type
176 - $line_break = $settings['email_content_type'.$slug] === 'html' ? '<br>' : "\n"; // Set line break type
177 -
178 - // Replace shortcodes by form data
179 - $content = $this->replace_content_shortcode( $settings['email_content'.$slug], $line_break );
180 -
181 - // Adds the metadata to content
182 - $content = $this->compose_metadata($content, $settings['form_metadata'.$slug], $line_break);
183 -
184 - // Set empty attachments to avoid undefined errors for widgets without attachment options
185 - if($data['widget_type'] !== 'contact-form') {
186 - $attachments = [ 'files' => '', 'errors' => '' ];
187 - } else {
188 - $attachments = !empty($attachments) ? $attachments : $this->prepare_attachments($files); // If theres attachments from previous submit action, use it, otherwhise prepare it from $files,
189 - }
190 -
191 - // Validate and replace fields shortcodes
192 - $mail_to = $this->replace_content_shortcode( $this->validate_field($settings['email_to'.$slug], 'Recipient (to)'));
193 - $mail_subject = $this->replace_content_shortcode( $this->validate_field($settings['email_subject'.$slug], 'Subject'));
194 - $mail_name = $this->replace_content_shortcode( $this->validate_field($settings['email_from_name'.$slug], 'From Name'));
195 - $mail_from = $this->replace_content_shortcode( $this->validate_field($settings['email_from'.$slug], 'From'));
196 - $mail_reply = $this->replace_content_shortcode( $this->validate_field($settings['email_reply_to'.$slug], 'Reply To'));
197 -
198 - // Build the data
199 - $mail_data = [
200 - 'to' => $mail_to,
201 - 'subject' => $mail_subject,
202 - 'message' => $content,
203 - 'headers' => [
204 - 'Content-Type: text/' . $settings['email_content_type'.$slug] . '; charset=UTF-8',
205 - 'From: ' . $mail_name . ' <'.$mail_from.'>',
206 - 'Reply-To: ' . $mail_reply,
207 - ],
208 - 'attachments' => $attachments['files']
209 - ];
210 -
211 - // Build optional data
212 - if (!empty($settings['email_to_cc'.$slug])) {
213 - $mail_data['headers'][] = 'Cc: ' . $settings['email_to_cc'];
214 - }
215 - if (!empty($settings['email_to_bcc'.$slug])) {
216 - $mail_data['headers'][] = 'Bcc: ' . $settings['email_to_bcc'];
217 - }
218 -
219 - return [
220 - 'email' => $mail_data,
221 - 'attachments' => $attachments
222 - ];
223 - }
224 - protected function replace_content_shortcode(string $content, string $line_break = ''){
225 -
226 - // Set short vars for the data
227 - $fields = $this->get_setting_fields();
228 - $form_data = $this->form_data;
229 -
230 - // [all-fieds] shortcode replacement
231 - if ( false !== strpos( $content, '[all-fields]' ) ) {
232 - $text = '';
233 - // Return formated text as key: value
234 - foreach ( $form_data['form_fields'] as $key => $field ) {
235 - $field_value = is_array($field) ? implode(', ', $field) : $field;
236 - $text .= !empty($field_value) ? sprintf('%s: %s', $key, $field_value) . $line_break : '';
237 - }
238 - $content = str_replace( '[all-fields]', $text, $content );
239 - }
240 -
241 - // Custom [field id="{id}"] shortcode replacement
242 - foreach ($fields as $field) {
243 - $shortcode = '[field id="' . $field['custom_id'] . '"]';
244 - $value = isset($form_data['form_fields'][$field['custom_id']]) ? $form_data['form_fields'][$field['custom_id']] : '';
245 - $value = is_array($value) ? implode(', ', $value) : $value;
246 - $content = str_replace($shortcode, $value, $content);
247 - }
248 -
249 - // Replaces all manual line breaks from content
250 - if(!empty($line_break)){
251 - $content = str_replace( array( "\r\n", "\r", "\n" ), $line_break, $content );
252 - }
253 -
254 - return $content;
255 - }
256 - protected function prepare_attachments(array $files) {
257 - $attachments = [];
258 - $errors = '';
259 -
260 - // Requires wp_handle_upload() file if unavailable
261 - if ( ! function_exists( 'wp_handle_upload' ) ) {
262 - require_once( ABSPATH . 'wp-admin/includes/file.php' );
263 - }
264 -
265 - // Check if theres a valid file to upload
266 - foreach ($files['form_fields']['tmp_name'] as $input => $value) {
267 - if ($files['form_fields']['error'][$input] !== UPLOAD_ERR_NO_FILE) {
268 - $file = [
269 - 'name' => $files['form_fields']['name'][$input],
270 - 'type' => $files['form_fields']['type'][$input],
271 - 'tmp_name' => $files['form_fields']['tmp_name'][$input],
272 - 'error' => $files['form_fields']['error'][$input],
273 - 'size' => $files['form_fields']['size'][$input],
274 - ];
275 -
276 - // Handle the file upload
277 - $uploaded_file = wp_handle_upload($file, ['test_form' => false]);
278 -
279 - if (!isset($uploaded_file['error'])) {
280 - $attachments = $uploaded_file['file'];
281 - } else {
282 - // Since throwing exceptions here will block the proper data flow, we return the error and let send_mail() handle it
283 - $errors = esc_html__('Failed to upload file: ', 'uicore-elements') . $uploaded_file['error'];
284 - }
285 -
286 - // Break after processing the first valid file
287 - break;
288 - }
289 - }
290 -
291 - return [
292 - 'files' => $attachments,
293 - 'errors' => $errors
294 - ];
295 - }
296 - protected function compose_metadata(string $content, array $metadada, string $line_break){
297 -
298 - if (empty($metadada)) {
299 - return $content;
300 - }
301 -
302 - $content = $content . $line_break . $line_break . '--' . $line_break . $line_break; // Adds spacing between content and metadata
303 -
304 - foreach ($metadada as $meta) {
305 - switch($meta){
306 - case 'date':
307 - $content .= sprintf( '%s: %s', 'Date', date('Y-m-d') . $line_break);
308 - break;
309 -
310 - case 'time' :
311 - $content .= sprintf( '%s: %s', 'Time', date('H:i:s') . $line_break);
312 - break;
313 -
314 - case 'remote_ip':
315 - $content .= sprintf( '%s: %s', 'IP', $_SERVER['REMOTE_ADDR'] . $line_break);
316 - break;
317 -
318 - case 'user_agent':
319 - $content .= sprintf( '%s: %s', 'User Agent', $_SERVER['HTTP_USER_AGENT'] . $line_break);
320 - break;
321 -
322 - case 'page_url':
323 - $content .= sprintf( '%s: %s', 'Page URL', $_SERVER['HTTP_REFERER'] . $line_break);
324 - break;
325 - }
326 - }
327 -
328 - return $content;
329 - }
330 -
331 - /**
332 - * Extra submissions
333 - */
334 - protected function redirect() {
335 -
336 - $validation = $this->validate_url( $this->settings['redirect_to'] );
337 -
338 - // Above function exception blocks this execution
339 - return [
340 - 'status' => 'success',
341 - 'url' => esc_url( $validation['url'] ),
342 - 'delay' => 1500,
343 - 'message' => esc_html( $this->get_response_message('redirect') )
344 - ];
345 - }
346 - protected function mailchimp() {
347 -
348 - // Get API data
349 - $key = get_option('uicore_elements_mailchimp_secret_key');
350 - $list_id = $this->settings['mailchimp_audience_id'];
351 - $server = explode('-', $key)[1]; // Server value can be found on API Key after the dash
352 -
353 - $this->validate_mailchimp($key, $list_id);
354 -
355 - // Check the widget type to determine the fields, before getting form data
356 - if( $this->form_data['widget_type'] === 'contact-form' ) {
357 -
358 - // Since contact form has custom IDs, we need to get the ID value from the settings
359 - $settings = $this->settings;
360 -
361 - $email = $this->form_data['form_fields'][$settings['mailchimp_email_id']];
362 - $merge_fields = [
363 - 'FNAME' => isset( $this->form_data['form_fields'][$settings['mailchimp_fname_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_fname_id']] : "",
364 - 'LNAME' => isset( $this->form_data['form_fields'][$settings['mailchimp_lname_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_lname_id']] : "",
365 - 'PHONE' => isset( $this->form_data['form_fields'][$settings['mailchimp_phone_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_phone_id']] : "",
366 - 'BIRTHDAY' => isset( $this->form_data['form_fields'][$settings['mailchimp_birthday_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_birthday_id']] : ""
367 - ];
368 -
369 - // Else can only be newsletter widget
370 - } else {
371 -
372 - // Since Newsletter has fixed field IDs, we get them directly
373 - $email = $this->form_data['form_fields']['email'];
374 - $merge_fields = [
375 - 'FNAME' => isset( $this->form_data['form_fields']['name'] ) ? $this->form_data['form_fields']['name'] : ""
376 - ];
377 - }
378 -
379 - // Build the request
380 - $url = 'https://' . esc_html($server) . '.api.mailchimp.com/3.0/lists/' . esc_html($list_id) . '/members/';
381 - $data = [
382 - "email_address" => $email,
383 - "status" => "subscribed",
384 - "merge_fields" => $merge_fields
385 - ];
386 -
387 -
388 - $request = curl_init();
389 - curl_setopt($request, CURLOPT_URL, $url);
390 - curl_setopt($request, CURLOPT_HTTPHEADER, [
391 - 'Content-Type: application/json',
392 - 'Authorization: Basic ' . base64_encode('anystring:' . $key)
393 - ]);
394 - curl_setopt($request, CURLOPT_POST, true);
395 - curl_setopt($request, CURLOPT_POSTFIELDS, json_encode($data));
396 - curl_setopt($request, CURLOPT_RETURNTRANSFER, true);
397 - $res = curl_exec($request);
398 -
399 - if(curl_errno($request)) {
400 - $res = curl_error($request);
401 - } else {
402 - $res = json_decode($res, true);
403 - }
404 -
405 - curl_close($request);
406 -
407 - return $res;
408 - }
409 -
410 - /**
411 - * Validations
412 - */
413 - protected function validate_recaptcha(string $token, string $version) {
414 -
415 - // Check if secret and site key are set
416 - if (!get_option('uicore_elements_recaptcha_secret_key') || !get_option('uicore_elements_recaptcha_site_key')) {
417 - return [
418 - 'success' => false,
419 - 'message' => esc_html__('reCAPTCHA API keys are not set.', 'uicore-elements')
420 - ];
421 - }
422 -
423 - $data = [
424 - 'secret' => get_option('uicore_elements_recaptcha_secret_key'),
425 - 'response' => sanitize_text_field($token)
426 - ];
427 -
428 - $verify = curl_init();
429 - curl_setopt($verify, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify");
430 - curl_setopt($verify, CURLOPT_POST, true);
431 - curl_setopt($verify, CURLOPT_POSTFIELDS, http_build_query($data));
432 - curl_setopt($verify, CURLOPT_SSL_VERIFYPEER, false);
433 - curl_setopt($verify, CURLOPT_RETURNTRANSFER, true);
434 - $res = curl_exec($verify);
435 -
436 - $captcha = json_decode($res);
437 -
438 - if($version === 'V3') {
439 - return ['success' => ($captcha->success && $captcha->score >= 0.5) ? true : false];
440 - }
441 -
442 - // V2 default
443 - return ['success' => $captcha->success];
444 -
445 - }
446 - protected function validate_spam() {
447 - // `ui-e-h-p` is the key for the honeypot
448 - return ( isset($this->form_data['ui-e-h-p']) && !empty($this->form_data['ui-e-h-p']) ) ? false : true;
449 - }
450 - protected function validate_url(string $url) {
451 - if (empty($url)) {
452 - throw new Redirect_Exception( $this->get_response_message('redirect_no_url') );
453 - }
454 -
455 - return [
456 - 'status' => true,
457 - 'url' => $url,
458 - ];
459 - }
460 - protected function validate_field(string $field, string $label) {
461 - if (empty($field)) {
462 - throw new Submit_Exception( $this->get_response_message('empty_field', $label) );
463 - }
464 - return $field;
465 - }
466 - protected function validate_mailchimp(string $key, string $list_id) {
467 - if (empty($key)) {
468 - throw new Mailchimp_Exception(esc_html__('Mailchimp API key is not set. Check Uicore Elements settings.', 'uicore-elements'));
469 - } else if (empty($list_id)) {
470 - throw new Mailchimp_Exception(esc_html__('Audience ID control is not set. Check your widget settings.', 'uicore-elements'));
471 - }
472 - }
473 -
474 - /**
475 - * Helpers
476 - */
477 - protected function get_setting_fields() {
478 - // Used to determine if the widget fields are repeaters with custom IDS or fixed fields, and if fixed fields
479 - // compose them into an array similar to repeaters, to simplify shortcode replacement function
480 -
481 - switch ($this->form_data['widget_type']) {
482 -
483 - case 'newsletter':
484 - return [
485 - ['custom_id' => 'email'],
486 - ['custom_id' => 'name'],
487 - ];
488 - break;
489 -
490 - // Dynamic fields values
491 - default:
492 - return $this->settings['form_fields'];
493 - break;
494 - }
495 - }
496 - protected function all_submissions_succedded($responses) {
497 - foreach ($responses as $submission => $data) {
498 - // Redirect action failure shouldn't return `error` to main status because is not properly a submission action, so we skip it.
499 - if( $submission == 'redirect') {
500 - continue;
501 - }
502 - // Failed submissions returns `error` or bool `false` status
503 - if( $data['status'] === 'error' || $data['status'] === false ) {
504 - return false;
505 - }
506 - }
507 - return true;
508 - }
509 -
510 - /**
511 - * Responses
512 - */
513 - // Also used by form widget(s), therefore public and static
514 - public static function get_default_messages(){
515 - return [
516 - // main messages
517 - 'success' => esc_html__( 'Your submission was successful.', 'uicore-elements' ),
518 - 'error' => esc_html__( 'Your submission failed because of an error.', 'uicore-elements' ),
519 - 'mail_error' => esc_html__( 'Failed to send email.', 'uicore-elements' ),
520 - 'redirect' => esc_html__( 'Redirecting...', 'uicore-elements' ),
521 - ];
522 - }
523 - protected function get_response_message(string $status, string $dinamic_data = '') {
524 - // non-customizable messages (for settings debugging only)
525 - $default_messages = [
526 - 'invalid_status' => esc_html__( 'Invalid status message.', 'uicore-elements' ),
527 - 'redirect_no_url' => esc_html__( 'Redirection failed. No URL set.', 'uicore-elements' ),
528 - 'empty_field' => esc_html__( 'The following field is empty.', 'uicore-elements') . $dinamic_data,
529 - ];
530 -
531 - if($this->settings['custom_messages'] === 'yes') {
532 - $messages = [
533 - 'success' => $this->settings['success_message'],
534 - 'error' => $this->settings['error_message'],
535 - 'mail_error' => $this->settings['mail_error_message'],
536 - 'redirect' => $this->settings['redirect_message'],
537 - ];
538 - } else {
539 - $messages = self::get_default_messages();
540 - }
541 -
542 - $messages = array_merge($default_messages, $messages);
543 -
544 - return isset($messages[$status]) ? $messages[$status] : $messages['invalid_status'];
545 - }
546 - protected function build_frontend_responses($responses) {
547 -
548 - $data = [];
549 -
550 - // Mail response
551 - if ( isset($responses['email']) && $responses['email']['status'] !== 'success' ) {
552 - $data['email'] = $responses['email'];
553 - }
554 -
555 - // Mail attachment response - is always an error
556 - if ( isset($responses['email']) && isset($responses['email']['attachment']) ) {
557 - $data['attachment'] = $responses['attachment'];
558 - }
559 -
560 - // Mailchimp response - Integer is also an error
561 - if ( isset($responses['mailchimp']) ) {
562 -
563 - // If Integer, is an error from mailchimp API so we pass their response
564 - if( is_int($responses['mailchimp']['status'])){
565 - $data['mailchimp'] = [
566 - 'status' => 'error',
567 - 'message' => sprintf( esc_html__('Mailchimp HTTP "%s" - "%s."', 'uicore-elements'), $responses['mailchimp']['status'], $responses['mailchimp']['detail'])
568 - ];
569 -
570 - // If error is from our validation
571 - } else if ( $responses['mailchimp']['status'] === 'error' ) {
572 - $data['mailchimp'] = [
573 - 'status' => $responses['mailchimp']['status'],
574 - 'message' => $responses['mailchimp']['message']
575 - ];
576 - }
577 - }
578 -
579 - // Submit Actions response - is always an error
580 - if ( isset($responses['submit']) ) {
581 - $data['submit'] = $responses['submit'];
582 - }
583 -
584 - // Main response
585 - $status = $this->all_submissions_succedded($responses) ? 'success' : 'error';
586 - $data['message'] = $this->get_response_message($status);
587 -
588 - // Redirect response (should work only if all previous submitions hasn't failed)
589 - if ( isset($responses['redirect']) && $status === 'success' ) {
590 - $data['redirect'] = $responses['redirect'];
591 - }
592 -
593 - // The only successfull response that should be sent is 'main' and 'redirect'
594 - return [
595 - 'status' => $status,
596 - 'data' => $data
597 - ];
598 - }
599 -
600 -}
1 +<?php
2 +
3 +namespace UiCoreElements\Utils;
4 +
5 +use UiCoreELements\Utils\Newsletter_Services as Services;
6 +
7 +class Email_Exception extends \Exception {}
8 +class Redirect_Exception extends \Exception {}
9 +class Submit_Exception extends \Exception {}
10 +class Newsletter_Service_Exception extends \Exception {}
11 +
12 +defined('ABSPATH') || exit();
13 +
14 +/**
15 + * Handles the form submissions and responses
16 + */
17 +
18 +class Contact_Form_Service
19 +{
20 +
21 + protected $form_data,
22 + $settings,
23 + $files;
24 +
25 + public function __construct($form_data, $settings, $files)
26 + {
27 + $this->form_data = $form_data;
28 + $this->settings = $settings;
29 + $this->files = $files;
30 + }
31 +
32 + public function handle()
33 + {
34 +
35 + $data = [];
36 + $responses = [];
37 +
38 + // Checks for reCAPTCHA validation
39 + if (isset($this->form_data['grecaptcha_token']) && !empty($this->form_data['grecaptcha_token'])) {
40 +
41 + $recaptcha = $this->validate_recaptcha($this->form_data['grecaptcha_token'], $this->form_data['grecaptcha_version']);
42 +
43 + if (!$recaptcha['success']) {
44 + return [
45 + 'status' => 'error',
46 + 'data' => [
47 + 'message' => esc_html__('reCAPTCHA validation failed.', 'uicore-elements'),
48 + ]
49 + ];
50 + }
51 + }
52 +
53 + // Check for honeypot spam
54 + if (!$this->validate_spam()) {
55 + return [
56 + 'status' => 'success',
57 + 'data' => [
58 + 'message' => $this->get_response_message('success') // Fakes a successfull submission
59 + ]
60 + ];
61 + }
62 +
63 + // Run all registered submit actions
64 + if (isset($this->settings['submit_actions']) && !empty($this->settings['submit_actions'])) {
65 + foreach ($this->settings['submit_actions'] as $action) {
66 + try {
67 + switch ($action) {
68 + case 'email':
69 + $data = $this->send_mail($action);
70 + $responses['email'] = $data['response'];
71 + break;
72 +
73 + case 'email_2':
74 + $data = $this->send_mail($action, $data);
75 + $responses['email'] = $data['response'];
76 + break;
77 +
78 + case 'redirect':
79 + $responses['redirect'] = $this->redirect();
80 + break;
81 +
82 + case 'popup':
83 + $responses['popup'] = $this->popup();
84 + break;
85 +
86 + case 'webhook':
87 + $responses['webhook'] = $this->webhook();
88 + break;
89 +
90 + case 'mailchimp':
91 + $responses['mailchimp'] = $this->newsletter_service('mailchimp');
92 + break;
93 +
94 + case 'brevo':
95 + $responses['brevo'] = $this->newsletter_service('brevo');
96 + break;
97 +
98 + case 'kit':
99 + $responses['kit'] = $this->newsletter_service('kit');
100 + break;
101 +
102 + case 'moosend':
103 + $responses['moosend'] = $this->newsletter_service('moosend');
104 + break;
105 +
106 + case 'getresponse':
107 + $responses['getresponse'] = $this->newsletter_service('getresponse');
108 + break;
109 +
110 + case 'mailerlite':
111 + $responses['mailerlite'] = $this->newsletter_service('mailerlite');
112 + break;
113 +
114 + default:
115 + throw new Submit_Exception(esc_html__('Unknown submit action: ', 'uicore-elements') . $action . esc_html__('. Check your settings.', 'uicore-elements'));
116 + }
117 + } catch (Email_Exception $e) {
118 + $responses['email'] = [
119 + 'status' => false,
120 + 'message' => $e->getMessage()
121 + ];
122 + } catch (Redirect_Exception $e) {
123 + $responses['redirect'] = [
124 + 'status' => 'error',
125 + 'message' => $e->getMessage()
126 + ];
127 + } catch (Newsletter_Service_Exception $e) {
128 + $responses['newsletter_service'] = [
129 + 'status' => 'error',
130 + 'service' => $action,
131 + 'message' => $e->getMessage()
132 + ];
133 + } catch (Submit_Exception $e) {
134 + $responses['submit'] = [
135 + 'status' => 'error',
136 + 'message' => $e->getMessage()
137 + ];
138 + }
139 + // We're avoiding throwing exception for mailchimp because would require a specific validation function, and is to much for now
140 + }
141 +
142 + // There's no need to continue without a submit action enabled
143 + } else {
144 + return [
145 + 'status' => 'error',
146 + 'data' => [
147 + 'message' => esc_html__('No submit action enabled.', 'uicore-elements')
148 + ]
149 + ];
150 + }
151 +
152 + // Consider `current_user_can( 'manage_options' )` as filter to return more specific messages on frontend (not tested)
153 +
154 + // Since attachments may be used up to two times (both emails), they need to be deleted only after processing submits
155 + if (isset($data['attachments']) && !empty($data['attachments']['files'])) {
156 + register_shutdown_function('unlink', $data['attachments']['files']);
157 + }
158 +
159 + $output = $this->build_frontend_responses($responses);
160 +
161 + return [
162 + 'status' => $output['status'],
163 + 'data' => $output['data'],
164 + ];
165 + }
166 +
167 + /**
168 + * Mail submition
169 + */
170 + protected function send_mail(string $action, array $data = [])
171 + {
172 +
173 + $attachments = isset($data['attachments']) ? $data['attachments'] : []; // Check if there's attachments from previous mail submit action
174 +
175 + $mail_data = $this->compose_mail_data($action, $attachments); // build mail data
176 +
177 + // Check if there's any attachment error before sending mail
178 + if (!empty($mail_data['attachments']['errors'])) {
179 + // throwing exceptions here will block proper data flow. Is best directly returning the error on email action
180 + return [
181 + 'response' => [
182 + 'status' => false,
183 + 'message' => $mail_data['attachments']['errors']
184 + ],
185 + ];
186 + }
187 +
188 + $email = wp_mail(
189 + $mail_data['email']['to'],
190 + $mail_data['email']['subject'],
191 + $mail_data['email']['message'],
192 + $mail_data['email']['headers'],
193 + $mail_data['email']['attachments']
194 + );
195 +
196 + return [
197 + 'response' => [
198 + 'status' => $email ? 'success' : 'error',
199 + 'message' => $email ? $this->get_response_message('success') : $this->get_response_message('mail_error')
200 + ],
201 + 'attachments' => $mail_data['attachments'] // Return attachments for deletion and error handling
202 + ];
203 + }
204 + protected function compose_mail_data(string $action, array $attachments = [])
205 + {
206 +
207 + // Set short vars for the data
208 + $settings = $this->settings;
209 + $data = $this->form_data;
210 + $files = $this->files;
211 +
212 + $slug = $action == 'email_2' ? '_2' : ''; // Update controls slugs based on the mail submit type
213 + $line_break = $settings['email_content_type' . $slug] === 'html' ? '<br>' : "\n"; // Set line break type
214 +
215 + // Replace shortcodes by form data
216 + $content = $this->replace_content_shortcode($settings['email_content' . $slug], $line_break);
217 +
218 + // Adds the metadata to content
219 + $content = $this->compose_metadata($content, $settings['form_metadata' . $slug], $line_break);
220 +
221 + // Set empty attachments to avoid undefined errors for widgets without attachment options
222 + if ($data['widget_type'] !== 'contact-form') {
223 + $attachments = ['files' => '', 'errors' => ''];
224 + } else {
225 + $attachments = !empty($attachments) ? $attachments : $this->prepare_attachments($files); // If theres attachments from previous submit action, use it, otherwhise prepare it from $files,
226 + }
227 +
228 + // Validate and replace fields shortcodes
229 + $mail_to = $this->replace_content_shortcode($this->validate_field($settings['email_to' . $slug], 'Recipient (to)'));
230 + $mail_subject = $this->replace_content_shortcode($this->validate_field($settings['email_subject' . $slug], 'Subject'));
231 + $mail_name = $this->replace_content_shortcode($this->validate_field($settings['email_from_name' . $slug], 'From Name'));
232 + $mail_from = $this->replace_content_shortcode($this->validate_field($settings['email_from' . $slug], 'From'));
233 + $mail_reply = $this->replace_content_shortcode($this->validate_field($settings['email_reply_to' . $slug], 'Reply To'));
234 +
235 + // Build the data
236 + $mail_data = [
237 + 'to' => $mail_to,
238 + 'subject' => $mail_subject,
239 + 'message' => $content,
240 + 'headers' => [
241 + 'Content-Type: text/' . $settings['email_content_type' . $slug] . '; charset=UTF-8',
242 + 'From: ' . $mail_name . ' <' . $mail_from . '>',
243 + 'Reply-To: ' . $mail_reply,
244 + ],
245 + 'attachments' => $attachments['files']
246 + ];
247 +
248 + // Build optional data
249 + if (!empty($settings['email_to_cc' . $slug])) {
250 + $mail_data['headers'][] = 'Cc: ' . $this->replace_content_shortcode($settings['email_to_cc' . $slug]);
251 + }
252 + if (!empty($settings['email_to_bcc' . $slug])) {
253 + $mail_data['headers'][] = 'Bcc: ' . $this->replace_content_shortcode($settings['email_to_bcc' . $slug]);
254 + }
255 +
256 + return [
257 + 'email' => $mail_data,
258 + 'attachments' => $attachments
259 + ];
260 + }
261 + protected function replace_content_shortcode(string $content, string $line_break = '')
262 + {
263 +
264 + // Set short vars for the data
265 + $fields = $this->get_setting_fields();
266 + $form_data = $this->form_data;
267 +
268 + // [all-fieds] shortcode replacement
269 + if (false !== strpos($content, '[all-fields]')) {
270 + $text = '';
271 + // Return formated text as key: value
272 + foreach ($form_data['form_fields'] as $key => $field) {
273 + $field_value = is_array($field) ? implode(', ', $field) : $field;
274 + $text .= !empty($field_value) ? sprintf('%s: %s', $key, $field_value) . $line_break : '';
275 + }
276 + $content = str_replace('[all-fields]', $text, $content);
277 + }
278 +
279 + // Custom [field id="{id}"] shortcode replacement
280 + foreach ($fields as $field) {
281 + $shortcode = '[field id="' . $field['custom_id'] . '"]';
282 + $value = isset($form_data['form_fields'][$field['custom_id']]) ? $form_data['form_fields'][$field['custom_id']] : '';
283 + $value = is_array($value) ? implode(', ', $value) : $value;
284 + $content = str_replace($shortcode, $value, $content);
285 + }
286 +
287 + // Replaces all manual line breaks from content
288 + if (!empty($line_break)) {
289 + $content = str_replace(array("\r\n", "\r", "\n"), $line_break, $content);
290 + }
291 +
292 + return $content;
293 + }
294 + protected function prepare_attachments(array $files)
295 + {
296 + $attachments = [];
297 + $errors = '';
298 +
299 + if (!isset($files['form_fields']) || empty($files['form_fields'])) {
300 + return [
301 + 'files' => '',
302 + 'errors' => ''
303 + ];
304 + }
305 +
306 + // Requires wp_handle_upload() file if unavailable
307 + if (! function_exists('wp_handle_upload')) {
308 + require_once(ABSPATH . 'wp-admin/includes/file.php');
309 + }
310 +
311 + // Check if theres a valid file to upload
312 + foreach ($files['form_fields']['tmp_name'] as $input => $value) {
313 + if ($files['form_fields']['error'][$input] !== UPLOAD_ERR_NO_FILE) {
314 + $file = [
315 + 'name' => $files['form_fields']['name'][$input],
316 + 'type' => $files['form_fields']['type'][$input],
317 + 'tmp_name' => $files['form_fields']['tmp_name'][$input],
318 + 'error' => $files['form_fields']['error'][$input],
319 + 'size' => $files['form_fields']['size'][$input],
320 + ];
321 +
322 + // Handle the file upload
323 + $uploaded_file = wp_handle_upload($file, ['test_form' => false]);
324 +
325 + if (!isset($uploaded_file['error'])) {
326 + $attachments = $uploaded_file['file'];
327 + } else {
328 + // Since throwing exceptions here will block the proper data flow, we return the error and let send_mail() handle it
329 + $errors = esc_html__('Failed to upload file: ', 'uicore-elements') . $uploaded_file['error'];
330 + }
331 +
332 + // Break after processing the first valid file
333 + break;
334 + }
335 + }
336 +
337 + return [
338 + 'files' => $attachments,
339 + 'errors' => $errors
340 + ];
341 + }
342 + protected function compose_metadata(string $content, array $metadada, string $line_break)
343 + {
344 +
345 + if (empty($metadada)) {
346 + return $content;
347 + }
348 +
349 + $content = $content . $line_break . $line_break . '--' . $line_break . $line_break; // Adds spacing between content and metadata
350 +
351 + foreach ($metadada as $meta) {
352 + switch ($meta) {
353 + case 'date':
354 + $content .= sprintf('%s: %s', 'Date', wp_date('Y-m-d') . $line_break);
355 + break;
356 +
357 + case 'time':
358 + $content .= sprintf('%s: %s', 'Time', wp_date('H:i:s') . $line_break);
359 + break;
360 +
361 + case 'remote_ip':
362 + $content .= isset($_SERVER['REMOTE_ADDR'])
363 + ? sprintf('%s: %s', 'IP', sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) . $line_break)
364 + : '';
365 + break;
366 +
367 + case 'user_agent':
368 + $content .= isset($_SERVER['HTTP_USER_AGENT'])
369 + ? sprintf('%s: %s', 'User Agent', sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) . $line_break)
370 + : '';
371 + break;
372 +
373 + case 'page_url':
374 + $content .= isset($_SERVER['HTTP_REFERER'])
375 + ? sprintf('%s: %s', 'Page URL', sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) . $line_break)
376 + : '';
377 + break;
378 + }
379 + }
380 +
381 + return $content;
382 + }
383 +
384 + /**
385 + * Extra submissions
386 + */
387 + protected function redirect()
388 + {
389 +
390 + $validation = $this->validate_url($this->settings['redirect_to']);
391 +
392 + // Above function exception blocks this execution
393 + return [
394 + 'status' => 'success',
395 + 'url' => esc_url($validation['url']),
396 + 'delay' => 1500,
397 + 'message' => esc_html($this->get_response_message('redirect'))
398 + ];
399 + }
400 + protected function popup()
401 + {
402 + $action = $this->settings['popup_action'];
403 +
404 + if ($action === 'open') {
405 + return [
406 + 'status' => 'success',
407 + 'action' => sanitize_text_field($action),
408 + 'id' => sanitize_text_field($this->settings['open_popup']),
409 + 'message' => sanitize_text_field($this->get_response_message('popup'))
410 + ];
411 + }
412 +
413 + return [
414 + 'status' => 'success',
415 + 'action' => sanitize_text_field($action),
416 + 'message' => sanitize_text_field($this->get_response_message('popup'))
417 + ];
418 + }
419 + protected function webhook(): array
420 + {
421 + $request = $this->build_webhook_request();
422 +
423 + $response = wp_remote_post($request['url'], $request['args']);
424 +
425 + if (is_wp_error($response)) {
426 + return [
427 + 'status' => 'error',
428 + 'message' => $response->get_error_message(),
429 + ];
430 + }
431 +
432 + $status_code = (int) wp_remote_retrieve_response_code($response);
433 +
434 + if ($status_code < 200 || $status_code >= 300) {
435 + $body = wp_remote_retrieve_body($response);
436 + $body = is_string($body) ? trim(wp_strip_all_tags($body)) : '';
437 + $body = $body ? wp_html_excerpt($body, 160, '...') : esc_html__('Unexpected response from webhook endpoint.', 'uicore-elements');
438 +
439 + return [
440 + 'status' => 'error',
441 + 'message' => sprintf(
442 + /* translators: 1: HTTP status code, 2: Response body excerpt. */
443 + esc_html__('Webhook request failed with HTTP %1$s: %2$s', 'uicore-elements'),
444 + $status_code,
445 + $body
446 + ),
447 + ];
448 + }
449 +
450 + return [
451 + 'status' => 'success',
452 + 'message' => esc_html__('Webhook sent successfully.', 'uicore-elements'),
453 + ];
454 + }
455 + protected function newsletter_service(string $service)
456 + {
457 + $settings = $this->settings;
458 + $data = $this->get_submission_data_to_service_fields();
459 +
460 + $api_key = get_option('uicore_elements_newsletter_service_key');
461 + $fields = $data['fields'];
462 + $list = array_map('trim', explode(',', $settings['mailchimp_audience_id']));
463 + $custom_fields = $data['custom_fields'];
464 +
465 + $instance = new Services($api_key, $fields, $list, $custom_fields);
466 + $response = $instance->handle($service);
467 +
468 + return $response;
469 + }
470 +
471 + /**
472 + * Validations
473 + */
474 + protected function validate_recaptcha(string $token, string $version)
475 + {
476 +
477 + // Check if secret and site key are set
478 + if (!get_option('uicore_elements_recaptcha_secret_key') || !get_option('uicore_elements_recaptcha_site_key')) {
479 + return [
480 + 'success' => false,
481 + 'message' => esc_html__('reCAPTCHA API keys are not set.', 'uicore-elements')
482 + ];
483 + }
484 +
485 + $data = [
486 + 'secret' => get_option('uicore_elements_recaptcha_secret_key'),
487 + 'response' => sanitize_text_field($token)
488 + ];
489 +
490 + $response = wp_remote_post("https://www.google.com/recaptcha/api/siteverify", [
491 + 'body' => $data,
492 + 'timeout' => 15,
493 + ]);
494 +
495 + if (is_wp_error($response)) {
496 + return [
497 + 'success' => false,
498 + 'message' => $response->get_error_message()
499 + ];
500 + }
501 +
502 + $captcha = json_decode(wp_remote_retrieve_body($response));
503 +
504 + if ($version === 'V3') {
505 + return ['success' => ($captcha->success && $captcha->score >= 0.5) ? true : false];
506 + }
507 +
508 + // V2 default
509 + return ['success' => $captcha->success];
510 + }
511 + protected function validate_spam()
512 + {
513 + // `ui-e-h-p` is the key for the honeypot
514 + return (isset($this->form_data['ui-e-h-p']) && !empty($this->form_data['ui-e-h-p'])) ? false : true;
515 + }
516 + protected function validate_url(string $url)
517 + {
518 + if (empty($url)) {
519 + throw new Redirect_Exception(esc_html($this->get_response_message('redirect_no_url')));
520 + }
521 +
522 + return [
523 + 'status' => true,
524 + 'url' => $url,
525 + ];
526 + }
527 + protected function validate_webhook(string $url)
528 + {
529 + if (empty($url)) {
530 + throw new Submit_Exception(esc_html__('Webhook URL is empty.', 'uicore-elements'));
531 + }
532 +
533 + $url = esc_url_raw($url);
534 +
535 + if (!$url || !wp_http_validate_url($url)) {
536 + throw new Submit_Exception(esc_html__('Webhook URL is invalid.', 'uicore-elements'));
537 + }
538 +
539 + return $url;
540 + }
541 + protected function validate_field(string $field, string $label)
542 + {
543 + if (empty($field)) {
544 + throw new Submit_Exception(esc_html($this->get_response_message('empty_field', esc_html($label))));
545 + }
546 + return $field;
547 + }
548 +
549 + /**
550 + * Helpers
551 + */
552 + protected function get_setting_fields()
553 + {
554 + // Used to determine if the widget fields are repeaters with custom IDS or fixed fields, and if fixed fields
555 + // compose them into an array similar to repeaters, to simplify shortcode replacement function
556 +
557 + switch ($this->form_data['widget_type']) {
558 +
559 + case 'newsletter':
560 + return [
561 + ['custom_id' => 'email'],
562 + ['custom_id' => 'name'],
563 + ];
564 + break;
565 +
566 + // Dynamic fields values
567 + default:
568 + return $this->settings['form_fields'];
569 + break;
570 + }
571 + }
572 + protected function get_submission_data_to_service_fields(): array
573 + {
574 + $settings = $this->settings;
575 + $widget = $this->form_data['widget_type'];
576 +
577 + // Newsletter widget works only with email and perpahs name
578 + if ($widget === 'newsletter') {
579 + return [
580 + 'fields' => [
581 + 'email' => $this->replace_content_shortcode('[field id="email"]'),
582 + 'name' => $this->replace_content_shortcode('[field id="name"]'),
583 + ],
584 + 'custom_fields' => []
585 + ];
586 + }
587 +
588 + // Contact Form case has custom fields and allows fields ID change
589 +
590 + $repeater_fields = $settings['newsletter_service_custom_fields'];
591 +
592 + $fields = [
593 + 'email' => 'mailchimp_email_id',
594 + 'name' => 'mailchimp_fname_id',
595 + 'last_name' => 'mailchimp_lname_id',
596 + 'phone' => 'mailchimp_phone_id',
597 + 'birthday' => 'mailchimp_birthday_id',
598 + ];
599 +
600 + $custom_fields = [];
601 +
602 + // Build custom field stack
603 + if (is_array($repeater_fields) && !empty($repeater_fields)) {
604 + foreach ($repeater_fields as $field) {
605 + $custom_fields[$field['field_name']] = [
606 + 'value' => $this->replace_content_shortcode($field['field_id']), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
607 + 'method' => $this->get_custom_field_sanitization_method($field['field_type']), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
608 + ];
609 + }
610 + }
611 +
612 + // Remove not used fields from the stack, and extract the content of used fields, if they're using shortcodes
613 + foreach ($fields as $data => $field) {
614 + if (isset($settings[$field]) && !empty($settings[$field])) {
615 + $fields[$data] = $this->replace_content_shortcode($settings[$field]);
616 + } else if ($data !== 'email' && isset($field[$data])) {
617 + unset($field[$data]);
618 + }
619 + }
620 +
621 + return [
622 + 'fields' => $fields,
623 + 'custom_fields' => $custom_fields
624 + ];
625 + }
626 + protected function get_custom_field_sanitization_method(string $type): string
627 + {
628 + // Values are default wp or custom methods from our newsletter services class,
629 + // while $type should be field type options from form component class
630 + switch ($type) {
631 + case 'email':
632 + return 'sanitize_email';
633 + case 'phone':
634 + return 'sanitize_phone';
635 + case 'birthday':
636 + return 'sanitize_birthday';
637 + default:
638 + return 'sanitize_text_field';
639 + }
640 + }
641 + protected function all_submissions_succedded($responses)
642 + {
643 + foreach ($responses as $submission => $data) {
644 + // Redirect and popup actions failure shouldn't return `error` to main status because is not properly a submission action, so we skip it.
645 + if (in_array($submission, ['redirect', ' popup'])) {
646 + continue;
647 + }
648 + if (
649 + isset($data['status']) &&
650 + (
651 + $data['status'] === 'error' ||
652 + $data['status'] === false
653 + )
654 + ) {
655 + return false;
656 + }
657 + }
658 + return true;
659 + }
660 + public static function get_form_submit_options()
661 + {
662 +
663 + $options = [
664 + 'email' => esc_html__('Email', 'uicore-elements'),
665 + 'email_2' => esc_html__('Email 2', 'uicore-elements'),
666 + 'redirect' => esc_html__('Redirect', 'uicore-elements'),
667 + 'webhook' => esc_html__('Webhook', 'uicore-elements')
668 + ];
669 +
670 + // Uicore Framework dependent actions
671 + if (defined('UICORE_ASSETS')) {
672 + $options['popup'] = esc_html('Popup', 'uicore-elements');
673 + }
674 +
675 + $services = Services::get_services_list();
676 +
677 + if (is_array($services) || empty($services)) {
678 + $options = array_merge($options, $services);
679 + }
680 +
681 + return $options;
682 + }
683 +
684 + /**
685 + * Webhook Helpers
686 + */
687 + protected function build_webhook_request(): array
688 + {
689 + $url = $this->replace_content_shortcode($this->validate_field($this->settings['webhook_url'] ?? '', 'Webhook URL'));
690 + $url = $this->validate_webhook($url);
691 +
692 + $advanced_data = ($this->settings['webhook_advanced_data'] ?? '') === 'true';
693 + $args = [
694 + 'timeout' => 15,
695 + ];
696 +
697 + if ($advanced_data) {
698 + $args['headers'] = [
699 + 'Content-Type' => 'application/json; charset=UTF-8',
700 + ];
701 + $args['body'] = wp_json_encode($this->get_advanced_webhook_payload());
702 + } else {
703 + $args['body'] = [
704 + 'widget_type' => sanitize_text_field($this->form_data['widget_type'] ?? ''),
705 + 'fields' => $this->get_submission_fields_payload(),
706 + 'meta' => $this->get_webhook_meta_payload(),
707 + ];
708 + }
709 +
710 + return [
711 + 'url' => $url,
712 + 'args' => $args,
713 + ];
714 + }
715 + protected function get_advanced_webhook_payload(): array
716 + {
717 + return $this->sanitize_recursive_payload($this->form_data);
718 + }
719 + protected function get_webhook_meta_payload(): array
720 + {
721 + $meta = [
722 + 'widget_id' => sanitize_text_field($this->form_data['widget_id'] ?? ''),
723 + 'submitted_at' => wp_date('c'),
724 + ];
725 +
726 + if (!empty($this->form_data['post_id'])) {
727 + $meta['post_id'] = absint($this->form_data['post_id']);
728 + }
729 +
730 + if (isset($_SERVER['HTTP_REFERER'])) {
731 + $meta['page_url'] = esc_url_raw(wp_unslash($_SERVER['HTTP_REFERER']));
732 + }
733 +
734 + return $meta;
735 + }
736 + protected function get_submission_fields_payload(): array
737 + {
738 + $payload = [];
739 +
740 + foreach ($this->get_setting_fields() as $field) {
741 + if (empty($field['custom_id'])) {
742 + continue;
743 + }
744 +
745 + $field_id = $field['custom_id'];
746 + $value = $this->form_data['form_fields'][$field_id] ?? '';
747 +
748 + if (is_array($value)) {
749 + $payload[$field_id] = array_map('sanitize_text_field', $value);
750 + continue;
751 + }
752 +
753 + $payload[$field_id] = sanitize_text_field($value);
754 + }
755 +
756 + return $payload;
757 + }
758 + protected function sanitize_recursive_payload($value)
759 + {
760 + if (is_array($value)) {
761 + foreach ($value as $key => $item) {
762 + $value[sanitize_text_field((string) $key)] = $this->sanitize_recursive_payload($item);
763 + if ((string) $key !== sanitize_text_field((string) $key)) {
764 + unset($value[$key]);
765 + }
766 + }
767 +
768 + return $value;
769 + }
770 +
771 + return sanitize_text_field((string) $value);
772 + }
773 +
774 + /**
775 + * Responses
776 + */
777 + // Also used by form widget(s), therefore public and static
778 + public static function get_default_messages()
779 + {
780 + return [
781 + // main messages
782 + 'success' => esc_html__('Your submission was successful.', 'uicore-elements'),
783 + 'error' => esc_html__('Your submission failed because of an error.', 'uicore-elements'),
784 + 'mail_error' => esc_html__('Failed to send email.', 'uicore-elements'),
785 + 'required' => esc_html__('Fill all required fields.', 'uicore-elements'),
786 + 'redirect' => esc_html__('Redirecting...', 'uicore-elements'),
787 + ];
788 + }
789 + protected function get_response_message(string $status, string $dinamic_data = '')
790 + {
791 + // non-customizable messages (for settings debugging only)
792 + $default_messages = [
793 + 'invalid_status' => esc_html__('Invalid status message.', 'uicore-elements'),
794 + 'redirect_no_url' => esc_html__('Redirection failed. No URL set.', 'uicore-elements'),
795 + 'empty_field' => esc_html__('The following field is empty: ', 'uicore-elements') . $dinamic_data,
796 + ];
797 +
798 + if ($this->settings['custom_messages'] === 'yes') {
799 + $messages = [
800 + 'success' => esc_html($this->settings['success_message']),
801 + 'error' => esc_html($this->settings['error_message']),
802 + 'mail_error' => esc_html($this->settings['mail_error_message']),
803 + 'redirect' => esc_html($this->settings['redirect_message']),
804 + ];
805 + } else {
806 + $messages = self::get_default_messages();
807 + }
808 +
809 + $messages = array_merge($default_messages, $messages);
810 +
811 + return isset($messages[$status]) ? $messages[$status] : $messages['invalid_status'];
812 + }
813 + protected function build_frontend_responses($responses)
814 + {
815 + $data = [];
816 +
817 + // Mail response
818 + if (isset($responses['email']) && $responses['email']['status'] !== 'success') {
819 + $data['email'] = $responses['email'];
820 + }
821 +
822 + // Mail attachment response - is always an error
823 + if (isset($responses['email']) && isset($responses['email']['attachment'])) {
824 + $data['attachment'] = $responses['attachment'];
825 + }
826 +
827 + // Submit Actions response - is always an error
828 + if (isset($responses['submit'])) {
829 + $data['submit'] = $responses['submit'];
830 + }
831 +
832 + if (isset($responses['webhook']) && $responses['webhook']['status'] !== 'success') {
833 + $data['submit'] = $responses['webhook'];
834 + }
835 +
836 + // Newsletter Services responses
837 + $data = $this->build_services_responses($responses);
838 +
839 + // Main response
840 + $status = $this->all_submissions_succedded($responses) ? 'success' : 'error';
841 + $data['message'] = $this->get_response_message($status);
842 +
843 + // Below responses should work only if all previous submitions hasn't failed
844 + if (isset($responses['redirect']) && $status === 'success') {
845 + $data['redirect'] = $responses['redirect'];
846 + }
847 + if (isset($responses['popup']) && $status === 'success') {
848 + $data['popup'] = $responses['popup'];
849 + }
850 +
851 + // The only successfull response that should be sent is 'main' and 'redirect'
852 + return [
853 + 'status' => $status,
854 + 'data' => $data
855 + ];
856 + }
857 +
858 + protected function build_services_responses($responses)
859 + {
860 + $success = true;
861 + $service = '';
862 + $status = '';
863 + $detail = '';
864 +
865 + if (isset($responses['mailchimp'])) {
866 + // We know mailchimp failed if status is an integer
867 + // If one response fail, in multiple list cases, returns the failure
868 + foreach ($responses['mailchimp'] as $response) {
869 + if (is_int($response['status'])) {
870 + $success = false;
871 + $service = 'Mailchimp';
872 + $status = 'HTTP: ' . $response['status'];
873 + $detail = $response['detail'];
874 + break;
875 + }
876 + }
877 + } else if (isset($responses['brevo'])) {
878 + // We know Brevo failed if 'code' is set in the response
879 + if (isset($responses['brevo']['code'])) {
880 + $success = false;
881 + $service = 'Brevo';
882 + $status = 'Error: ' . $responses['brevo']['code'];
883 + $detail = $responses['brevo']['message'] ?? '';
884 + }
885 + } else if (isset($responses['getresponse'])) {
886 + // We know GetResponse failed if 'httpStatus' is set in the response
887 + foreach ($responses['getresponse'] as $response) {
888 + if (isset($response['httpStatus'])) {
889 + $success = false;
890 + $service = 'GetResponse';
891 + $status = 'HTTP: ' . $response['httpStatus'];
892 + $detail = 'Error: ' . $response['code'] . ' - ' . $response['message'];
893 + break;
894 + }
895 + }
896 + } else if (isset($responses['kit'])) {
897 + // We know Kit failed if 'error' is set in the response
898 + foreach ($responses['kit'] as $response) {
899 + if (isset($response['error'])) {
900 + $success = false;
901 + $service = 'Kit';
902 + $status = 'Error: ' . $response['error'];
903 + $detail = $response['message'];
904 + break;
905 + }
906 + }
907 + } else if (isset($responses['moosend'])) {
908 + // We know Moosend failed if 'Error' is set in the response
909 + foreach ($responses['moosend'] as $response) {
910 + if (!empty($response['Error'])) {
911 + $success = false;
912 + $service = 'Moosend';
913 + $status = 'HTTP: ' . $response['Code'];
914 + $detail = $response['Error'];
915 + break;
916 + }
917 + }
918 + } else if (isset($responses['mailerlite'])) {
919 + // We know MailerLite failed if 'message' is set in the response
920 + if (isset($responses['mailerlite']['message'])) {
921 + $success = false;
922 + $service = 'MailerLite';
923 + $status = 'Error';
924 + $detail = $responses['mailerlite']['message'] ?? '';
925 + }
926 + }
927 +
928 + // Return the error response if the service fails
929 + if ($success === false) {
930 + $data['newsletter_service'] = [
931 + 'status' => 'error',
932 + /* translators: 1: Service Provider, 2: Service Error code, 3: Service Status Response */
933 + 'message' => sprintf(esc_html__('[%1$s] %2$s - %3$s', 'uicore-elements'), $service, $status, $detail)
934 + ];
935 + }
936 +
937 + return [];
938 + }
939 +}