PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.5.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.5.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 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 3.1.0 All 34 releases
double-opt-in / src / EmailTemplates / EmailHtmlGenerator.php

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

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