PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 releases
bit-form / includes / Core / Util / MailNotifier.php

MailNotifier.php in Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder 3.3.1, at includes/Core/Util/MailNotifier.php

356 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace BitCode\BitForm\Core\Util;
4
5 if (!defined('ABSPATH')) {
6 exit;
7 }
8
9 use BitCode\BitForm\Admin\Form\Helpers;
10 use BitCode\BitForm\Core\Database\FormEntryMetaModel;
11 use BitCode\BitForm\Core\Form\FormManager;
12 use BitCode\BitForm\Core\Messages\EmailTemplateHandler;
13
14 final class MailNotifier
15 {
16 public static function notify($notifyDetails, $formID, $fieldValue, $entryID, $isDblOptin = false, $logId = '')
17 {
18 $apiResponse = new ApiResponse();
19 $formManager = FormManager::getInstance($formID);
20 $entryDetails = ['formId' => $formID, 'entryId' => $entryID, 'fieldValues' => $fieldValue];
21 $emailTemplateHandler = new EmailTemplateHandler($formID);
22 $attachments = [];
23 $tempPdfLinks = [];
24 if (is_string($notifyDetails->id)) {
25 $mailTemplateID = Utilities::jsonObj($notifyDetails->id)->id ?? null;
26 $mailTemplate = $emailTemplateHandler->getATemplate($mailTemplateID);
27 if (!is_wp_error($mailTemplate)) {
28 // Honor template enable/disable: a disabled email template is never sent.
29 if (isset($mailTemplate[0]->status) && empty($mailTemplate[0]->status)) {
30 return;
31 }
32 $mailTo = FieldValueHandler::validateMailArry($notifyDetails->to, $fieldValue);
33 // Conditional email routing (Pro): add recipients resolved from value-based rules in the template config.
34 $mailTo = apply_filters('bitform_email_conditional_routing', $mailTo, $mailTemplate[0], $fieldValue, $formID);
35 if (!empty($mailTo)) {
36 $from_name = '';
37 if (isset($notifyDetails->from_name) && !empty($notifyDetails->from_name)) {
38 $from_name = $notifyDetails->from_name;
39 }
40 $mailHeaders = [
41 // 'Content-Type: text/html; charset=UTF-8',
42 // $embeddedMailHeader
43 ];
44 $from_mail = '';
45 if (!empty($notifyDetails->from)) {
46 $fromMail = FieldValueHandler::validateMailArry($notifyDetails->from, $fieldValue);
47 $headerFromName = !empty($notifyDetails->from_name) ? $notifyDetails->from_name : explode('@', $fromMail[0])[0];
48 $mailHeaders[] = "FROM: $headerFromName " . '<' . sanitize_email($fromMail[0]) . '>';
49 $from_mail = $fromMail[0];
50 }
51 (new MailConfig())->sendMail(['from_name' => $from_name, 'from_email' => $from_mail]);
52 // Translate before smart-tag replacement. One language per submission,
53 // shared by the admin and submitter emails.
54 $mailSubjectTemplate = (string) apply_filters(
55 'bitform_translate_form_string',
56 (string) $mailTemplate[0]->sub,
57 'mail-sub-' . $mailTemplateID,
58 $formID
59 );
60 $mailSubject = FieldValueHandler::replaceFieldWithValue($mailSubjectTemplate, $fieldValue, $formID);
61
62 // allow developers to modify email subject
63 $mailSubject = apply_filters(
64 'bitform_filter_email_subject',
65 $mailSubject,
66 [
67 'form_id' => $formID,
68 'entry_id' => $entryID,
69 'field_values' => $fieldValue,
70 'template' => $mailTemplate[0] ?? null,
71 'is_double_optin' => (bool) $isDblOptin,
72 ]
73 );
74
75 $mailBody = (string) apply_filters(
76 'bitform_translate_form_string',
77 (string) $mailTemplate[0]->body,
78 'mail-body-' . $mailTemplateID,
79 $formID
80 );
81 if (class_exists('\BitCode\BitFormPro\Admin\DownloadFile')) {
82 $downloadFile = new \BitCode\BitFormPro\Admin\DownloadFile();
83 $mailBody = $downloadFile->replacePdfShortCodeToLink($mailBody, $formID, $entryID);
84 $mailBody = $downloadFile->replaceShortCodeToPdfPassword($mailBody, $formID, $entryID);
85 }
86
87 $mailBody = FieldValueHandler::replaceFieldWithValue($mailBody, $fieldValue, $formID);
88 // Signature images: embed inline (cid:) so they render for non-logged-in recipients.
89 // Must run before changeImagePathInHTMLString so the src is still the raw filename.
90 $cidMap = [];
91 $sigBasePath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR;
92 $mailBody = self::embedSignatureImages($mailBody, $formManager, $fieldValue, $sigBasePath, $cidMap);
93 $webUrl = Helpers::getWebPathWithEncryptedEntryId($formID, $entryID);
94 $mailBody = FieldValueHandler::changeImagePathInHTMLString($mailBody, $webUrl);
95 $mailBody = FieldValueHandler::changeHrefPathInHTMLString($mailBody, $webUrl); // replace anchor tag href with constructed weburl
96
97 // allow developers to modify email body
98 $mailBody = apply_filters(
99 'bitform_filter_email_body',
100 $mailBody,
101 [
102 'form_id' => $formID,
103 'entry_id' => $entryID,
104 'field_values' => $fieldValue,
105 'template' => $mailTemplate[0] ?? null,
106 'is_double_optin' => (bool) $isDblOptin,
107 ]
108 );
109 if (!empty($notifyDetails->replyto)) {
110 $mailReplyTo = FieldValueHandler::validateMailArry($notifyDetails->replyto, $fieldValue);
111 if (is_array($mailReplyTo)) {
112 foreach ($mailReplyTo as $key => $emailAddress) {
113 $mailHeaders[] = 'Reply-To: ' . explode('@', $emailAddress)[0] . '<' . sanitize_email($emailAddress) . '>';
114 }
115 } else {
116 $mailHeaders[] = 'Reply-To: ' . explode('@', $mailReplyTo)[0] . '<' . sanitize_email($mailReplyTo) . '>';
117 }
118 }
119 $oldMailBody = $mailBody;
120 $data = [];
121 if ($isDblOptin && true === has_filter('bitform_email_body_text')) {
122 $urlParams = $formID . '_' . $entryID . '_' . $logId;
123 $data = apply_filters('bitform_email_body_text', $mailBody, $urlParams);
124 $mailBody = $data['mailbody'];
125 }
126
127 if (!empty($notifyDetails->bcc)) {
128 $mailBCC = FieldValueHandler::validateMailArry($notifyDetails->bcc, $fieldValue);
129 if (is_array($mailBCC)) {
130 foreach ($mailBCC as $key => $emailAddress) {
131 $mailHeaders[] = 'Bcc: ' . sanitize_email($emailAddress);
132 }
133 } else {
134 $mailHeaders[] = 'Bcc: ' . sanitize_email($mailBCC);
135 }
136 }
137 if (!empty($notifyDetails->cc)) {
138 $mailCC = FieldValueHandler::validateMailArry($notifyDetails->cc, $fieldValue);
139 if (is_array($mailCC)) {
140 foreach ($mailCC as $key => $emailAddress) {
141 $mailHeaders[] = 'Cc: ' . sanitize_email($emailAddress);
142 }
143 } else {
144 $mailHeaders[] = 'Cc: ' . sanitize_email($mailCC);
145 }
146 }
147 // PDF generation is a Pro feature; Pro hooks this filter and returns the generated
148 // file path(s) — an array (one per configured template), or a single path string
149 // from Pro versions that predate multi-PDF. Generated here (not before the
150 // template/recipient checks) so disabled templates and empty recipients never
151 // generate PDFs — and never leak temp files.
152 $tempPdfLinks = apply_filters('bitform_generate_pdf_attachment', [], $notifyDetails, $entryDetails, $logId);
153 $tempPdfLinks = is_array($tempPdfLinks) ? $tempPdfLinks : [$tempPdfLinks];
154 $tempPdfLinks = array_values(array_unique(array_filter($tempPdfLinks, static function ($pdfPath) {
155 return !empty($pdfPath) && is_string($pdfPath) && file_exists($pdfPath);
156 })));
157 $attachments = array_merge($attachments, $tempPdfLinks);
158 if (!empty($notifyDetails->attachment)) {
159 $fileFldKeys = $notifyDetails->attachment;
160 $fileBasePath = FileHandler::getEntriesFileUploadDir($formID, $entryID) . DIRECTORY_SEPARATOR;
161
162 // Normalize to array for consistent processing
163 $fileFldKeys = is_array($fileFldKeys) ? $fileFldKeys : [$fileFldKeys];
164
165 foreach ($fileFldKeys as $fldKey) {
166 $repeaterFieldKey = $formManager->isRepeatedField($fldKey);
167
168 if ($repeaterFieldKey) {
169 // Handle repeated file field
170 FileHandler::processRepeaterAttachment(
171 $repeaterFieldKey,
172 $fldKey,
173 $fieldValue,
174 $fileBasePath,
175 $attachments
176 );
177 } else {
178 // Handle regular file field
179 FileHandler::processRegularAttachment(
180 $fldKey,
181 $fieldValue,
182 $fileBasePath,
183 $attachments
184 );
185 }
186 }
187 }
188 // WordPress Media Library attachments (stored as attachment IDs in the template config).
189 if (!empty($notifyDetails->mediaAttachment)) {
190 $mediaIds = is_array($notifyDetails->mediaAttachment) ? $notifyDetails->mediaAttachment : [$notifyDetails->mediaAttachment];
191 foreach ($mediaIds as $mediaId) {
192 $mediaPath = get_attached_file(absint($mediaId));
193 if ($mediaPath && file_exists($mediaPath)) {
194 $attachments[] = $mediaPath;
195 }
196 }
197 }
198 $mailBody = stripcslashes($mailBody);
199 $mailSubject = stripcslashes($mailSubject);
200 $embedCb = static function ($phpmailer) use ($cidMap) {
201 foreach ($cidMap as $cid => $info) {
202 try {
203 $phpmailer->addEmbeddedImage($info['path'], $cid, $info['name']);
204 } catch (\Throwable $e) {
205 Log::debug_log("[Signature Embed] failed for {$info['path']} - " . $e->getMessage());
206 }
207 }
208 };
209 if (!empty($cidMap)) {
210 add_action('phpmailer_init', $embedCb);
211 }
212 add_filter('wp_mail_content_type', [self::class, 'filterMailContentType']);
213 $fromNameCb = null;
214 if (!empty($from_name)) {
215 $fromNameCb = static function () use ($from_name) {
216 return $from_name;
217 };
218 add_filter('wp_mail_from_name', $fromNameCb);
219 }
220 $status = wp_mail($mailTo, $mailSubject, $mailBody, $mailHeaders, $attachments);
221
222 if (!$status) {
223 Log::debug_log([
224 'status' => 'error',
225 'code' => 'mail_not_sent',
226 'message' => 'Mail not sent',
227 'inputDetails' => [
228 'to' => $mailTo,
229 'subject' => $mailSubject,
230 'body' => $mailBody,
231 'headers' => $mailHeaders,
232 'attachments' => $attachments,
233 'notifyDetails' => $notifyDetails,
234 'formID' => $formID,
235 'entryID' => $entryID,
236 'isDblOptin' => $isDblOptin,
237 'logId' => $logId
238 ],
239 'responseDetails' => $status
240 ]);
241 $apiResponse->apiResponse($logId, '', ['type' => 'record', 'type_name' => 'smtp'], 'errors', 'Mail dose not send successfully', $entryDetails);
242 } else {
243 Log::debug_log([
244 'status' => 'success',
245 'code' => 'mail_sent',
246 'message' => 'Mail successfully sent',
247 'inputDetails' => [
248 'to' => $mailTo,
249 'subject' => $mailSubject,
250 'body' => $mailBody,
251 'headers' => $mailHeaders,
252 'attachments' => $attachments,
253 'notifyDetails' => $notifyDetails,
254 'formID' => $formID,
255 'entryID' => $entryID,
256 'isDblOptin' => $isDblOptin,
257 'logId' => $logId
258 ],
259 'responseDetails' => $status
260 ]);
261 $apiResponse->apiResponse($logId, '', ['type' => 'record', 'type_name' => 'smtp'], 'success', 'Mail successfully send.', $entryDetails);
262 }
263 if ($status && $isDblOptin && false !== strpos($oldMailBody, 'entry_confirmation_url')) {
264 $entryMeta = new FormEntryMetaModel();
265 $apiResponse->apiResponse($logId, '', ['type' => 'record', 'type_name' => 'smtp'], 'success', 'Mail successfully send.', $entryDetails);
266 // Form entry meta insert; meta_key/meta_value required to store dynamic field data per entry.
267 $entryMeta->insert(
268 [
269 'bitforms_form_entry_id' => $entryID,
270 'meta_key' => 'entry_confirm_activation',
271 'meta_value' => $data['token']
272 ]
273 );
274 }
275 remove_filter('wp_mail_content_type', [self::class, 'filterMailContentType']);
276 if (null !== $fromNameCb) {
277 remove_filter('wp_mail_from_name', $fromNameCb);
278 }
279 if (!empty($cidMap)) {
280 remove_action('phpmailer_init', $embedCb);
281 }
282 }
283 }
284 }
285
286 foreach ($tempPdfLinks as $tempPdfLink) {
287 wp_delete_file($tempPdfLink);
288 }
289 }
290
291 public static function filterMailContentType()
292 {
293 return 'text/html; charset=UTF-8';
294 }
295
296 /**
297 * Rewrite signature <img> tags to inline cid: references and collect the files
298 * to embed. Only signature-field images that are local & readable are embedded;
299 * external URLs and non-signature images are left untouched. When the form has
300 * no signature (or none is in the body) $cidMap stays empty and nothing changes.
301 */
302 private static function embedSignatureImages($html, $formManager, $fieldValue, $baseDir, &$cidMap)
303 {
304 if (empty($html)) {
305 return $html;
306 }
307
308 // Collect signature filenames for this entry.
309 $sigFiles = [];
310 foreach ($formManager->getFields() as $key => $detail) {
311 if (!isset($detail['type']) || 'signature' !== $detail['type']) {
312 continue;
313 }
314 $val = isset($fieldValue[$key]) ? $fieldValue[$key] : '';
315 foreach ((array) $val as $fn) {
316 $fn = is_string($fn) ? trim($fn) : '';
317 if ('' !== $fn && 'signature-failed.png' !== $fn) {
318 $sigFiles[basename($fn)] = true;
319 }
320 }
321 }
322 if (empty($sigFiles)) {
323 return $html;
324 }
325
326 return preg_replace_callback(
327 '/<img\s+[^>]*src=[\'"]([^\'"]+)[\'"][^>]*>/i',
328 function ($m) use ($baseDir, $sigFiles, &$cidMap) {
329 $src = $m[1];
330 if (filter_var($src, FILTER_VALIDATE_URL)) {
331 return $m[0]; // external URL, leave as-is
332 }
333 $name = basename($src);
334 if (!isset($sigFiles[$name])) {
335 return $m[0]; // not a signature image
336 }
337 $file = $baseDir . $name;
338 if (!is_readable($file)) {
339 return $m[0];
340 }
341 $cid = 'bfsig_' . md5($file);
342 $cidMap[$cid] = ['path' => $file, 'name' => $name];
343
344 // Replace only the src attribute value (leave alt untouched).
345 return preg_replace(
346 '/(src=[\'"])' . preg_quote($src, '/') . '([\'"])/i',
347 '${1}cid:' . $cid . '${2}',
348 $m[0],
349 1
350 );
351 },
352 $html
353 );
354 }
355 }
356