PluginProbe
UiCore Elements – Free widgets and templates for Elementor / 1.0.6
UiCore Elements – Free widgets and templates for Elementor v1.0.6
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 / contact-form-service.php

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

425 lines 16.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
8 class Contact_Form_Service {
9
10 protected $form_data,
11 $settings,
12 $files;
13 public function __construct($form_data, $settings, $files) {
14 $this->form_data = $form_data;
15 $this->settings = $settings;
16 $this->files = $files;
17 }
18
19 public function handle() {
20
21 $processed_data = [];
22 $responses = [];
23 $data = [];
24
25 // Checks for reCAPTCHA validation
26 if (isset($this->form_data['grecaptcha_token']) && !empty($this->form_data['grecaptcha_token'])) {
27
28 $recaptcha = $this->validate_recaptcha($this->form_data['grecaptcha_token'], $this->form_data['grecaptcha_version']);
29
30 if(!$recaptcha['success']){
31 return [
32 'status' => 'error',
33 'data' => [
34 'message' => esc_html__('reCAPTCHA validation failed.', 'uicore-elements'),
35 ]
36 ];
37 }
38 }
39
40 // Check for honeypot spam
41 if(!$this->validate_spam()){
42 return [
43 'status' => 'success',
44 'data' => [
45 'message' => $this->get_response_message('success') // Fakes a successfull submission
46 ]
47 ];
48 }
49
50 // Run all registered submit actions
51 if (isset($this->settings['submit_actions']) && !empty($this->settings['submit_actions'])) {
52 foreach ($this->settings['submit_actions'] as $action) {
53 try {
54 switch ($action) {
55 case 'email':
56 $data = $this->send_mail($action);
57 $responses['email'] = $data['response'];
58 break;
59
60 case 'email_2' :
61 $data = $this->send_mail($action, $data);
62 $responses['email'] = $data['response'];
63 break;
64
65 case 'redirect':
66 $responses['redirect'] = $this->redirect();
67 break;
68
69 default:
70 throw new Submit_Exception(esc_html__('Unknown submit action: ', 'uicore-elements') . $action . esc_html__('. Check your settings.', 'uicore-elements'));
71 }
72 } catch (Email_Exception $e) {
73 $responses['email'] = [
74 'status' => false,
75 'message' => $e->getMessage()
76 ];
77 } catch (Redirect_Exception $e) {
78 $responses['redirect'] = [
79 'status' => 'error',
80 'message' => $e->getMessage()
81 ];
82 } catch (Submit_Exception $e) {
83 $responses['submit'] = [
84 'message' => $e->getMessage()
85 ];
86 }
87 }
88
89 // There's no need to continue without a submit action enabled
90 } else {
91 return [
92 'status' => 'error',
93 'data' => [
94 'message' => esc_html__('No submit action enabled.', 'uicore-elements')
95 ]
96 ];
97 }
98
99 // Consider `current_user_can( 'manage_options' )` as filter to return more specific messages on frontend (not tested)
100
101 // Since attachments may be used up to two times (both emails), they need to be deleted only after processing submits
102 if (isset($data['attachments']) && !empty($data['attachments']['files'])) {
103 register_shutdown_function('unlink', $data['attachments']['files']);
104 }
105
106 // Build mail response
107 if (isset($responses['email'])) {
108 $status = $responses['email']['status'] ? 'success' : 'error';
109 $processed_data['message'] = $responses['email']['message'];
110
111 // Build attachment response
112 if(isset($responses['email']['attachment'])) {
113 $processed_data['attachment'] = $responses['attachment'];
114 }
115 }
116 // Build redirect response (only if email was successful)
117 if ($status === 'success' && isset($responses['redirect'])) {
118 $processed_data['redirect'] = $responses['redirect'];
119 }
120 // Build submit response
121 if (isset($responses['submit'])) {
122 $processed_data['submit'] = $responses['submit'];
123 }
124
125 return [
126 'status' => $status,
127 'data' => $processed_data,
128 ];
129 }
130
131 /**
132 * Mail submition
133 */
134 protected function send_mail(string $action, array $data = []){
135
136 $attachments = isset($data['attachments']) ? $data['attachments'] : []; // Check if there's attachments from previous mail submit action
137
138 $mail_data = $this->compose_mail_data($action, $attachments); // build mail data
139
140 // Check if there's any attachment error before sending mail
141 if (!empty($mail_data['attachments']['errors'])) {
142 // throwing exceptions here will block proper data flow. Is best directly returning the error on email action
143 return [
144 'response' => [
145 'status' => false,
146 'message' => $mail_data['attachments']['errors']
147 ],
148 ];
149 }
150
151 $email = wp_mail(
152 $mail_data['email']['to'],
153 $mail_data['email']['subject'],
154 $mail_data['email']['message'],
155 $mail_data['email']['headers'],
156 $mail_data['email']['attachments']
157 );
158
159 return [
160 'response' => [
161 'status' => $email,
162 'message' => $email ? $this->get_response_message('success') : $this->get_response_message('error')
163 ],
164 'attachments' => $mail_data['attachments'] // Return attachments for deletion and error handling
165 ];
166 }
167 protected function compose_mail_data(string $action, array $attachments = []) {
168
169 // Set short vars for the data
170 $settings = $this->settings;
171 $data = $this->form_data;
172 $files = $this->files;
173
174 $slug = $action == 'email_2' ? '_2' : ''; // Update controls slugs based on the mail submit type
175 $line_break = $settings['email_content_type'.$slug] === 'html' ? '<br>' : "\n"; // Set line break type
176
177 // Replace shortcodes by form data
178 $content = $this->replace_content_shortcode( $settings['email_content'.$slug], $line_break );
179
180 // Adds the metadata to content
181 $content = $this->compose_metadata($content, $settings['form_metadata'.$slug], $line_break);
182
183 // If theres attachments from previous submit action, use it, otherwhise prepare it from $files
184 $attachments = !empty($attachments) ? $attachments : $this->prepare_attachments($files);
185
186 // Validate and replace fields shortcodes
187 $mail_to = $this->replace_content_shortcode( $this->validate_field($settings['email_to'.$slug], 'Recipient (to)'));
188 $mail_subject = $this->replace_content_shortcode( $this->validate_field($settings['email_subject'.$slug], 'Subject'));
189 $mail_name = $this->replace_content_shortcode( $this->validate_field($settings['email_from_name'.$slug], 'From Name'));
190 $mail_from = $this->replace_content_shortcode( $this->validate_field($settings['email_from'.$slug], 'From'));
191 $mail_reply = $this->replace_content_shortcode( $this->validate_field($settings['email_reply_to'.$slug], 'Reply To'));
192
193 // Build the data
194 $mail_data = [
195 'to' => $mail_to,
196 'subject' => $mail_subject,
197 'message' => $content,
198 'headers' => [
199 'Content-Type: text/' . $settings['email_content_type'.$slug] . '; charset=UTF-8',
200 'From: ' . $mail_name . ' <'.$mail_from.'>',
201 'Reply-To: ' . $mail_reply,
202 ],
203 'attachments' => $attachments['files']
204 ];
205
206 // Build optional data
207 if (!empty($settings['email_to_cc'.$slug])) {
208 $mail_data['headers'][] = 'Cc: ' . $settings['email_to_cc'];
209 }
210 if (!empty($settings['email_to_bcc'.$slug])) {
211 $mail_data['headers'][] = 'Bcc: ' . $settings['email_to_bcc'];
212 }
213
214 return [
215 'email' => $mail_data,
216 'attachments' => $attachments
217 ];
218 }
219 protected function replace_content_shortcode(string $content, string $line_break = ''){
220
221 // Set short vars for the data
222 $fields = $this->settings['form_fields'];
223 $form_data = $this->form_data;
224
225 // [all-fieds] shortcode replacement
226 if ( false !== strpos( $content, '[all-fields]' ) ) {
227 $text = '';
228 // Return formated text as key: value
229 foreach ( $form_data['form_fields'] as $key => $field ) {
230 $field_value = is_array($field) ? implode(', ', $field) : $field;
231 $text .= !empty($field_value) ? sprintf('%s: %s', $key, $field_value) . $line_break : '';
232 }
233 $content = str_replace( '[all-fields]', $text, $content );
234 }
235
236 // Custom [field id="{id}"] shortcode replacement
237 foreach ($fields as $field) {
238 $shortcode = '[field id="' . $field['custom_id'] . '"]';
239 $value = isset($form_data['form_fields'][$field['custom_id']]) ? $form_data['form_fields'][$field['custom_id']] : '';
240 $value = is_array($value) ? implode(', ', $value) : $value;
241 $content = str_replace($shortcode, $value, $content);
242 }
243
244 // Replaces all manual line breaks from content
245 if(!empty($line_break)){
246 $content = str_replace( array( "\r\n", "\r", "\n" ), $line_break, $content );
247 }
248
249 return $content;
250 }
251 protected function prepare_attachments(array $files) {
252 $attachments = [];
253 $errors = '';
254
255 // Requires wp_handle_upload() file if unavailable
256 if ( ! function_exists( 'wp_handle_upload' ) ) {
257 require_once( ABSPATH . 'wp-admin/includes/file.php' );
258 }
259
260 // Check if theres a valid file to upload
261 foreach ($files['form_fields']['tmp_name'] as $input => $value) {
262 if ($files['form_fields']['error'][$input] !== UPLOAD_ERR_NO_FILE) {
263 $file = [
264 'name' => $files['form_fields']['name'][$input],
265 'type' => $files['form_fields']['type'][$input],
266 'tmp_name' => $files['form_fields']['tmp_name'][$input],
267 'error' => $files['form_fields']['error'][$input],
268 'size' => $files['form_fields']['size'][$input],
269 ];
270
271 // Handle the file upload
272 $uploaded_file = wp_handle_upload($file, ['test_form' => false]);
273
274 if (!isset($uploaded_file['error'])) {
275 $attachments = $uploaded_file['file'];
276 } else {
277 // Since throwing exceptions here will block the proper data flow, we return the error and let send_mail() handle it
278 $errors = esc_html__('Failed to upload file: ', 'uicore-elements') . $uploaded_file['error'];
279 }
280
281 // Break after processing the first valid file
282 break;
283 }
284 }
285
286 return [
287 'files' => $attachments,
288 'errors' => $errors
289 ];
290 }
291 protected function compose_metadata(string $content, array $metadada, string $line_break){
292
293 if (empty($metadada)) {
294 return $content;
295 }
296
297 $content = $content . $line_break . $line_break . '--' . $line_break . $line_break; // Adds spacing between content and metadata
298
299 foreach ($metadada as $meta) {
300 switch($meta){
301 case 'date':
302 $content .= sprintf( '%s: %s', 'Date', date('Y-m-d') . $line_break);
303 break;
304
305 case 'time' :
306 $content .= sprintf( '%s: %s', 'Time', date('H:i:s') . $line_break);
307 break;
308
309 case 'remote_ip':
310 $content .= sprintf( '%s: %s', 'IP', $_SERVER['REMOTE_ADDR'] . $line_break); // TODO: test if indeed working
311 break;
312
313 case 'user_agent':
314 $content .= sprintf( '%s: %s', 'User Agent', $_SERVER['HTTP_USER_AGENT'] . $line_break);
315 break;
316
317 case 'page_url':
318 $content .= sprintf( '%s: %s', 'Page URL', $_SERVER['HTTP_REFERER'] . $line_break);
319 break;
320 }
321 }
322
323 return $content;
324 }
325
326 /**
327 * Extra submition
328 */
329 protected function redirect() {
330 $url = $this->validate_url($this->settings['redirect_to']);
331 $url = $this->replace_content_shortcode($url);
332 return [
333 'status' => 'success',
334 'url' => $url,
335 'delay' => 1500,
336 'message' => $this->get_response_message('redirect')
337 ];
338 }
339
340 /**
341 * Validations
342 */
343 protected function validate_recaptcha(string $token, string $version) {
344
345 // Check if secret and site key are set
346 if (!get_option('uicore_elements_recaptcha_secret_key') || !get_option('uicore_elements_recaptcha_site_key')) {
347 return [
348 'success' => false,
349 'message' => esc_html__('reCAPTCHA API keys are not set.', 'uicore-elements')
350 ];
351 }
352
353 $data = [
354 'secret' => get_option('uicore_elements_recaptcha_secret_key'),
355 'response' => sanitize_text_field($token)
356 ];
357
358 $verify = curl_init();
359 curl_setopt($verify, CURLOPT_URL, "https://www.google.com/recaptcha/api/siteverify");
360 curl_setopt($verify, CURLOPT_POST, true);
361 curl_setopt($verify, CURLOPT_POSTFIELDS, http_build_query($data));
362 curl_setopt($verify, CURLOPT_SSL_VERIFYPEER, false);
363 curl_setopt($verify, CURLOPT_RETURNTRANSFER, true);
364 $res = curl_exec($verify);
365
366 $captcha = json_decode($res);
367
368 if($version === 'V3') {
369 return ['success' => ($captcha->success && $captcha->score >= 0.5) ? true : false];
370 }
371
372 // V2 default
373 return ['success' => $captcha->success];
374
375 }
376
377 protected function validate_spam() {
378 // `ui-e-h-p` is the key for the honeypot
379 return ( isset($this->form_data['ui-e-h-p']) && !empty($this->form_data['ui-e-h-p']) ) ? false : true;
380 }
381 protected function validate_url(string $url) {
382 if (!$url) {
383 throw new Redirect_Exception(esc_html__('No redirect URL set.', 'uicore-elements'));
384 }
385 return $url;
386 }
387 protected function validate_field(string $field, string $label) {
388 if (empty($field)) {
389 throw new Submit_Exception(esc_html__('The field "' . $label . '" is empty.', 'uicore-elements'));
390 }
391 return $field;
392 }
393
394 /**
395 * Responses
396 */
397 // Also used by form widget(s), therefore public and static
398 public static function get_default_messages(){
399 return [
400 'success' => esc_html__( 'Your submission was successful.', 'uicore-elements' ),
401 'error' => esc_html__( 'Your submission failed because of an error.', 'uicore-elements' ),
402 'redirect' => esc_html__( 'Redirecting...', 'uicore-elements' ),
403 ];
404 }
405 protected function get_response_message($status){
406 // non-customizable messages
407 $default_messages = [
408 'invalid_status' => esc_html__( 'Invalid status message.', 'uicore-elements' ),
409 ];
410
411 if($this->settings['custom_messages'] === 'yes') {
412 $messages = [
413 'success' => $this->settings['success_message'],
414 'error' => $this->settings['error_message'],
415 'redirect' => $this->settings['redirect_message'],
416 ];
417 } else {
418 $messages = self::get_default_messages();
419 }
420
421 $messages = array_merge($default_messages, $messages);
422
423 return isset($messages[$status]) ? $messages[$status] : $messages['invalid_status'];
424 }
425 }