Files
yogibook_aury_new/public/class/NewsletterRenderer.php
T
2026-10-16 15:10:11 +02:00

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;">&nbsp;</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>&nbsp;&nbsp;';
}
$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 . '">&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;</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));
}
}