PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / trunk
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification vtrunk
5.8.0 5.8.1 5.7.0 5.6.2 5.6.3 5.6.1 5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 All 41 releases
double-opt-in / src / EmailTemplates / EmailHtmlGenerator.php

EmailHtmlGenerator.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification trunk, at src/EmailTemplates/EmailHtmlGenerator.php

712 lines 22.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Email HTML Generator
4 *
5 * @package Forge12\DoubleOptIn\EmailTemplates
6 * @since 4.0.0
7 */
8
9 namespace Forge12\DoubleOptIn\EmailTemplates;
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class EmailHtmlGenerator
17 *
18 * Converts block structure to email-compatible HTML.
19 */
20 class EmailHtmlGenerator {
21
22 /**
23 * Default global styles.
24 *
25 * @var array
26 */
27 private array $defaultStyles = array(
28 'fontFamily' => 'Arial, Helvetica, sans-serif',
29 'fontSize' => '16px',
30 'lineHeight' => '1.5',
31 'backgroundColor' => '#f4f4f4',
32 'contentWidth' => '600',
33 'primaryColor' => '#0073aa',
34 'textColor' => '#333333',
35 'linkColor' => '#0073aa',
36 );
37
38 /**
39 * Current global styles.
40 *
41 * @var array
42 */
43 private array $globalStyles = array();
44
45 /**
46 * Generate email HTML from blocks.
47 *
48 * @param array $blocks Array of block data.
49 * @param array $globalStyles Optional. Global styles.
50 * @return string Generated HTML.
51 */
52 public function generate( array $blocks, array $globalStyles = array() ): string {
53 $this->globalStyles = wp_parse_args( $globalStyles, $this->defaultStyles );
54
55 $contentWidth = (int) $this->globalStyles['contentWidth'];
56 $backgroundColor = esc_attr( $this->globalStyles['backgroundColor'] );
57 $fontFamily = esc_attr( $this->globalStyles['fontFamily'] );
58 $textColor = esc_attr( $this->globalStyles['textColor'] );
59
60 $blocksHtml = $this->renderBlocks( $blocks );
61
62 return <<<HTML
63 <!DOCTYPE html>
64 <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
65 <head>
66 <meta charset="UTF-8">
67 <meta name="viewport" content="width=device-width, initial-scale=1.0">
68 <meta http-equiv="X-UA-Compatible" content="IE=edge">
69 <meta name="x-apple-disable-message-reformatting">
70 <title></title>
71 <!--[if mso]>
72 <noscript>
73 <xml>
74 <o:OfficeDocumentSettings>
75 <o:PixelsPerInch>96</o:PixelsPerInch>
76 </o:OfficeDocumentSettings>
77 </xml>
78 </noscript>
79 <![endif]-->
80 <style type="text/css">
81 body { margin: 0; padding: 0; width: 100% !important; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
82 table { border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
83 img { border: 0; outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; }
84 a { text-decoration: none; }
85 @media only screen and (max-width: 620px) {
86 .container { width: 100% !important; }
87 .content { padding: 10px !important; }
88 .column { width: 100% !important; display: block !important; }
89 }
90 </style>
91 </head>
92 <body style="margin: 0; padding: 0; background-color: {$backgroundColor}; font-family: {$fontFamily}; color: {$textColor};">
93 <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: {$backgroundColor};">
94 <tr>
95 <td align="center" style="padding: 20px 0;">
96 <!--[if mso]>
97 <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="{$contentWidth}">
98 <tr>
99 <td>
100 <![endif]-->
101 <table role="presentation" class="container" border="0" cellpadding="0" cellspacing="0" width="{$contentWidth}" style="max-width: {$contentWidth}px; width: 100%;">
102 {$blocksHtml}
103 </table>
104 <!--[if mso]>
105 </td>
106 </tr>
107 </table>
108 <![endif]-->
109 </td>
110 </tr>
111 </table>
112 </body>
113 </html>
114 HTML;
115 }
116
117 /**
118 * Render all blocks.
119 *
120 * @param array $blocks Array of blocks.
121 * @param string $parentBgColor Parent background color for inheritance.
122 * @return string Rendered HTML.
123 */
124 private function renderBlocks( array $blocks, string $parentBgColor = '' ): string {
125 $html = '';
126
127 foreach ( $blocks as $block ) {
128 $html .= $this->renderBlock( $block, $parentBgColor );
129 }
130
131 return $html;
132 }
133
134 /**
135 * Render a single block.
136 *
137 * @param array $block Block data.
138 * @param string $parentBgColor Parent background color for inheritance.
139 * @return string Rendered HTML.
140 */
141 private function renderBlock( array $block, string $parentBgColor = '' ): string {
142 $type = $block['type'] ?? '';
143 $attributes = $block['attributes'] ?? array();
144 $children = $block['children'] ?? array();
145 $customCss = $attributes['customCss'] ?? '';
146
147 $html = $this->renderBlockContent( $type, $attributes, $children, $parentBgColor );
148
149 // Custom CSS saved with the template is always rendered. Only admins
150 // can store it (the editor add-on, manage_options), and a mail must
151 // not change its look because a licence ran out.
152 if ( ! empty( $customCss ) ) {
153 $html = $this->injectCustomCss( $html, $customCss );
154 }
155
156 return $html;
157 }
158
159 /**
160 * Render block content by type.
161 *
162 * @param string $type Block type.
163 * @param array $attributes Block attributes.
164 * @param array $children Child blocks.
165 * @param string $parentBgColor Parent background color for inheritance.
166 * @return string Rendered HTML.
167 */
168 private function renderBlockContent( string $type, array $attributes, array $children, string $parentBgColor = '' ): string {
169 switch ( $type ) {
170 case 'email-wrapper':
171 return $this->renderEmailWrapper( $attributes, $children );
172
173 case 'row':
174 return $this->renderRow( $attributes, $children, $parentBgColor );
175
176 case 'columns-1':
177 case 'columns-2':
178 case 'columns-2-sidebar':
179 case 'columns-3':
180 return $this->renderColumns( $type, $attributes, $children, $parentBgColor );
181
182 case 'header':
183 return $this->renderHeader( $attributes );
184
185 case 'heading':
186 return $this->renderHeading( $attributes );
187
188 case 'text':
189 return $this->renderText( $attributes );
190
191 case 'button':
192 return $this->renderButton( $attributes );
193
194 case 'image':
195 return $this->renderImage( $attributes );
196
197 case 'divider':
198 return $this->renderDivider( $attributes );
199
200 case 'spacer':
201 return $this->renderSpacer( $attributes, $parentBgColor );
202
203 case 'social-icons':
204 return $this->renderSocialIcons( $attributes );
205
206 case 'footer':
207 return $this->renderFooter( $attributes );
208
209 case 'placeholder-confirm-link':
210 return $this->renderPlaceholder( '[doubleoptinlink]', $attributes );
211
212 case 'placeholder-optout-link':
213 return $this->renderPlaceholder( '[doubleoptoutlink]', $attributes );
214
215 case 'placeholder-date':
216 return $this->renderPlaceholder( '[doubleoptin_form_date]', $attributes );
217
218 case 'placeholder-time':
219 return $this->renderPlaceholder( '[doubleoptin_form_time]', $attributes );
220
221 case 'placeholder-url':
222 return $this->renderPlaceholder( '[doubleoptin_form_url]', $attributes );
223
224 case 'placeholder-custom':
225 $fieldName = $attributes['fieldName'] ?? 'field_name';
226 return $this->renderPlaceholder( "[{$fieldName}]", $attributes );
227
228 case 'conditional-content':
229 return $this->renderConditionalContent( $attributes, $children, $parentBgColor );
230
231 default:
232 return '';
233 }
234 }
235
236 /**
237 * Render email wrapper.
238 */
239 private function renderEmailWrapper( array $attrs, array $children ): string {
240 $bgColor = esc_attr( $attrs['backgroundColor'] ?? '#ffffff' );
241 $padding = esc_attr( $attrs['padding'] ?? '20px' );
242
243 $innerHtml = $this->renderBlocks( $children, $bgColor );
244
245 return <<<HTML
246 <tr>
247 <td style="background-color: {$bgColor}; padding: {$padding};">
248 <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
249 {$innerHtml}
250 </table>
251 </td>
252 </tr>
253 HTML;
254 }
255
256 /**
257 * Render row.
258 */
259 private function renderRow( array $attrs, array $children, string $parentBgColor = '' ): string {
260 $bgColor = esc_attr( $attrs['backgroundColor'] ?? 'transparent' );
261 $padding = esc_attr( $attrs['padding'] ?? '0' );
262
263 // Pass row's own bg color if set, otherwise inherit parent's
264 $childBgColor = ( $bgColor !== 'transparent' && ! empty( $bgColor ) ) ? $bgColor : $parentBgColor;
265 $innerHtml = $this->renderBlocks( $children, $childBgColor );
266
267 return <<<HTML
268 <tr>
269 <td style="background-color: {$bgColor}; padding: {$padding};">
270 <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
271 {$innerHtml}
272 </table>
273 </td>
274 </tr>
275 HTML;
276 }
277
278 /**
279 * Render columns.
280 */
281 private function renderColumns( string $type, array $attrs, array $children, string $parentBgColor = '' ): string {
282 $bgColor = esc_attr( $attrs['backgroundColor'] ?? 'transparent' );
283
284 // Determine column widths - use custom columnWidths if set
285 $customWidths = $attrs['columnWidths'] ?? null;
286 if ( is_array( $customWidths ) && ! empty( $customWidths ) ) {
287 $widths = array_map( 'intval', $customWidths );
288 } else {
289 switch ( $type ) {
290 case 'columns-1':
291 $widths = array( 100 );
292 break;
293 case 'columns-2':
294 $widths = array( 50, 50 );
295 break;
296 case 'columns-2-sidebar':
297 $widths = ( $attrs['layout'] ?? '70-30' ) === '70-30' ? array( 70, 30 ) : array( 30, 70 );
298 break;
299 case 'columns-3':
300 $widths = array( 33, 34, 33 );
301 break;
302 default:
303 $widths = array( 100 );
304 }
305 }
306
307 $contentWidth = (int) $this->globalStyles['contentWidth'];
308
309 $childBgColor = ( $bgColor !== 'transparent' && ! empty( $bgColor ) ) ? $bgColor : $parentBgColor;
310 $columnsHtml = '';
311 foreach ( $children as $index => $child ) {
312 $width = $widths[ $index ] ?? $widths[0];
313 $pixelWidth = (int) ( $contentWidth * $width / 100 );
314 $childHtml = $this->renderBlock( $child, $childBgColor );
315
316 $columnsHtml .= <<<HTML
317 <!--[if mso]>
318 <td valign="top" width="{$pixelWidth}">
319 <![endif]-->
320 <div class="column" style="display: inline-block; vertical-align: top; width: 100%; max-width: {$pixelWidth}px;">
321 <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
322 {$childHtml}
323 </table>
324 </div>
325 <!--[if mso]>
326 </td>
327 <![endif]-->
328 HTML;
329 }
330
331 return <<<HTML
332 <tr>
333 <td style="background-color: {$bgColor};">
334 <!--[if mso]>
335 <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
336 <tr>
337 <![endif]-->
338 {$columnsHtml}
339 <!--[if mso]>
340 </tr>
341 </table>
342 <![endif]-->
343 </td>
344 </tr>
345 HTML;
346 }
347
348 /**
349 * Render header block.
350 */
351 private function renderHeader( array $attrs ): string {
352 $bgColor = esc_attr( $attrs['backgroundColor'] ?? $this->globalStyles['primaryColor'] );
353 $padding = esc_attr( $attrs['padding'] ?? '30px 20px' );
354 $logoUrl = esc_url( $attrs['logoUrl'] ?? '' );
355 $logoAlt = esc_attr( $attrs['logoAlt'] ?? get_bloginfo( 'name' ) );
356 $logoWidth = (int) ( $attrs['logoWidth'] ?? 200 );
357 $align = esc_attr( $attrs['align'] ?? 'center' );
358
359 $logoHtml = '';
360 if ( $logoUrl ) {
361 $logoHtml = "<img src=\"{$logoUrl}\" alt=\"{$logoAlt}\" width=\"{$logoWidth}\" style=\"display: block; max-width: 100%; height: auto;\">";
362 } else {
363 $siteTitle = esc_html( get_bloginfo( 'name' ) );
364 $logoHtml = "<h1 style=\"margin: 0; color: #ffffff; font-size: 24px;\">{$siteTitle}</h1>";
365 }
366
367 return <<<HTML
368 <tr>
369 <td align="{$align}" style="background-color: {$bgColor}; padding: {$padding};">
370 {$logoHtml}
371 </td>
372 </tr>
373 HTML;
374 }
375
376 /**
377 * Render heading block.
378 */
379 private function renderHeading( array $attrs ): string {
380 $level = (int) ( $attrs['level'] ?? 2 );
381 $level = max( 1, min( 6, $level ) );
382 $tag = "h{$level}";
383
384 $text = wp_kses_post( $attrs['text'] ?? '' );
385 $color = esc_attr( $attrs['color'] ?? $this->globalStyles['textColor'] );
386 $align = esc_attr( $attrs['align'] ?? 'left' );
387 $fontSize = esc_attr( $attrs['fontSize'] ?? $this->getHeadingFontSize( $level ) );
388 $padding = esc_attr( $attrs['padding'] ?? '10px 0' );
389
390 return <<<HTML
391 <tr>
392 <td style="padding: {$padding};">
393 <{$tag} style="margin: 0; color: {$color}; text-align: {$align}; font-size: {$fontSize}; font-weight: bold;">{$text}</{$tag}>
394 </td>
395 </tr>
396 HTML;
397 }
398
399 /**
400 * Get default font size for heading level.
401 */
402 private function getHeadingFontSize( int $level ): string {
403 $sizes = array(
404 1 => '32px',
405 2 => '28px',
406 3 => '24px',
407 4 => '20px',
408 5 => '18px',
409 6 => '16px',
410 );
411 return $sizes[ $level ] ?? '16px';
412 }
413
414 /**
415 * Render text block.
416 */
417 private function renderText( array $attrs ): string {
418 $content = wp_kses_post( $attrs['content'] ?? '' );
419 $color = esc_attr( $attrs['color'] ?? $this->globalStyles['textColor'] );
420 $fontSize = esc_attr( $attrs['fontSize'] ?? $this->globalStyles['fontSize'] );
421 $lineHeight = esc_attr( $attrs['lineHeight'] ?? $this->globalStyles['lineHeight'] );
422 $align = esc_attr( $attrs['align'] ?? 'left' );
423 $padding = esc_attr( $attrs['padding'] ?? '10px 0' );
424 $fontFamily = esc_attr( $this->globalStyles['fontFamily'] );
425
426 return <<<HTML
427 <tr>
428 <td style="padding: {$padding};">
429 <div style="margin: 0; color: {$color}; font-size: {$fontSize}; line-height: {$lineHeight}; text-align: {$align}; font-family: {$fontFamily};">{$content}</div>
430 </td>
431 </tr>
432 HTML;
433 }
434
435 /**
436 * Render button block.
437 */
438 private function renderButton( array $attrs ): string {
439 $text = esc_html( $attrs['text'] ?? __( 'Click Here', 'double-opt-in' ) );
440 $rawUrl = $attrs['url'] ?? '#';
441 // Check if URL is a placeholder (contains [...]) - don't escape it to avoid adding http:// prefix
442 // Placeholders will be replaced later with actual URLs that already have proper protocols
443 if ( preg_match( '/\[.+\]/', $rawUrl ) ) {
444 $url = $rawUrl;
445 } else {
446 $url = esc_url( $rawUrl );
447 }
448 $bgColor = esc_attr( $attrs['backgroundColor'] ?? $this->globalStyles['primaryColor'] );
449 $textColor = esc_attr( $attrs['textColor'] ?? '#ffffff' );
450 $borderRadius = (int) ( $attrs['borderRadius'] ?? 4 );
451 $fontSize = esc_attr( $attrs['fontSize'] ?? '16px' );
452 $paddingV = (int) ( $attrs['paddingV'] ?? 12 );
453 $paddingH = (int) ( $attrs['paddingH'] ?? 24 );
454 $align = esc_attr( $attrs['align'] ?? 'center' );
455 $padding = esc_attr( $attrs['padding'] ?? '10px 0' );
456 $width = esc_attr( $attrs['width'] ?? 'auto' );
457
458 // Build width styles
459 $tableWidth = ( $width !== 'auto' ) ? ' width="100%"' : '';
460 $widthStyle = ( $width !== 'auto' ) ? "width: {$width}; box-sizing: border-box; text-align: center;" : '';
461
462 return <<<HTML
463 <tr>
464 <td align="{$align}" style="padding: {$padding};">
465 <table role="presentation" border="0" cellpadding="0" cellspacing="0"{$tableWidth}>
466 <tr>
467 <td align="center" bgcolor="{$bgColor}" style="border-radius: {$borderRadius}px;">
468 <!--[if mso]>
469 <v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="{$url}" style="height:auto;v-text-anchor:middle;width:auto;" arcsize="10%" strokecolor="{$bgColor}" fillcolor="{$bgColor}">
470 <w:anchorlock/>
471 <center style="color:{$textColor};font-family:Arial,sans-serif;font-size:{$fontSize};">
472 <![endif]-->
473 <a href="{$url}" target="_blank" style="display: inline-block; min-width: 200px; padding: {$paddingV}px {$paddingH}px; font-family: Arial, sans-serif; font-size: {$fontSize}; color: {$textColor}; text-decoration: none; background-color: {$bgColor}; border-radius: {$borderRadius}px; font-weight: bold; white-space: nowrap; text-align: center; mso-padding-alt: 0; {$widthStyle}">{$text}</a>
474 <!--[if mso]>
475 </center>
476 </v:roundrect>
477 <![endif]-->
478 </td>
479 </tr>
480 </table>
481 </td>
482 </tr>
483 HTML;
484 }
485
486 /**
487 * Render image block.
488 */
489 private function renderImage( array $attrs ): string {
490 $url = esc_url( $attrs['url'] ?? '' );
491 $alt = esc_attr( $attrs['alt'] ?? '' );
492 $width = esc_attr( $attrs['width'] ?? '100%' );
493 $rawLink = $attrs['link'] ?? '';
494 // Check if link is a placeholder (contains [...]) - don't escape it to avoid adding http:// prefix
495 if ( ! empty( $rawLink ) && preg_match( '/\[.+\]/', $rawLink ) ) {
496 $link = $rawLink;
497 } else {
498 $link = esc_url( $rawLink );
499 }
500 $align = esc_attr( $attrs['align'] ?? 'center' );
501 $padding = esc_attr( $attrs['padding'] ?? '10px 0' );
502
503 if ( empty( $url ) ) {
504 return '';
505 }
506
507 $imgHtml = "<img src=\"{$url}\" alt=\"{$alt}\" width=\"{$width}\" style=\"display: block; max-width: 100%; height: auto;\">";
508
509 if ( $link ) {
510 $imgHtml = "<a href=\"{$link}\" target=\"_blank\">{$imgHtml}</a>";
511 }
512
513 return <<<HTML
514 <tr>
515 <td align="{$align}" style="padding: {$padding};">
516 {$imgHtml}
517 </td>
518 </tr>
519 HTML;
520 }
521
522 /**
523 * Render divider block.
524 */
525 private function renderDivider( array $attrs ): string {
526 $color = esc_attr( $attrs['color'] ?? '#dddddd' );
527 $thickness = (int) ( $attrs['thickness'] ?? 1 );
528 $style = esc_attr( $attrs['style'] ?? 'solid' );
529 $padding = esc_attr( $attrs['padding'] ?? '20px 0' );
530
531 return <<<HTML
532 <tr>
533 <td style="padding: {$padding};">
534 <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
535 <tr>
536 <td style="border-top: {$thickness}px {$style} {$color};"></td>
537 </tr>
538 </table>
539 </td>
540 </tr>
541 HTML;
542 }
543
544 /**
545 * Render spacer block.
546 */
547 private function renderSpacer( array $attrs, string $parentBgColor = '' ): string {
548 $height = (int) ( $attrs['height'] ?? 20 );
549 $bgColor = ! empty( $attrs['backgroundColor'] ) && $attrs['backgroundColor'] !== 'transparent'
550 ? esc_attr( $attrs['backgroundColor'] )
551 : ( ! empty( $parentBgColor ) ? esc_attr( $parentBgColor ) : 'transparent' );
552
553 return <<<HTML
554 <tr>
555 <td style="height: {$height}px; font-size: 0; line-height: 0; background-color: {$bgColor};">&nbsp;</td>
556 </tr>
557 HTML;
558 }
559
560 /**
561 * Render social icons block.
562 */
563 private function renderSocialIcons( array $attrs ): string {
564 $icons = $attrs['icons'] ?? array();
565 $iconSize = (int) ( $attrs['iconSize'] ?? 32 );
566 $align = esc_attr( $attrs['align'] ?? 'center' );
567 $padding = esc_attr( $attrs['padding'] ?? '20px 0' );
568 $spacing = (int) ( $attrs['spacing'] ?? 10 );
569
570 $iconColors = array(
571 'facebook' => '#1877f2',
572 'twitter' => '#1da1f2',
573 'instagram' => '#e4405f',
574 'linkedin' => '#0a66c2',
575 'youtube' => '#ff0000',
576 );
577
578 $iconsHtml = '';
579 foreach ( $icons as $icon ) {
580 $network = esc_attr( $icon['network'] ?? '' );
581 $url = esc_url( $icon['url'] ?? '#' );
582 $color = esc_attr( $iconColors[ $network ] ?? '#666666' );
583 $label = ucfirst( $network );
584
585 // Simple text-based icons for email compatibility
586 $iconsHtml .= <<<HTML
587 <td style="padding: 0 {$spacing}px;">
588 <a href="{$url}" target="_blank" style="display: inline-block; width: {$iconSize}px; height: {$iconSize}px; background-color: {$color}; border-radius: 50%; text-align: center; line-height: {$iconSize}px; color: #ffffff; font-size: 12px; font-weight: bold; text-decoration: none;" title="{$label}">{$label[0]}</a>
589 </td>
590 HTML;
591 }
592
593 if ( empty( $iconsHtml ) ) {
594 return '';
595 }
596
597 return <<<HTML
598 <tr>
599 <td align="{$align}" style="padding: {$padding};">
600 <table role="presentation" border="0" cellpadding="0" cellspacing="0">
601 <tr>
602 {$iconsHtml}
603 </tr>
604 </table>
605 </td>
606 </tr>
607 HTML;
608 }
609
610 /**
611 * Render footer block.
612 */
613 private function renderFooter( array $attrs ): string {
614 $bgColor = esc_attr( $attrs['backgroundColor'] ?? '#f4f4f4' );
615 $textColor = esc_attr( $attrs['textColor'] ?? '#666666' );
616 $content = wp_kses_post( $attrs['content'] ?? '' );
617 $padding = esc_attr( $attrs['padding'] ?? '30px 20px' );
618 $align = esc_attr( $attrs['align'] ?? 'center' );
619 $fontSize = esc_attr( $attrs['fontSize'] ?? '12px' );
620
621 if ( empty( $content ) ) {
622 $year = date( 'Y' );
623 $siteName = esc_html( get_bloginfo( 'name' ) );
624 $content = "&copy; {$year} {$siteName}. All rights reserved.";
625 }
626
627 return <<<HTML
628 <tr>
629 <td style="background-color: {$bgColor}; padding: {$padding};">
630 <div style="margin: 0; color: {$textColor}; font-size: {$fontSize}; text-align: {$align}; line-height: 1.5;">{$content}</div>
631 </td>
632 </tr>
633 HTML;
634 }
635
636 /**
637 * Inject custom CSS as inline styles on the outermost HTML element.
638 *
639 * @param string $html The block HTML.
640 * @param string $customCss CSS properties to inject (e.g., "background: #fff; border: 1px solid #ccc;").
641 * @return string HTML with custom CSS injected.
642 */
643 private function injectCustomCss( string $html, string $customCss ): string {
644 $sanitizedCss = esc_attr( trim( $customCss ) );
645 if ( empty( $sanitizedCss ) ) {
646 return $html;
647 }
648
649 // Wrap in a div with inline styles (email-safe approach)
650 return '<div style="' . $sanitizedCss . '">' . $html . '</div>';
651 }
652
653 /**
654 * Render conditional content block.
655 *
656 * @param array $attrs Block attributes.
657 * @param array $children Child blocks.
658 * @return string Rendered HTML with condition markers.
659 */
660 private function renderConditionalContent( array $attrs, array $children, string $parentBgColor = '' ): string {
661 $field = esc_attr( $attrs['field'] ?? '' );
662 $operator = esc_attr( $attrs['operator'] ?? 'not_empty' );
663 $value = esc_attr( $attrs['value'] ?? '' );
664 $padding = esc_attr( $attrs['padding'] ?? '0' );
665
666 $condition = wp_json_encode(
667 array(
668 'field' => $field,
669 'operator' => $operator,
670 'value' => $value,
671 )
672 );
673
674 $innerHtml = $this->renderBlocks( $children, $parentBgColor );
675
676 return '<div data-doi-condition="' . esc_attr( $condition ) . '" style="padding: ' . $padding . ';">' . $innerHtml . '</div>';
677 }
678
679 /**
680 * Render placeholder block.
681 */
682 private function renderPlaceholder( string $placeholder, array $attrs ): string {
683 $display = esc_attr( $attrs['display'] ?? 'inline' );
684 $padding = esc_attr( $attrs['padding'] ?? '0' );
685
686 if ( $display === 'button' ) {
687 // Render as a styled button placeholder
688 $bgColor = esc_attr( $attrs['backgroundColor'] ?? $this->globalStyles['primaryColor'] );
689 $textColor = esc_attr( $attrs['textColor'] ?? '#ffffff' );
690 $text = esc_html( $attrs['buttonText'] ?? __( 'Confirm', 'double-opt-in' ) );
691 $align = esc_attr( $attrs['align'] ?? 'center' );
692
693 return <<<HTML
694 <tr>
695 <td align="{$align}" style="padding: {$padding};">
696 <table role="presentation" border="0" cellpadding="0" cellspacing="0">
697 <tr>
698 <td align="center" bgcolor="{$bgColor}" style="border-radius: 4px;">
699 <a href="{$placeholder}" target="_blank" style="display: inline-block; padding: 12px 24px; font-family: Arial, sans-serif; font-size: 16px; color: {$textColor}; text-decoration: none; background-color: {$bgColor}; border-radius: 4px; font-weight: bold;">{$text}</a>
700 </td>
701 </tr>
702 </table>
703 </td>
704 </tr>
705 HTML;
706 }
707
708 // Inline display
709 return $placeholder;
710 }
711 }
712