PluginProbe
UiCore Elements – Free widgets and templates for Elementor / 1.3.17
UiCore Elements – Free widgets and templates for Elementor v1.3.17
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 1.2.4 All 41 releases
← All changes | includes/utils/form-service.php +485 -163 1.2.11.3.17 View file →
@@ -1,11 +1,14 @@
1 1 <?php
2 +
2 3 namespace UiCoreElements\Utils;
3 4
5 +use UiCoreELements\Utils\Newsletter_Services as Services;
6 +
4 7 class Email_Exception extends \Exception {}
5 8 class Redirect_Exception extends \Exception {}
6 9 class Submit_Exception extends \Exception {}
7 -class Mailchimp_Exception extends \Exception {}
10 +class Newsletter_Service_Exception extends \Exception {}
8 11
9 12 defined('ABSPATH') || exit();
10 13
11 14 /**
@@ -11,21 +14,24 @@
11 14 /**
12 15 * Handles the form submissions and responses
13 16 */
14 17
15 -class Contact_Form_Service {
18 +class Contact_Form_Service
19 +{
16 20
17 21 protected $form_data,
18 - $settings,
19 - $files;
22 + $settings,
23 + $files;
20 24
21 - public function __construct($form_data, $settings, $files) {
25 + public function __construct($form_data, $settings, $files)
26 + {
22 27 $this->form_data = $form_data;
23 28 $this->settings = $settings;
24 29 $this->files = $files;
25 30 }
26 31
27 - public function handle() {
32 + public function handle()
33 + {
28 34
29 35 $data = [];
30 36 $responses = [];
31 37
@@ -33,9 +39,9 @@
33 39 if (isset($this->form_data['grecaptcha_token']) && !empty($this->form_data['grecaptcha_token'])) {
34 40
35 41 $recaptcha = $this->validate_recaptcha($this->form_data['grecaptcha_token'], $this->form_data['grecaptcha_version']);
36 42
37 - if(!$recaptcha['success']){
43 + if (!$recaptcha['success']) {
38 44 return [
39 45 'status' => 'error',
40 46 'data' => [
41 47 'message' => esc_html__('reCAPTCHA validation failed.', 'uicore-elements'),
@@ -44,9 +50,9 @@
44 50 }
45 51 }
46 52
47 53 // Check for honeypot spam
48 - if(!$this->validate_spam()){
54 + if (!$this->validate_spam()) {
49 55 return [
50 56 'status' => 'success',
51 57 'data' => [
52 58 'message' => $this->get_response_message('success') // Fakes a successfull submission
@@ -63,9 +69,9 @@
63 69 $data = $this->send_mail($action);
64 70 $responses['email'] = $data['response'];
65 71 break;
66 72
67 - case 'email_2' :
73 + case 'email_2':
68 74 $data = $this->send_mail($action, $data);
69 75 $responses['email'] = $data['response'];
70 76 break;
71 77
@@ -72,12 +78,40 @@
72 78 case 'redirect':
73 79 $responses['redirect'] = $this->redirect();
74 80 break;
75 81
82 + case 'popup':
83 + $responses['popup'] = $this->popup();
84 + break;
85 +
86 + case 'webhook':
87 + $responses['webhook'] = $this->webhook();
88 + break;
89 +
76 90 case 'mailchimp':
77 - $responses['mailchimp'] = $this->mailchimp();
91 + $responses['mailchimp'] = $this->newsletter_service('mailchimp');
78 92 break;
79 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 +
80 114 default:
81 115 throw new Submit_Exception(esc_html__('Unknown submit action: ', 'uicore-elements') . $action . esc_html__('. Check your settings.', 'uicore-elements'));
82 116 }
83 117 } catch (Email_Exception $e) {
@@ -89,11 +123,12 @@
89 123 $responses['redirect'] = [
90 124 'status' => 'error',
91 125 'message' => $e->getMessage()
92 126 ];
93 - } catch (Mailchimp_Exception $e) {
94 - $responses['mailchimp'] = [
127 + } catch (Newsletter_Service_Exception $e) {
128 + $responses['newsletter_service'] = [
95 129 'status' => 'error',
130 + 'service' => $action,
96 131 'message' => $e->getMessage()
97 132 ];
98 133 } catch (Submit_Exception $e) {
99 134 $responses['submit'] = [
@@ -103,9 +138,9 @@
103 138 }
104 139 // We're avoiding throwing exception for mailchimp because would require a specific validation function, and is to much for now
105 140 }
106 141
107 - // There's no need to continue without a submit action enabled
142 + // There's no need to continue without a submit action enabled
108 143 } else {
109 144 return [
110 145 'status' => 'error',
111 146 'data' => [
@@ -116,9 +151,9 @@
116 151
117 152 // Consider `current_user_can( 'manage_options' )` as filter to return more specific messages on frontend (not tested)
118 153
119 154 // 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']) ) {
155 + if (isset($data['attachments']) && !empty($data['attachments']['files'])) {
121 156 register_shutdown_function('unlink', $data['attachments']['files']);
122 157 }
123 158
124 159 $output = $this->build_frontend_responses($responses);
@@ -131,9 +166,10 @@
131 166
132 167 /**
133 168 * Mail submition
134 169 */
135 - protected function send_mail(string $action, array $data = []){
170 + protected function send_mail(string $action, array $data = [])
171 + {
136 172
137 173 $attachments = isset($data['attachments']) ? $data['attachments'] : []; // Check if there's attachments from previous mail submit action
138 174
139 175 $mail_data = $this->compose_mail_data($action, $attachments); // build mail data
@@ -164,9 +200,10 @@
164 200 ],
165 201 'attachments' => $mail_data['attachments'] // Return attachments for deletion and error handling
166 202 ];
167 203 }
168 - protected function compose_mail_data(string $action, array $attachments = []) {
204 + protected function compose_mail_data(string $action, array $attachments = [])
205 + {
169 206
170 207 // Set short vars for the data
171 208 $settings = $this->settings;
172 209 $data = $this->form_data;
@@ -172,29 +209,29 @@
172 209 $data = $this->form_data;
173 210 $files = $this->files;
174 211
175 212 $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
213 + $line_break = $settings['email_content_type' . $slug] === 'html' ? '<br>' : "\n"; // Set line break type
177 214
178 215 // Replace shortcodes by form data
179 - $content = $this->replace_content_shortcode( $settings['email_content'.$slug], $line_break );
216 + $content = $this->replace_content_shortcode($settings['email_content' . $slug], $line_break);
180 217
181 218 // Adds the metadata to content
182 - $content = $this->compose_metadata($content, $settings['form_metadata'.$slug], $line_break);
219 + $content = $this->compose_metadata($content, $settings['form_metadata' . $slug], $line_break);
183 220
184 221 // Set empty attachments to avoid undefined errors for widgets without attachment options
185 - if($data['widget_type'] !== 'contact-form') {
186 - $attachments = [ 'files' => '', 'errors' => '' ];
222 + if ($data['widget_type'] !== 'contact-form') {
223 + $attachments = ['files' => '', 'errors' => ''];
187 224 } else {
188 225 $attachments = !empty($attachments) ? $attachments : $this->prepare_attachments($files); // If theres attachments from previous submit action, use it, otherwhise prepare it from $files,
189 226 }
190 227
191 228 // 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'));
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'));
197 234
198 235 // Build the data
199 236 $mail_data = [
200 237 'to' => $mail_to,
@@ -200,10 +237,10 @@
200 237 'to' => $mail_to,
201 238 'subject' => $mail_subject,
202 239 'message' => $content,
203 240 'headers' => [
204 - 'Content-Type: text/' . $settings['email_content_type'.$slug] . '; charset=UTF-8',
205 - 'From: ' . $mail_name . ' <'.$mail_from.'>',
241 + 'Content-Type: text/' . $settings['email_content_type' . $slug] . '; charset=UTF-8',
242 + 'From: ' . $mail_name . ' <' . $mail_from . '>',
206 243 'Reply-To: ' . $mail_reply,
207 244 ],
208 245 'attachments' => $attachments['files']
209 246 ];
@@ -208,13 +245,13 @@
208 245 'attachments' => $attachments['files']
209 246 ];
210 247
211 248 // Build optional data
212 - if (!empty($settings['email_to_cc'.$slug])) {
213 - $mail_data['headers'][] = 'Cc: ' . $settings['email_to_cc'];
249 + if (!empty($settings['email_to_cc' . $slug])) {
250 + $mail_data['headers'][] = 'Cc: ' . $this->replace_content_shortcode($settings['email_to_cc' . $slug]);
214 251 }
215 - if (!empty($settings['email_to_bcc'.$slug])) {
216 - $mail_data['headers'][] = 'Bcc: ' . $settings['email_to_bcc'];
252 + if (!empty($settings['email_to_bcc' . $slug])) {
253 + $mail_data['headers'][] = 'Bcc: ' . $this->replace_content_shortcode($settings['email_to_bcc' . $slug]);
217 254 }
218 255
219 256 return [
220 257 'email' => $mail_data,
@@ -220,9 +257,10 @@
220 257 'email' => $mail_data,
221 258 'attachments' => $attachments
222 259 ];
223 260 }
224 - protected function replace_content_shortcode(string $content, string $line_break = ''){
261 + protected function replace_content_shortcode(string $content, string $line_break = '')
262 + {
225 263
226 264 // Set short vars for the data
227 265 $fields = $this->get_setting_fields();
228 266 $form_data = $this->form_data;
@@ -227,16 +265,16 @@
227 265 $fields = $this->get_setting_fields();
228 266 $form_data = $this->form_data;
229 267
230 268 // [all-fieds] shortcode replacement
231 - if ( false !== strpos( $content, '[all-fields]' ) ) {
269 + if (false !== strpos($content, '[all-fields]')) {
232 270 $text = '';
233 271 // Return formated text as key: value
234 - foreach ( $form_data['form_fields'] as $key => $field ) {
272 + foreach ($form_data['form_fields'] as $key => $field) {
235 273 $field_value = is_array($field) ? implode(', ', $field) : $field;
236 274 $text .= !empty($field_value) ? sprintf('%s: %s', $key, $field_value) . $line_break : '';
237 275 }
238 - $content = str_replace( '[all-fields]', $text, $content );
276 + $content = str_replace('[all-fields]', $text, $content);
239 277 }
240 278
241 279 // Custom [field id="{id}"] shortcode replacement
242 280 foreach ($fields as $field) {
@@ -246,19 +284,20 @@
246 284 $content = str_replace($shortcode, $value, $content);
247 285 }
248 286
249 287 // Replaces all manual line breaks from content
250 - if(!empty($line_break)){
251 - $content = str_replace( array( "\r\n", "\r", "\n" ), $line_break, $content );
288 + if (!empty($line_break)) {
289 + $content = str_replace(array("\r\n", "\r", "\n"), $line_break, $content);
252 290 }
253 291
254 292 return $content;
255 293 }
256 - protected function prepare_attachments(array $files) {
294 + protected function prepare_attachments(array $files)
295 + {
257 296 $attachments = [];
258 297 $errors = '';
259 298
260 - if( !isset($files['form_fields']) || empty($files['form_fields']) ) {
299 + if (!isset($files['form_fields']) || empty($files['form_fields'])) {
261 300 return [
262 301 'files' => '',
263 302 'errors' => ''
264 303 ];
@@ -264,10 +303,10 @@
264 303 ];
265 304 }
266 305
267 306 // Requires wp_handle_upload() file if unavailable
268 - if ( ! function_exists( 'wp_handle_upload' ) ) {
269 - require_once( ABSPATH . 'wp-admin/includes/file.php' );
307 + if (! function_exists('wp_handle_upload')) {
308 + require_once(ABSPATH . 'wp-admin/includes/file.php');
270 309 }
271 310
272 311 // Check if theres a valid file to upload
273 312 foreach ($files['form_fields']['tmp_name'] as $input => $value) {
@@ -299,9 +338,10 @@
299 338 'files' => $attachments,
300 339 'errors' => $errors
301 340 ];
302 341 }
303 - protected function compose_metadata(string $content, array $metadada, string $line_break){
342 + protected function compose_metadata(string $content, array $metadada, string $line_break)
343 + {
304 344
305 345 if (empty($metadada)) {
306 346 return $content;
307 347 }
@@ -308,32 +348,32 @@
308 348
309 349 $content = $content . $line_break . $line_break . '--' . $line_break . $line_break; // Adds spacing between content and metadata
310 350
311 351 foreach ($metadada as $meta) {
312 - switch($meta){
352 + switch ($meta) {
313 353 case 'date':
314 - $content .= sprintf( '%s: %s', 'Date', gmdate('Y-m-d') . $line_break);
354 + $content .= sprintf('%s: %s', 'Date', wp_date('Y-m-d') . $line_break);
315 355 break;
316 356
317 - case 'time' :
318 - $content .= sprintf( '%s: %s', 'Time', gmdate('H:i:s') . $line_break);
357 + case 'time':
358 + $content .= sprintf('%s: %s', 'Time', wp_date('H:i:s') . $line_break);
319 359 break;
320 360
321 361 case 'remote_ip':
322 362 $content .= isset($_SERVER['REMOTE_ADDR'])
323 - ? sprintf( '%s: %s', 'IP', sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])). $line_break)
363 + ? sprintf('%s: %s', 'IP', sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) . $line_break)
324 364 : '';
325 365 break;
326 366
327 367 case 'user_agent':
328 368 $content .= isset($_SERVER['HTTP_USER_AGENT'])
329 - ? sprintf( '%s: %s', 'User Agent', sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) . $line_break)
369 + ? sprintf('%s: %s', 'User Agent', sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) . $line_break)
330 370 : '';
331 371 break;
332 372
333 373 case 'page_url':
334 374 $content .= isset($_SERVER['HTTP_REFERER'])
335 - ? sprintf( '%s: %s', 'Page URL', sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) . $line_break)
375 + ? sprintf('%s: %s', 'Page URL', sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) . $line_break)
336 376 : '';
337 377 break;
338 378 }
339 379 }
@@ -343,87 +383,97 @@
343 383
344 384 /**
345 385 * Extra submissions
346 386 */
347 - protected function redirect() {
387 + protected function redirect()
388 + {
348 389
349 - $validation = $this->validate_url( $this->settings['redirect_to'] );
390 + $validation = $this->validate_url($this->settings['redirect_to']);
350 391
351 392 // Above function exception blocks this execution
352 393 return [
353 394 'status' => 'success',
354 - 'url' => esc_url( $validation['url'] ),
395 + 'url' => esc_url($validation['url']),
355 396 'delay' => 1500,
356 - 'message' => esc_html( $this->get_response_message('redirect') )
397 + 'message' => esc_html($this->get_response_message('redirect'))
357 398 ];
358 399 }
359 - protected function mailchimp() {
400 + protected function popup()
401 + {
402 + $action = $this->settings['popup_action'];
360 403
361 - // Get API data
362 - $key = get_option('uicore_elements_mailchimp_secret_key');
363 - $list_id = $this->settings['mailchimp_audience_id'];
364 - $server = explode('-', $key)[1]; // Server value can be found on API Key after the dash
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 + }
365 412
366 - $this->validate_mailchimp($key, $list_id);
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();
367 422
368 - // Check the widget type to determine the fields, before getting form data
369 - if( $this->form_data['widget_type'] === 'contact-form' ) {
423 + $response = wp_remote_post($request['url'], $request['args']);
370 424
371 - // Since contact form has custom IDs, we need to get the ID value from the settings
372 - $settings = $this->settings;
373 -
374 - $email = $this->form_data['form_fields'][$settings['mailchimp_email_id']];
375 - $merge_fields = [
376 - 'FNAME' => isset( $this->form_data['form_fields'][$settings['mailchimp_fname_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_fname_id']] : "",
377 - 'LNAME' => isset( $this->form_data['form_fields'][$settings['mailchimp_lname_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_lname_id']] : "",
378 - 'PHONE' => isset( $this->form_data['form_fields'][$settings['mailchimp_phone_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_phone_id']] : "",
379 - 'BIRTHDAY' => isset( $this->form_data['form_fields'][$settings['mailchimp_birthday_id']] ) ? $this->form_data['form_fields'][$settings['mailchimp_birthday_id']] : ""
425 + if (is_wp_error($response)) {
426 + return [
427 + 'status' => 'error',
428 + 'message' => $response->get_error_message(),
380 429 ];
381 -
382 - // Else can only be newsletter widget
383 - } else {
384 -
385 - // Since Newsletter has fixed field IDs, we get them directly
386 - $email = $this->form_data['form_fields']['email'];
387 - $merge_fields = [
388 - 'FNAME' => isset( $this->form_data['form_fields']['name'] ) ? $this->form_data['form_fields']['name'] : ""
389 - ];
390 430 }
391 431
392 - // Build the request
393 - $url = 'https://' . esc_html($server) . '.api.mailchimp.com/3.0/lists/' . esc_html($list_id) . '/members/';
394 - $data = [
395 - "email_address" => $email,
396 - "status" => "subscribed",
397 - "merge_fields" => $merge_fields
398 - ];
432 + $status_code = (int) wp_remote_retrieve_response_code($response);
399 433
400 - $response = wp_remote_post($url, [
401 - 'headers' => [
402 - 'Content-Type' => 'application/json',
403 - 'Authorization' => 'Basic ' . base64_encode('anystring:' . $key)
404 - ],
405 - 'body' => json_encode($data),
406 - 'timeout' => 15,
407 - ]);
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');
408 438
409 - if ( is_wp_error($response) ) {
410 439 return [
411 440 'status' => 'error',
412 - 'message' => $response->get_error_message()
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 + ),
413 447 ];
414 448 }
415 449
416 - $body = wp_remote_retrieve_body($response);
417 - $result = json_decode($body, true);
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();
418 459
419 - return $result;
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;
420 469 }
421 470
422 471 /**
423 472 * Validations
424 473 */
425 - protected function validate_recaptcha(string $token, string $version) {
474 + protected function validate_recaptcha(string $token, string $version)
475 + {
426 476
427 477 // Check if secret and site key are set
428 478 if (!get_option('uicore_elements_recaptcha_secret_key') || !get_option('uicore_elements_recaptcha_site_key')) {
429 479 return [
@@ -441,9 +491,9 @@
441 491 'body' => $data,
442 492 'timeout' => 15,
443 493 ]);
444 494
445 - if ( is_wp_error($response) ) {
495 + if (is_wp_error($response)) {
446 496 return [
447 497 'success' => false,
448 498 'message' => $response->get_error_message()
449 499 ];
@@ -448,9 +498,9 @@
448 498 'message' => $response->get_error_message()
449 499 ];
450 500 }
451 501
452 - $captcha = json_decode( wp_remote_retrieve_body($response));
502 + $captcha = json_decode(wp_remote_retrieve_body($response));
453 503
454 504 if ($version === 'V3') {
455 505 return ['success' => ($captcha->success && $captcha->score >= 0.5) ? true : false];
456 506 }
@@ -457,15 +507,17 @@
457 507
458 508 // V2 default
459 509 return ['success' => $captcha->success];
460 510 }
461 - protected function validate_spam() {
511 + protected function validate_spam()
512 + {
462 513 // `ui-e-h-p` is the key for the honeypot
463 - return ( isset($this->form_data['ui-e-h-p']) && !empty($this->form_data['ui-e-h-p']) ) ? false : true;
514 + return (isset($this->form_data['ui-e-h-p']) && !empty($this->form_data['ui-e-h-p'])) ? false : true;
464 515 }
465 - protected function validate_url(string $url) {
516 + protected function validate_url(string $url)
517 + {
466 518 if (empty($url)) {
467 - throw new Redirect_Exception( esc_html( $this->get_response_message('redirect_no_url')));
519 + throw new Redirect_Exception(esc_html($this->get_response_message('redirect_no_url')));
468 520 }
469 521
470 522 return [
471 523 'status' => true,
@@ -471,26 +523,35 @@
471 523 'status' => true,
472 524 'url' => $url,
473 525 ];
474 526 }
475 - protected function validate_field(string $field, string $label) {
476 - if (empty($field)) {
477 - throw new Submit_Exception( esc_html( $this->get_response_message('empty_field', esc_html($label))));
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'));
478 531 }
479 - return esc_html($field);
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;
480 540 }
481 - protected function validate_mailchimp(string $key, string $list_id) {
482 - if (empty($key)) {
483 - throw new Mailchimp_Exception(esc_html__('Mailchimp API key is not set. Check Uicore Elements settings.', 'uicore-elements'));
484 - } else if (empty($list_id)) {
485 - throw new Mailchimp_Exception(esc_html__('Audience ID control is not set. Check your widget settings.', 'uicore-elements'));
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))));
486 545 }
546 + return $field;
487 547 }
488 548
489 549 /**
490 550 * Helpers
491 551 */
492 - protected function get_setting_fields() {
552 + protected function get_setting_fields()
553 + {
493 554 // Used to determine if the widget fields are repeaters with custom IDS or fixed fields, and if fixed fields
494 555 // compose them into an array similar to repeaters, to simplify shortcode replacement function
495 556
496 557 switch ($this->form_data['widget_type']) {
@@ -507,50 +568,240 @@
507 568 return $this->settings['form_fields'];
508 569 break;
509 570 }
510 571 }
511 - protected function all_submissions_succedded($responses) {
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 + {
512 643 foreach ($responses as $submission => $data) {
513 - // Redirect action failure shouldn't return `error` to main status because is not properly a submission action, so we skip it.
514 - if( $submission == 'redirect') {
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'])) {
515 646 continue;
516 647 }
517 - // Failed submissions returns `error` or bool `false` status
518 - if( $data['status'] === 'error' || $data['status'] === false ) {
648 + if (
649 + isset($data['status']) &&
650 + (
651 + $data['status'] === 'error' ||
652 + $data['status'] === false
653 + )
654 + ) {
519 655 return false;
520 656 }
521 657 }
522 658 return true;
523 659 }
660 + public static function get_form_submit_options()
661 + {
524 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 +
525 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 + /**
526 775 * Responses
527 776 */
528 777 // Also used by form widget(s), therefore public and static
529 - public static function get_default_messages(){
778 + public static function get_default_messages()
779 + {
530 780 return [
531 781 // main messages
532 - 'success' => esc_html__( 'Your submission was successful.', 'uicore-elements' ),
533 - 'error' => esc_html__( 'Your submission failed because of an error.', 'uicore-elements' ),
534 - 'mail_error' => esc_html__( 'Failed to send email.', 'uicore-elements' ),
535 - 'required' => esc_html__( 'Fill all required fields.', 'uicore-elements' ),
536 - 'redirect' => esc_html__( 'Redirecting...', 'uicore-elements' ),
537 - ];
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 + ];
538 788 }
539 - protected function get_response_message(string $status, string $dinamic_data = '') {
789 + protected function get_response_message(string $status, string $dinamic_data = '')
790 + {
540 791 // non-customizable messages (for settings debugging only)
541 792 $default_messages = [
542 - 'invalid_status' => esc_html__( 'Invalid status message.', 'uicore-elements' ),
543 - 'redirect_no_url' => esc_html__( 'Redirection failed. No URL set.', 'uicore-elements' ),
544 - 'empty_field' => esc_html__( 'The following field is empty.', 'uicore-elements') . $dinamic_data,
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,
545 796 ];
546 797
547 - if($this->settings['custom_messages'] === 'yes') {
798 + if ($this->settings['custom_messages'] === 'yes') {
548 799 $messages = [
549 800 'success' => esc_html($this->settings['success_message']),
550 801 'error' => esc_html($this->settings['error_message']),
551 802 'mail_error' => esc_html($this->settings['mail_error_message']),
552 - 'redirect' =>esc_html( $this->settings['redirect_message']),
803 + 'redirect' => esc_html($this->settings['redirect_message']),
553 804 ];
554 805 } else {
555 806 $messages = self::get_default_messages();
556 807 }
@@ -558,55 +809,45 @@
558 809 $messages = array_merge($default_messages, $messages);
559 810
560 811 return isset($messages[$status]) ? $messages[$status] : $messages['invalid_status'];
561 812 }
562 - protected function build_frontend_responses($responses) {
563 -
813 + protected function build_frontend_responses($responses)
814 + {
564 815 $data = [];
565 816
566 817 // Mail response
567 - if ( isset($responses['email']) && $responses['email']['status'] !== 'success' ) {
818 + if (isset($responses['email']) && $responses['email']['status'] !== 'success') {
568 819 $data['email'] = $responses['email'];
569 820 }
570 821
571 822 // Mail attachment response - is always an error
572 - if ( isset($responses['email']) && isset($responses['email']['attachment']) ) {
823 + if (isset($responses['email']) && isset($responses['email']['attachment'])) {
573 824 $data['attachment'] = $responses['attachment'];
574 825 }
575 826
576 - // Mailchimp response - Integer is also an error
577 - if ( isset($responses['mailchimp']) ) {
827 + // Submit Actions response - is always an error
828 + if (isset($responses['submit'])) {
829 + $data['submit'] = $responses['submit'];
830 + }
578 831
579 - // If Integer, is an error from mailchimp API so we pass their response
580 - if( is_int($responses['mailchimp']['status'])){
581 - $data['mailchimp'] = [
582 - 'status' => 'error',
583 - /* translators: 1: Mailchimp HTTP status code, 2: Mailchimp status response */
584 - 'message' => sprintf( esc_html__('Mailchimp HTTP "%1$s," - "%2$s,."', 'uicore-elements'), $responses['mailchimp']['status'], $responses['mailchimp']['detail'])
585 - ];
586 -
587 - // If error is from our validation
588 - } else if ( $responses['mailchimp']['status'] === 'error' ) {
589 - $data['mailchimp'] = [
590 - 'status' => $responses['mailchimp']['status'],
591 - 'message' => $responses['mailchimp']['message']
592 - ];
593 - }
832 + if (isset($responses['webhook']) && $responses['webhook']['status'] !== 'success') {
833 + $data['submit'] = $responses['webhook'];
594 834 }
595 835
596 - // Submit Actions response - is always an error
597 - if ( isset($responses['submit']) ) {
598 - $data['submit'] = $responses['submit'];
599 - }
836 + // Newsletter Services responses
837 + $data = $this->build_services_responses($responses);
600 838
601 839 // Main response
602 840 $status = $this->all_submissions_succedded($responses) ? 'success' : 'error';
603 841 $data['message'] = $this->get_response_message($status);
604 842
605 - // Redirect response (should work only if all previous submitions hasn't failed)
606 - if ( isset($responses['redirect']) && $status === 'success' ) {
843 + // Below responses should work only if all previous submitions hasn't failed
844 + if (isset($responses['redirect']) && $status === 'success') {
607 845 $data['redirect'] = $responses['redirect'];
608 846 }
847 + if (isset($responses['popup']) && $status === 'success') {
848 + $data['popup'] = $responses['popup'];
849 + }
609 850
610 851 // The only successfull response that should be sent is 'main' and 'redirect'
611 852 return [
612 853 'status' => $status,
@@ -613,5 +854,86 @@
613 854 'data' => $data
614 855 ];
615 856 }
616 857
617 -}
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 +}