| @@ -6,12 +6,11 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | namespace SRFM\Inc\Email; |
| 9 | 9 | |
| 10 | +use SRFM\Inc\Helper; | |
| 10 | 11 | use SRFM\Inc\Traits\Get_Instance; |
| 11 | -use SRFM\Inc\Helper; | |
| 12 | 12 | |
| 13 | - | |
| 14 | 13 | if ( ! defined( 'ABSPATH' ) ) { |
| 15 | 14 | exit; // Exit if accessed directly. |
| 16 | 15 | } |
| 17 | 16 | |
| @@ -20,9 +19,8 @@ | ||
| 20 | 19 | * |
| 21 | 20 | * @since 0.0.1 |
| 22 | 21 | */ |
| 23 | 22 | class Email_Template { |
| 24 | - | |
| 25 | 23 | use Get_Instance; |
| 26 | 24 | |
| 27 | 25 | /** |
| 28 | 26 | * Class Constructor |
| @@ -30,9 +28,8 @@ | ||
| 30 | 28 | * @since 0.0.1 |
| 31 | 29 | * @return void |
| 32 | 30 | */ |
| 33 | 31 | public function __construct() { |
| 34 | - | |
| 35 | 32 | } |
| 36 | 33 | |
| 37 | 34 | /** |
| 38 | 35 | * Get email header. |
| @@ -69,9 +66,9 @@ | ||
| 69 | 66 | <table border="0" cellpadding="20" cellspacing="0" width="100%"> |
| 70 | 67 | <tbody> |
| 71 | 68 | <tr> |
| 72 | 69 | <td valign="top" style="padding:32px"> |
| 73 | - <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: 1;text-align: left;"> | |
| 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;"> | |
| 74 | 71 | <?php |
| 75 | 72 | return ob_get_clean(); |
| 76 | 73 | } |
| 77 | 74 | |
| @@ -81,10 +78,8 @@ | ||
| 81 | 78 | * @since 0.0.1 |
| 82 | 79 | * @return string|false footer tags. |
| 83 | 80 | */ |
| 84 | 81 | public function get_footer() { |
| 85 | - // Translators: Site URL. | |
| 86 | - $site_link = sprintf( __( '<a href=%1$s>%2$s</a>', 'sureforms' ), home_url( '/' ), get_bloginfo( 'name' ) ); | |
| 87 | 82 | ob_start(); |
| 88 | 83 | ?> |
| 89 | 84 | </div> |
| 90 | 85 | </td> |
| @@ -119,53 +114,308 @@ | ||
| 119 | 114 | * @since 0.0.1 |
| 120 | 115 | * @return string |
| 121 | 116 | */ |
| 122 | 117 | public function render( $fields, $email_body ) { |
| 123 | - $message = $this->get_header(); | |
| 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 | + | |
| 124 | 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;'; | |
| 125 | 210 | |
| 126 | - $message .= $email_body; | |
| 127 | - if ( strpos( $email_body, '{all_data}' ) !== false ) { | |
| 211 | + ob_start(); | |
| 128 | 212 | |
| 129 | - ob_start(); | |
| 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 | + } | |
| 130 | 232 | |
| 131 | - ?> | |
| 132 | - <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;"> | |
| 133 | - <tbody> | |
| 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;"> | |
| 134 | 328 | <?php |
| 135 | - foreach ( $fields as $field_name => $value ) { | |
| 136 | - if ( is_array( $value ) ) { | |
| 137 | - $values_array = $value; | |
| 138 | - } else { | |
| 329 | + if ( ! empty( $values_array ) && is_array( $values_array ) ) { | |
| 330 | + $clean_values = []; | |
| 331 | + | |
| 332 | + foreach ( $values_array as $value ) { | |
| 139 | 333 | $value = Helper::get_string_value( $value ); |
| 334 | + if ( ! empty( $value ) && is_string( $value ) ) { | |
| 335 | + $clean_values[] = $value; | |
| 336 | + } | |
| 140 | 337 | } |
| 141 | - if ( in_array( $field_name, $excluded_fields, true ) || false === str_contains( $field_name, '-lbl-' ) ) { | |
| 142 | - continue; | |
| 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 | |
| 143 | 360 | } |
| 144 | - | |
| 145 | - $label = explode( '-lbl-', $field_name )[1]; | |
| 146 | - $label = explode( '-', $label )[0]; | |
| 147 | - $field_label = $label ? esc_html( Helper::decrypt( $label ) ) : ''; | |
| 361 | + } elseif ( is_string( $value ) && filter_var( $value, FILTER_VALIDATE_URL ) ) { | |
| 362 | + ob_start(); | |
| 148 | 363 | ?> |
| 149 | - <tr class="field-label"> | |
| 150 | - <th style="font-weight: 500;font-size: 14px;color: #1E293B;padding: 8px 16px;background-color: #F1F5F9;text-align: left;"> | |
| 151 | - <strong><?php echo esc_html( $field_label ); ?><?php echo esc_html__( ':', 'sureforms' ); ?><strong/> | |
| 152 | - </th> | |
| 153 | - </tr> | |
| 154 | - <tr class="field-value"> | |
| 155 | - <td style="font-size: 14px;color: #475569;padding: 8px 16px 16px 16px;padding-bottom: 10px;"> | |
| 364 | + <a target="_blank" href="<?php echo esc_url( $value ); ?>"> | |
| 365 | + <?php echo esc_html( esc_url( $value ) ); ?> | |
| 366 | + </a> | |
| 156 | 367 | <?php |
| 157 | - if ( ! empty( $values_array ) && is_array( $values_array ) ) { | |
| 158 | - foreach ( $values_array as $value ) { | |
| 159 | - $value = Helper::get_string_value( $value ); | |
| 160 | - if ( ! empty( $value ) && is_string( $value ) ) { | |
| 161 | - ?> | |
| 162 | - <a target="_blank" href="<?php echo esc_attr( urldecode( $value ) ); ?>"><?php echo esc_html__( 'View', 'sureforms' ); ?></a> | |
| 163 | - <?php | |
| 164 | - } | |
| 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>'; | |
| 165 | 410 | } |
| 166 | - } else { | |
| 167 | - if ( is_string( $value ) ) { | |
| 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 { | |
| 168 | 418 | echo false !== strpos( $value, PHP_EOL ) ? wp_kses_post( wpautop( $value ) ) : wp_kses( |
| 169 | 419 | $value, |
| 170 | 420 | [ |
| 171 | 421 | 'a' => [ |
| @@ -175,20 +425,19 @@ | ||
| 175 | 425 | ] |
| 176 | 426 | ); |
| 177 | 427 | } |
| 178 | 428 | } |
| 179 | - ?> | |
| 180 | - </td> | |
| 181 | - </tr> | |
| 182 | - <?php } ?> | |
| 183 | - </tbody> | |
| 184 | - </table> | |
| 185 | - <?php | |
| 186 | - $table_data = ob_get_clean(); | |
| 187 | - $current_table_data = $table_data ? $table_data : ''; // This is done as str_replace expects array|string but ob_get_clean() returns string|false. | |
| 188 | - $message = str_replace( '{all_data}', $current_table_data, $message ); | |
| 189 | - } | |
| 190 | - $message .= $this->get_footer(); | |
| 191 | - return $message; | |
| 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 ); | |
| 192 | 442 | } |
| 193 | - | |
| 194 | 443 | } |