PluginProbe
UiCore Elements – Free widgets and templates for Elementor / 1.2.3
UiCore Elements – Free widgets and templates for Elementor v1.2.3
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
uicore-elements / includes / utils / form-service.php

form-service.php in UiCore Elements – Free widgets and templates for Elementor 1.2.3, at includes/utils/form-service.php

617 lines 24.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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', gmdate('Y-m-d') . $line_break);
315 break;
316
317 case 'time' :
318 $content .= sprintf( '%s: %s', 'Time', gmdate('H:i:s') . $line_break);
319 break;
320
321 case 'remote_ip':
322 $content .= isset($_SERVER['REMOTE_ADDR'])
323 ? sprintf( '%s: %s', 'IP', sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])). $line_break)
324 : '';
325 break;
326
327 case 'user_agent':
328 $content .= isset($_SERVER['HTTP_USER_AGENT'])
329 ? sprintf( '%s: %s', 'User Agent', sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) . $line_break)
330 : '';
331 break;
332
333 case 'page_url':
334 $content .= isset($_SERVER['HTTP_REFERER'])
335 ? sprintf( '%s: %s', 'Page URL', sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])) . $line_break)
336 : '';
337 break;
338 }
339 }
340
341 return $content;
342 }
343
344 /**
345 * Extra submissions
346 */
347 protected function redirect() {
348
349 $validation = $this->validate_url( $this->settings['redirect_to'] );
350
351 // Above function exception blocks this execution
352 return [
353 'status' => 'success',
354 'url' => esc_url( $validation['url'] ),
355 'delay' => 1500,
356 'message' => esc_html( $this->get_response_message('redirect') )
357 ];
358 }
359 protected function mailchimp() {
360
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
365
366 $this->validate_mailchimp($key, $list_id);
367
368 // Check the widget type to determine the fields, before getting form data
369 if( $this->form_data['widget_type'] === 'contact-form' ) {
370
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']] : ""
380 ];
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 }
391
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 ];
399
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 ]);
408
409 if ( is_wp_error($response) ) {
410 return [
411 'status' => 'error',
412 'message' => $response->get_error_message()
413 ];
414 }
415
416 $body = wp_remote_retrieve_body($response);
417 $result = json_decode($body, true);
418
419 return $result;
420 }
421
422 /**
423 * Validations
424 */
425 protected function validate_recaptcha(string $token, string $version) {
426
427 // Check if secret and site key are set
428 if (!get_option('uicore_elements_recaptcha_secret_key') || !get_option('uicore_elements_recaptcha_site_key')) {
429 return [
430 'success' => false,
431 'message' => esc_html__('reCAPTCHA API keys are not set.', 'uicore-elements')
432 ];
433 }
434
435 $data = [
436 'secret' => get_option('uicore_elements_recaptcha_secret_key'),
437 'response' => sanitize_text_field($token)
438 ];
439
440 $response = wp_remote_post("https://www.google.com/recaptcha/api/siteverify", [
441 'body' => $data,
442 'timeout' => 15,
443 ]);
444
445 if ( is_wp_error($response) ) {
446 return [
447 'success' => false,
448 'message' => $response->get_error_message()
449 ];
450 }
451
452 $captcha = json_decode( wp_remote_retrieve_body($response));
453
454 if ($version === 'V3') {
455 return ['success' => ($captcha->success && $captcha->score >= 0.5) ? true : false];
456 }
457
458 // V2 default
459 return ['success' => $captcha->success];
460 }
461 protected function validate_spam() {
462 // `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;
464 }
465 protected function validate_url(string $url) {
466 if (empty($url)) {
467 throw new Redirect_Exception( esc_html( $this->get_response_message('redirect_no_url')));
468 }
469
470 return [
471 'status' => true,
472 'url' => $url,
473 ];
474 }
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))));
478 }
479 return $field;
480 }
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'));
486 }
487 }
488
489 /**
490 * Helpers
491 */
492 protected function get_setting_fields() {
493 // Used to determine if the widget fields are repeaters with custom IDS or fixed fields, and if fixed fields
494 // compose them into an array similar to repeaters, to simplify shortcode replacement function
495
496 switch ($this->form_data['widget_type']) {
497
498 case 'newsletter':
499 return [
500 ['custom_id' => 'email'],
501 ['custom_id' => 'name'],
502 ];
503 break;
504
505 // Dynamic fields values
506 default:
507 return $this->settings['form_fields'];
508 break;
509 }
510 }
511 protected function all_submissions_succedded($responses) {
512 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') {
515 continue;
516 }
517 // Failed submissions returns `error` or bool `false` status
518 if( $data['status'] === 'error' || $data['status'] === false ) {
519 return false;
520 }
521 }
522 return true;
523 }
524
525 /**
526 * Responses
527 */
528 // Also used by form widget(s), therefore public and static
529 public static function get_default_messages(){
530 return [
531 // 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 ];
538 }
539 protected function get_response_message(string $status, string $dinamic_data = '') {
540 // non-customizable messages (for settings debugging only)
541 $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,
545 ];
546
547 if($this->settings['custom_messages'] === 'yes') {
548 $messages = [
549 'success' => esc_html($this->settings['success_message']),
550 'error' => esc_html($this->settings['error_message']),
551 'mail_error' => esc_html($this->settings['mail_error_message']),
552 'redirect' =>esc_html( $this->settings['redirect_message']),
553 ];
554 } else {
555 $messages = self::get_default_messages();
556 }
557
558 $messages = array_merge($default_messages, $messages);
559
560 return isset($messages[$status]) ? $messages[$status] : $messages['invalid_status'];
561 }
562 protected function build_frontend_responses($responses) {
563
564 $data = [];
565
566 // Mail response
567 if ( isset($responses['email']) && $responses['email']['status'] !== 'success' ) {
568 $data['email'] = $responses['email'];
569 }
570
571 // Mail attachment response - is always an error
572 if ( isset($responses['email']) && isset($responses['email']['attachment']) ) {
573 $data['attachment'] = $responses['attachment'];
574 }
575
576 // Mailchimp response - Integer is also an error
577 if ( isset($responses['mailchimp']) ) {
578
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 }
594 }
595
596 // Submit Actions response - is always an error
597 if ( isset($responses['submit']) ) {
598 $data['submit'] = $responses['submit'];
599 }
600
601 // Main response
602 $status = $this->all_submissions_succedded($responses) ? 'success' : 'error';
603 $data['message'] = $this->get_response_message($status);
604
605 // Redirect response (should work only if all previous submitions hasn't failed)
606 if ( isset($responses['redirect']) && $status === 'success' ) {
607 $data['redirect'] = $responses['redirect'];
608 }
609
610 // The only successfull response that should be sent is 'main' and 'redirect'
611 return [
612 'status' => $status,
613 'data' => $data
614 ];
615 }
616
617 }