659 lines
34 KiB
PHP
659 lines
34 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Trasforma i blocchi della newsletter (JSON di YogiBook) in HTML email
|
|
* compatibile con il composer di The Newsletter (editor = 2):
|
|
* ogni blocco ha il wrapper "tnpc-row" e le sue opzioni in data-json (base64),
|
|
* così in WordPress la newsletter resta modificabile nel composer.
|
|
*
|
|
* Funziona anche al contrario: fromComposerHtml() importa una newsletter del plugin.
|
|
*/
|
|
class NewsletterRenderer
|
|
{
|
|
public const WIDTH = 600;
|
|
|
|
public const BLOCK_TYPES = ['header', 'heading', 'text', 'image', 'button', 'columns', 'separator', 'social', 'footer', 'raw'];
|
|
|
|
/** Impostazioni grafiche predefinite (come le newsletter YogaSoul attuali). */
|
|
public static function defaultSettings(): array
|
|
{
|
|
return [
|
|
'background' => '#f4f4f4',
|
|
'block_background' => '#ffffff',
|
|
'font_family' => 'Verdana, Geneva, sans-serif',
|
|
'font_size' => 16,
|
|
'text_color' => '#222222',
|
|
'link_color' => '#007bff',
|
|
'title_font_size' => 28,
|
|
'title_color' => '#222222',
|
|
'button_background' => '#256f9c',
|
|
'button_color' => '#ffffff',
|
|
'social' => [
|
|
'facebook' => 'https://www.facebook.com/yogasoul.it/',
|
|
'instagram' => 'https://www.instagram.com/aura_yogasoul/',
|
|
'youtube' => 'https://www.youtube.com/@yogasoul6746',
|
|
'whatsapp' => 'https://wa.me/393534982534',
|
|
],
|
|
];
|
|
}
|
|
|
|
/** Blocchi di una newsletter nuova. */
|
|
public static function starterBlocks(): array
|
|
{
|
|
return [
|
|
self::newBlock('header'),
|
|
self::newBlock('text', ['html' => '<h1 style="text-align: center;">Ciao {name}</h1><p>come stai?</p><p>Scrivi qui il testo della newsletter…</p>']),
|
|
self::newBlock('image'),
|
|
self::newBlock('button'),
|
|
self::newBlock('separator'),
|
|
self::newBlock('social'),
|
|
self::newBlock('footer'),
|
|
];
|
|
}
|
|
|
|
/** Valori predefiniti per tipo di blocco. */
|
|
public static function newBlock(string $type, array $values = []): array
|
|
{
|
|
$defaults = [
|
|
'header' => ['logo_url' => 'https://yogasoul.it/wp-content/uploads/2025/01/Logo-YogaSoul_parziale-1.png', 'logo_width' => 80, 'link' => 'https://yogasoul.it', 'pad_top' => 24, 'pad_bottom' => 24],
|
|
'heading' => ['text' => 'Titolo', 'align' => 'center', 'font_size' => '', 'color' => '', 'background' => '', 'pad_top' => 30, 'pad_bottom' => 10],
|
|
'text' => ['html' => '<p>Testo…</p>', 'background' => '', 'background_2' => '', 'pad_top' => 20, 'pad_bottom' => 20],
|
|
'image' => ['url' => '', 'alt' => '', 'link' => '', 'width' => 600, 'pad_top' => 0, 'pad_bottom' => 0],
|
|
'button' => ['label' => 'Prenota la tua lezione', 'url' => 'https://yogasoul.it', 'button_bg' => '', 'color' => '', 'font_size' => 20, 'width' => 300, 'radius' => 6, 'pad_top' => 20, 'pad_bottom' => 20],
|
|
'columns' => ['image_url' => '', 'image_alt' => '', 'link' => '', 'image_side' => 'left', 'html' => '<p>Testo accanto all\'immagine…</p>', 'pad_top' => 20, 'pad_bottom' => 20],
|
|
'separator' => ['color' => '#dddddd', 'height' => 1, 'pad_top' => 20, 'pad_bottom' => 20],
|
|
'social' => ['icon_size' => 32, 'pad_top' => 15, 'pad_bottom' => 15],
|
|
'footer' => ['profile_label' => "Modifica l'iscrizione", 'view_label' => 'Vedi online', 'unsub_label' => 'Disiscriviti', 'pad_top' => 15, 'pad_bottom' => 15],
|
|
'raw' => ['html' => '', 'wp_id' => ''],
|
|
];
|
|
|
|
return array_merge(
|
|
['id' => bin2hex(random_bytes(6)), 'type' => $type],
|
|
$defaults[$type] ?? [],
|
|
$values
|
|
);
|
|
}
|
|
|
|
/* =====================================================================
|
|
* Rendering
|
|
* ===================================================================== */
|
|
|
|
/**
|
|
* HTML completo della newsletter.
|
|
* $forPreview = true sostituisce i tag del plugin con valori di esempio.
|
|
*/
|
|
public static function renderDocument(array $blocks, array $settings, string $subject, string $preheader, bool $forPreview = false): string
|
|
{
|
|
$s = self::settings($settings);
|
|
|
|
$head = file_get_contents(__DIR__ . '/newsletter_head.html');
|
|
$head = str_replace('{{LINK_COLOR}}', self::color($s['link_color'], '#007bff'), $head);
|
|
|
|
$body = '';
|
|
foreach ($blocks as $b) {
|
|
if (is_array($b) && isset($b['type'])) {
|
|
$body .= self::renderBlock($b, $s) . "\n";
|
|
}
|
|
}
|
|
|
|
$bg = self::color($s['background'], '#f4f4f4');
|
|
|
|
$html = $head
|
|
. '<body style="margin: 0; padding: 0; line-height: normal; word-spacing: normal;" dir="ltr">' . "\n"
|
|
. '<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">' . self::h($preheader) . "</div>\n\n"
|
|
. "<table cellpadding='0' cellspacing='0' border='0' width='100%' role='presentation'>\n<tr>\n"
|
|
. "<td bgcolor='{$bg}' valign='top' class='wrapper'><!-- tnp -->"
|
|
. $body
|
|
. "<!-- /tnp -->\n</td>\n</tr>\n</table>\n\n</body>\n</html>";
|
|
|
|
if ($forPreview) {
|
|
$html = strtr($html, [
|
|
'{name}' => 'Maria',
|
|
'{email_subject}' => self::h($subject),
|
|
'{unsubscription_url}' => '#',
|
|
'{profile_url}' => '#',
|
|
'{email_url}' => '#',
|
|
'{subscription_confirm_url}' => '#',
|
|
]);
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
/** Versione solo testo (campo message_text del plugin). */
|
|
public static function renderText(array $blocks, string $preheader): string
|
|
{
|
|
$out = [];
|
|
if ($preheader !== '') $out[] = $preheader;
|
|
|
|
foreach ($blocks as $b) {
|
|
switch ($b['type'] ?? '') {
|
|
case 'heading':
|
|
$out[] = strtoupper(trim((string) ($b['text'] ?? '')));
|
|
break;
|
|
case 'text':
|
|
case 'columns':
|
|
case 'raw':
|
|
$out[] = self::htmlToText((string) ($b['html'] ?? ''));
|
|
break;
|
|
case 'button':
|
|
$out[] = trim(strip_tags(str_ireplace(['<br>', '<br/>', '<br />'], ' ', (string) ($b['label'] ?? '')))) . ': ' . ($b['url'] ?? '');
|
|
break;
|
|
case 'image':
|
|
if (!empty($b['link'])) $out[] = ($b['alt'] ?: 'Immagine') . ' (' . $b['link'] . ')';
|
|
break;
|
|
case 'footer':
|
|
$out[] = "Modifica l'iscrizione: {profile_url}";
|
|
break;
|
|
}
|
|
}
|
|
|
|
return trim(preg_replace("/\n{3,}/", "\n\n", implode("\n\n", array_filter($out, fn($x) => trim($x) !== ''))));
|
|
}
|
|
|
|
public static function renderBlock(array $b, array $s): string
|
|
{
|
|
$type = $b['type'];
|
|
|
|
if ($type === 'raw') {
|
|
return (string) ($b['html'] ?? '');
|
|
}
|
|
|
|
$padTop = (int) ($b['pad_top'] ?? 15);
|
|
$padBottom = (int) ($b['pad_bottom'] ?? 15);
|
|
$bg = self::color($b['background'] ?? '', self::color($s['block_background'], '#ffffff'));
|
|
// Sfumatura (solo blocchi di testo, come fa il composer)
|
|
$bg2 = in_array($type, ['text', 'columns'], true) ? self::color($b['background_2'] ?? '', '') : '';
|
|
$bgCss = 'background-color: ' . $bg . ';' . ($bg2 !== '' ? ' background-image: linear-gradient(180deg, ' . $bg . ', ' . $bg2 . ');' : '');
|
|
|
|
switch ($type) {
|
|
case 'header':
|
|
[$inner, $padLR, $wpId, $opts] = self::header($b, $s);
|
|
break;
|
|
case 'heading':
|
|
[$inner, $padLR, $wpId, $opts] = self::heading($b, $s);
|
|
break;
|
|
case 'text':
|
|
[$inner, $padLR, $wpId, $opts] = self::text($b, $s);
|
|
break;
|
|
case 'image':
|
|
[$inner, $padLR, $wpId, $opts] = self::image($b, $s);
|
|
break;
|
|
case 'button':
|
|
[$inner, $padLR, $wpId, $opts] = self::button($b, $s);
|
|
break;
|
|
case 'columns':
|
|
[$inner, $padLR, $wpId, $opts] = self::columns($b, $s);
|
|
break;
|
|
case 'separator':
|
|
[$inner, $padLR, $wpId, $opts] = self::separator($b, $s);
|
|
break;
|
|
case 'social':
|
|
[$inner, $padLR, $wpId, $opts] = self::social($b, $s);
|
|
break;
|
|
case 'footer':
|
|
[$inner, $padLR, $wpId, $opts] = self::footer($b, $s);
|
|
break;
|
|
default:
|
|
return '';
|
|
}
|
|
|
|
$opts = array_merge([
|
|
'block_padding_top' => $padTop,
|
|
'block_padding_bottom' => $padBottom,
|
|
'block_padding_right' => $padLR,
|
|
'block_padding_left' => $padLR,
|
|
'block_background' => self::color($b['background'] ?? '', ''),
|
|
'block_background_2' => $bg2,
|
|
'block_width' => (string) self::WIDTH,
|
|
'block_align' => 'center',
|
|
'block_border_color' => '',
|
|
'block_border_radius' => '',
|
|
'inline_edits' => '',
|
|
'block_background_angle' => '180',
|
|
'block_background_wide' => '0',
|
|
'block_class' => '',
|
|
], $opts, ['block_id' => $wpId]);
|
|
|
|
$json = base64_encode(json_encode($opts));
|
|
$w = self::WIDTH;
|
|
|
|
// Colonna centrale di 600px con lo sfondo del blocco, fuori lo sfondo generale
|
|
// (come le newsletter YogaSoul attuali: block_background_wide = 0)
|
|
return '<table border="0" cellpadding="0" cellspacing="0" align="center" width="100%" style="border-collapse: collapse; width: 100%;" class="tnpc-row tnpc-row-block" data-id="' . $wpId . '">' . "\n"
|
|
. '<tbody><tr><td align="center" style="padding: 0;" class="edit-block">'
|
|
. '<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" align="center" cellspacing="0" width="' . $w . '"><tr><td width="' . $w . '" style="vertical-align:top;width:' . $w . 'px;"><![endif]-->'
|
|
. '<table role="presentation" type="options" data-json="' . $json . '" class="tnpc-block-content" border="0" cellpadding="0" align="center" cellspacing="0" width="100%" style="border-collapse: separate !important; width: 100%!important; max-width: ' . $w . 'px!important">' . "\n"
|
|
. '<tbody><tr><td align="center" style="text-align: center; line-height: normal !important; letter-spacing: normal; mso-line-height-rule: exactly; outline: none; padding: ' . $padTop . 'px ' . $padLR . 'px ' . $padBottom . 'px ' . $padLR . 'px;' . $bgCss . '" bgcolor="' . $bg . '" class="">'
|
|
. $inner
|
|
. '</td></tr></tbody></table><!--[if mso | IE]></td></tr></table><![endif]--></td></tr></tbody></table>';
|
|
}
|
|
|
|
/* ---- singoli blocchi: [html interno, padding laterale, id composer, opzioni composer] ---- */
|
|
|
|
private static function header(array $b, array $s): array
|
|
{
|
|
$logo = self::url($b['logo_url'] ?? '');
|
|
$width = max(20, min(600, (int) ($b['logo_width'] ?? 80)));
|
|
$link = self::url($b['link'] ?? '');
|
|
|
|
$img = $logo !== ''
|
|
? '<img src="' . self::h($logo) . '" width="' . $width . '" border="0" alt="YogaSoul" style="display:inline-block; height:auto; max-width:100%;">'
|
|
: '';
|
|
$inner = $link !== '' && $img !== '' ? '<a href="' . self::h($link) . '" target="_blank">' . $img . '</a>' : $img;
|
|
|
|
return [$inner, 16, 'header', [
|
|
'font_family' => '', 'font_size' => '', 'font_color' => '', 'font_weight' => '',
|
|
'logo_width' => (string) $width, 'block_layout' => 'logo', 'block_style' => '',
|
|
'title_font_family' => '', 'title_font_size' => '', 'title_font_weight' => '', 'title_font_color' => '',
|
|
]];
|
|
}
|
|
|
|
private static function heading(array $b, array $s): array
|
|
{
|
|
$size = (int) ($b['font_size'] ?: $s['title_font_size']);
|
|
$color = self::color($b['color'] ?? '', self::color($s['title_color'], '#222222'));
|
|
$align = in_array($b['align'] ?? '', ['left', 'center', 'right'], true) ? $b['align'] : 'center';
|
|
$font = self::font($s['font_family']);
|
|
$text = (string) ($b['text'] ?? '');
|
|
|
|
$inner = '<table border="0" cellspacing="0" cellpadding="0" width="100%" role="presentation"><tbody><tr>'
|
|
. '<td align="' . $align . '" valign="middle" dir="ltr" style="text-align: ' . $align . ';">'
|
|
. '<div style="font-size: ' . $size . 'px;font-family: ' . $font . ';font-weight: bold;color: ' . $color . '; padding: 0; line-height: 130% !important; letter-spacing: normal;" role="heading" aria-level="1">'
|
|
. nl2br(self::h($text))
|
|
. '</div></td></tr></tbody></table>';
|
|
|
|
return [$inner, 15, 'heading', [
|
|
'text' => $text, 'align' => $align, 'font_family' => '', 'font_size' => (string) $size,
|
|
'font_color' => $color, 'font_weight' => 'bold',
|
|
]];
|
|
}
|
|
|
|
private static function text(array $b, array $s): array
|
|
{
|
|
$html = self::cleanHtml((string) ($b['html'] ?? ''));
|
|
$inner = self::textCell($html, $s);
|
|
|
|
return [$inner, 15, 'text', ['html' => $html]];
|
|
}
|
|
|
|
private static function textCell(string $html, array $s): string
|
|
{
|
|
return '<table width="100%" style="width: 100%!important" border="0" cellpadding="0" cellspacing="0"><tbody><tr>'
|
|
. '<td width="100%" valign="top" align="left" class="text" style="mso-line-height-rule: exactly; font-size: ' . (int) $s['font_size'] . 'px;font-family: ' . self::font($s['font_family']) . ';font-weight: normal;color: ' . self::color($s['text_color'], '#222222') . '; line-height: 1.5;">'
|
|
. $html
|
|
. '</td></tr></tbody></table>';
|
|
}
|
|
|
|
private static function image(array $b, array $s): array
|
|
{
|
|
$url = self::url($b['url'] ?? '');
|
|
$alt = (string) ($b['alt'] ?? '');
|
|
$link = self::url($b['link'] ?? '');
|
|
$width = max(50, min(self::WIDTH, (int) ($b['width'] ?? self::WIDTH)));
|
|
|
|
if ($url === '') {
|
|
$inner = '<div style="padding:30px;border:2px dashed #cccccc;color:#999999;font-family:Arial,sans-serif;font-size:14px;">Immagine non ancora scelta</div>';
|
|
} else {
|
|
$img = '<img src="' . self::h($url) . '" width="' . $width . '" alt="' . self::h($alt) . '" border="0" style="display: block; margin: 0 auto; height: auto; max-width: ' . $width . 'px !important; width: 100%; padding: 0; border: 0; font-size: 12px">';
|
|
$inner = $link !== '' ? '<a href="' . self::h($link) . '" target="_blank" style="display:block;">' . $img . '</a>' : $img;
|
|
}
|
|
|
|
return [$inner, 0, 'image', [
|
|
'image' => ['id' => '', 'url' => $url],
|
|
'image-alt' => '', 'image_alt' => $alt, 'url' => $link, 'image-url' => $link,
|
|
'width' => (string) $width, 'align' => 'center', 'placeholder' => '',
|
|
]];
|
|
}
|
|
|
|
private static function button(array $b, array $s): array
|
|
{
|
|
$label = self::cleanHtml((string) ($b['label'] ?? ''), '<br><strong><em><b><i>');
|
|
$url = self::url($b['url'] ?? '');
|
|
$bg = self::color($b['button_bg'] ?? '', self::color($s['button_background'], '#256f9c'));
|
|
$color = self::color($b['color'] ?? '', self::color($s['button_color'], '#ffffff'));
|
|
$size = max(10, min(60, (int) ($b['font_size'] ?? 20)));
|
|
$width = max(80, min(self::WIDTH, (int) ($b['width'] ?? 300)));
|
|
$radius = max(0, min(50, (int) ($b['radius'] ?? 6)));
|
|
$font = self::font($s['font_family']);
|
|
|
|
$inner = '<table border="0" cellpadding="0" cellspacing="0" role="presentation" align="center" style="border-collapse:separate !important;line-height:100%;width:' . $width . 'px;max-width:100%;"><tbody><tr>'
|
|
. '<td align="center" bgcolor="' . $bg . '" role="presentation" style="border-collapse:separate !important;cursor:auto;mso-padding-alt:12px 25px;background:' . $bg . ';border-radius:' . $radius . 'px;" valign="middle">'
|
|
. '<a href="' . self::h($url) . '" style="display:inline-block;color:' . $color . ';font-family:' . $font . ';font-size:' . $size . 'px;font-weight:bold;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:12px 25px;mso-padding-alt:0px;border-radius:' . $radius . 'px;" target="_blank">'
|
|
. $label
|
|
. '</a></td></tr></tbody></table>';
|
|
|
|
return [$inner, 0, 'cta', [
|
|
'button_label' => $label, 'button_url' => $url,
|
|
'button_font_family' => $s['font_family'], 'button_font_size' => (string) $size, 'button_font_weight' => 'bold',
|
|
'button_font_color' => $color, 'button_background' => $bg, 'button_border_color' => '',
|
|
'align' => 'center', 'button_width' => (string) $width, 'button_align' => 'center',
|
|
'block_style' => '', 'button_border_radius' => (string) $radius, 'list' => '', 'unlist' => '',
|
|
]];
|
|
}
|
|
|
|
/** Immagine + testo affiancati (in WordPress diventa un blocco "testo"). */
|
|
private static function columns(array $b, array $s): array
|
|
{
|
|
$url = self::url($b['image_url'] ?? '');
|
|
$alt = self::h($b['image_alt'] ?? '');
|
|
$link = self::url($b['link'] ?? '');
|
|
$html = self::cleanHtml((string) ($b['html'] ?? ''));
|
|
$right = ($b['image_side'] ?? 'left') === 'right';
|
|
|
|
$img = $url !== ''
|
|
? '<img src="' . self::h($url) . '" width="270" alt="' . $alt . '" border="0" style="display:block;width:100%;max-width:270px;height:auto;border:0;">'
|
|
: '<div style="padding:30px 10px;border:2px dashed #cccccc;color:#999999;font-size:13px;text-align:center;">Immagine</div>';
|
|
if ($url !== '' && $link !== '') {
|
|
$img = '<a href="' . self::h($link) . '" target="_blank">' . $img . '</a>';
|
|
}
|
|
|
|
$imgCell = '<td class="responsive" width="50%" valign="top" style="padding:0 10px 10px 10px;">' . $img . '</td>';
|
|
$textCell = '<td class="responsive" width="50%" valign="top" style="padding:0 10px 10px 10px;text-align:left;">' . $html . '</td>';
|
|
|
|
$table = '<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"><tbody><tr>'
|
|
. ($right ? $textCell . $imgCell : $imgCell . $textCell)
|
|
. '</tr></tbody></table>';
|
|
|
|
return [self::textCell($table, $s), 15, 'text', ['html' => $table]];
|
|
}
|
|
|
|
private static function separator(array $b, array $s): array
|
|
{
|
|
$color = self::color($b['color'] ?? '', '#dddddd');
|
|
$height = max(1, min(10, (int) ($b['height'] ?? 1)));
|
|
|
|
$inner = '<table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr>'
|
|
. '<td style="border-bottom: ' . $height . 'px solid ' . $color . '; font-size:0; line-height:0;"> </td>'
|
|
. '</tr></tbody></table>';
|
|
|
|
return [$inner, 20, 'separator', ['color' => $color, 'height' => $height]];
|
|
}
|
|
|
|
private static function social(array $b, array $s): array
|
|
{
|
|
$size = max(16, min(64, (int) ($b['icon_size'] ?? 32)));
|
|
$links = '';
|
|
|
|
foreach (['facebook', 'youtube', 'instagram', 'whatsapp'] as $net) {
|
|
$url = self::url($s['social'][$net] ?? '');
|
|
if ($url === '') continue;
|
|
$links .= '<a href="' . self::h($url) . '" style="line-height: normal; text-decoration: none;" target="_blank">'
|
|
. '<img src="https://yogasoul.it/wp-content/plugins/newsletter/images/social-1/' . $net . '.png" width="' . $size . '" height="' . $size . '" alt="' . $net . '" border="0"></a> ';
|
|
}
|
|
|
|
$inner = '<table border="0" cellspacing="0" cellpadding="0" width="100%" role="presentation"><tbody><tr><td align="center" valign="middle">'
|
|
. $links . '</td></tr></tbody></table>';
|
|
|
|
return [$inner, 15, 'social', ['type' => '1', 'width' => (string) $size]];
|
|
}
|
|
|
|
private static function footer(array $b, array $s): array
|
|
{
|
|
$style = 'font-size: 13px; font-family: Helvetica, Arial, sans-serif; font-weight: normal; color: #444444; text-decoration: none;';
|
|
$sep = '<span style="' . $style . '"> | </span>';
|
|
|
|
$parts = [];
|
|
if (trim((string) ($b['profile_label'] ?? '')) !== '') {
|
|
$parts[] = '<a style="' . $style . '" href="{profile_url}">' . self::h($b['profile_label']) . '</a>';
|
|
}
|
|
if (trim((string) ($b['view_label'] ?? '')) !== '') {
|
|
$parts[] = '<a style="' . $style . '" href="{email_url}">' . self::h($b['view_label']) . '</a>';
|
|
}
|
|
if (trim((string) ($b['unsub_label'] ?? '')) !== '') {
|
|
$parts[] = '<a style="' . $style . '" href="{unsubscription_url}">' . self::h($b['unsub_label']) . '</a>';
|
|
}
|
|
|
|
return [implode($sep, $parts), 15, 'footer', [
|
|
'view' => (string) ($b['view_label'] ?? ''), 'profile' => (string) ($b['profile_label'] ?? ''),
|
|
'font_family' => 'Helvetica, Arial, sans-serif', 'font_size' => '13', 'font_color' => '#444444', 'font_weight' => 'normal',
|
|
]];
|
|
}
|
|
|
|
/* =====================================================================
|
|
* Import da The Newsletter (HTML del composer -> blocchi YogiBook)
|
|
* ===================================================================== */
|
|
|
|
public static function fromComposerHtml(string $html, string $options = ''): array
|
|
{
|
|
$blocks = [];
|
|
|
|
$start = strpos($html, '<!-- tnp -->');
|
|
$end = strrpos($html, '<!-- /tnp -->');
|
|
$body = ($start !== false && $end !== false) ? substr($html, $start + 12, $end - $start - 12) : $html;
|
|
|
|
// Ogni blocco inizia con la tabella "tnpc-row"
|
|
preg_match_all('/<table[^>]*class="[^"]*tnpc-row[^"]*"[^>]*>/i', $body, $m, PREG_OFFSET_CAPTURE);
|
|
$starts = array_map(fn($x) => $x[1], $m[0]);
|
|
|
|
foreach ($starts as $i => $pos) {
|
|
$chunk = substr($body, $pos, isset($starts[$i + 1]) ? $starts[$i + 1] - $pos : null);
|
|
$chunk = rtrim($chunk);
|
|
|
|
preg_match('/data-id="([^"]+)"/', $chunk, $idm);
|
|
preg_match('/data-json="([^"]+)"/', $chunk, $jm);
|
|
$wpId = $idm[1] ?? '';
|
|
$o = $jm ? (json_decode(base64_decode($jm[1]), true) ?: []) : [];
|
|
|
|
$pad = ['pad_top' => (int) ($o['block_padding_top'] ?? 15), 'pad_bottom' => (int) ($o['block_padding_bottom'] ?? 15)];
|
|
$bg = ['background' => (string) ($o['block_background'] ?? '')];
|
|
|
|
switch ($wpId) {
|
|
case 'text':
|
|
$blocks[] = self::newBlock('text', $pad + $bg + [
|
|
'html' => (string) ($o['html'] ?? ''),
|
|
'background_2' => (string) ($o['block_background_2'] ?? ''),
|
|
]);
|
|
break;
|
|
case 'heading':
|
|
$blocks[] = self::newBlock('heading', $pad + $bg + [
|
|
'text' => (string) ($o['text'] ?? ''), 'align' => $o['align'] ?? 'center',
|
|
'font_size' => (string) ($o['font_size'] ?? ''), 'color' => (string) ($o['font_color'] ?? ''),
|
|
]);
|
|
break;
|
|
case 'image':
|
|
$blocks[] = self::newBlock('image', $pad + [
|
|
'url' => (string) ($o['image']['url'] ?? ''), 'alt' => (string) ($o['image_alt'] ?? ''),
|
|
'link' => (string) ($o['url'] ?? ''),
|
|
'width' => min(self::WIDTH, (int) ($o['width'] ?? self::WIDTH) ?: self::WIDTH),
|
|
]);
|
|
break;
|
|
case 'cta':
|
|
$blocks[] = self::newBlock('button', $pad + [
|
|
'label' => (string) ($o['button_label'] ?? ''), 'url' => (string) ($o['button_url'] ?? ''),
|
|
'button_bg' => (string) ($o['button_background'] ?? ''), 'color' => (string) ($o['button_font_color'] ?? ''),
|
|
'font_size' => (int) ($o['button_font_size'] ?? 20) ?: 20,
|
|
'width' => (int) ($o['button_width'] ?? 300) ?: 300,
|
|
'radius' => (int) ($o['button_border_radius'] ?? 0),
|
|
]);
|
|
break;
|
|
case 'separator':
|
|
$blocks[] = self::newBlock('separator', $pad + ['color' => (string) ($o['color'] ?? '#dddddd'), 'height' => (int) ($o['height'] ?? 1) ?: 1]);
|
|
break;
|
|
case 'social':
|
|
$blocks[] = self::newBlock('social', $pad + ['icon_size' => (int) ($o['width'] ?? 32) ?: 32]);
|
|
break;
|
|
case 'footer':
|
|
$blocks[] = self::newBlock('footer', $pad + [
|
|
'profile_label' => (string) ($o['profile'] ?? "Modifica l'iscrizione"),
|
|
'view_label' => (string) ($o['view'] ?? 'Vedi online'),
|
|
'unsub_label' => '',
|
|
]);
|
|
break;
|
|
case 'header':
|
|
preg_match('/<img[^>]*src="([^"]+)"/i', $chunk, $im);
|
|
preg_match('/<a[^>]*href="([^"]+)"/i', $chunk, $am);
|
|
$blocks[] = self::newBlock('header', $pad + [
|
|
'logo_url' => $im[1] ?? '', 'link' => $am[1] ?? '', 'logo_width' => (int) ($o['logo_width'] ?? 80) ?: 80,
|
|
]);
|
|
break;
|
|
default:
|
|
// Blocco che YogiBook non sa modificare: lo conserviamo identico
|
|
$blocks[] = self::newBlock('raw', ['html' => $chunk, 'wp_id' => $wpId]);
|
|
}
|
|
}
|
|
|
|
return $blocks;
|
|
}
|
|
|
|
/** Impostazioni grafiche ricavate dalle opzioni serializzate del plugin. */
|
|
public static function settingsFromWpOptions(array $o): array
|
|
{
|
|
$s = self::defaultSettings();
|
|
$map = [
|
|
'composer_background' => 'background',
|
|
'composer_block_background' => 'block_background',
|
|
'composer_text_font_family' => 'font_family',
|
|
'composer_text_font_size' => 'font_size',
|
|
'composer_text_font_color' => 'text_color',
|
|
'composer_title_font_color' => 'title_color',
|
|
'composer_button_background_color' => 'button_background',
|
|
'composer_button_font_color' => 'button_color',
|
|
];
|
|
foreach ($map as $wp => $ours) {
|
|
if (!empty($o[$wp])) $s[$ours] = is_numeric($o[$wp]) ? (int) $o[$wp] : (string) $o[$wp];
|
|
}
|
|
return $s;
|
|
}
|
|
|
|
/** Opzioni "composer_*" da scrivere nel plugin. */
|
|
public static function wpComposerOptions(array $settings): array
|
|
{
|
|
$s = self::settings($settings);
|
|
return [
|
|
'composer_background' => $s['background'],
|
|
'composer_block_background' => $s['block_background'],
|
|
'composer_width' => (string) self::WIDTH,
|
|
'composer_text_font_family' => $s['font_family'],
|
|
'composer_text_font_size' => (string) $s['font_size'],
|
|
'composer_text_font_weight' => 'normal',
|
|
'composer_text_font_color' => $s['text_color'],
|
|
'composer_title_font_family' => $s['font_family'],
|
|
'composer_title_font_size' => (string) $s['title_font_size'],
|
|
'composer_title_font_weight' => 'bold',
|
|
'composer_title_font_color' => $s['title_color'],
|
|
'composer_button_font_family' => $s['font_family'],
|
|
'composer_button_font_size' => '16',
|
|
'composer_button_font_weight' => 'bold',
|
|
'composer_button_font_color' => $s['button_color'],
|
|
'composer_button_background_color' => $s['button_background'],
|
|
'composer_button_background' => $s['button_background'],
|
|
'composer_button_border_color' => $s['button_background'],
|
|
];
|
|
}
|
|
|
|
/* =====================================================================
|
|
* Controlli prima di pubblicare
|
|
* ===================================================================== */
|
|
|
|
public static function checks(array $blocks, string $subject, string $preheader, string $html): array
|
|
{
|
|
$warn = [];
|
|
$len = fn($t) => mb_strlen(trim($t));
|
|
|
|
if ($len($subject) === 0) $warn[] = ['error', "Manca l'oggetto."];
|
|
elseif ($len($subject) > 70) $warn[] = ['warning', "Oggetto lungo (" . $len($subject) . " caratteri): sul telefono viene tagliato verso i 40-60."];
|
|
|
|
if ($len($preheader) === 0) $warn[] = ['warning', "Manca il preheader (la riga di anteprima sotto l'oggetto)."];
|
|
elseif ($len($preheader) > 120) $warn[] = ['warning', 'Preheader molto lungo: viene mostrato solo l\'inizio.'];
|
|
|
|
$hasFooter = false;
|
|
foreach ($blocks as $i => $b) {
|
|
$n = $i + 1;
|
|
switch ($b['type'] ?? '') {
|
|
case 'image':
|
|
if (empty($b['url'])) $warn[] = ['error', "Blocco $n: immagine non scelta."];
|
|
elseif (trim((string) ($b['alt'] ?? '')) === '') $warn[] = ['warning', "Blocco $n: immagine senza testo alternativo (serve se le immagini sono bloccate)."];
|
|
break;
|
|
case 'columns':
|
|
if (empty($b['image_url'])) $warn[] = ['warning', "Blocco $n: colonne senza immagine."];
|
|
break;
|
|
case 'button':
|
|
if (trim((string) ($b['url'] ?? '')) === '' || ($b['url'] ?? '') === '#') $warn[] = ['error', "Blocco $n: pulsante senza link."];
|
|
break;
|
|
case 'footer':
|
|
$hasFooter = true;
|
|
break;
|
|
case 'raw':
|
|
if (preg_match('/unsubscription_url|profile_url/', (string) ($b['html'] ?? ''))) $hasFooter = true;
|
|
break;
|
|
}
|
|
$all = json_encode($b);
|
|
if (strpos($all, 'data:image') !== false) $warn[] = ['error', "Blocco $n: contiene un'immagine incollata dentro il testo. Usa un blocco immagine."];
|
|
if (preg_match('/src=\\\\?"http:\/\//', $all)) $warn[] = ['warning', "Blocco $n: immagine con indirizzo http (non sicuro)."];
|
|
}
|
|
|
|
if (!$hasFooter) $warn[] = ['error', 'Manca il piè di pagina con il link per gestire o annullare l\'iscrizione.'];
|
|
|
|
$kb = (int) round(strlen($html) / 1024);
|
|
if ($kb > 100) $warn[] = ['error', "La newsletter pesa {$kb} KB: Gmail taglia i messaggi oltre 102 KB e nasconde il fondo (con il link di disiscrizione)."];
|
|
elseif ($kb > 85) $warn[] = ['warning', "La newsletter pesa {$kb} KB: vicina al limite di Gmail (102 KB)."];
|
|
|
|
return $warn;
|
|
}
|
|
|
|
/* =====================================================================
|
|
* Utilità
|
|
* ===================================================================== */
|
|
|
|
public static function settings(array $settings): array
|
|
{
|
|
$s = array_merge(self::defaultSettings(), $settings);
|
|
$s['social'] = array_merge(self::defaultSettings()['social'], is_array($settings['social'] ?? null) ? $settings['social'] : []);
|
|
$s['font_size'] = max(12, min(22, (int) $s['font_size']));
|
|
$s['title_font_size'] = max(16, min(60, (int) $s['title_font_size']));
|
|
return $s;
|
|
}
|
|
|
|
public static function h($v): string
|
|
{
|
|
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
/** Colore esadecimale valido oppure $fallback. */
|
|
public static function color($c, string $fallback): string
|
|
{
|
|
$c = trim((string) $c);
|
|
return preg_match('/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/', $c) ? $c : $fallback;
|
|
}
|
|
|
|
/** Solo link http(s), mailto, tel o tag del plugin. */
|
|
public static function url($u): string
|
|
{
|
|
$u = trim((string) $u);
|
|
if ($u === '') return '';
|
|
if (preg_match('#^(https?://|mailto:|tel:|\{[a-z_]+\})#i', $u)) return $u;
|
|
if (preg_match('#^[\w.-]+\.[a-z]{2,}#i', $u)) return 'https://' . $u;
|
|
return '';
|
|
}
|
|
|
|
private static function font($f): string
|
|
{
|
|
$f = preg_replace('/[^a-zA-Z0-9 ,\'-]/', '', (string) $f);
|
|
return $f !== '' ? $f : 'Verdana, Geneva, sans-serif';
|
|
}
|
|
|
|
/** Toglie script, eventi e altri pericoli dall'HTML scritto nell'editor. */
|
|
public static function cleanHtml(string $html, ?string $allowed = null): string
|
|
{
|
|
$html = preg_replace('#<(script|style|iframe|object|embed|form)[^>]*>.*?</\1>#is', '', $html);
|
|
$html = preg_replace('#<(script|style|iframe|object|embed|form|input|button|meta|link)[^>]*/?>#i', '', $html);
|
|
$html = preg_replace('/\son\w+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)/i', '', $html);
|
|
$html = preg_replace('/(href|src)\s*=\s*(["\'])\s*javascript:[^"\']*\2/i', '$1="#"', $html);
|
|
if ($allowed !== null) $html = strip_tags($html, $allowed);
|
|
return trim($html);
|
|
}
|
|
|
|
private static function htmlToText(string $html): string
|
|
{
|
|
$html = preg_replace('#<a[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>#is', '$2 ($1)', $html);
|
|
$html = preg_replace('#<br\s*/?>#i', "\n", $html);
|
|
$html = preg_replace('#</(p|h[1-6]|div|li|tr)>#i', "\n", $html);
|
|
$html = preg_replace('#<li[^>]*>#i', '- ', $html);
|
|
$text = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
$text = preg_replace("/[ \t\x{00A0}]+/u", ' ', $text);
|
|
$text = preg_replace("/ *\n */", "\n", $text);
|
|
return trim(preg_replace("/\n{3,}/", "\n\n", $text));
|
|
}
|
|
}
|