init
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\AutomaticEmails\WooCommerce\Events\AbandonedCart;
|
||||
use MailPoet\AutomaticEmails\WooCommerce\WooCommerce as WooCommerceEmail;
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\NewsletterOptionEntity;
|
||||
use MailPoet\Entities\SendingQueueEntity;
|
||||
|
||||
class AbandonedCartContent {
|
||||
/** @var AutomatedLatestContentBlock */
|
||||
private $ALCBlock;
|
||||
|
||||
public function __construct(
|
||||
AutomatedLatestContentBlock $ALCBlock
|
||||
) {
|
||||
$this->ALCBlock = $ALCBlock;
|
||||
}
|
||||
|
||||
public function render(
|
||||
NewsletterEntity $newsletter,
|
||||
array $args,
|
||||
bool $preview = false,
|
||||
SendingQueueEntity $sendingQueue = null
|
||||
): array {
|
||||
if (
|
||||
!in_array(
|
||||
$newsletter->getType(),
|
||||
[
|
||||
NewsletterEntity::TYPE_AUTOMATIC,
|
||||
NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL,
|
||||
NewsletterEntity::TYPE_AUTOMATION,
|
||||
],
|
||||
true
|
||||
)
|
||||
) {
|
||||
// Do not display the block if not an automatic email
|
||||
return [];
|
||||
}
|
||||
$groupOption = $newsletter->getOptions()->filter(function (NewsletterOptionEntity $newsletterOption = null) {
|
||||
if (!$newsletterOption) return false;
|
||||
$optionField = $newsletterOption->getOptionField();
|
||||
return $optionField && $optionField->getName() === 'group';
|
||||
})->first();
|
||||
$eventOption = $newsletter->getOptions()->filter(function (NewsletterOptionEntity $newsletterOption = null) {
|
||||
if (!$newsletterOption) return false;
|
||||
$optionField = $newsletterOption->getOptionField();
|
||||
return $optionField && $optionField->getName() === 'event';
|
||||
})->first();
|
||||
if (
|
||||
!$groupOption
|
||||
|| $groupOption->getValue() !== WooCommerceEmail::SLUG
|
||||
|| !$eventOption
|
||||
|| $eventOption->getValue() !== AbandonedCart::SLUG
|
||||
) {
|
||||
// Do not display the block if not an AbandonedCart email
|
||||
return [];
|
||||
}
|
||||
if ($preview) {
|
||||
// Display latest products for preview (no 'posts' argument specified)
|
||||
return $this->ALCBlock->render($newsletter, $args);
|
||||
}
|
||||
if (!$sendingQueue) {
|
||||
// Do not display the block if we're not sending an email
|
||||
return [];
|
||||
}
|
||||
$meta = $sendingQueue->getMeta();
|
||||
if (empty($meta[AbandonedCart::TASK_META_NAME])) {
|
||||
// Do not display the block if a cart is empty
|
||||
return [];
|
||||
}
|
||||
$args['amount'] = 50;
|
||||
$args['posts'] = $meta[AbandonedCart::TASK_META_NAME];
|
||||
return $this->ALCBlock->render($newsletter, $args);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\NewsletterPostEntity;
|
||||
use MailPoet\Newsletter\AutomatedLatestContent;
|
||||
use MailPoet\Newsletter\BlockPostQuery;
|
||||
use MailPoet\Newsletter\NewsletterPostsRepository;
|
||||
|
||||
class AutomatedLatestContentBlock {
|
||||
/**
|
||||
* Cache for rendered posts in newsletter.
|
||||
* Used to prevent duplicate post in case a newsletter contains 2 ALC blocks
|
||||
* @var array
|
||||
*/
|
||||
public $renderedPostsInNewsletter;
|
||||
|
||||
/** @var AutomatedLatestContent */
|
||||
private $ALC;
|
||||
|
||||
/** @var NewsletterPostsRepository */
|
||||
private $newsletterPostsRepository;
|
||||
|
||||
public function __construct(
|
||||
NewsletterPostsRepository $newsletterPostsRepository,
|
||||
AutomatedLatestContent $ALC
|
||||
) {
|
||||
$this->renderedPostsInNewsletter = [];
|
||||
$this->ALC = $ALC;
|
||||
$this->newsletterPostsRepository = $newsletterPostsRepository;
|
||||
}
|
||||
|
||||
public function render(NewsletterEntity $newsletter, $args) {
|
||||
$newerThanTimestamp = false;
|
||||
$newsletterId = false;
|
||||
if ($newsletter->getType() === NewsletterEntity::TYPE_NOTIFICATION_HISTORY) {
|
||||
$parent = $newsletter->getParent();
|
||||
if ($parent instanceof NewsletterEntity) {
|
||||
$newsletterId = $parent->getId();
|
||||
|
||||
$lastPost = $this->newsletterPostsRepository->findOneBy(['newsletter' => $parent], ['createdAt' => 'desc']);
|
||||
if ($lastPost instanceof NewsletterPostEntity) {
|
||||
$newerThanTimestamp = $lastPost->getCreatedAt();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
$postsToExclude = $this->getRenderedPosts((int)$newsletterId);
|
||||
$query = new BlockPostQuery([
|
||||
'args' => $args,
|
||||
'postsToExclude' => $postsToExclude,
|
||||
'newsletterId' => $newsletterId,
|
||||
'newerThanTimestamp' => $newerThanTimestamp,
|
||||
'dynamic' => true,
|
||||
]);
|
||||
$aLCPosts = $this->ALC->getPosts($query);
|
||||
foreach ($aLCPosts as $post) {
|
||||
$postsToExclude[] = $post->ID;
|
||||
}
|
||||
$this->setRenderedPosts((int)$newsletterId, $postsToExclude);
|
||||
return $this->ALC->transformPosts($args, $aLCPosts);
|
||||
}
|
||||
|
||||
private function getRenderedPosts(int $newsletterId) {
|
||||
return $this->renderedPostsInNewsletter[$newsletterId] ?? [];
|
||||
}
|
||||
|
||||
private function setRenderedPosts(int $newsletterId, array $posts) {
|
||||
return $this->renderedPostsInNewsletter[$newsletterId] = $posts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
|
||||
class Button {
|
||||
public function render($element, $columnBaseWidth) {
|
||||
$originalWidth = $this->getOriginalWidth($element, $columnBaseWidth);
|
||||
$element['styles']['block']['width'] = $this->calculateWidth($element, $columnBaseWidth);
|
||||
$styles = 'display:block;text-decoration:none;text-align:center;' . StylesHelper::getBlockStyles($element, $exclude = ['textAlign']);
|
||||
$styles = EHelper::escapeHtmlStyleAttr($styles);
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_padded_vertical mailpoet_padded_side" valign="top">
|
||||
<div>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;">
|
||||
<tr>
|
||||
<td class="mailpoet_button-container" style="text-align:' . $element['styles']['block']['textAlign'] . ';"><!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
|
||||
href="' . EHelper::escapeHtmlLinkAttr($element['url']) . '"
|
||||
style="height:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['lineHeight']) . ';
|
||||
width:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['width']) . ';
|
||||
v-text-anchor:middle;"
|
||||
arcsize="' . round((int)$element['styles']['block']['borderRadius'] / ((int)$element['styles']['block']['lineHeight'] ?: 1) * 100) . '%"
|
||||
strokeweight="' . EHelper::escapeHtmlAttr($element['styles']['block']['borderWidth']) . '"
|
||||
strokecolor="' . EHelper::escapeHtmlAttr($element['styles']['block']['borderColor']) . '"
|
||||
fillcolor="' . EHelper::escapeHtmlAttr($element['styles']['block']['backgroundColor']) . '">
|
||||
<w:anchorlock/>
|
||||
<center style="color:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['fontColor']) . ';
|
||||
font-family:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['fontFamily']) . ';
|
||||
font-size:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['fontSize']) . ';
|
||||
font-weight:bold;">' . EHelper::escapeHtmlText($element['text']) . '
|
||||
</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-- -->
|
||||
<table
|
||||
border="0"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
role="presentation"
|
||||
style="display:inline-block;border-collapse:separate;mso-table-lspace:0;mso-table-rspace:0;width:' . EHelper::escapeHtmlStyleAttr($originalWidth) . '"
|
||||
width="' . EHelper::escapeHtmlStyleAttr($originalWidth) . '"
|
||||
>
|
||||
<tr>
|
||||
<td class="mailpoet_table_button"
|
||||
valign="middle"
|
||||
role="presentation"
|
||||
style="mso-table-lspace: 0;mso-table-rspace: 0;' . $styles . '"
|
||||
>
|
||||
<a class="mailpoet_button" style="
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
line-height: ' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['lineHeight']) . ';
|
||||
color: ' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['fontColor']) . ';
|
||||
" href="' . EHelper::escapeHtmlLinkAttr($element['url']) . '" target="_blank">' . EHelper::escapeHtmlText($element['text']) . '</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
|
||||
public function getOriginalWidth($element, $columnBaseWidth): string {
|
||||
$columnWidth = $columnBaseWidth - (StylesHelper::$paddingWidth * 2);
|
||||
$originalWidth = (int)$element['styles']['block']['width'];
|
||||
$originalWidth = ($originalWidth > $columnWidth) ?
|
||||
$columnWidth :
|
||||
$originalWidth;
|
||||
|
||||
return $originalWidth . 'px';
|
||||
}
|
||||
|
||||
public function calculateWidth($element, $columnBaseWidth): string {
|
||||
$columnWidth = $columnBaseWidth - (StylesHelper::$paddingWidth * 2);
|
||||
$borderWidth = (int)$element['styles']['block']['borderWidth'];
|
||||
$buttonWidth = (int)$element['styles']['block']['width'];
|
||||
$buttonWidth = ($buttonWidth > $columnWidth) ?
|
||||
$columnWidth :
|
||||
$buttonWidth;
|
||||
$buttonWidth = $buttonWidth - (2 * $borderWidth) . 'px';
|
||||
return $buttonWidth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
use MailPoet\NewsletterProcessingException;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
|
||||
class Coupon {
|
||||
const TYPE = 'coupon';
|
||||
|
||||
const CODE_PLACEHOLDER = 'XXXX-XXXXXXX-XXXX';
|
||||
|
||||
/*** @var Helper */
|
||||
private $helper;
|
||||
|
||||
public function __construct(
|
||||
Helper $helper
|
||||
) {
|
||||
$this->helper = $helper;
|
||||
}
|
||||
|
||||
public function render($element, $columnBaseWidth) {
|
||||
$couponCode = self::CODE_PLACEHOLDER;
|
||||
if (!empty($element['couponId'])) {
|
||||
try {
|
||||
$couponCode = $this->helper->wcGetCouponCodeById((int)$element['couponId']);
|
||||
} catch (\Exception $e) {
|
||||
if (!$this->helper->isWooCommerceActive()) {
|
||||
throw NewsletterProcessingException::create()->withMessage(__('WooCommerce is not active', 'mailpoet'));
|
||||
} else {
|
||||
throw NewsletterProcessingException::create()->withMessage($e->getMessage())->withCode($e->getCode());
|
||||
}
|
||||
}
|
||||
if (empty($couponCode)) {
|
||||
throw NewsletterProcessingException::create()->withMessage(__('Couldn\'t find the coupon. Please update the email if the coupon was removed.', 'mailpoet'));
|
||||
}
|
||||
}
|
||||
$element['styles']['block']['width'] = $this->calculateWidth($element, $columnBaseWidth);
|
||||
$styles = 'display:inline-block;-webkit-text-size-adjust:none;mso-hide:all;text-decoration:none;text-align:center;' . StylesHelper::getBlockStyles($element, $exclude = ['textAlign']);
|
||||
$styles = EHelper::escapeHtmlStyleAttr($styles);
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_padded_vertical mailpoet_padded_side" valign="top">
|
||||
<div>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;">
|
||||
<tr>
|
||||
<td class="mailpoet_coupon-container" style="text-align:' . $element['styles']['block']['textAlign'] . ';"><!--[if mso]>
|
||||
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
|
||||
style="height:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['lineHeight']) . ';
|
||||
width:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['width']) . ';
|
||||
v-text-anchor:middle;"
|
||||
arcsize="' . round((int)$element['styles']['block']['borderRadius'] / ((int)$element['styles']['block']['lineHeight'] ?: 1) * 100) . '%"
|
||||
strokeweight="' . EHelper::escapeHtmlAttr($element['styles']['block']['borderWidth']) . '"
|
||||
strokecolor="' . EHelper::escapeHtmlAttr($element['styles']['block']['borderColor']) . '"
|
||||
fillcolor="' . EHelper::escapeHtmlAttr($element['styles']['block']['backgroundColor']) . '">
|
||||
<w:anchorlock/>
|
||||
<center style="color:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['fontColor']) . ';
|
||||
font-family:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['fontFamily']) . ';
|
||||
font-size:' . EHelper::escapeHtmlStyleAttr($element['styles']['block']['fontSize']) . ';
|
||||
font-weight:bold;">' . EHelper::escapeHtmlText($couponCode) . '
|
||||
</center>
|
||||
</v:roundrect>
|
||||
<![endif]-->
|
||||
<!--[if !mso]><!-- -->
|
||||
<div class="mailpoet_coupon" style="' . $styles . '">' . EHelper::escapeHtmlText($couponCode) . '</div>
|
||||
<!--<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
|
||||
public function calculateWidth($element, $columnBaseWidth): string {
|
||||
$columnWidth = $columnBaseWidth - (StylesHelper::$paddingWidth * 2);
|
||||
$borderWidth = (int)$element['styles']['block']['borderWidth'];
|
||||
$width = (int)$element['styles']['block']['width'];
|
||||
$width = ($width > $columnWidth) ?
|
||||
$columnWidth :
|
||||
$width;
|
||||
return ($width - (2 * $borderWidth) . 'px');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
|
||||
class Divider {
|
||||
public function render($element) {
|
||||
$backgroundColor = $element['styles']['block']['backgroundColor'];
|
||||
$dividerCellStyle = "border-top-width: {$element['styles']['block']['borderWidth']};";
|
||||
$dividerCellStyle .= "border-top-style: {$element['styles']['block']['borderStyle']};";
|
||||
$dividerCellStyle .= "border-top-color: {$element['styles']['block']['borderColor']};";
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_divider" valign="top" ' .
|
||||
(($element['styles']['block']['backgroundColor'] !== 'transparent') ?
|
||||
'bgColor="' . EHelper::escapeHtmlAttr($backgroundColor) . '" style="background-color:' . EHelper::escapeHtmlStyleAttr($backgroundColor) . ';' :
|
||||
'style="'
|
||||
) .
|
||||
sprintf(
|
||||
'padding: %s %spx %s %spx;',
|
||||
EHelper::escapeHtmlStyleAttr($element['styles']['block']['padding']),
|
||||
StylesHelper::$paddingWidth,
|
||||
EHelper::escapeHtmlStyleAttr($element['styles']['block']['padding']),
|
||||
StylesHelper::$paddingWidth
|
||||
) . '">
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0"
|
||||
style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;">
|
||||
<tr>
|
||||
<td class="mailpoet_divider-cell" style="' . EHelper::escapeHtmlStyleAttr($dividerCellStyle) . '">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\NewsletterHtmlSanitizer;
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
use MailPoet\Util\pQuery\pQuery;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\CSS;
|
||||
|
||||
class Footer {
|
||||
private NewsletterHtmlSanitizer $htmlSanitizer;
|
||||
private WPFunctions $wp;
|
||||
|
||||
public function __construct(
|
||||
NewsletterHtmlSanitizer $htmlSanitizer,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->htmlSanitizer = $htmlSanitizer;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function render($element) {
|
||||
$element['text'] = preg_replace('/\n/', '<br />', $element['text']);
|
||||
$element['text'] = preg_replace('/(<\/?p.*?>)/i', '', $element['text']);
|
||||
$lineHeight = sprintf(
|
||||
'%spx',
|
||||
StylesHelper::$defaultLineHeight * (int)$element['styles']['text']['fontSize']
|
||||
);
|
||||
if (!is_string($element['text'])) {
|
||||
throw new \RuntimeException('$element[\'text\'] should be a string.');
|
||||
}
|
||||
$dOMParser = new pQuery();
|
||||
$DOM = $dOMParser->parseStr($element['text']);
|
||||
if (isset($element['styles']['link'])) {
|
||||
$links = $DOM->query('a');
|
||||
if ($links->count()) {
|
||||
$css = new CSS();
|
||||
foreach ($links as $link) {
|
||||
$elementLinkStyles = StylesHelper::getStyles($element['styles'], 'link');
|
||||
$link->style = $css->mergeInlineStyles($elementLinkStyles, $link->style);
|
||||
}
|
||||
}
|
||||
}
|
||||
$backgroundColor = $element['styles']['block']['backgroundColor'];
|
||||
$backgroundColor = ($backgroundColor !== 'transparent') ?
|
||||
'bgcolor="' . $this->wp->escAttr($backgroundColor) . '"' :
|
||||
false;
|
||||
if (!$backgroundColor) unset($element['styles']['block']['backgroundColor']);
|
||||
$style = 'line-height: ' . $lineHeight . ';' . StylesHelper::getBlockStyles($element) . StylesHelper::getStyles($element['styles'], 'text');
|
||||
$style = EHelper::escapeHtmlStyleAttr($style);
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_header_footer_padded mailpoet_footer" ' . $backgroundColor . ' style="' . $style . '">
|
||||
' . $this->htmlSanitizer->sanitize($DOM->__toString()) . '
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\NewsletterHtmlSanitizer;
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
use MailPoet\Util\pQuery\pQuery;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\CSS;
|
||||
|
||||
class Header {
|
||||
private NewsletterHtmlSanitizer $htmlSanitizer;
|
||||
private WPFunctions $wp;
|
||||
|
||||
public function __construct(
|
||||
NewsletterHtmlSanitizer $htmlSanitizer,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->htmlSanitizer = $htmlSanitizer;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function render($element) {
|
||||
$element['text'] = preg_replace('/\n/', '<br />', $element['text']);
|
||||
$element['text'] = preg_replace('/(<\/?p.*?>)/i', '', $element['text']);
|
||||
$lineHeight = sprintf(
|
||||
'%spx',
|
||||
StylesHelper::$defaultLineHeight * (int)$element['styles']['text']['fontSize']
|
||||
);
|
||||
if (!is_string($element['text'])) {
|
||||
throw new \RuntimeException('$element[\'text\'] should be a string.');
|
||||
}
|
||||
$dOMParser = new pQuery();
|
||||
$DOM = $dOMParser->parseStr($element['text']);
|
||||
if (isset($element['styles']['link'])) {
|
||||
$links = $DOM->query('a');
|
||||
if ($links->count()) {
|
||||
$css = new CSS();
|
||||
foreach ($links as $link) {
|
||||
$elementLinkStyles = StylesHelper::getStyles($element['styles'], 'link');
|
||||
$link->style = $css->mergeInlineStyles($elementLinkStyles, $link->style);
|
||||
}
|
||||
}
|
||||
}
|
||||
$backgroundColor = $element['styles']['block']['backgroundColor'];
|
||||
$backgroundColor = ($backgroundColor !== 'transparent') ?
|
||||
'bgcolor="' . $this->wp->escAttr($backgroundColor) . '"' :
|
||||
false;
|
||||
if (!$backgroundColor) unset($element['styles']['block']['backgroundColor']);
|
||||
$style = 'line-height: ' . $lineHeight . ';' . StylesHelper::getBlockStyles($element) . StylesHelper::getStyles($element['styles'], 'text');
|
||||
$style = EHelper::escapeHtmlStyleAttr($style);
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_header_footer_padded mailpoet_header" ' . $backgroundColor . ' style="' . $style . '">
|
||||
' . $this->htmlSanitizer->sanitize($DOM->__toString()) . '
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class Image {
|
||||
public function render($element, $columnBaseWidth) {
|
||||
if (empty($element['src'])) {
|
||||
return '';
|
||||
}
|
||||
if (substr($element['src'], 0, 1) == '/' && substr($element['src'], 1, 1) != '/') {
|
||||
$element['src'] = WPFunctions::get()->getOption('siteurl') . $element['src'];
|
||||
}
|
||||
|
||||
$element['width'] = str_replace('px', '', $element['width']);
|
||||
$element['height'] = str_replace('px', '', $element['height']);
|
||||
$originalWidth = 0;
|
||||
if (is_numeric($element['width']) && is_numeric($element['height'])) {
|
||||
$element['width'] = (int)$element['width'];
|
||||
$element['height'] = (int)$element['height'];
|
||||
$originalWidth = $element['width'];
|
||||
$element = $this->adjustImageDimensions($element, $columnBaseWidth);
|
||||
}
|
||||
|
||||
// If image was downsized because of column width set width to aways fill full column (e.g. on mobile)
|
||||
$style = '';
|
||||
if ($element['fullWidth'] === true && $originalWidth > $element['width']) {
|
||||
$style = 'style="width:100%"';
|
||||
}
|
||||
|
||||
$imageTemplate = '
|
||||
<img src="' . EHelper::escapeHtmlLinkAttr($element['src']) . '" width="' . EHelper::escapeHtmlAttr($element['width']) . '" alt="' . EHelper::escapeHtmlAttr($element['alt']) . '"' . $style . '/>
|
||||
';
|
||||
if (!empty($element['link'])) {
|
||||
$imageTemplate = '<a href="' . EHelper::escapeHtmlLinkAttr($element['link']) . '">' . trim($imageTemplate) . '</a>';
|
||||
}
|
||||
$align = 'center';
|
||||
if (!empty($element['styles']['block']['textAlign']) && in_array($element['styles']['block']['textAlign'], ['left', 'right'])) {
|
||||
$align = $element['styles']['block']['textAlign'];
|
||||
}
|
||||
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_image ' . (($element['fullWidth'] === false) ? 'mailpoet_padded_vertical mailpoet_padded_side' : '') . '" align="' . EHelper::escapeHtmlAttr($align) . '" valign="top">
|
||||
' . trim($imageTemplate) . '
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
|
||||
public function adjustImageDimensions($element, $columnBaseWidth) {
|
||||
$paddedWidth = StylesHelper::$paddingWidth * 2;
|
||||
// scale image to fit column width
|
||||
if ($element['width'] > $columnBaseWidth) {
|
||||
$ratio = $element['width'] / $columnBaseWidth;
|
||||
$element['width'] = $columnBaseWidth;
|
||||
$element['height'] = (int)ceil($element['height'] / $ratio);
|
||||
}
|
||||
// resize image if the image is padded and wider than padded column width
|
||||
if (
|
||||
$element['fullWidth'] === false &&
|
||||
$element['width'] > ($columnBaseWidth - $paddedWidth)
|
||||
) {
|
||||
$ratio = $element['width'] / ($columnBaseWidth - $paddedWidth);
|
||||
$element['width'] = $columnBaseWidth - $paddedWidth;
|
||||
$element['height'] = (int)ceil($element['height'] / $ratio);
|
||||
}
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class Placeholder {
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function render($element): string {
|
||||
$placeholder = $element['placeholder'];
|
||||
$class = $element['class'] ?? '';
|
||||
$style = $element['style'] ?? '';
|
||||
return '
|
||||
<tr>
|
||||
<td class="' . $this->wp->escAttr($class) . '" style="' . $this->wp->escAttr($style) . '">
|
||||
' . $this->wp->escHtml($placeholder) . '
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Newsletter\Renderer\Columns\ColumnsHelper;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
|
||||
class Renderer {
|
||||
/** @var AutomatedLatestContentBlock */
|
||||
private $ALC;
|
||||
|
||||
/** @var Button */
|
||||
private $button;
|
||||
|
||||
/** @var Divider */
|
||||
private $divider;
|
||||
|
||||
/** @var Footer */
|
||||
private $footer;
|
||||
|
||||
/** @var Header */
|
||||
private $header;
|
||||
|
||||
/** @var Image */
|
||||
private $image;
|
||||
|
||||
/** @var Social */
|
||||
private $social;
|
||||
|
||||
/** @var Spacer */
|
||||
private $spacer;
|
||||
|
||||
/** @var Text */
|
||||
private $text;
|
||||
|
||||
/** @var Placeholder */
|
||||
private $placeholder;
|
||||
|
||||
/** @var Coupon */
|
||||
private $coupon;
|
||||
|
||||
public function __construct(
|
||||
AutomatedLatestContentBlock $ALC,
|
||||
Button $button,
|
||||
Divider $divider,
|
||||
Footer $footer,
|
||||
Header $header,
|
||||
Image $image,
|
||||
Social $social,
|
||||
Spacer $spacer,
|
||||
Text $text,
|
||||
Placeholder $placeholder,
|
||||
Coupon $coupon
|
||||
) {
|
||||
$this->ALC = $ALC;
|
||||
$this->button = $button;
|
||||
$this->divider = $divider;
|
||||
$this->footer = $footer;
|
||||
$this->header = $header;
|
||||
$this->image = $image;
|
||||
$this->social = $social;
|
||||
$this->spacer = $spacer;
|
||||
$this->text = $text;
|
||||
$this->placeholder = $placeholder;
|
||||
$this->coupon = $coupon;
|
||||
}
|
||||
|
||||
public function render(NewsletterEntity $newsletter, $data) {
|
||||
if (is_null($data['blocks']) && isset($data['type'])) {
|
||||
return null;
|
||||
}
|
||||
$columnCount = count($data['blocks']);
|
||||
$columnsLayout = isset($data['columnLayout']) ? $data['columnLayout'] : null;
|
||||
$columnWidths = ColumnsHelper::columnWidth($columnCount, $columnsLayout);
|
||||
$columnContent = [];
|
||||
|
||||
foreach ($data['blocks'] as $index => $columnBlocks) {
|
||||
$renderedBlockElement = $this->renderBlocksInColumn($newsletter, $columnBlocks, $columnWidths[$index]);
|
||||
$columnContent[] = $renderedBlockElement;
|
||||
}
|
||||
|
||||
return $columnContent;
|
||||
}
|
||||
|
||||
private function renderBlocksInColumn(NewsletterEntity $newsletter, $block, $columnBaseWidth) {
|
||||
$blockContent = '';
|
||||
$_this = $this;
|
||||
array_map(function($block) use (&$blockContent, $columnBaseWidth, $newsletter, $_this) {
|
||||
$renderedBlockElement = $_this->createElementFromBlockType($newsletter, $block, $columnBaseWidth);
|
||||
if (isset($block['blocks'])) {
|
||||
$renderedBlockElement = $_this->renderBlocksInColumn($newsletter, $block, $columnBaseWidth);
|
||||
// nested vertical column container is rendered as an array
|
||||
if (is_array($renderedBlockElement)) {
|
||||
$renderedBlockElement = implode('', $renderedBlockElement);
|
||||
}
|
||||
}
|
||||
|
||||
$blockContent .= $renderedBlockElement;
|
||||
}, $block['blocks']);
|
||||
return $blockContent;
|
||||
}
|
||||
|
||||
public function createElementFromBlockType(NewsletterEntity $newsletter, $block, $columnBaseWidth) {
|
||||
if ($block['type'] === 'automatedLatestContent') {
|
||||
return $this->processAutomatedLatestContent($newsletter, $block, $columnBaseWidth);
|
||||
}
|
||||
$block = StylesHelper::applyTextAlignment($block);
|
||||
switch ($block['type']) {
|
||||
case 'button':
|
||||
return $this->button->render($block, $columnBaseWidth);
|
||||
case 'divider':
|
||||
return $this->divider->render($block);
|
||||
case 'footer':
|
||||
return $this->footer->render($block);
|
||||
case 'header':
|
||||
return $this->header->render($block);
|
||||
case 'image':
|
||||
return $this->image->render($block, $columnBaseWidth);
|
||||
case 'social':
|
||||
return $this->social->render($block);
|
||||
case 'spacer':
|
||||
return $this->spacer->render($block);
|
||||
case 'text':
|
||||
return $this->text->render($block);
|
||||
case 'placeholder':
|
||||
return $this->placeholder->render($block);
|
||||
case Coupon::TYPE:
|
||||
return $this->coupon->render($block, $columnBaseWidth);
|
||||
}
|
||||
return "<!-- Skipped unsupported block type: {$block['type']} -->";
|
||||
}
|
||||
|
||||
public function processAutomatedLatestContent(NewsletterEntity $newsletter, $args, $columnBaseWidth) {
|
||||
$transformedPosts = [
|
||||
'blocks' => $this->ALC->render($newsletter, $args),
|
||||
];
|
||||
$transformedPosts = StylesHelper::applyTextAlignment($transformedPosts);
|
||||
return $this->renderBlocksInColumn($newsletter, $transformedPosts, $columnBaseWidth);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
|
||||
class Social {
|
||||
public function render($element) {
|
||||
$iconsBlock = '';
|
||||
if (is_array($element['icons'])) {
|
||||
foreach ($element['icons'] as $index => $icon) {
|
||||
if (empty($icon['image'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$style = 'width:' . $icon['width'] . ';height:' . $icon['width'] . ';-ms-interpolation-mode:bicubic;border:0;display:inline;outline:none;';
|
||||
$iconsBlock .= '<a href="' . EHelper::escapeHtmlLinkAttr($icon['link']) . '" style="text-decoration:none!important;"
|
||||
><img
|
||||
src="' . EHelper::escapeHtmlLinkAttr($icon['image']) . '"
|
||||
width="' . (int)$icon['width'] . '"
|
||||
height="' . (int)$icon['height'] . '"
|
||||
style="' . EHelper::escapeHtmlStyleAttr($style) . '"
|
||||
alt="' . EHelper::escapeHtmlAttr($icon['iconType']) . '"
|
||||
></a> ';
|
||||
}
|
||||
}
|
||||
$alignment = isset($element['styles']['block']['textAlign']) ? $element['styles']['block']['textAlign'] : 'center';
|
||||
if (!empty($iconsBlock)) {
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_padded_side mailpoet_padded_vertical" valign="top" align="' . EHelper::escapeHtmlAttr($alignment) . '">
|
||||
' . $iconsBlock . '
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
|
||||
class Spacer {
|
||||
public function render($element) {
|
||||
$height = (int)$element['styles']['block']['height'];
|
||||
$backgroundColor = EHelper::escapeHtmlAttr($element['styles']['block']['backgroundColor']);
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_spacer" ' .
|
||||
(($backgroundColor !== 'transparent') ? 'bgcolor="' . $backgroundColor . '" ' : '') .
|
||||
'height="' . $height . '" valign="top"></td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Blocks;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Editor\PostContentManager;
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
use MailPoet\Newsletter\Renderer\StylesHelper;
|
||||
use MailPoet\Util\pQuery\pQuery;
|
||||
|
||||
class Text {
|
||||
public function render($element) {
|
||||
$html = $element['text'];
|
||||
// replace with spaces
|
||||
$html = str_replace(' ', ' ', $html);
|
||||
$html = str_replace('\xc2\xa0', ' ', $html);
|
||||
$html = $this->convertBlockquotesToTables($html);
|
||||
$html = $this->convertParagraphsToTables($html);
|
||||
$html = $this->styleLists($html);
|
||||
$html = $this->styleHeadings($html);
|
||||
$html = $this->removeLastLineBreak($html);
|
||||
$template = '
|
||||
<tr>
|
||||
<td class="mailpoet_text mailpoet_padded_vertical mailpoet_padded_side" valign="top" style="word-break:break-word;word-wrap:break-word;">
|
||||
' . $html . '
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
|
||||
public function convertBlockquotesToTables($html) {
|
||||
$dOMParser = new pQuery();
|
||||
$DOM = $dOMParser->parseStr($html);
|
||||
$blockquotes = $DOM->query('blockquote');
|
||||
foreach ($blockquotes as $blockquote) {
|
||||
$contents = [];
|
||||
$paragraphs = $blockquote->query('p, h1, h2, h3, h4', 0);
|
||||
foreach ($paragraphs as $index => $paragraph) {
|
||||
if (preg_match('/h\d/', $paragraph->getTag())) {
|
||||
$contents[] = $paragraph->getOuterText();
|
||||
} else {
|
||||
$contents[] = $paragraph->toString(true, true, 1);
|
||||
}
|
||||
if ($index + 1 < $paragraphs->count()) $contents[] = '<br />';
|
||||
$paragraph->remove();
|
||||
}
|
||||
if (empty($contents)) continue;
|
||||
$blockquote->setTag('table');
|
||||
$blockquote->addClass('mailpoet_blockquote');
|
||||
$blockquote->width = '100%';
|
||||
$blockquote->spacing = 0;
|
||||
$blockquote->border = 0;
|
||||
$blockquote->cellpadding = 0;
|
||||
$blockquote->html('
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="2" bgcolor="#565656"></td>
|
||||
<td width="10"></td>
|
||||
<td valign="top">
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
|
||||
<tr>
|
||||
<td class="mailpoet_blockquote">
|
||||
' . implode('', $contents) . '
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>');
|
||||
$blockquote = $this->insertLineBreak($blockquote);
|
||||
}
|
||||
return $DOM->__toString();
|
||||
}
|
||||
|
||||
public function convertParagraphsToTables($html) {
|
||||
$dOMParser = new pQuery();
|
||||
$DOM = $dOMParser->parseStr($html);
|
||||
$paragraphs = $DOM->query('p');
|
||||
if (!$paragraphs->count()) return $html;
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
// process empty paragraphs
|
||||
if (!trim($paragraph->html())) {
|
||||
$nextElement = ($paragraph->getNextSibling()) ?
|
||||
trim($paragraph->getNextSibling()->text()) :
|
||||
false;
|
||||
$previousElement = ($paragraph->getPreviousSibling()) ?
|
||||
trim($paragraph->getPreviousSibling()->text()) :
|
||||
false;
|
||||
$previousElementTag = ($previousElement) ?
|
||||
$paragraph->getPreviousSibling()->tag :
|
||||
false;
|
||||
// if previous or next paragraphs are empty OR previous paragraph
|
||||
// is a heading, insert a break line
|
||||
if (
|
||||
!$nextElement ||
|
||||
!$previousElement ||
|
||||
(preg_match('/h\d+/', $previousElementTag))
|
||||
) {
|
||||
$paragraph = $this->insertLineBreak($paragraph);
|
||||
}
|
||||
$paragraph->remove();
|
||||
continue;
|
||||
}
|
||||
$style = (string)$paragraph->style;
|
||||
if (!preg_match('/text-align/i', $style)) {
|
||||
$style = 'text-align: left;' . $style;
|
||||
}
|
||||
$contents = $paragraph->toString(true, true, 1);
|
||||
$paragraph->setTag('table');
|
||||
$paragraph->style = 'border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;';
|
||||
$paragraph->width = '100%';
|
||||
$paragraph->cellpadding = 0;
|
||||
$nextElement = $paragraph->getNextSibling();
|
||||
// unless this is the last element in column, add double line breaks
|
||||
$lineBreaks = ($nextElement && !trim($nextElement->text())) ?
|
||||
'<br /><br />' :
|
||||
'';
|
||||
// if this element is followed by a list, add single line break
|
||||
$lineBreaks = ($nextElement && preg_match('/<li/i', $nextElement->getOuterText())) ?
|
||||
'<br />' :
|
||||
$lineBreaks;
|
||||
if ($paragraph->hasClass(PostContentManager::WP_POST_CLASS)) {
|
||||
$paragraph->removeClass(PostContentManager::WP_POST_CLASS);
|
||||
// if this element is followed by a paragraph or heading, add double line breaks
|
||||
$lineBreaks = ($nextElement && preg_match('/<(p|h[1-6]{1})/i', $nextElement->getOuterText())) ?
|
||||
'<br /><br />' :
|
||||
$lineBreaks;
|
||||
}
|
||||
$paragraph->html('
|
||||
<tr>
|
||||
<td class="mailpoet_paragraph" style="word-break:break-word;word-wrap:break-word;' . EHelper::escapeHtmlStyleAttr($style) . '">
|
||||
' . $contents . $lineBreaks . '
|
||||
</td>
|
||||
</tr>');
|
||||
}
|
||||
return $DOM->__toString();
|
||||
}
|
||||
|
||||
public function styleLists($html) {
|
||||
$dOMParser = new pQuery();
|
||||
$DOM = $dOMParser->parseStr($html);
|
||||
$lists = $DOM->query('ol, ul, li');
|
||||
if (!$lists->count()) return $html;
|
||||
foreach ($lists as $list) {
|
||||
if ($list->tag === 'li') {
|
||||
$list->setInnertext($list->toString(true, true, 1));
|
||||
$list->class = 'mailpoet_paragraph';
|
||||
} else {
|
||||
$list->class = 'mailpoet_paragraph';
|
||||
$list->style = StylesHelper::joinStyles($list->style, 'padding-top:0;padding-bottom:0;margin-top:10px;');
|
||||
}
|
||||
$list->style = StylesHelper::applyTextAlignment($list->style);
|
||||
$list->style = StylesHelper::joinStyles($list->style, 'margin-bottom:10px;');
|
||||
$list->style = EHelper::escapeHtmlStyleAttr($list->style);
|
||||
}
|
||||
return $DOM->__toString();
|
||||
}
|
||||
|
||||
public function styleHeadings($html) {
|
||||
$dOMParser = new pQuery();
|
||||
$DOM = $dOMParser->parseStr($html);
|
||||
$headings = $DOM->query('h1, h2, h3, h4');
|
||||
if (!$headings->count()) return $html;
|
||||
foreach ($headings as $heading) {
|
||||
$heading->style = StylesHelper::applyTextAlignment($heading->style);
|
||||
$heading->style = StylesHelper::joinStyles($heading->style, 'padding:0;font-style:normal;font-weight:normal;');
|
||||
$heading->style = EHelper::escapeHtmlStyleAttr($heading->style);
|
||||
}
|
||||
return $DOM->__toString();
|
||||
}
|
||||
|
||||
public function removeLastLineBreak($html) {
|
||||
return preg_replace('/(^)?(<br[^>]*?\/?>)+$/i', '', $html);
|
||||
}
|
||||
|
||||
public function insertLineBreak($element) {
|
||||
$element->parent->insertChild(
|
||||
[
|
||||
'tag_name' => 'br',
|
||||
'self_close' => true,
|
||||
'attributes' => [],
|
||||
],
|
||||
$element->index() + 1
|
||||
);
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
|
||||
class BodyRenderer {
|
||||
/** @var Blocks\Renderer */
|
||||
private $blocksRenderer;
|
||||
|
||||
/** @var Columns\Renderer */
|
||||
private $columnsRenderer;
|
||||
|
||||
public function __construct(
|
||||
Blocks\Renderer $blocksRenderer,
|
||||
Columns\Renderer $columnsRenderer
|
||||
) {
|
||||
$this->blocksRenderer = $blocksRenderer;
|
||||
$this->columnsRenderer = $columnsRenderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param NewsletterEntity $newsletter
|
||||
* @param array $content
|
||||
* @return string
|
||||
*/
|
||||
public function renderBody(NewsletterEntity $newsletter, array $content) {
|
||||
$blocks = (array_key_exists('blocks', $content))
|
||||
? $content['blocks']
|
||||
: [];
|
||||
|
||||
$renderedContent = [];
|
||||
foreach ($blocks as $contentBlock) {
|
||||
$columnsData = $this->blocksRenderer->render($newsletter, $contentBlock);
|
||||
|
||||
$renderedContent[] = $this->columnsRenderer->render(
|
||||
$contentBlock,
|
||||
$columnsData
|
||||
);
|
||||
}
|
||||
return implode('', $renderedContent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Columns;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class ColumnsHelper {
|
||||
public static $columnsWidth = [
|
||||
1 => [660],
|
||||
2 => [330, 330],
|
||||
"1_2" => [220, 440],
|
||||
"2_1" => [440, 220],
|
||||
3 => [220, 220, 220],
|
||||
];
|
||||
|
||||
public static $columnsClass = [
|
||||
1 => 'cols-one',
|
||||
2 => 'cols-two',
|
||||
3 => 'cols-three',
|
||||
];
|
||||
|
||||
public static $columnsAlignment = [
|
||||
1 => null,
|
||||
2 => 'left',
|
||||
3 => 'right',
|
||||
];
|
||||
|
||||
/** @return int[] */
|
||||
public static function columnWidth($columnsCount, $columnsLayout) {
|
||||
if (isset(self::$columnsWidth[$columnsLayout])) {
|
||||
return self::$columnsWidth[$columnsLayout];
|
||||
}
|
||||
return self::$columnsWidth[$columnsCount];
|
||||
}
|
||||
|
||||
public static function columnClass($columnsCount) {
|
||||
return self::$columnsClass[$columnsCount];
|
||||
}
|
||||
|
||||
public static function columnClasses() {
|
||||
return self::$columnsClass;
|
||||
}
|
||||
|
||||
public static function columnAlignment($columnsCount) {
|
||||
return self::$columnsAlignment[$columnsCount];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\Columns;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
|
||||
class Renderer {
|
||||
public function render($contentBlock, $columnsData) {
|
||||
if (is_null($contentBlock['blocks']) && isset($contentBlock['type'])) {
|
||||
return "<!-- Skipped unsupported block type: {$contentBlock['type']} -->";
|
||||
}
|
||||
|
||||
$columnsCount = count($contentBlock['blocks']);
|
||||
|
||||
if ($columnsCount === 1) {
|
||||
return $this->renderOneColumn($contentBlock, $columnsData[0]);
|
||||
}
|
||||
return $this->renderMultipleColumns($contentBlock, $columnsData);
|
||||
}
|
||||
|
||||
private function renderOneColumn($contentBlock, $content) {
|
||||
$template = $this->getOneColumnTemplate(
|
||||
$contentBlock['styles']['block'],
|
||||
isset($contentBlock['image']) ? $contentBlock['image'] : null
|
||||
);
|
||||
return $template['content_start'] . $content . $template['content_end'];
|
||||
}
|
||||
|
||||
public function getOneColumnTemplate($styles, $image) {
|
||||
$backgroundCss = $this->getBackgroundCss($styles, $image);
|
||||
$template['content_start'] = '
|
||||
<tr>
|
||||
<td class="mailpoet_content" align="center" style="border-collapse:collapse;' . $backgroundCss . '" ' . $this->getBgColorAttribute($styles, $image) . '>
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding-left:0;padding-right:0">
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" class="mailpoet_' . ColumnsHelper::columnClass(1) . '" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;table-layout:fixed;margin-left:auto;margin-right:auto;padding-left:0;padding-right:0;">
|
||||
<tbody>';
|
||||
$template['content_end'] = '
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>';
|
||||
return $template;
|
||||
}
|
||||
|
||||
private function renderMultipleColumns($contentBlock, $columnsData) {
|
||||
$columnsCount = count($contentBlock['blocks']);
|
||||
$columnsLayout = isset($contentBlock['columnLayout']) ? $contentBlock['columnLayout'] : null;
|
||||
|
||||
$widths = ColumnsHelper::columnWidth($columnsCount, $columnsLayout);
|
||||
$class = ColumnsHelper::columnClass($columnsCount);
|
||||
$alignment = ColumnsHelper::columnAlignment($columnsCount);
|
||||
$index = 0;
|
||||
$result = $this->getMultipleColumnsContainerStart($class, $contentBlock['styles']['block'], isset($contentBlock['image']) ? $contentBlock['image'] : null);
|
||||
foreach ($columnsData as $content) {
|
||||
$result .= $this->getMultipleColumnsContentStart($widths[$index++], $alignment, $class);
|
||||
$result .= $content;
|
||||
$result .= $this->getMultipleColumnsContentEnd();
|
||||
}
|
||||
$result .= $this->getMultipleColumnsContainerEnd();
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getMultipleColumnsContainerStart($class, $styles, $image) {
|
||||
return '
|
||||
<tr>
|
||||
<td class="mailpoet_content-' . $class . '" align="left" style="border-collapse:collapse;' . $this->getBackgroundCss($styles, $image) . '" ' . $this->getBgColorAttribute($styles, $image) . '>
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" style="font-size:0;"><!--[if mso]>
|
||||
<table border="0" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>';
|
||||
}
|
||||
|
||||
private function getMultipleColumnsContainerEnd() {
|
||||
return '
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<![endif]--></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>';
|
||||
}
|
||||
|
||||
private function getMultipleColumnsContentEnd() {
|
||||
return '
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!--[if mso]>
|
||||
</td>';
|
||||
}
|
||||
|
||||
public function getMultipleColumnsContentStart($width, $alignment, $class) {
|
||||
return '
|
||||
<td width="' . $width . '" valign="top">
|
||||
<![endif]--><div style="display:inline-block; max-width:' . $width . 'px; vertical-align:top; width:100%;">
|
||||
<table width="' . $width . '" class="mailpoet_' . $class . '" border="0" cellpadding="0" cellspacing="0" align="' . $alignment . '" style="width:100%;max-width:' . $width . 'px;border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;table-layout:fixed;margin-left:auto;margin-right:auto;padding-left:0;padding-right:0;">
|
||||
<tbody>';
|
||||
}
|
||||
|
||||
private function getBackgroundCss($styles, $image) {
|
||||
if ($image !== null && $image['src'] !== null) {
|
||||
$backgroundColor = isset($styles['backgroundColor']) && $styles['backgroundColor'] !== 'transparent' ? $styles['backgroundColor'] : '#ffffff';
|
||||
$repeat = $image['display'] === 'tile' ? 'repeat' : 'no-repeat';
|
||||
$size = $image['display'] === 'scale' ? 'cover' : 'contain';
|
||||
$style = sprintf(
|
||||
'background: %s url(%s) %s center/%s;background-color: %s;background-image: url(%s);background-repeat: %s;background-position: center;background-size: %s;',
|
||||
$backgroundColor,
|
||||
$image['src'],
|
||||
$repeat,
|
||||
$size,
|
||||
$backgroundColor,
|
||||
$image['src'],
|
||||
$repeat,
|
||||
$size
|
||||
);
|
||||
return EHelper::escapeHtmlStyleAttr($style);
|
||||
} else {
|
||||
if (!isset($styles['backgroundColor'])) return false;
|
||||
$backgroundColor = $styles['backgroundColor'];
|
||||
return ($backgroundColor !== 'transparent') ?
|
||||
EHelper::escapeHtmlStyleAttr(sprintf('background-color:%s!important;', $backgroundColor)) :
|
||||
false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getBgColorAttribute($styles, $image) {
|
||||
if (
|
||||
($image === null || $image['src'] === null)
|
||||
&& isset($styles['backgroundColor'])
|
||||
&& $styles['backgroundColor'] !== 'transparent'
|
||||
) {
|
||||
return 'bgcolor="' . EHelper::escapeHtmlAttr($styles['backgroundColor']) . '"';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class EscapeHelper {
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function escapeHtmlText($string) {
|
||||
return htmlspecialchars((string)$string, ENT_NOQUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function escapeHtmlAttr($string) {
|
||||
return htmlspecialchars((string)$string, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes Style attributes, but preserves single quotes. Some email clients
|
||||
* (e.g. Yahoo webmail) don't support encoded quoted font names.
|
||||
* Previously used htmlspecialchars but switched to esc_attr which is more appropriate.
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function escapeHtmlStyleAttr($string) {
|
||||
return str_replace(''', "'", esc_attr((string)$string));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function unescapeHtmlStyleAttr($string) {
|
||||
// This decodes entities which may have been added by esc_attr.
|
||||
return htmlspecialchars_decode((string)$string, ENT_QUOTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function escapeHtmlLinkAttr($string) {
|
||||
$string = self::escapeHtmlAttr($string);
|
||||
if (preg_match('/\s*(javascript:|data:text|data:application)/ui', $string) === 1) {
|
||||
return '';
|
||||
}
|
||||
return $string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer\PostProcess;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Newsletter\Links\Links;
|
||||
use MailPoet\Newsletter\Renderer\Renderer;
|
||||
use MailPoet\Util\pQuery\pQuery;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class OpenTracking {
|
||||
public static function process($template) {
|
||||
$DOM = new pQuery();
|
||||
$DOM = $DOM->parseStr($template);
|
||||
$template = $DOM->select('body');
|
||||
// url is a temporary data tag that will be further replaced with
|
||||
// the proper track API URL during sending
|
||||
$url = Links::DATA_TAG_OPEN;
|
||||
$openTrackingImage = sprintf(
|
||||
'<img alt="" class="" src="%s"/>',
|
||||
$url
|
||||
);
|
||||
self::appendToDomNodes($template, $openTrackingImage);
|
||||
return $DOM->__toString();
|
||||
}
|
||||
|
||||
public static function addTrackingImage() {
|
||||
WPFunctions::get()->addFilter(Renderer::FILTER_POST_PROCESS, function ($template) {
|
||||
return OpenTracking::process($template);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function appendToDomNodes($template, $openTrackingImage): void {
|
||||
// Preserve backward compatibility with pQuery::html()
|
||||
// by processing an array of DomNodes
|
||||
if (!empty($template)) {
|
||||
$template = is_array($template) ? $template : [$template];
|
||||
array_map(
|
||||
function ($item) use ($openTrackingImage) {
|
||||
$itemHtml = $item->toString(true, true, 1);
|
||||
$item->html($itemHtml . $openTrackingImage);
|
||||
return $item;
|
||||
},
|
||||
$template
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\SendingQueueEntity;
|
||||
use MailPoet\Newsletter\Renderer\Blocks\AbandonedCartContent;
|
||||
use MailPoet\Newsletter\Renderer\Blocks\AutomatedLatestContentBlock;
|
||||
use MailPoet\WooCommerce\CouponPreProcessor;
|
||||
use MailPoet\WooCommerce\TransactionalEmails\ContentPreprocessor;
|
||||
|
||||
class Preprocessor {
|
||||
const WC_HEADING_BEFORE = '
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
|
||||
<tr>
|
||||
<td class="mailpoet_text" valign="top" style="padding-top:20px;padding-bottom:20px;word-break:break-word;word-wrap:break-word;">';
|
||||
const WC_HEADING_AFTER = '
|
||||
</td>
|
||||
</tr>
|
||||
</table>';
|
||||
|
||||
/** @var AbandonedCartContent */
|
||||
private $abandonedCartContent;
|
||||
|
||||
/** @var AutomatedLatestContentBlock */
|
||||
private $automatedLatestContent;
|
||||
|
||||
/** @var ContentPreprocessor */
|
||||
private $wooCommerceContentPreprocessor;
|
||||
|
||||
/*** @var CouponPreProcessor */
|
||||
private $couponPreProcessor;
|
||||
|
||||
public function __construct(
|
||||
AbandonedCartContent $abandonedCartContent,
|
||||
AutomatedLatestContentBlock $automatedLatestContent,
|
||||
ContentPreprocessor $wooCommerceContentPreprocessor,
|
||||
CouponPreProcessor $couponPreProcessor
|
||||
) {
|
||||
$this->abandonedCartContent = $abandonedCartContent;
|
||||
$this->automatedLatestContent = $automatedLatestContent;
|
||||
$this->wooCommerceContentPreprocessor = $wooCommerceContentPreprocessor;
|
||||
$this->couponPreProcessor = $couponPreProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $content
|
||||
* @param NewsletterEntity $newsletter
|
||||
* @return array
|
||||
*/
|
||||
public function process(NewsletterEntity $newsletter, $content, bool $preview = false, SendingQueueEntity $sendingQueue = null) {
|
||||
if (!array_key_exists('blocks', $content)) {
|
||||
return $content;
|
||||
}
|
||||
$contentBlocks = $content['blocks'];
|
||||
$contentBlocks = $this->couponPreProcessor->processCoupons($newsletter, $contentBlocks, $preview);
|
||||
$content['blocks'] = $this->processContainer($newsletter, $contentBlocks, $preview, $sendingQueue);
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function processContainer(NewsletterEntity $newsletter, $blocks, bool $preview, ?SendingQueueEntity $sendingQueue): array {
|
||||
$containerBlocks = [];
|
||||
foreach ($blocks as $block) {
|
||||
if ($block['type'] === 'container' && isset($block['blocks'])) {
|
||||
$block['blocks'] = $this->processContainer($newsletter, $block['blocks'], $preview, $sendingQueue);
|
||||
$containerBlocks = array_merge($containerBlocks, [$block]);
|
||||
} else {
|
||||
$processedBlock = $this->processBlock($newsletter, $block, $preview, $sendingQueue);
|
||||
if (!empty($processedBlock)) {
|
||||
$containerBlocks = array_merge($containerBlocks, $processedBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $containerBlocks;
|
||||
}
|
||||
|
||||
public function processBlock(NewsletterEntity $newsletter, array $block, bool $preview = false, SendingQueueEntity $sendingQueue = null): array {
|
||||
switch ($block['type']) {
|
||||
case 'abandonedCartContent':
|
||||
return $this->abandonedCartContent->render($newsletter, $block, $preview, $sendingQueue);
|
||||
case 'automatedLatestContentLayout':
|
||||
return $this->automatedLatestContent->render($newsletter, $block);
|
||||
case 'woocommerceHeading':
|
||||
return $this->wooCommerceContentPreprocessor->preprocessHeader();
|
||||
case 'woocommerceContent':
|
||||
return $this->wooCommerceContentPreprocessor->preprocessContent();
|
||||
}
|
||||
return [$block];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Config\Env;
|
||||
use MailPoet\EmailEditor\Engine\Renderer\Renderer as GuntenbergRenderer;
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\SendingQueueEntity;
|
||||
use MailPoet\Logging\LoggerFactory;
|
||||
use MailPoet\Newsletter\NewslettersRepository;
|
||||
use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
|
||||
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
|
||||
use MailPoet\NewsletterProcessingException;
|
||||
use MailPoet\Util\License\Features\CapabilitiesManager;
|
||||
use MailPoet\Util\pQuery\DomNode;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use MailPoetVendor\Html2Text\Html2Text;
|
||||
|
||||
class Renderer {
|
||||
const NEWSLETTER_TEMPLATE = 'Template.html';
|
||||
const FILTER_POST_PROCESS = 'mailpoet_rendering_post_process';
|
||||
|
||||
/** @var BodyRenderer */
|
||||
private $bodyRenderer;
|
||||
|
||||
/** @var GuntenbergRenderer */
|
||||
private $guntenbergRenderer;
|
||||
|
||||
/** @var Preprocessor */
|
||||
private $preprocessor;
|
||||
|
||||
/** @var \MailPoetVendor\CSS */
|
||||
private $cSSInliner;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/*** @var LoggerFactory */
|
||||
private $loggerFactory;
|
||||
|
||||
/*** @var NewslettersRepository */
|
||||
private $newslettersRepository;
|
||||
|
||||
/*** @var SendingQueuesRepository */
|
||||
private $sendingQueuesRepository;
|
||||
|
||||
private CapabilitiesManager $capabilitiesManager;
|
||||
|
||||
public function __construct(
|
||||
BodyRenderer $bodyRenderer,
|
||||
GuntenbergRenderer $guntenbergRenderer,
|
||||
Preprocessor $preprocessor,
|
||||
\MailPoetVendor\CSS $cSSInliner,
|
||||
WPFunctions $wp,
|
||||
LoggerFactory $loggerFactory,
|
||||
NewslettersRepository $newslettersRepository,
|
||||
SendingQueuesRepository $sendingQueuesRepository,
|
||||
CapabilitiesManager $capabilitiesManager
|
||||
) {
|
||||
$this->bodyRenderer = $bodyRenderer;
|
||||
$this->guntenbergRenderer = $guntenbergRenderer;
|
||||
$this->preprocessor = $preprocessor;
|
||||
$this->cSSInliner = $cSSInliner;
|
||||
$this->wp = $wp;
|
||||
$this->loggerFactory = $loggerFactory;
|
||||
$this->newslettersRepository = $newslettersRepository;
|
||||
$this->sendingQueuesRepository = $sendingQueuesRepository;
|
||||
$this->capabilitiesManager = $capabilitiesManager;
|
||||
}
|
||||
|
||||
public function render(NewsletterEntity $newsletter, SendingQueueEntity $sendingQueue = null, $type = false) {
|
||||
return $this->_render($newsletter, $sendingQueue, $type);
|
||||
}
|
||||
|
||||
public function renderAsPreview(NewsletterEntity $newsletter, $type = false, ?string $subject = null) {
|
||||
return $this->_render($newsletter, null, $type, true, $subject);
|
||||
}
|
||||
|
||||
private function _render(NewsletterEntity $newsletter, SendingQueueEntity $sendingQueue = null, $type = false, $preview = false, $subject = null) {
|
||||
$language = $this->wp->getBloginfo('language');
|
||||
$metaRobots = $preview ? '<meta name="robots" content="noindex, nofollow" />' : '';
|
||||
$subject = $subject ?: $newsletter->getSubject();
|
||||
$wpPostEntity = $newsletter->getWpPost();
|
||||
$wpPost = $wpPostEntity ? $wpPostEntity->getWpPostInstance() : null;
|
||||
if ($wpPost instanceof \WP_Post) {
|
||||
$renderedNewsletter = $this->guntenbergRenderer->render($wpPost, $subject, $newsletter->getPreheader(), $language, $metaRobots);
|
||||
} else {
|
||||
$body = (is_array($newsletter->getBody()))
|
||||
? $newsletter->getBody()
|
||||
: [];
|
||||
$content = (array_key_exists('content', $body))
|
||||
? $body['content']
|
||||
: [];
|
||||
$styles = (array_key_exists('globalStyles', $body))
|
||||
? $body['globalStyles']
|
||||
: [];
|
||||
|
||||
$mailPoetLogoInEmails = $this->capabilitiesManager->getCapability('mailpoetLogoInEmails');
|
||||
if (
|
||||
(isset($mailPoetLogoInEmails) && $mailPoetLogoInEmails->isRestricted) && !$preview
|
||||
) {
|
||||
$content = $this->addMailpoetLogoContentBlock($content, $styles);
|
||||
}
|
||||
|
||||
$renderedBody = "";
|
||||
try {
|
||||
$content = $this->preprocessor->process($newsletter, $content, $preview, $sendingQueue);
|
||||
$renderedBody = $this->bodyRenderer->renderBody($newsletter, $content);
|
||||
} catch (NewsletterProcessingException $e) {
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_COUPONS)->error(
|
||||
$e->getMessage(),
|
||||
['newsletter_id' => $newsletter->getId()]
|
||||
);
|
||||
$this->newslettersRepository->setAsCorrupt($newsletter);
|
||||
if ($sendingQueue) {
|
||||
$this->sendingQueuesRepository->pause($sendingQueue);
|
||||
}
|
||||
}
|
||||
$renderedStyles = $this->renderStyles($styles);
|
||||
$customFontsLinks = StylesHelper::getCustomFontsLinks($styles);
|
||||
|
||||
$template = $this->injectContentIntoTemplate(
|
||||
(string)file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE),
|
||||
[
|
||||
$language,
|
||||
$metaRobots,
|
||||
htmlspecialchars($subject),
|
||||
$renderedStyles,
|
||||
$customFontsLinks,
|
||||
EHelper::escapeHtmlText($newsletter->getPreheader()),
|
||||
$renderedBody,
|
||||
]
|
||||
);
|
||||
if ($template === null) {
|
||||
$template = '';
|
||||
}
|
||||
$templateDom = $this->inlineCSSStyles($template);
|
||||
$template = $this->postProcessTemplate($templateDom);
|
||||
|
||||
$renderedNewsletter = [
|
||||
'html' => $template,
|
||||
'text' => $this->renderTextVersion($template),
|
||||
];
|
||||
}
|
||||
|
||||
return ($type && !empty($renderedNewsletter[$type])) ?
|
||||
$renderedNewsletter[$type] :
|
||||
$renderedNewsletter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $styles
|
||||
* @return string
|
||||
*/
|
||||
private function renderStyles(array $styles) {
|
||||
$css = '';
|
||||
foreach ($styles as $selector => $style) {
|
||||
switch ($selector) {
|
||||
case 'text':
|
||||
$selector = 'td.mailpoet_paragraph, td.mailpoet_blockquote, li.mailpoet_paragraph';
|
||||
break;
|
||||
case 'body':
|
||||
$selector = 'body, .mailpoet-wrapper';
|
||||
break;
|
||||
case 'link':
|
||||
$selector = '.mailpoet-wrapper a';
|
||||
break;
|
||||
case 'wrapper':
|
||||
$selector = '.mailpoet_content-wrapper';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!is_array($style)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$css .= StylesHelper::setStyle($style, $selector);
|
||||
}
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
* @param string[] $content
|
||||
* @return string|null
|
||||
*/
|
||||
private function injectContentIntoTemplate($template, $content) {
|
||||
return preg_replace_callback('/{{\w+}}/', function($matches) use (&$content) {
|
||||
return array_shift($content);
|
||||
}, $template);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
* @return DomNode
|
||||
*/
|
||||
private function inlineCSSStyles($template) {
|
||||
return $this->cSSInliner->inlineCSS($template);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
* @return string
|
||||
*/
|
||||
private function renderTextVersion($template) {
|
||||
$template = (mb_detect_encoding($template, 'UTF-8', true)) ? $template : mb_convert_encoding($template, 'UTF-8', mb_list_encodings());
|
||||
return @Html2Text::convert($template);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DomNode $templateDom
|
||||
* @return string
|
||||
*/
|
||||
private function postProcessTemplate(DomNode $templateDom) {
|
||||
// replace spaces in image tag URLs
|
||||
foreach ($templateDom->query('img') as $image) {
|
||||
$image->src = str_replace(' ', '%20', $image->src);
|
||||
}
|
||||
foreach ($templateDom->query('a') as $anchor) {
|
||||
// Fix for a TinyMCE bug in smart paste which encodes & as & which is then additionally encoded to &amp;
|
||||
// when saving the text block content in the editor
|
||||
$href = str_replace('&amp;', '&', $anchor->href);
|
||||
// Replace & with & in the href attributes of anchors. URLs are encoded when TinyMCE extracts Text block content via content.innerHTML.
|
||||
// Links containing & work when placed in an anchor tag in a browser, but they don't work when we redirect to them for example in tracking.
|
||||
$href = str_replace('&', '&', $href);
|
||||
$anchor->href = $href;
|
||||
}
|
||||
$template = $templateDom->__toString();
|
||||
$template = $this->wp->applyFilters(
|
||||
self::FILTER_POST_PROCESS,
|
||||
$template
|
||||
);
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $content
|
||||
* @param array $styles
|
||||
* @return array
|
||||
*/
|
||||
private function addMailpoetLogoContentBlock(array $content, array $styles) {
|
||||
if (empty($content['blocks'])) return $content;
|
||||
$content['blocks'][] = [
|
||||
'type' => 'container',
|
||||
'orientation' => 'horizontal',
|
||||
'styles' => [
|
||||
'block' => [
|
||||
'backgroundColor' => (!empty($styles['body']['backgroundColor'])) ?
|
||||
$styles['body']['backgroundColor'] :
|
||||
'transparent',
|
||||
],
|
||||
],
|
||||
'blocks' => [
|
||||
[
|
||||
'type' => 'container',
|
||||
'orientation' => 'vertical',
|
||||
'styles' => [
|
||||
],
|
||||
'blocks' => [
|
||||
[
|
||||
'type' => 'image',
|
||||
'link' => 'https://www.mailpoet.com/?ref=free-plan-user-email&utm_source=free_plan_user_email&utm_medium=email',
|
||||
'src' => Env::$assetsUrl . '/img/mailpoet_logo_newsletter.png',
|
||||
'fullWidth' => false,
|
||||
'alt' => 'Email Marketing Powered by MailPoet',
|
||||
'width' => '108px',
|
||||
'height' => '65px',
|
||||
'styles' => [
|
||||
'block' => [
|
||||
'textAlign' => 'center',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Newsletter\Renderer;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
class StylesHelper {
|
||||
public static $cssAttributes = [
|
||||
'backgroundColor' => 'background-color',
|
||||
'fontColor' => 'color',
|
||||
'fontFamily' => 'font-family',
|
||||
'textDecoration' => 'text-decoration',
|
||||
'textAlign' => 'text-align',
|
||||
'fontSize' => 'font-size',
|
||||
'fontWeight' => 'font-weight',
|
||||
'borderWidth' => 'border-width',
|
||||
'borderStyle' => 'border-style',
|
||||
'borderColor' => 'border-color',
|
||||
'borderRadius' => 'border-radius',
|
||||
'lineHeight' => 'line-height',
|
||||
'msoLineHeightAlt' => 'mso-line-height-alt',
|
||||
'msoFontSize' => 'mso-ansi-font-size',
|
||||
];
|
||||
public static $font = [
|
||||
'Arial' => "Arial, 'Helvetica Neue', Helvetica, sans-serif",
|
||||
'Comic Sans MS' => "'Comic Sans MS', 'Marker Felt-Thin', Arial, sans-serif",
|
||||
'Courier New' => "'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace",
|
||||
'Georgia' => "Georgia, Times, 'Times New Roman', serif",
|
||||
'Lucida' => "'Lucida Sans Unicode', 'Lucida Grande', sans-serif",
|
||||
'Tahoma' => 'Tahoma, Verdana, Segoe, sans-serif',
|
||||
'Times New Roman' => "'Times New Roman', Times, Baskerville, Georgia, serif",
|
||||
'Trebuchet MS' => "'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif",
|
||||
'Verdana' => 'Verdana, Geneva, sans-serif',
|
||||
'Arvo' => 'arvo, courier, georgia, serif',
|
||||
'Lato' => "lato, 'helvetica neue', helvetica, arial, sans-serif",
|
||||
'Lora' => "lora, georgia, 'times new roman', serif",
|
||||
'Merriweather' => "merriweather, georgia, 'times new roman', serif",
|
||||
'Merriweather Sans' => "'merriweather sans', 'helvetica neue', helvetica, arial, sans-serif",
|
||||
'Noticia Text' => "'noticia text', georgia, 'times new roman', serif",
|
||||
'Open Sans' => "'open sans', 'helvetica neue', helvetica, arial, sans-serif",
|
||||
'Playfair Display' => "'playfair display', georgia, 'times new roman', serif",
|
||||
'Roboto' => "roboto, 'helvetica neue', helvetica, arial, sans-serif",
|
||||
'Source Sans Pro' => "'source sans pro', 'helvetica neue', helvetica, arial, sans-serif",
|
||||
'Oswald' => "Oswald, 'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', 'Lucida Sans', Tahoma, sans-serif",
|
||||
'Raleway' => "Raleway, 'Century Gothic', CenturyGothic, AppleGothic, sans-serif",
|
||||
'Permanent Marker' => "'Permanent Marker', Tahoma, Verdana, Segoe, sans-serif",
|
||||
'Pacifico' => "Pacifico, 'Arial Narrow', Arial, sans-serif",
|
||||
];
|
||||
public static $customFonts = [
|
||||
'Arvo',
|
||||
'Lato',
|
||||
'Lora',
|
||||
'Merriweather',
|
||||
'Merriweather Sans',
|
||||
'Noticia Text',
|
||||
'Open Sans',
|
||||
'Playfair Display',
|
||||
'Roboto',
|
||||
'Source Sans Pro',
|
||||
'Oswald',
|
||||
'Raleway',
|
||||
'Permanent Marker',
|
||||
'Pacifico',
|
||||
];
|
||||
public static $defaultLineHeight = 1.6;
|
||||
public static $headingMarginMultiplier = 0.3;
|
||||
public static $paddingWidth = 20;
|
||||
|
||||
public static function getBlockStyles($element, $ignoreSpecificStyles = false) {
|
||||
if (!isset($element['styles']['block'])) {
|
||||
return;
|
||||
}
|
||||
return self::getStyles($element['styles'], 'block', $ignoreSpecificStyles);
|
||||
}
|
||||
|
||||
public static function getStyles($data, $type, $ignoreSpecificStyles = false) {
|
||||
$styles = array_map(function($attribute, $style) use ($ignoreSpecificStyles) {
|
||||
if (!$ignoreSpecificStyles || !in_array($attribute, $ignoreSpecificStyles)) {
|
||||
$style = StylesHelper::applyFontFamily($attribute, $style);
|
||||
return StylesHelper::translateCSSAttribute($attribute) . ': ' . $style . ';';
|
||||
}
|
||||
}, array_keys($data[$type]), $data[$type]);
|
||||
return implode('', $styles);
|
||||
}
|
||||
|
||||
public static function translateCSSAttribute($attribute) {
|
||||
return (array_key_exists($attribute, self::$cssAttributes)) ?
|
||||
self::$cssAttributes[$attribute] :
|
||||
$attribute;
|
||||
}
|
||||
|
||||
public static function setStyle(array $style, string $selector): string {
|
||||
$css = $selector . '{' . PHP_EOL;
|
||||
$style = self::applyHeadingMargin($style, $selector);
|
||||
$style = self::applyLineHeight($style, $selector);
|
||||
foreach ($style as $attribute => $individualStyle) {
|
||||
$individualStyle = self::applyFontFamily($attribute, $individualStyle);
|
||||
$css .= self::translateCSSAttribute($attribute) . ':' . $individualStyle . ';' . PHP_EOL;
|
||||
}
|
||||
$css .= '}' . PHP_EOL;
|
||||
return $css;
|
||||
}
|
||||
|
||||
public static function applyTextAlignment($block) {
|
||||
if (is_array($block)) {
|
||||
$textAlignment = isset($block['styles']['block']['textAlign']) ?
|
||||
strtolower($block['styles']['block']['textAlign']) :
|
||||
'';
|
||||
if (preg_match('/center|right|justify/i', (string)$textAlignment)) {
|
||||
return $block;
|
||||
}
|
||||
$block['styles']['block']['textAlign'] = 'left';
|
||||
return $block;
|
||||
}
|
||||
return (preg_match('/text-align.*?[center|justify|right]/i', (string)$block)) ?
|
||||
$block :
|
||||
$block . 'text-align:left;';
|
||||
}
|
||||
|
||||
/**
|
||||
* Join styles and makes sure they are separated by ;
|
||||
*/
|
||||
public static function joinStyles(?string $styles1, ?string $styles2): string {
|
||||
if ($styles1 === null) $styles1 = '';
|
||||
if ($styles2 === null) $styles2 = '';
|
||||
|
||||
$style = trim($styles1);
|
||||
if (
|
||||
(strlen($style) > 0)
|
||||
&& (substr($style, -1) !== ';')
|
||||
) $style .= ';';
|
||||
$style .= $styles2;
|
||||
return $style;
|
||||
}
|
||||
|
||||
public static function applyFontFamily($attribute, $style) {
|
||||
if ($attribute !== 'fontFamily') return $style;
|
||||
return (isset(self::$font[$style])) ?
|
||||
self::$font[$style] :
|
||||
self::$font['Arial'];
|
||||
}
|
||||
|
||||
public static function applyHeadingMargin(array $style, string $selector): array {
|
||||
if (!preg_match('/h[1-4]/i', $selector)) return $style;
|
||||
$fontSize = (int)$style['fontSize'];
|
||||
$style['margin'] = sprintf('0 0 %spx 0', self::$headingMarginMultiplier * $fontSize);
|
||||
return $style;
|
||||
}
|
||||
|
||||
public static function applyLineHeight(array $style, string $selector): array {
|
||||
if (!preg_match('/mailpoet_paragraph|h[1-4]/i', $selector)) return $style;
|
||||
$lineHeight = isset($style['lineHeight']) ? (float)$style['lineHeight'] : self::$defaultLineHeight;
|
||||
$fontSize = (int)$style['fontSize'];
|
||||
$msoLineHeight = round($lineHeight * $fontSize);
|
||||
if ($msoLineHeight % 2 === 1) {
|
||||
$msoLineHeight++;
|
||||
}
|
||||
$msoFontSize = $fontSize;
|
||||
if ($msoFontSize % 2 === 1) {
|
||||
$msoFontSize++;
|
||||
}
|
||||
$style['msoLineHeightAlt'] = sprintf('%spx', $msoLineHeight);
|
||||
$style = ['msoFontSize' => sprintf('%spx', $msoFontSize)] + $style;
|
||||
$style['lineHeight'] = sprintf('%spx', $lineHeight * $fontSize);
|
||||
|
||||
return $style;
|
||||
}
|
||||
|
||||
private static function getCustomFontsNames($styles) {
|
||||
$fontNames = [];
|
||||
foreach ($styles as $style) {
|
||||
if (isset($style['fontFamily']) && in_array($style['fontFamily'], self::$customFonts)) {
|
||||
$fontNames[$style['fontFamily']] = true;
|
||||
}
|
||||
}
|
||||
return array_keys($fontNames);
|
||||
}
|
||||
|
||||
public static function getCustomFontsLinks($styles) {
|
||||
$links = [];
|
||||
foreach (self::getCustomFontsNames($styles) as $name) {
|
||||
$links[] = urlencode($name) . ':400,400i,700,700i';
|
||||
}
|
||||
if (!count($links)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// see https://stackoverflow.com/a/48214207
|
||||
return '<!--[if !mso]><!-- --><link href="https://fonts.googleapis.com/css?family='
|
||||
. implode("|", $links)
|
||||
. '" rel="stylesheet"><!--<![endif]-->';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<html lang="{{newsletter_language}}">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
{{newsletter_meta_robots}}
|
||||
<title>{{newsletter_subject}}</title>
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.ExternalClass * {
|
||||
line-height: 130%;
|
||||
}
|
||||
.ExternalClass a {
|
||||
line-height: 140%;
|
||||
}
|
||||
.ExternalClass h1, h2, h3, h1, h2, h3 {
|
||||
Margin: 0;
|
||||
}
|
||||
.mailpoet_text h1:last-child, .mailpoet_text h2:last-child, .mailpoet_text h3:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.ExternalClass ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
table, td {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.mailpoet_image img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
border: 0;
|
||||
display: block;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
}
|
||||
.mailpoet_padded_vertical {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.mailpoet_padded_side {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.mailpoet_header_footer_padded {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
/* https://www.emailonacid.com/blog/article/email-development/tips-for-coding-email-preheaders */
|
||||
.mailpoet_preheader, .mailpoet_preheader * {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
color: #ffffff;
|
||||
line-height: 1px;
|
||||
max-height: 0px;
|
||||
max-width: 0px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Hide text meant for screen readers. Styles copied from WordPress' common.css file */
|
||||
/* In addition, there are color, font-size, line-height, and mso-hide properties for clients where it doesn't work */
|
||||
.screen-reader-text {
|
||||
border: 0;
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
-webkit-clip-path: inset(50%);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
word-wrap: normal !important;
|
||||
|
||||
color: transparent;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
mso-hide: all;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.mailpoet_button {width:100% !important;}
|
||||
}
|
||||
@media screen and (max-width: 599px) {
|
||||
.mailpoet_header {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.mailpoet_button {
|
||||
width: 100% !important;
|
||||
padding: 5px 0 !important;
|
||||
box-sizing:border-box !important;
|
||||
}
|
||||
div, .mailpoet_cols-two, .mailpoet_cols-three {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
{{newsletter_styles}}
|
||||
</style>
|
||||
{{newsletter_custom_fonts}}
|
||||
</head>
|
||||
<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
|
||||
<table class="mailpoet_template" border="0" width="100%" cellpadding="0" cellspacing="0"
|
||||
style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="mailpoet_preheader" style="-webkit-text-size-adjust:none;font-size:1px;line-height:1px;color:#333333;" height="1">
|
||||
{{newsletter_preheader}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" class="mailpoet-wrapper" valign="top"><!--[if mso]>
|
||||
<table align="center" border="0" cellspacing="0" cellpadding="0"
|
||||
width="660">
|
||||
<tr>
|
||||
<td class="mailpoet_content-wrapper" align="center" valign="top" width="660">
|
||||
<![endif]--><table class="mailpoet_content-wrapper" border="0" width="660" cellpadding="0" cellspacing="0"
|
||||
style="border-spacing:0;mso-table-lspace:0;mso-table-rspace:0;max-width:660px;width:100%;">
|
||||
<tbody>
|
||||
{{newsletter_body}}
|
||||
</tbody>
|
||||
</table><!--[if mso]>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<![endif]--></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user