PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Newsletter / Renderer / Renderer.php
mailpoet / lib / Newsletter / Renderer Last commit date
Blocks 2 months ago Columns 5 months ago PostProcess 4 days ago BodyRenderer.php 2 months ago EscapeHelper.php 2 years ago Preprocessor.php 1 year ago Renderer.php 1 month ago StylesHelper.php 2 months ago Template.html 2 months ago index.php 3 years ago
Renderer.php
354 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Newsletter\Renderer;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use Automattic\WooCommerce\EmailEditor\Email_Editor_Container;
9 use Automattic\WooCommerce\EmailEditor\Engine\Renderer\Html2Text;
10 use Automattic\WooCommerce\EmailEditor\Engine\Renderer\Renderer as GuntenbergRenderer;
11 use MailPoet\Config\Env;
12 use MailPoet\EmailEditor\Integrations\MailPoet\Coupons\CouponBlockFailureTranslator;
13 use MailPoet\EmailEditor\Integrations\MailPoet\Coupons\CouponBlockGenerationFailureCollector;
14 use MailPoet\EmailEditor\Integrations\MailPoet\Coupons\EmailContextBuilder;
15 use MailPoet\EmailEditor\Integrations\MailPoet\ProductCollection\OrderProductCollectionProcessor;
16 use MailPoet\Entities\NewsletterEntity;
17 use MailPoet\Entities\SendingQueueEntity;
18 use MailPoet\Logging\LoggerFactory;
19 use MailPoet\Newsletter\NewslettersRepository;
20 use MailPoet\Newsletter\Renderer\EscapeHelper as EHelper;
21 use MailPoet\Newsletter\Sending\NewsletterReplayMetadata;
22 use MailPoet\Newsletter\Sending\SendingQueuesRepository;
23 use MailPoet\NewsletterProcessingException;
24 use MailPoet\Util\License\Features\CapabilitiesManager;
25 use MailPoet\Util\pQuery\DomNode;
26 use MailPoet\WP\Functions as WPFunctions;
27
28 class Renderer {
29 const NEWSLETTER_TEMPLATE = 'Template.html';
30 const FILTER_POST_PROCESS = 'mailpoet_rendering_post_process';
31
32 /** @var BodyRenderer */
33 private $bodyRenderer;
34
35 /** @var GuntenbergRenderer */
36 private $guntenbergRenderer;
37
38 /** @var Preprocessor */
39 private $preprocessor;
40
41 /** @var \MailPoetVendor\CSS */
42 private $cSSInliner;
43
44 /** @var WPFunctions */
45 private $wp;
46
47 /*** @var LoggerFactory */
48 private $loggerFactory;
49
50 /*** @var NewslettersRepository */
51 private $newslettersRepository;
52
53 /*** @var SendingQueuesRepository */
54 private $sendingQueuesRepository;
55
56 private CapabilitiesManager $capabilitiesManager;
57
58 private CouponBlockGenerationFailureCollector $couponBlockFailureCollector;
59
60 private EmailContextBuilder $emailContextBuilder;
61
62 private CouponBlockFailureTranslator $couponBlockFailureTranslator;
63
64 private OrderProductCollectionProcessor $orderProductCollectionProcessor;
65
66 public function __construct(
67 BodyRenderer $bodyRenderer,
68 Preprocessor $preprocessor,
69 \MailPoetVendor\CSS $cSSInliner,
70 WPFunctions $wp,
71 LoggerFactory $loggerFactory,
72 NewslettersRepository $newslettersRepository,
73 SendingQueuesRepository $sendingQueuesRepository,
74 CapabilitiesManager $capabilitiesManager,
75 CouponBlockGenerationFailureCollector $couponBlockFailureCollector,
76 EmailContextBuilder $emailContextBuilder,
77 CouponBlockFailureTranslator $couponBlockFailureTranslator,
78 OrderProductCollectionProcessor $orderProductCollectionProcessor
79 ) {
80 $this->bodyRenderer = $bodyRenderer;
81 $this->guntenbergRenderer = Email_Editor_Container::container()->get(GuntenbergRenderer::class);
82 $this->preprocessor = $preprocessor;
83 $this->cSSInliner = $cSSInliner;
84 $this->wp = $wp;
85 $this->loggerFactory = $loggerFactory;
86 $this->newslettersRepository = $newslettersRepository;
87 $this->sendingQueuesRepository = $sendingQueuesRepository;
88 $this->capabilitiesManager = $capabilitiesManager;
89 $this->couponBlockFailureCollector = $couponBlockFailureCollector;
90 $this->emailContextBuilder = $emailContextBuilder;
91 $this->couponBlockFailureTranslator = $couponBlockFailureTranslator;
92 $this->orderProductCollectionProcessor = $orderProductCollectionProcessor;
93 }
94
95 public function render(NewsletterEntity $newsletter, ?SendingQueueEntity $sendingQueue = null, $type = false) {
96 return $this->_render($newsletter, $sendingQueue, $type);
97 }
98
99 public function renderAsPreview(NewsletterEntity $newsletter, $type = false, ?string $subject = null) {
100 return $this->_render($newsletter, null, $type, true, $subject);
101 }
102
103 private function _render(NewsletterEntity $newsletter, ?SendingQueueEntity $sendingQueue = null, $type = false, $preview = false, $subject = null) {
104 $language = $this->wp->getBloginfo('language');
105 $isRtl = (bool)$this->wp->isRtl();
106 $metaRobots = $preview ? '<meta name="robots" content="noindex, nofollow" />' : '';
107 $subject = $subject ?: $newsletter->getSubject();
108 $wpPostEntity = $newsletter->getWpPost();
109 $wpPost = $wpPostEntity ? $wpPostEntity->getWpPostInstance() : null;
110 if ($wpPost instanceof \WP_Post) {
111 $this->couponBlockFailureCollector->clear();
112 $renderContext = $this->emailContextBuilder->build($newsletter, $sendingQueue, (bool)$preview);
113 $filterCallback = function (array $context) use ($renderContext): array {
114 return array_merge($context, $renderContext);
115 };
116 $orderProductsFilter = null;
117 $abandonedCartPersistentCartFilter = null;
118
119 try {
120 $this->wp->addFilter('woocommerce_email_editor_rendering_email_context', $filterCallback);
121 $abandonedCartPersistentCartFilter = $this->orderProductCollectionProcessor
122 ->createAbandonedCartPersistentCartFilter($renderContext, $sendingQueue);
123 if ($abandonedCartPersistentCartFilter) {
124 $this->wp->addFilter('get_user_metadata', $abandonedCartPersistentCartFilter, 10, 4);
125 }
126 $orderProductsFilter = $this->orderProductCollectionProcessor->createBlocksFilter($renderContext);
127 if ($orderProductsFilter) {
128 $this->wp->addFilter('woocommerce_email_blocks_renderer_parsed_blocks', $orderProductsFilter);
129 }
130 $renderedNewsletter = $this->guntenbergRenderer->render($wpPost, $subject, $newsletter->getPreheader(), $language, $metaRobots);
131 if ($this->couponBlockFailureCollector->hasFailures()) {
132 throw NewsletterProcessingException::create()
133 ->withMessage($this->couponBlockFailureTranslator->getFailureMessage($this->couponBlockFailureCollector));
134 }
135 $filteredHtml = $this->wp->applyFilters(
136 self::FILTER_POST_PROCESS,
137 $renderedNewsletter['html']
138 );
139 if (is_string($filteredHtml)) {
140 $renderedNewsletter['html'] = $filteredHtml;
141 }
142 } finally {
143 $this->wp->removeFilter('woocommerce_email_editor_rendering_email_context', $filterCallback);
144 if ($orderProductsFilter) {
145 $this->wp->removeFilter('woocommerce_email_blocks_renderer_parsed_blocks', $orderProductsFilter);
146 }
147 if ($abandonedCartPersistentCartFilter) {
148 $this->wp->removeFilter('get_user_metadata', $abandonedCartPersistentCartFilter);
149 }
150 $this->couponBlockFailureCollector->clear();
151 }
152 } else {
153 $body = (is_array($newsletter->getBody()))
154 ? $newsletter->getBody()
155 : [];
156 $content = (array_key_exists('content', $body))
157 ? $body['content']
158 : [];
159 $styles = (array_key_exists('globalStyles', $body))
160 ? $body['globalStyles']
161 : [];
162
163 $mailPoetLogoInEmails = $this->capabilitiesManager->getCapability('mailpoetLogoInEmails');
164 if (
165 (isset($mailPoetLogoInEmails) && $mailPoetLogoInEmails->isRestricted) && !$preview
166 ) {
167 $content = $this->addMailpoetLogoContentBlock($content, $styles);
168 }
169
170 $renderedBody = "";
171 try {
172 $content = $this->preprocessor->process($newsletter, $content, $preview, $sendingQueue);
173 $renderedBody = $this->bodyRenderer->renderBody($newsletter, $content, $isRtl);
174 } catch (NewsletterProcessingException $e) {
175 $this->loggerFactory->getLogger(LoggerFactory::TOPIC_COUPONS)->error(
176 $e->getMessage(),
177 ['newsletter_id' => $newsletter->getId()]
178 );
179 if (!$sendingQueue || !NewsletterReplayMetadata::isLatestNewsletterReplayMeta($sendingQueue->getMeta())) {
180 $this->newslettersRepository->setAsCorrupt($newsletter);
181 }
182 if ($sendingQueue) {
183 $this->sendingQueuesRepository->pause($sendingQueue);
184 }
185 }
186 $renderedStyles = $this->renderStyles($styles);
187 $customFontsLinks = StylesHelper::getCustomFontsLinks($styles);
188
189 $template = $this->injectContentIntoTemplate(
190 (string)file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE),
191 [
192 $language,
193 $isRtl ? ' dir="rtl"' : '',
194 $metaRobots,
195 htmlspecialchars($subject, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401),
196 $renderedStyles,
197 $customFontsLinks,
198 $isRtl ? ' style="direction: rtl;"' : '',
199 $isRtl ? ';direction:rtl' : '',
200 EHelper::escapeHtmlText($newsletter->getPreheader()),
201 $renderedBody,
202 ]
203 );
204 if ($template === null) {
205 $template = '';
206 }
207 $templateDom = $this->inlineCSSStyles($template);
208 $template = $this->postProcessTemplate($templateDom);
209
210 $renderedNewsletter = [
211 'html' => $template,
212 'text' => $this->renderTextVersion($template),
213 ];
214 }
215
216 return ($type && !empty($renderedNewsletter[$type])) ?
217 $renderedNewsletter[$type] :
218 $renderedNewsletter;
219 }
220
221 /**
222 * @param array $styles
223 * @return string
224 */
225 private function renderStyles(array $styles) {
226 $css = '';
227 foreach ($styles as $selector => $style) {
228 switch ($selector) {
229 case 'text':
230 $selector = 'td.mailpoet_paragraph, td.mailpoet_blockquote, li.mailpoet_paragraph, td.mailpoet_footer';
231 break;
232 case 'body':
233 $selector = 'body, .mailpoet-wrapper';
234 break;
235 case 'link':
236 $selector = '.mailpoet-wrapper a';
237 break;
238 case 'wrapper':
239 $selector = '.mailpoet_content-wrapper';
240 break;
241 }
242
243 if (!is_array($style)) {
244 continue;
245 }
246
247 $css .= StylesHelper::setStyle($style, $selector);
248 }
249 return $css;
250 }
251
252 /**
253 * @param string $template
254 * @param string[] $content
255 * @return string|null
256 */
257 private function injectContentIntoTemplate($template, $content) {
258 return preg_replace_callback('/{{\w+}}/', function($matches) use (&$content) {
259 return array_shift($content);
260 }, $template);
261 }
262
263 /**
264 * @param string $template
265 * @return DomNode
266 */
267 private function inlineCSSStyles($template) {
268 return $this->cSSInliner->inlineCSS($template);
269 }
270
271 /**
272 * @param string $template
273 * @return string
274 */
275 private function renderTextVersion($template) {
276 if (!mb_detect_encoding($template, 'UTF-8', true)) {
277 $converted = mb_convert_encoding($template, 'UTF-8', mb_list_encodings());
278 $template = $converted !== false ? $converted : $template;
279 }
280 return @Html2Text::convert($template);
281 }
282
283 /**
284 * @param DomNode $templateDom
285 * @return string
286 */
287 private function postProcessTemplate(DomNode $templateDom) {
288 // replace spaces in image tag URLs
289 foreach ($templateDom->query('img') as $image) {
290 $image->src = str_replace(' ', '%20', $image->src);
291 }
292 foreach ($templateDom->query('a') as $anchor) {
293 // Fix for a TinyMCE bug in smart paste which encodes & as &amp; which is then additionally encoded to &amp;amp;
294 // when saving the text block content in the editor
295 $href = str_replace('&amp;amp;', '&amp;', $anchor->href);
296 // Replace &amp; with & in the href attributes of anchors. URLs are encoded when TinyMCE extracts Text block content via content.innerHTML.
297 // 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.
298 $href = str_replace('&amp;', '&', $href);
299 $anchor->href = $href;
300 }
301 $template = $templateDom->__toString();
302 $filtered = $this->wp->applyFilters(
303 self::FILTER_POST_PROCESS,
304 $template
305 );
306 return is_string($filtered) ? $filtered : $template;
307 }
308
309 /**
310 * @param array $content
311 * @param array $styles
312 * @return array
313 */
314 private function addMailpoetLogoContentBlock(array $content, array $styles) {
315 if (empty($content['blocks'])) return $content;
316 $content['blocks'][] = [
317 'type' => 'container',
318 'orientation' => 'horizontal',
319 'styles' => [
320 'block' => [
321 'backgroundColor' => (!empty($styles['body']['backgroundColor'])) ?
322 $styles['body']['backgroundColor'] :
323 'transparent',
324 ],
325 ],
326 'blocks' => [
327 [
328 'type' => 'container',
329 'orientation' => 'vertical',
330 'styles' => [
331 ],
332 'blocks' => [
333 [
334 'type' => 'image',
335 'link' => 'https://www.mailpoet.com/?ref=free-plan-user-email&utm_source=free_plan_user_email&utm_medium=email',
336 'src' => Env::$assetsUrl . '/img/mailpoet_logo_newsletter.png',
337 'fullWidth' => false,
338 'alt' => 'Email Marketing Powered by MailPoet',
339 'width' => '108px',
340 'height' => '65px',
341 'styles' => [
342 'block' => [
343 'textAlign' => 'center',
344 ],
345 ],
346 ],
347 ],
348 ],
349 ],
350 ];
351 return $content;
352 }
353 }
354