PluginProbe
UiCore Elements – Free widgets and templates for Elementor / 1.0.11
UiCore Elements – Free widgets and templates for Elementor v1.0.11
1.3.18 1.3.17 1.3.16 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.2.0 1.2.1 1.2.2 1.2.3 All 42 releases
uicore-elements / includes / utils / form-service.php

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

607 lines 24.2 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 'redirect' => esc_html__( 'Redirecting...', 'uicore-elements' ),
528 ];
529 }
530 protected function get_response_message(string $status, string $dinamic_data = '') {
531 // non-customizable messages (for settings debugging only)
532 $default_messages = [
533 'invalid_status' => esc_html__( 'Invalid status message.', 'uicore-elements' ),
534 'redirect_no_url' => esc_html__( 'Redirection failed. No URL set.', 'uicore-elements' ),
535 'empty_field' => esc_html__( 'The following field is empty.', 'uicore-elements') . $dinamic_data,
536 ];
537
538 if($this->settings['custom_messages'] === 'yes') {
539 $messages = [
540 'success' => $this->settings['success_message'],
541 'error' => $this->settings['error_message'],
542 'mail_error' => $this->settings['mail_error_message'],
543 'redirect' => $this->settings['redirect_message'],
544 ];
545 } else {
546 $messages = self::get_default_messages();
547 }
548
549 $messages = array_merge($default_messages, $messages);
550
551 return isset($messages[$status]) ? $messages[$status] : $messages['invalid_status'];
552 }
553 protected function build_frontend_responses($responses) {
554
555 $data = [];
556
557 // Mail response
558 if ( isset($responses['email']) && $responses['email']['status'] !== 'success' ) {
559 $data['email'] = $responses['email'];
560 }
561
562 // Mail attachment response - is always an error
563 if ( isset($responses['email']) && isset($responses['email']['attachment']) ) {
564 $data['attachment'] = $responses['attachment'];
565 }
566
567 // Mailchimp response - Integer is also an error
568 if ( isset($responses['mailchimp']) ) {
569
570 // If Integer, is an error from mailchimp API so we pass their response
571 if( is_int($responses['mailchimp']['status'])){
572 $data['mailchimp'] = [
573 'status' => 'error',
574 'message' => sprintf( esc_html__('Mailchimp HTTP "%s" - "%s."', 'uicore-elements'), $responses['mailchimp']['status'], $responses['mailchimp']['detail'])
575 ];
576
577 // If error is from our validation
578 } else if ( $responses['mailchimp']['status'] === 'error' ) {
579 $data['mailchimp'] = [
580 'status' => $responses['mailchimp']['status'],
581 'message' => $responses['mailchimp']['message']
582 ];
583 }
584 }
585
586 // Submit Actions response - is always an error
587 if ( isset($responses['submit']) ) {
588 $data['submit'] = $responses['submit'];
589 }
590
591 // Main response
592 $status = $this->all_submissions_succedded($responses) ? 'success' : 'error';
593 $data['message'] = $this->get_response_message($status);
594
595 // Redirect response (should work only if all previous submitions hasn't failed)
596 if ( isset($responses['redirect']) && $status === 'success' ) {
597 $data['redirect'] = $responses['redirect'];
598 }
599
600 // The only successfull response that should be sent is 'main' and 'redirect'
601 return [
602 'status' => $status,
603 'data' => $data
604 ];
605 }
606
607 }