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 -608 1.0.12 → 1.3.18 View file →
@@ -1,608 +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 - if( !isset($files['form_fields']) || empty($files['form_fields']) ) {
261 - return [
262 - 'files' => '',
263 - 'errors' => ''
264 - ];
265 - }
266 -
267 - // Requires wp_handle_upload() file if unavailable
268 - if ( ! function_exists( 'wp_handle_upload' ) ) {
269 - require_once( ABSPATH . 'wp-admin/includes/file.php' );
270 - }
271 -
272 - // Check if theres a valid file to upload
273 - foreach ($files['form_fields']['tmp_name'] as $input => $value) {
274 - if ($files['form_fields']['error'][$input] !== UPLOAD_ERR_NO_FILE) {
275 - $file = [
276 - 'name' => $files['form_fields']['name'][$input],
277 - 'type' => $files['form_fields']['type'][$input],
278 - 'tmp_name' => $files['form_fields']['tmp_name'][$input],
279 - 'error' => $files['form_fields']['error'][$input],
280 - 'size' => $files['form_fields']['size'][$input],
281 - ];
282 -
283 - // Handle the file upload
284 - $uploaded_file = wp_handle_upload($file, ['test_form' => false]);
285 -
286 - if (!isset($uploaded_file['error'])) {
287 - $attachments = $uploaded_file['file'];
288 - } else {
289 - // Since throwing exceptions here will block the proper data flow, we return the error and let send_mail() handle it
290 - $errors = esc_html__('Failed to upload file: ', 'uicore-elements') . $uploaded_file['error'];
291 - }
292 -
293 - // Break after processing the first valid file
294 - break;
295 - }
296 - }
297 -
298 - return [
299 - 'files' => $attachments,
300 - 'errors' => $errors
301 - ];
302 - }
303 - protected function compose_metadata(string $content, array $metadada, string $line_break){
304 -
305 - if (empty($metadada)) {
306 - return $content;
307 - }
308 -
309 - $content = $content . $line_break . $line_break . '--' . $line_break . $line_break; // Adds spacing between content and metadata
310 -
311 - foreach ($metadada as $meta) {
312 - switch($meta){
313 - case 'date':
314 - $content .= sprintf( '%s: %s', 'Date', date('Y-m-d') . $line_break);
315 - break;
316 -
317 - case 'time' :
318 - $content .= sprintf( '%s: %s', 'Time', date('H:i:s') . $line_break);
319 - break;
320 -
321 - case 'remote_ip':
322 - $content .= sprintf( '%s: %s', 'IP', $_SERVER['REMOTE_ADDR'] . $line_break);
323 - break;
324 -
325 - case 'user_agent':
326 - $content .= sprintf( '%s: %s', 'User Agent', $_SERVER['HTTP_USER_AGENT'] . $line_break);
327 - break;
328 -
329 - case 'page_url':
330 - $content .= sprintf( '%s: %s', 'Page URL', $_SERVER['HTTP_REFERER'] . $line_break);
331 - break;
332 - }
333 - }
334 -
335 - return $content;
336 - }
337 -
338 - /**
339 - * Extra submissions
340 - */
341 - protected function redirect() {
342 -
343 - $validation = $this->validate_url( $this->settings['redirect_to'] );
344 -
345 - // Above function exception blocks this execution
346 - return [
347 - 'status' => 'success',
348 - 'url' => esc_url( $validation['url'] ),
349 - 'delay' => 1500,
350 - 'message' => esc_html( $this->get_response_message('redirect') )
351 - ];
352 - }
353 - protected function mailchimp() {
354 -
355 - // Get API data
356 - $key = get_option('uicore_elements_mailchimp_secret_key');
357 - $list_id = $this->settings['mailchimp_audience_id'];
358 - $server = explode('-', $key)[1]; // Server value can be found on API Key after the dash
359 -
360 - $this->validate_mailchimp($key, $list_id);
361 -
362 - // Check the widget type to determine the fields, before getting form data
363 - if( $this->form_data['widget_type'] === 'contact-form' ) {
364 -
365 - // Since contact form has custom IDs, we need to get the ID value from the settings
366 - $settings = $this->settings;
367 -
368 - $email = $this->form_data['form_fields'][$settings['mailchimp_email_id']];
369 - $merge_fields = [
370 - 'FNAME' => isset( $this->form_data['form_fields'][$settings['mailchimp_fname_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_fname_id']] : "",
371 - 'LNAME' => isset( $this->form_data['form_fields'][$settings['mailchimp_lname_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_lname_id']] : "",
372 - 'PHONE' => isset( $this->form_data['form_fields'][$settings['mailchimp_phone_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_phone_id']] : "",
373 - 'BIRTHDAY' => isset( $this->form_data['form_fields'][$settings['mailchimp_birthday_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_birthday_id']] : ""
374 - ];
375 -
376 - // Else can only be newsletter widget
377 - } else {
378 -
379 - // Since Newsletter has fixed field IDs, we get them directly
380 - $email = $this->form_data['form_fields']['email'];
381 - $merge_fields = [
382 - 'FNAME' => isset( $this->form_data['form_fields']['name'] ) ? $this->form_data['form_fields']['name'] : ""
383 - ];
384 - }
385 -
386 - // Build the request
387 - $url = 'https://' . esc_html($server) . '.api.mailchimp.com/3.0/lists/' . esc_html($list_id) . '/members/';
388 - $data = [
389 - "email_address" => $email,
390 - "status" => "subscribed",
391 - "merge_fields" => $merge_fields
392 - ];
393 -
394 -
395 - $request = curl_init();
396 - curl_setopt($request, CURLOPT_URL, $url);
397 - curl_setopt($request, CURLOPT_HTTPHEADER, [
398 - 'Content-Type: application/json',
399 - 'Authorization: Basic ' . base64_encode('anystring:' . $key)
400 - ]);
401 - curl_setopt($request, CURLOPT_POST, true);
402 - curl_setopt($request, CURLOPT_POSTFIELDS, json_encode($data));
403 - curl_setopt($request, CURLOPT_RETURNTRANSFER, true);
404 - $res = curl_exec($request);
405 -
406 - if(curl_errno($request)) {
407 - $res = curl_error($request);
408 - } else {
409 - $res = json_decode($res, true);
410 - }
411 -
412 - curl_close($request);
413 -
414 - return $res;
415 - }
416 -
417 - /**
418 - * Validations
419 - */
420 - protected function validate_recaptcha(string $token, string $version) {
421 -
422 - // Check if secret and site key are set
423 - if (!get_option('uicore_elements_recaptcha_secret_key') || !get_option('uicore_elements_recaptcha_site_key')) {
424 - return [
425 - 'success' => false,
426 - 'message' => esc_html__('reCAPTCHA API keys are not set.', 'uicore-elements')
427 - ];
428 - }
429 -
430 - $data = [
431 - 'secret' => get_option('uicore_elements_recaptcha_secret_key'),
432 - 'response' => sanitize_text_field($token)
433 - ];
434 -
435 - $verify = curl_init();
436 - curl_setopt($verify, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify");
437 - curl_setopt($verify, CURLOPT_POST, true);
438 - curl_setopt($verify, CURLOPT_POSTFIELDS, http_build_query($data));
439 - curl_setopt($verify, CURLOPT_SSL_VERIFYPEER, false);
440 - curl_setopt($verify, CURLOPT_RETURNTRANSFER, true);
441 - $res = curl_exec($verify);
442 -
443 - $captcha = json_decode($res);
444 -
445 - if($version === 'V3') {
446 - return ['success' => ($captcha->success && $captcha->score >= 0.5) ? true : false];
447 - }
448 -
449 - // V2 default
450 - return ['success' => $captcha->success];
451 -
452 - }
453 - protected function validate_spam() {
454 - // `ui-e-h-p` is the key for the honeypot
455 - return ( isset($this->form_data['ui-e-h-p']) && !empty($this->form_data['ui-e-h-p']) ) ? false : true;
456 - }
457 - protected function validate_url(string $url) {
458 - if (empty($url)) {
459 - throw new Redirect_Exception( $this->get_response_message('redirect_no_url') );
460 - }
461 -
462 - return [
463 - 'status' => true,
464 - 'url' => $url,
465 - ];
466 - }
467 - protected function validate_field(string $field, string $label) {
468 - if (empty($field)) {
469 - throw new Submit_Exception( $this->get_response_message('empty_field', $label) );
470 - }
471 - return $field;
472 - }
473 - protected function validate_mailchimp(string $key, string $list_id) {
474 - if (empty($key)) {
475 - throw new Mailchimp_Exception(esc_html__('Mailchimp API key is not set. Check Uicore Elements settings.', 'uicore-elements'));
476 - } else if (empty($list_id)) {
477 - throw new Mailchimp_Exception(esc_html__('Audience ID control is not set. Check your widget settings.', 'uicore-elements'));
478 - }
479 - }
480 -
481 - /**
482 - * Helpers
483 - */
484 - protected function get_setting_fields() {
485 - // Used to determine if the widget fields are repeaters with custom IDS or fixed fields, and if fixed fields
486 - // compose them into an array similar to repeaters, to simplify shortcode replacement function
487 -
488 - switch ($this->form_data['widget_type']) {
489 -
490 - case 'newsletter':
491 - return [
492 - ['custom_id' => 'email'],
493 - ['custom_id' => 'name'],
494 - ];
495 - break;
496 -
497 - // Dynamic fields values
498 - default:
499 - return $this->settings['form_fields'];
500 - break;
501 - }
502 - }
503 - protected function all_submissions_succedded($responses) {
504 - foreach ($responses as $submission => $data) {
505 - // Redirect action failure shouldn't return `error` to main status because is not properly a submission action, so we skip it.
506 - if( $submission == 'redirect') {
507 - continue;
508 - }
509 - // Failed submissions returns `error` or bool `false` status
510 - if( $data['status'] === 'error' || $data['status'] === false ) {
511 - return false;
512 - }
513 - }
514 - return true;
515 - }
516 -
517 - /**
518 - * Responses
519 - */
520 - // Also used by form widget(s), therefore public and static
521 - public static function get_default_messages(){
522 - return [
523 - // main messages
524 - 'success' => esc_html__( 'Your submission was successful.', 'uicore-elements' ),
525 - 'error' => esc_html__( 'Your submission failed because of an error.', 'uicore-elements' ),
526 - 'mail_error' => esc_html__( 'Failed to send email.', 'uicore-elements' ),
527 - 'required' => esc_html__( 'Fill all required fields.', 'uicore-elements' ),
528 - 'redirect' => esc_html__( 'Redirecting...', 'uicore-elements' ),
529 - ];
530 - }
531 - protected function get_response_message(string $status, string $dinamic_data = '') {
532 - // non-customizable messages (for settings debugging only)
533 - $default_messages = [
534 - 'invalid_status' => esc_html__( 'Invalid status message.', 'uicore-elements' ),
535 - 'redirect_no_url' => esc_html__( 'Redirection failed. No URL set.', 'uicore-elements' ),
536 - 'empty_field' => esc_html__( 'The following field is empty.', 'uicore-elements') . $dinamic_data,
537 - ];
538 -
539 - if($this->settings['custom_messages'] === 'yes') {
540 - $messages = [
541 - 'success' => $this->settings['success_message'],
542 - 'error' => $this->settings['error_message'],
543 - 'mail_error' => $this->settings['mail_error_message'],
544 - 'redirect' => $this->settings['redirect_message'],
545 - ];
546 - } else {
547 - $messages = self::get_default_messages();
548 - }
549 -
550 - $messages = array_merge($default_messages, $messages);
551 -
552 - return isset($messages[$status]) ? $messages[$status] : $messages['invalid_status'];
553 - }
554 - protected function build_frontend_responses($responses) {
555 -
556 - $data = [];
557 -
558 - // Mail response
559 - if ( isset($responses['email']) && $responses['email']['status'] !== 'success' ) {
560 - $data['email'] = $responses['email'];
561 - }
562 -
563 - // Mail attachment response - is always an error
564 - if ( isset($responses['email']) && isset($responses['email']['attachment']) ) {
565 - $data['attachment'] = $responses['attachment'];
566 - }
567 -
568 - // Mailchimp response - Integer is also an error
569 - if ( isset($responses['mailchimp']) ) {
570 -
571 - // If Integer, is an error from mailchimp API so we pass their response
572 - if( is_int($responses['mailchimp']['status'])){
573 - $data['mailchimp'] = [
574 - 'status' => 'error',
575 - 'message' => sprintf( esc_html__('Mailchimp HTTP "%s" - "%s."', 'uicore-elements'), $responses['mailchimp']['status'], $responses['mailchimp']['detail'])
576 - ];
577 -
578 - // If error is from our validation
579 - } else if ( $responses['mailchimp']['status'] === 'error' ) {
580 - $data['mailchimp'] = [
581 - 'status' => $responses['mailchimp']['status'],
582 - 'message' => $responses['mailchimp']['message']
583 - ];
584 - }
585 - }
586 -
587 - // Submit Actions response - is always an error
588 - if ( isset($responses['submit']) ) {
589 - $data['submit'] = $responses['submit'];
590 - }
591 -
592 - // Main response
593 - $status = $this->all_submissions_succedded($responses) ? 'success' : 'error';
594 - $data['message'] = $this->get_response_message($status);
595 -
596 - // Redirect response (should work only if all previous submitions hasn't failed)
597 - if ( isset($responses['redirect']) && $status === 'success' ) {
598 - $data['redirect'] = $responses['redirect'];
599 - }
600 -
601 - // The only successfull response that should be sent is 'main' and 'redirect'
602 - return [
603 - 'status' => $status,
604 - 'data' => $data
605 - ];
606 - }
607 -
608 -}
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 +}