PluginProbe
UiCore Elements – Free widgets and templates for Elementor / 1.0.15
UiCore Elements – Free widgets and templates for Elementor v1.0.15
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.0.15, at includes/utils/form-service.php

608 lines 23.7 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', 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 }