PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.2.0
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.2.0
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 2.10.2 All 137 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.2.0, at includes/Core/Util/MailNotifier.php

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