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
← All changes | includes/Core/Util/MailNotifier.php +276 -44 2.03.3.1 View file →
@@ -1,27 +1,112 @@
1 1 <?php
2 2
3 3 namespace BitCode\BitForm\Core\Util;
4 4
5 +if (!defined('ABSPATH')) {
6 + exit;
7 +}
8 +
9 +use BitCode\BitForm\Admin\Form\Helpers;
5 10 use BitCode\BitForm\Core\Database\FormEntryMetaModel;
11 +use BitCode\BitForm\Core\Form\FormManager;
6 12 use BitCode\BitForm\Core\Messages\EmailTemplateHandler;
7 13
8 -final class MailNotifier {
9 - public static function notify($notifyDetails, $formID, $fieldValue, $entryID, $isDblOptin = false, $logId = '') {
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];
10 21 $emailTemplateHandler = new EmailTemplateHandler($formID);
22 + $attachments = [];
23 + $tempPdfLinks = [];
11 24 if (is_string($notifyDetails->id)) {
12 - $mailTemplateID = json_decode($notifyDetails->id)->id;
25 + $mailTemplateID = Utilities::jsonObj($notifyDetails->id)->id ?? null;
13 26 $mailTemplate = $emailTemplateHandler->getATemplate($mailTemplateID);
14 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 + }
15 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);
16 35 if (!empty($mailTo)) {
17 - (new MailConfig())->sendMail();
18 - $mailSubject = FieldValueHandler::replaceFieldWithValue($mailTemplate[0]->sub, $fieldValue);
19 - $mailBody = FieldValueHandler::replaceFieldWithValue($mailTemplate[0]->body, $fieldValue);
20 -
36 + $from_name = '';
37 + if (isset($notifyDetails->from_name) && !empty($notifyDetails->from_name)) {
38 + $from_name = $notifyDetails->from_name;
39 + }
21 40 $mailHeaders = [
22 - // "Content-Type: text/html; charset=UTF-8",
41 + // 'Content-Type: text/html; charset=UTF-8',
42 + // $embeddedMailHeader
23 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 + );
24 109 if (!empty($notifyDetails->replyto)) {
25 110 $mailReplyTo = FieldValueHandler::validateMailArry($notifyDetails->replyto, $fieldValue);
26 111 if (is_array($mailReplyTo)) {
27 112 foreach ($mailReplyTo as $key => $emailAddress) {
@@ -32,11 +117,11 @@
32 117 }
33 118 }
34 119 $oldMailBody = $mailBody;
35 120 $data = [];
36 - if ($isDblOptin && true === has_filter('bf_email_body_text')) {
121 + if ($isDblOptin && true === has_filter('bitform_email_body_text')) {
37 122 $urlParams = $formID . '_' . $entryID . '_' . $logId;
38 - $data = apply_filters('bf_email_body_text', $mailBody, $urlParams);
123 + $data = apply_filters('bitform_email_body_text', $mailBody, $urlParams);
39 124 $mailBody = $data['mailbody'];
40 125 }
41 126
42 127 if (!empty($notifyDetails->bcc)) {
@@ -58,66 +143,213 @@
58 143 } else {
59 144 $mailHeaders[] = 'Cc: ' . sanitize_email($mailCC);
60 145 }
61 146 }
62 - if (!empty($notifyDetails->from)) {
63 - $mailFrom = FieldValueHandler::validateMailArry($notifyDetails->from, $fieldValue);
64 - $fromName = !empty($notifyDetails->fromName) ? $notifyDetails->fromName : explode('@', $mailFrom[0])[0];
65 - $mailHeaders[] = "FROM: $fromName " . '<' . sanitize_email($mailFrom[0]) . '>';
66 - }
67 - $attachments = [];
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);
68 158 if (!empty($notifyDetails->attachment)) {
69 - $files = $notifyDetails->attachment;
70 - $fileBasePath = BITFORMS_UPLOAD_DIR . DIRECTORY_SEPARATOR . $formID . DIRECTORY_SEPARATOR . $entryID . DIRECTORY_SEPARATOR;
71 - if (is_array($files)) {
72 - foreach ($files as $file) {
73 - if (isset($fieldValue[$file])) {
74 - if (is_array($fieldValue[$file])) {
75 - foreach ($fieldValue[$file] as $singleFile) {
76 - if (\is_readable("{$fileBasePath}{$singleFile}")) {
77 - $attachments[] = "{$fileBasePath}{$singleFile}";
78 - }
79 - }
80 - } elseif (\is_readable("{$fileBasePath}{$fieldValue[$file]}")) {
81 - $attachments[] = "{$fileBasePath}{$fieldValue[$file]}";
82 - }
83 - }
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 + );
84 185 }
85 - } elseif (isset($fieldValue[$files])) {
86 - if (is_array($fieldValue[$files])) {
87 - foreach ($fieldValue[$files] as $singleFile) {
88 - if (\is_readable("{$fileBasePath}{$singleFile}")) {
89 - $attachments[] = "{$fileBasePath}{$singleFile}";
90 - }
91 - }
92 - } elseif (\is_readable("{$fileBasePath}{$fieldValue[$files]}")) {
93 - $attachments[] = "{$fileBasePath}{$fieldValue[$files]}";
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;
94 195 }
95 196 }
96 197 }
97 198 $mailBody = stripcslashes($mailBody);
98 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 + }
99 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 + }
100 220 $status = wp_mail($mailTo, $mailSubject, $mailBody, $mailHeaders, $attachments);
221 +
101 222 if (!$status) {
102 - $status = wp_mail($mailTo, $mailSubject, $mailBody, $mailHeaders);
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);
103 262 }
104 263 if ($status && $isDblOptin && false !== strpos($oldMailBody, 'entry_confirmation_url')) {
105 264 $entryMeta = new FormEntryMetaModel();
106 -
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.
107 267 $entryMeta->insert(
108 268 [
109 269 'bitforms_form_entry_id' => $entryID,
110 270 'meta_key' => 'entry_confirm_activation',
111 - 'meta_value' => $data['token']]
271 + 'meta_value' => $data['token']
272 + ]
112 273 );
113 274 }
114 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 + }
115 282 }
116 283 }
117 284 }
285 +
286 + foreach ($tempPdfLinks as $tempPdfLink) {
287 + wp_delete_file($tempPdfLink);
288 + }
118 289 }
119 290
120 - public static function filterMailContentType() {
291 + public static function filterMailContentType()
292 + {
121 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 + );
122 354 }
123 355 }