This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -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);
}
}
@@ -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>&nbsp;';
}
}
$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 &nbsp; with spaces
$html = str_replace('&nbsp;', ' ', $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('&#039;', "'", 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,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 &amp; which is then additionally encoded to &amp;amp;
// when saving the text block content in the editor
$href = str_replace('&amp;amp;', '&amp;', $anchor->href);
// Replace &amp; with & in the href attributes of anchors. URLs are encoded when TinyMCE extracts Text block content via content.innerHTML.
// Links containing &amp; 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('&amp;', '&', $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