PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.3
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.3
2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / email / email-template.php
sureforms / inc / email Last commit date
email-template.php 3 weeks ago
email-template.php
444 lines
1 <?php
2 /**
3 * Email template loader.
4 *
5 * @package SureForms.
6 */
7
8 namespace SRFM\Inc\Email;
9
10 use SRFM\Inc\Helper;
11 use SRFM\Inc\Traits\Get_Instance;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit; // Exit if accessed directly.
15 }
16
17 /**
18 * Email Class
19 *
20 * @since 0.0.1
21 */
22 class Email_Template {
23 use Get_Instance;
24
25 /**
26 * Class Constructor
27 *
28 * @since 0.0.1
29 * @return void
30 */
31 public function __construct() {
32 }
33
34 /**
35 * Get email header.
36 *
37 * @since 0.0.1
38 * @return string|false
39 */
40 public function get_header() {
41 ob_start(); ?>
42 <html>
43
44 <head>
45 <meta charset="utf-8">
46 <title><?php echo esc_html__( 'New form submission', 'sureforms' ); ?></title>
47 </head>
48
49 <body style="margin: 0; padding: 0;">
50 <div id="srfm_wrapper" dir="ltr" style="margin: 0; background-color: #F8F8FC; padding: 40px 0 0 0; width: 100%">
51 <table border="0" cellpadding="0" cellspacing="0" width="100%">
52 <tbody>
53 <tr>
54 <td align="center" valign="top">
55 <table border="0" cellpadding="0" cellspacing="0" width="600" id="srfm_template_container" style="background-color: #ffffff;border: 1px solid #dce0e6;margin-bottom: 25px;
56 ">
57 <tbody>
58 <tr>
59 <td align="center" valign="top">
60 <table border="0" cellpadding="0" cellspacing="0" width="600"
61 id="srfm_template_body">
62 <tbody>
63 <tr>
64 <td valign="top" id="srfm_body_content"
65 style="background-color: #ffffff">
66 <table border="0" cellpadding="20" cellspacing="0" width="100%">
67 <tbody>
68 <tr>
69 <td valign="top" style="padding:32px">
70 <div id="srfm_body_content_inner" style="color: #384860;font-family: Roboto-Medium,Roboto,-apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,Arial,sans-serif;font-size: 14px;line-height: 20px;text-align: left;">
71 <?php
72 return ob_get_clean();
73 }
74
75 /**
76 * Get email footer.
77 *
78 * @since 0.0.1
79 * @return string|false footer tags.
80 */
81 public function get_footer() {
82 ob_start();
83 ?>
84 </div>
85 </td>
86 </tr>
87 </tbody>
88 </table>
89 </td>
90 </tr>
91 </tbody>
92 </table>
93 </td>
94 </tr>
95 </tbody>
96 </table>
97 </td>
98 </tr>
99 </tbody>
100 </table>
101 </div>
102 </body>
103 </html>
104 <?php
105
106 return ob_get_clean();
107 }
108
109 /**
110 * Render email template.
111 *
112 * @param array<mixed> $fields Submission fields.
113 * @param string $email_body email body.
114 * @since 0.0.1
115 * @return string
116 */
117 public function render( $fields, $email_body ) {
118 $message = $this->get_header();
119 $message .= $this->process_all_data_tag( $fields, $email_body );
120 return $message . $this->get_footer();
121 }
122
123 /**
124 * Render email as raw HTML with no template wrapping.
125 *
126 * Returns the email body exactly as written with only smart tag
127 * processing applied. No wrapper, no container, no layout, no
128 * styles, no footer — true raw HTML output.
129 *
130 * @param array<mixed> $fields Submission fields.
131 * @param string $email_body Email body content.
132 * @since 2.5.2
133 * @return string The raw email body with smart tags processed.
134 */
135 public function render_raw( $fields, $email_body ) {
136 return $this->process_all_data_tag( $fields, $email_body );
137 }
138
139 /**
140 * Remove border from the last table row in repeater table HTML.
141 *
142 * This method finds the last <tr> element in the provided HTML content
143 * and removes the border-bottom style from all <td> elements within it.
144 * This is used to clean up the visual appearance of repeater tables.
145 *
146 * @param string $content HTML content containing table structure.
147 *
148 * @since 1.11.0
149 * @return string Modified HTML content with border removed from last row.
150 */
151 public function remove_border_from_last_tr_td_table( $content ) {
152 // Check if content contains table and tr elements.
153 if ( empty( $content ) || ! preg_match( '/<tr[^>]*>/i', $content ) ) {
154 return $content;
155 }
156
157 // Find and modify the last tr in one go.
158 $modified_html = preg_replace_callback(
159 '/(.*)(<tr[^>]*>.*?<\/tr>)(?!.*<tr)/is',
160 static function( $matches ) {
161 $before_last_tr = $matches[1];
162 $last_tr = $matches[2];
163
164 // Remove border-bottom from all td elements in this tr.
165 $modified_tr = preg_replace_callback(
166 '/(<td[^>]*style\s*=\s*["\'])([^"\']*?)(["\'][^>]*>)/i',
167 static function( $td_match ) {
168 $start = $td_match[1];
169 $style = $td_match[2];
170
171 // Remove ONLY border-bottom (not border-bottom-width, etc.).
172 $style = preg_replace( '/\s*border-bottom\s*:[^;]*;?/i', '', $style );
173 $style = is_string( $style ) ? $style : '';
174
175 // Clean up multiple semicolons and trim.
176 $style = preg_replace( '/;+/', ';', $style );
177 $style = is_string( $style ) ? trim( $style, '; ' ) : '';
178
179 $end = $td_match[3];
180 return $start . $style . $end;
181 },
182 $last_tr
183 );
184
185 return $before_last_tr . $modified_tr;
186 },
187 $content
188 );
189
190 return is_string( $modified_html ) ? $modified_html : '';
191 }
192
193 /**
194 * Process the {all_data} smart tag in the email body.
195 *
196 * Replaces {all_data} with a formatted HTML table of all submission fields.
197 *
198 * @param array<mixed> $fields Submission fields.
199 * @param string $email_body Email body content.
200 * @since 2.5.2
201 * @return string Email body with {all_data} replaced.
202 */
203 private function process_all_data_tag( $fields, $email_body ) {
204 if ( strpos( $email_body, '{all_data}' ) === false ) {
205 return $email_body;
206 }
207
208 $excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field' ];
209 $td_style = 'font-weight: 500;font-size: 14px;line-height: 20px;padding: 12px;text-align:left;word-break: break-word;border-bottom: 1px solid #E5E7EB;';
210
211 ob_start();
212
213 ?>
214 <table class="srfm_all_data" width="536" cellpadding="0" cellspacing="0" style="border: 1px solid #dce0e6;border-radius: 6px;margin-top: 25px;margin-bottom: 25px;overflow:hidden;">
215 <style>
216 .srfm_all_data tr:last-child td {
217 border: none !important;
218 }
219 </style>
220 <tbody>
221 <?php
222 foreach ( $fields as $field_name => $value ) {
223 $values_array = [];
224 if ( is_array( $value ) ) {
225 $values_array = $value;
226 } else {
227 $value = Helper::get_string_value( $value );
228 }
229 if ( in_array( $field_name, $excluded_fields, true ) || false === str_contains( $field_name, '-lbl-' ) ) {
230 continue;
231 }
232
233 $label = explode( '-lbl-', $field_name )[1];
234 $label = explode( '-', $label )[0];
235 $field_label = $label ? Helper::decode( $label ) : '';
236
237 $field_block_name = Helper::get_block_name_from_field( $field_name );
238
239 /**
240 * Fires before rendering a field in the all data section of emails.
241 *
242 * This action allows other packages (like Pro, Business) to process and render fields
243 * with custom data structures that the core plugin cannot handle. Since the core plugin
244 * does not know the structure of data from other packages, this action provides a way
245 * for those packages to properly process and display their field data.
246 *
247 * @since 1.11.0
248 *
249 * @param array $field_data Field data containing:
250 * 'value' => mixed The field value
251 * 'label' => string The field name/key
252 * 'block_name' => string The block type identifier
253 * 'processed_label' => string The human readable label, base64-decoded
254 * out of the submitted field key. Submitter-
255 * controlled and unauthenticated — escape it
256 * for the output context (esc_html() for HTML).
257 */
258 do_action(
259 'srfm_before_processing_all_data_field',
260 [
261 'value' => $value,
262 'label' => $field_name,
263 'block_name' => $field_block_name,
264 'processed_label' => $field_label,
265 ]
266 );
267
268 /**
269 * Filters whether to add a field row in the all data section.
270 *
271 * This filter allows skipping rows for fields that cannot be processed with the
272 * core plugin's structure. Fields from other packages may have complex data structures
273 * that could cause fatal errors if processed normally. Those packages can use the
274 * 'srfm_before_processing_all_data_field' action to render their fields and return false here
275 * to prevent the core plugin from attempting to process them.
276 *
277 * @since 1.11.0
278 *
279 * @param bool $should_add_field_row Whether to add the field row. Default true.
280 * @param array $field_data Field data containing:
281 * 'value' => mixed The field value
282 * 'field_name' => string The field name/key
283 * 'block_name' => string The block type identifier
284 *
285 * @return bool Whether to add the field row to the table.
286 */
287 $should_add_field_row = apply_filters(
288 'srfm_all_data_field_row',
289 true,
290 [
291 'value' => $value,
292 'field_name' => $field_name,
293 'block_name' => $field_block_name,
294 ]
295 );
296
297 if ( true !== $should_add_field_row ) {
298 continue;
299 }
300
301 $is_array_value = is_array( $value );
302
303 if ( $is_array_value ) {
304 $values_array = array_filter(
305 $value,
306 static function( $input_value ) {
307 return ! empty( Helper::get_string_value( $input_value ) );
308 }
309 );
310 } else {
311 $value = Helper::get_string_value( $value );
312 }
313
314 // Skip if both $value and $values_array are empty.
315 if ( empty( $value ) || ( $is_array_value && empty( $values_array ) ) ) {
316 continue;
317 }
318
319 ?>
320 <tr class="field-label">
321 <th style="<?php echo esc_attr( $td_style ); ?>color: #1E293B;background-color: #F1F5F9;">
322 <?php // The label is decoded from the submitted field key, so it is attacker-controllable — escape it as text, never as markup. ?>
323 <strong><?php echo esc_html( html_entity_decode( $field_label ) ); ?>:</strong>
324 </th>
325 </tr>
326 <tr class="field-value">
327 <td style="<?php echo esc_attr( $td_style ); ?>color: #475569;">
328 <?php
329 if ( ! empty( $values_array ) && is_array( $values_array ) ) {
330 $clean_values = [];
331
332 foreach ( $values_array as $value ) {
333 $value = Helper::get_string_value( $value );
334 if ( ! empty( $value ) && is_string( $value ) ) {
335 $clean_values[] = $value;
336 }
337 }
338
339 if ( count( $clean_values ) === 1 ) {
340 $value = reset( $clean_values );
341 $decoded_value = urldecode( $value );
342 ?>
343 <a target="_blank" rel="noopener noreferrer" href="<?php echo esc_url( $decoded_value ); ?>">
344 <?php echo esc_html( $decoded_value ); ?>
345 </a>
346 <?php
347 } elseif ( count( $clean_values ) > 1 ) {
348 ?>
349 <ol style="list-style: decimal; padding-left: 20px; margin: 0;">
350 <?php foreach ( $clean_values as $value ) { ?>
351 <?php $decoded_value = urldecode( $value ); ?>
352 <li style="margin-bottom: 6px;">
353 <a target="_blank" rel="noopener noreferrer" href="<?php echo esc_url( $decoded_value ); ?>">
354 <?php echo esc_html( $decoded_value ); ?>
355 </a>
356 </li>
357 <?php } ?>
358 </ol>
359 <?php
360 }
361 } elseif ( is_string( $value ) && filter_var( $value, FILTER_VALIDATE_URL ) ) {
362 ob_start();
363 ?>
364 <a target="_blank" href="<?php echo esc_url( $value ); ?>">
365 <?php echo esc_html( esc_url( $value ) ); ?>
366 </a>
367 <?php
368 $template_html = ob_get_clean();
369 // Apply filter.
370 $render_url = apply_filters(
371 'srfm_email_template_render_url',
372 $template_html,
373 [
374 'block_type' => $field_name,
375 'submission_item_value' => $value,
376 ]
377 );
378 // Validate fallback.
379 if ( empty( $render_url ) ) {
380 ob_start();
381 ?>
382 <a target="_blank" href="<?php echo esc_url( $value ); ?>">
383 <?php echo esc_html( esc_url( $value ) ); ?>
384 </a>
385 <?php
386 $render_url = ob_get_clean();
387 }
388
389 echo wp_kses(
390 Helper::get_string_value( $render_url ),
391 [
392 'a' => [
393 'href' => [],
394 'target' => [],
395 ],
396 'img' => [
397 'src' => [],
398 'alt' => [],
399 'width' => [],
400 ],
401 ]
402 );
403
404 } else {
405 if ( strpos( Helper::get_string_value( $field_name ), 'srfm-input-multi-choice' ) !== false && strpos( Helper::get_string_value( $value ), '|' ) !== false ) {
406 $options = array_map( 'trim', explode( '|', Helper::get_string_value( $value ) ) );
407 foreach ( $options as $index => $option ) {
408 $border_style = $index < count( $options ) - 1 ? 'border-bottom:1px solid #E5E7EB;padding-bottom:4px;margin-bottom:4px;' : '';
409 echo '<div style="' . esc_attr( $border_style ) . '">' . esc_html( $option ) . '</div>';
410 }
411 continue;
412 }
413
414 if ( is_string( $value ) ) {
415 if ( false !== strpos( $field_name, 'srfm-textarea' ) ) {
416 echo Helper::esc_textarea( html_entity_decode( $value ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- using a custom escaping function.
417 } else {
418 echo false !== strpos( $value, PHP_EOL ) ? wp_kses_post( wpautop( $value ) ) : wp_kses(
419 $value,
420 [
421 'a' => [
422 'href' => [],
423 'target' => [],
424 ],
425 ]
426 );
427 }
428 }
429 }
430 ?>
431 </td>
432 </tr>
433 <?php } ?>
434 </tbody>
435 </table>
436 <?php
437 $table_data = ob_get_clean();
438 $current_table_data = $table_data ? $table_data : ''; // This is done as str_replace expects array|string but ob_get_clean() returns string|false.
439 $current_table_data = $this->remove_border_from_last_tr_td_table( $current_table_data );
440
441 return str_replace( '{all_data}', $current_table_data, $email_body );
442 }
443 }
444