PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.0
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.0
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / form / class-form-email-handler.php
superb-blocks / src / gutenberg / form Last commit date
class-form-access-control.php 2 months ago class-form-captcha-handler.php 2 months ago class-form-controller.php 2 months ago class-form-email-config-check.php 2 months ago class-form-email-handler.php 2 months ago class-form-encryption.php 2 months ago class-form-exporter.php 2 months ago class-form-field-validator.php 2 months ago class-form-file-handler.php 2 months ago class-form-google-auth.php 2 months ago class-form-integration-handler.php 2 months ago class-form-math-parser.php 2 months ago class-form-permissions.php 2 months ago class-form-registry.php 2 months ago class-form-settings.php 2 months ago class-form-submission-cpt.php 2 months ago class-form-submission-handler.php 2 months ago
class-form-email-handler.php
399 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Form;
4
5 defined('ABSPATH') || exit();
6
7 class FormEmailHandler
8 {
9 /**
10 * Track the last wp_mail_failed error for status capture.
11 */
12 private static $last_mail_error = null;
13
14 /**
15 * Send admin notification email.
16 *
17 * @param array $form_data Form configuration
18 * @param array $fields Submitted field data (field_id => value)
19 * @param int $post_id Optional submission post ID for status tracking
20 * @return bool
21 */
22 public static function SendAdminNotification($form_data, $fields, $post_id = 0)
23 {
24 $to_raw = !empty($form_data['email_to']) ? $form_data['email_to'] : get_option('admin_email');
25 $to = array_filter(array_map(function ($email) {
26 return sanitize_email(trim($email));
27 }, explode(',', $to_raw)), 'is_email');
28 if (empty($to)) {
29 return false;
30 }
31 $subject = self::ProcessMergeTags(
32 !empty($form_data['email_subject']) ? $form_data['email_subject'] : __('New form submission', 'superb-blocks'),
33 $form_data,
34 $fields
35 );
36
37 $body = self::BuildEmailBody($form_data, $fields);
38 $headers = array('Content-Type: text/html; charset=UTF-8');
39
40 // From name/email: form-level -> global default -> WordPress default
41 $from_name = self::ResolveFromName($form_data);
42 $from_email = self::ResolveFromEmail($form_data);
43 if (!empty($from_email) && is_email($from_email)) {
44 $from_header = !empty($from_name) ? ($from_name . ' <' . $from_email . '>') : $from_email;
45 $headers[] = 'From: ' . $from_header;
46 }
47
48 $form_fields = isset($form_data['form_fields']) ? $form_data['form_fields'] : array();
49
50 if (!empty($form_data['email_reply_to'])) {
51 $reply_to_value = self::FindFieldValue($fields, $form_data['email_reply_to'], $form_fields);
52 if (!empty($reply_to_value) && is_email($reply_to_value)) {
53 $headers[] = 'Reply-To: ' . sanitize_email($reply_to_value);
54 }
55 }
56
57 if (!empty($form_data['email_cc'])) {
58 $cc_emails = array_map('trim', explode(',', $form_data['email_cc']));
59 foreach ($cc_emails as $cc) {
60 if (is_email($cc)) {
61 $headers[] = 'Cc: ' . sanitize_email($cc);
62 }
63 }
64 }
65
66 if (!empty($form_data['email_bcc'])) {
67 $bcc_emails = array_map('trim', explode(',', $form_data['email_bcc']));
68 foreach ($bcc_emails as $bcc) {
69 if (is_email($bcc)) {
70 $headers[] = 'Bcc: ' . sanitize_email($bcc);
71 }
72 }
73 }
74
75 self::$last_mail_error = null;
76 add_action('wp_mail_failed', array(__CLASS__, 'CaptureMailError'));
77 $result = wp_mail($to, $subject, $body, $headers);
78 remove_action('wp_mail_failed', array(__CLASS__, 'CaptureMailError'));
79
80 if ($post_id > 0) {
81 self::UpdateEmailStatus($post_id, 'admin', $result, self::$last_mail_error);
82 }
83
84 return $result;
85 }
86
87 /**
88 * Send confirmation email to the form submitter.
89 *
90 * @param array $form_data Form configuration
91 * @param array $fields Submitted field data
92 * @param int $post_id Optional submission post ID for status tracking
93 * @return bool
94 */
95 public static function SendConfirmation($form_data, $fields, $post_id = 0)
96 {
97 $form_fields = isset($form_data['form_fields']) ? $form_data['form_fields'] : array();
98
99 // Find the email field value — use configured field if set, otherwise auto-detect
100 $user_email = '';
101 if (!empty($form_data['confirmation_email_field'])) {
102 $found = self::FindFieldValue($fields, $form_data['confirmation_email_field'], $form_fields);
103 if (!empty($found) && is_email($found)) {
104 $user_email = $found;
105 }
106 }
107 // Fallback: auto-detect first email value
108 if (empty($user_email)) {
109 foreach ($fields as $field_id => $value) {
110 if (is_email($value)) {
111 $user_email = $value;
112 break;
113 }
114 }
115 }
116
117 if (empty($user_email)) {
118 return false;
119 }
120
121 $subject = self::ProcessMergeTags(
122 !empty($form_data['confirmation_subject']) ? $form_data['confirmation_subject'] : __('We received your submission', 'superb-blocks'),
123 $form_data,
124 $fields
125 );
126
127 $message = self::ProcessMergeTags(
128 !empty($form_data['confirmation_message']) ? $form_data['confirmation_message'] : __('Thank you for your submission.', 'superb-blocks'),
129 $form_data,
130 $fields,
131 true // Escape for HTML email body
132 );
133
134 $body = self::WrapInTemplate($message);
135 $headers = array('Content-Type: text/html; charset=UTF-8');
136
137 // From name/email: form-level -> global default -> WordPress default
138 $from_name = self::ResolveFromName($form_data);
139 $from_email = self::ResolveFromEmail($form_data);
140 if (!empty($from_email) && is_email($from_email)) {
141 $from_header = !empty($from_name) ? ($from_name . ' <' . $from_email . '>') : $from_email;
142 $headers[] = 'From: ' . $from_header;
143 }
144
145 self::$last_mail_error = null;
146 add_action('wp_mail_failed', array(__CLASS__, 'CaptureMailError'));
147 $result = wp_mail(sanitize_email($user_email), $subject, $body, $headers);
148 remove_action('wp_mail_failed', array(__CLASS__, 'CaptureMailError'));
149
150 if ($post_id > 0) {
151 self::UpdateEmailStatus($post_id, 'user', $result, self::$last_mail_error);
152 }
153
154 return $result;
155 }
156
157 /**
158 * Capture wp_mail_failed error for status tracking.
159 *
160 * @param \WP_Error $error
161 */
162 public static function CaptureMailError($error)
163 {
164 if (is_wp_error($error)) {
165 self::$last_mail_error = $error->get_error_message();
166 }
167 }
168
169 /**
170 * Update email delivery status meta on a submission.
171 *
172 * @param int $post_id
173 * @param string $type 'admin' or 'user'
174 * @param bool $sent
175 * @param string|null $error
176 */
177 public static function UpdateEmailStatus($post_id, $type, $sent, $error = null)
178 {
179 $status = get_post_meta($post_id, '_spb_form_email_status', true);
180 if (!is_array($status)) {
181 $status = array();
182 }
183 $status[$type] = array(
184 'sent' => (bool) $sent,
185 'time' => time(),
186 'error' => $error,
187 );
188 update_post_meta($post_id, '_spb_form_email_status', $status);
189 }
190
191 private static function BuildEmailBody($form_data, $fields)
192 {
193 $form_name = !empty($form_data['form_name']) ? esc_html($form_data['form_name']) : __('Form Submission', 'superb-blocks');
194
195 // Build fieldId => deduplicated label lookup
196 $form_fields = isset($form_data['form_fields']) ? $form_data['form_fields'] : array();
197 $label_map = array();
198 foreach (self::BuildFieldTagMap($form_fields) as $entry) {
199 $label_map[$entry['fieldId']] = $entry['label'];
200 }
201
202 // Build sensitive field lookup to skip in email body
203 $sensitive_ids = array();
204 foreach ($form_fields as $field_def) {
205 if (!empty($field_def['sensitive']) && !empty($field_def['fieldId'])) {
206 $sensitive_ids[$field_def['fieldId']] = true;
207 }
208 }
209
210 // Build signature field lookup to skip in email body
211 $signature_ids = array();
212 foreach ($form_fields as $field_def) {
213 if (isset($field_def['fieldType']) && $field_def['fieldType'] === 'signature' && !empty($field_def['fieldId'])) {
214 $signature_ids[$field_def['fieldId']] = true;
215 }
216 }
217
218 $rows = '';
219 foreach ($fields as $field_id => $value) {
220 // Skip sensitive fields
221 if (isset($sensitive_ids[$field_id])) {
222 continue;
223 }
224 // Skip signature fields (PNG data URLs are too large for email)
225 if (isset($signature_ids[$field_id])) {
226 continue;
227 }
228 $label = esc_html(isset($label_map[$field_id]) ? $label_map[$field_id] : $field_id);
229
230 // File fields store an array of file metadata
231 if (is_array($value)) {
232 $names = array();
233 foreach ($value as $file) {
234 if (is_array($file) && isset($file['name'])) {
235 $names[] = esc_html($file['name']);
236 }
237 }
238 $val = implode(', ', array_filter($names));
239 if (empty($val)) {
240 continue;
241 }
242 } else {
243 $val = nl2br(esc_html($value));
244 }
245
246 $rows .= '<tr><td style="padding:8px 12px;border-bottom:1px solid #eee;font-weight:500;vertical-align:top;width:30%;">' . $label . '</td>';
247 $rows .= '<td style="padding:8px 12px;border-bottom:1px solid #eee;">' . $val . '</td></tr>';
248 }
249
250 $content = '<h2 style="margin:0 0 16px;font-size:18px;">' . $form_name . '</h2>';
251 $content .= '<table style="width:100%;border-collapse:collapse;font-size:14px;">' . $rows . '</table>';
252
253 return self::WrapInTemplate($content);
254 }
255
256 private static function WrapInTemplate($content)
257 {
258 $site_name = esc_html(get_bloginfo('name'));
259 /* translators: %s: site name */
260 $footer_text = sprintf(__('Sent from %s', 'superb-blocks'), $site_name);
261 return '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="margin:0;padding:20px;background:#f5f5f5;font-family:-apple-system,BlinkMacSystemFont,sans-serif;">'
262 . '<div style="max-width:600px;margin:0 auto;background:#fff;border-radius:4px;padding:24px;border:1px solid #e0e0e0;">'
263 . $content
264 . '<p style="margin:24px 0 0;font-size:12px;color:#999;">' . $footer_text . '</p>'
265 . '</div></body></html>';
266 }
267
268 /**
269 * Build a deduplicated tag map from form field definitions.
270 * Mirrors the JS buildFieldTagMap — duplicate labels get numeric suffixes.
271 *
272 * @param array $form_fields Array of field attribute arrays.
273 * @return array Array of [ 'fieldId' => string, 'label' => string ]
274 */
275 private static function BuildFieldTagMap($form_fields)
276 {
277 $counts = array();
278 $result = array();
279 foreach ($form_fields as $field_def) {
280 $fid = isset($field_def['fieldId']) ? $field_def['fieldId'] : '';
281 if ($fid === '') {
282 continue;
283 }
284 $field_type = isset($field_def['fieldType']) ? $field_def['fieldType'] : '';
285 if ($field_type === 'hidden' || $field_type === 'signature') {
286 continue;
287 }
288 if (!empty($field_def['sensitive'])) {
289 continue;
290 }
291 $base_label = isset($field_def['label']) && $field_def['label'] !== ''
292 ? $field_def['label']
293 : ($field_type !== '' ? 'Unlabeled ' . ucfirst($field_type) . ' Field' : 'Unlabeled Field');
294 $counts[$base_label] = isset($counts[$base_label]) ? $counts[$base_label] + 1 : 1;
295 $n = $counts[$base_label];
296 $label = $n > 1 ? $base_label . ' ' . $n : $base_label;
297 $result[] = array('fieldId' => $fid, 'label' => $label);
298 }
299 return $result;
300 }
301
302 /**
303 * @param string $text Text with merge tags
304 * @param array $form_data Form configuration
305 * @param array $fields Submitted field data
306 * @param bool $escape_html Whether to escape values for HTML context
307 */
308 private static function ProcessMergeTags($text, $form_data, $fields, $escape_html = false)
309 {
310 $form_name = isset($form_data['form_name']) ? $form_data['form_name'] : '';
311 $site_name = get_bloginfo('name');
312 if ($escape_html) {
313 $form_name = esc_html($form_name);
314 $site_name = esc_html($site_name);
315 }
316 $text = str_replace('{form_name}', $form_name, $text);
317 $text = str_replace('{site_name}', $site_name, $text);
318
319 $form_fields = isset($form_data['form_fields']) ? $form_data['form_fields'] : array();
320 $tag_map = self::BuildFieldTagMap($form_fields);
321 foreach ($tag_map as $entry) {
322 if (isset($fields[$entry['fieldId']])) {
323 $val = $fields[$entry['fieldId']];
324 // Skip non-string values (e.g. file metadata arrays)
325 if (!is_string($val)) {
326 continue;
327 }
328 if ($escape_html) {
329 $val = esc_html($val);
330 }
331 $text = str_replace('{' . $entry['label'] . '}', $val, $text);
332 }
333 }
334
335 return $text;
336 }
337
338 /**
339 * Resolve the "From" name: form-level -> global default -> site name.
340 *
341 * @param array $form_data
342 * @return string
343 */
344 private static function ResolveFromName($form_data)
345 {
346 // 1. Form-level setting
347 if (!empty($form_data['email_from_name'])) {
348 return sanitize_text_field($form_data['email_from_name']);
349 }
350 // 2. Global default setting
351 $defaults = get_option('superbaddons_form_default_email', array());
352 if (is_array($defaults) && !empty($defaults['from_name'])) {
353 return sanitize_text_field($defaults['from_name']);
354 }
355 // 3. WordPress default
356 return get_bloginfo('name');
357 }
358
359 /**
360 * Resolve the "From" email: form-level -> global default -> admin email.
361 *
362 * @param array $form_data
363 * @return string
364 */
365 private static function ResolveFromEmail($form_data)
366 {
367 // 1. Form-level setting
368 if (!empty($form_data['email_from_address'])) {
369 return sanitize_email($form_data['email_from_address']);
370 }
371 // 2. Global default setting
372 $defaults = get_option('superbaddons_form_default_email', array());
373 if (is_array($defaults) && !empty($defaults['from_email'])) {
374 return sanitize_email($defaults['from_email']);
375 }
376 // 3. WordPress default
377 return get_option('admin_email');
378 }
379
380 /**
381 * Resolve a deduplicated label to its submitted value.
382 */
383 private static function FindFieldValue($fields, $field_name, $form_fields = array())
384 {
385 // First try direct match (fieldId as key)
386 if (isset($fields[$field_name])) {
387 return $fields[$field_name];
388 }
389 // Resolve deduplicated label to fieldId via tag map
390 $tag_map = self::BuildFieldTagMap($form_fields);
391 foreach ($tag_map as $entry) {
392 if ($entry['label'] === $field_name && isset($fields[$entry['fieldId']])) {
393 return $fields[$entry['fieldId']];
394 }
395 }
396 return '';
397 }
398 }
399