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

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