diff --git a/mailpoet/lib/DI/ContainerConfigurator.php b/mailpoet/lib/DI/ContainerConfigurator.php index d00678ad29..29cd1d3e4c 100644 --- a/mailpoet/lib/DI/ContainerConfigurator.php +++ b/mailpoet/lib/DI/ContainerConfigurator.php @@ -315,6 +315,7 @@ class ContainerConfigurator implements IContainerConfigurator { // Email Editor $container->autowire(\MailPoet\EmailEditor\Engine\EmailEditor::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Engine\AssetsCleaner::class)->setPublic(true); + $container->autowire(\MailPoet\EmailEditor\Engine\Renderer\Renderer::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Engine\Renderer\BodyRenderer::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailEditor::class)->setPublic(true); $container->autowire(\MailPoet\EmailEditor\Integrations\MailPoet\EmailApiController::class)->setPublic(true); diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php b/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php index 95543a960e..9885fe6b84 100644 --- a/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/BodyRenderer.php @@ -3,11 +3,11 @@ namespace MailPoet\EmailEditor\Engine\Renderer; class BodyRenderer { - public function renderBody(\WP_Post $emailPost): string { + public function renderBody(string $postContent): string { // @todo Parse blocks \WP_Block_Parser // @todo We need to wrap top level blocks which are not in columns into a column // @todo Add rendering of columns (inspire by/reuse code from mailpoet/lib/Newsletter/Renderer/Columns/Renderer) // @todo Add rendering of blocks - return $emailPost->post_content ?: ''; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps + return $postContent ?: ''; } } diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php b/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php new file mode 100644 index 0000000000..2e3cccf788 --- /dev/null +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/Renderer.php @@ -0,0 +1,99 @@ +cssInliner = $cssInliner; + $this->bodyRenderer = $bodyRenderer; + } + + public function render(string $postContent, string $subject, string $preHeader, string $language, $metaRobots = ''): array { + $renderedBody = $this->bodyRenderer->renderBody($postContent); + $styles = (string)file_get_contents(dirname(__FILE__) . '/' . self::STYLES_FILE); + + /** + * {{email_language}} + * {{email_subject}} + * {{email_meta_robots}} + * {{email_styles}} + * {{email_preheader}} + * {{email_body}} + */ + $templateWithContents = $this->injectContentIntoTemplate( + (string)file_get_contents(dirname(__FILE__) . '/' . self::TEMPLATE_FILE), + [ + $language, + esc_html($subject), + $metaRobots, + $styles, + esc_html($preHeader), + $renderedBody, + ] + ); + + $templateWithContentsDom = $this->inlineCSSStyles($templateWithContents); + $templateWithContents = $this->postProcessTemplate($templateWithContentsDom); + return [ + 'html' => $templateWithContents, + 'text' => $this->renderTextVersion($templateWithContents), + ]; + } + + private function injectContentIntoTemplate($template, array $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); + } + // because tburry/pquery contains a bug and replaces the opening non mso condition incorrectly we have to replace the opening tag with correct value + $template = $templateDom->__toString(); + $template = str_replace('', '', $template); + return $template; + } +} diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/styles.css b/mailpoet/lib/EmailEditor/Engine/Renderer/styles.css new file mode 100644 index 0000000000..831b9cb444 --- /dev/null +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/styles.css @@ -0,0 +1,61 @@ +/* Base CSS rules to be applied to all emails */ +/* Created based on original MailPoet template for rendering emails */ +/* StyleLint is disabled because some rules contain properties that linter marks as unknown, but they are valid for email rendering */ +/* stylelint-disable property-no-unknown */ +body { + margin: 0; + padding: 0; + -webkit-text-size-adjust: 100%; /* From MJMJ - Automatic test adjustment on mobile max to 100% */ + -ms-text-size-adjust: 100%; /* From MJMJ - Automatic test adjustment on mobile max to 100% */ +} + +table, +td { + border-collapse: collapse; + mso-table-lspace: 0; + mso-table-rspace: 0; +} + +img { + border: 0; + height: auto; + -ms-interpolation-mode: bicubic; + line-height: 100%; + outline: none; + text-decoration: none; +} + +p { + display: block; + margin: 13px 0; +} + +/* https://www.emailonacid.com/blog/article/email-development/tips-for-coding-email-preheaders */ +.email_preheader, +.email_preheader * { + color: #fff; + display: none; + font-size: 1px; + line-height: 1px; + max-height: 0; + max-width: 0; + mso-hide: all; + opacity: 0; + overflow: hidden; + visibility: hidden; +} + +@media screen and (max-width: 480px) { + .email_button { + width: 100% !important; + } +} + +@media screen and (max-width: 599px) { + .email_button { + box-sizing: border-box !important; + padding: 5px 0 !important; + width: 100% !important; + } +} +/* stylelint-enable property-no-unknown */ diff --git a/mailpoet/lib/EmailEditor/Engine/Renderer/template.html b/mailpoet/lib/EmailEditor/Engine/Renderer/template.html new file mode 100644 index 0000000000..c118c97574 --- /dev/null +++ b/mailpoet/lib/EmailEditor/Engine/Renderer/template.html @@ -0,0 +1,78 @@ + + + {{newsletter_subject}} + + + + + {{email_meta_robots}} + + + + + + + + + + + + +
+ + diff --git a/mailpoet/lib/Newsletter/Renderer/Renderer.php b/mailpoet/lib/Newsletter/Renderer/Renderer.php index 6cb6808146..3031a631c9 100644 --- a/mailpoet/lib/Newsletter/Renderer/Renderer.php +++ b/mailpoet/lib/Newsletter/Renderer/Renderer.php @@ -4,7 +4,7 @@ namespace MailPoet\Newsletter\Renderer; use MailPoet\Config\Env; use MailPoet\Config\ServicesChecker; -use MailPoet\EmailEditor\Engine\Renderer\BodyRenderer as GuntenbergBodyRenderer; +use MailPoet\EmailEditor\Engine\Renderer\Renderer as GuntenbergRenderer; use MailPoet\Entities\NewsletterEntity; use MailPoet\Features\FeaturesController; use MailPoet\Logging\LoggerFactory; @@ -24,8 +24,8 @@ class Renderer { /** @var BodyRenderer */ private $bodyRenderer; - /** @var GuntenbergBodyRenderer */ - private $guntenbergBodyRenderer; + /** @var GuntenbergRenderer */ + private $guntenbergRenderer; /** @var Preprocessor */ private $preprocessor; @@ -53,7 +53,7 @@ class Renderer { public function __construct( BodyRenderer $bodyRenderer, - GuntenbergBodyRenderer $guntenbergBodyRenderer, + GuntenbergRenderer $guntenbergRenderer, Preprocessor $preprocessor, \MailPoetVendor\CSS $cSSInliner, ServicesChecker $servicesChecker, @@ -64,7 +64,7 @@ class Renderer { FeaturesController $featuresController ) { $this->bodyRenderer = $bodyRenderer; - $this->guntenbergBodyRenderer = $guntenbergBodyRenderer; + $this->guntenbergRenderer = $guntenbergRenderer; $this->preprocessor = $preprocessor; $this->cSSInliner = $cSSInliner; $this->servicesChecker = $servicesChecker; @@ -84,72 +84,69 @@ class Renderer { } private function _render(NewsletterEntity $newsletter, SendingTask $sendingTask = null, $type = false, $preview = false, $subject = null) { - $body = (is_array($newsletter->getBody())) - ? $newsletter->getBody() - : []; - $content = (array_key_exists('content', $body)) - ? $body['content'] - : []; - $styles = (array_key_exists('globalStyles', $body)) - ? $body['globalStyles'] - : []; - - if ( - !$this->servicesChecker->isUserActivelyPaying() && !$preview - ) { - $content = $this->addMailpoetLogoContentBlock($content, $styles); - } - $language = $this->wp->getBloginfo('language'); $metaRobots = $preview ? '' : ''; - $renderedBody = ""; - try { - $content = $this->preprocessor->process($newsletter, $content, $preview, $sendingTask); - if ($this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR) && $newsletter->getWpPostId()) { - $post = $newsletter->getWpPost(); - if (!$post instanceof \WP_Post) { - throw new NewsletterProcessingException('Missing email post object'); - } - $renderedBody = $this->guntenbergBodyRenderer->renderBody($post); - } else { + $subject = $subject ?: $newsletter->getSubject(); + $wpPost = $newsletter->getWpPost(); + if ($this->featuresController->isSupported(FeaturesController::GUTENBERG_EMAIL_EDITOR) && $wpPost instanceof \WP_Post) { + $renderedNewsletter = $this->guntenbergRenderer->render($wpPost->post_content, $subject, $newsletter->getPreheader(), $language, $metaRobots); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps + } else { + $body = (is_array($newsletter->getBody())) + ? $newsletter->getBody() + : []; + $content = (array_key_exists('content', $body)) + ? $body['content'] + : []; + $styles = (array_key_exists('globalStyles', $body)) + ? $body['globalStyles'] + : []; + + if ( + !$this->servicesChecker->isUserActivelyPaying() && !$preview + ) { + $content = $this->addMailpoetLogoContentBlock($content, $styles); + } + + $renderedBody = ""; + try { + $content = $this->preprocessor->process($newsletter, $content, $preview, $sendingTask); $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 ($newsletter->getLatestQueue()) { + $this->sendingQueuesRepository->pause($newsletter->getLatestQueue()); + } } + $renderedStyles = $this->renderStyles($styles); + $customFontsLinks = StylesHelper::getCustomFontsLinks($styles); - } catch (NewsletterProcessingException $e) { - $this->loggerFactory->getLogger(LoggerFactory::TOPIC_COUPONS)->error( - $e->getMessage(), - ['newsletter_id' => $newsletter->getId()] + $template = $this->injectContentIntoTemplate( + (string)file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE), + [ + $language, + $metaRobots, + htmlspecialchars($subject), + $renderedStyles, + $customFontsLinks, + EHelper::escapeHtmlText($newsletter->getPreheader()), + $renderedBody, + ] ); - $this->newslettersRepository->setAsCorrupt($newsletter); - if ($newsletter->getLatestQueue()) { - $this->sendingQueuesRepository->pause($newsletter->getLatestQueue()); + if ($template === null) { + $template = ''; } - } - $renderedStyles = $this->renderStyles($styles); - $customFontsLinks = StylesHelper::getCustomFontsLinks($styles); + $templateDom = $this->inlineCSSStyles($template); + $template = $this->postProcessTemplate($templateDom); - $template = $this->injectContentIntoTemplate( - (string)file_get_contents(dirname(__FILE__) . '/' . self::NEWSLETTER_TEMPLATE), - [ - $language, - $metaRobots, - htmlspecialchars($subject ?: $newsletter->getSubject()), - $renderedStyles, - $customFontsLinks, - EHelper::escapeHtmlText($newsletter->getPreheader()), - $renderedBody, - ] - ); - if ($template === null) { - $template = ''; + $renderedNewsletter = [ + 'html' => $template, + 'text' => $this->renderTextVersion($template), + ]; } - $templateDom = $this->inlineCSSStyles($template); - $template = $this->postProcessTemplate($templateDom); - - $renderedNewsletter = [ - 'html' => $template, - 'text' => $this->renderTextVersion($template), - ]; return ($type && !empty($renderedNewsletter[$type])) ? $renderedNewsletter[$type] : diff --git a/mailpoet/tests/integration/Newsletter/RendererTest.php b/mailpoet/tests/integration/Newsletter/RendererTest.php index 35be8e28f1..6fe313bddb 100644 --- a/mailpoet/tests/integration/Newsletter/RendererTest.php +++ b/mailpoet/tests/integration/Newsletter/RendererTest.php @@ -55,7 +55,7 @@ class RendererTest extends \MailPoetTest { $this->servicesChecker = $this->createMock(ServicesChecker::class); $this->renderer = new Renderer( $this->diContainer->get(BodyRenderer::class), - $this->diContainer->get(\MailPoet\EmailEditor\Engine\Renderer\BodyRenderer::class), + $this->diContainer->get(\MailPoet\EmailEditor\Engine\Renderer\Renderer::class), $this->diContainer->get(Preprocessor::class), $this->diContainer->get(\MailPoetVendor\CSS::class), $this->servicesChecker, diff --git a/mailpoet/tests/integration/WooCommerce/TransactionalEmails/RendererTest.php b/mailpoet/tests/integration/WooCommerce/TransactionalEmails/RendererTest.php index 0a68ce9c1d..f56ee2e075 100644 --- a/mailpoet/tests/integration/WooCommerce/TransactionalEmails/RendererTest.php +++ b/mailpoet/tests/integration/WooCommerce/TransactionalEmails/RendererTest.php @@ -139,7 +139,7 @@ class RendererTest extends \MailPoetTest { )); return new NewsletterRenderer( $this->diContainer->get(\MailPoet\Newsletter\Renderer\BodyRenderer::class), - $this->diContainer->get(\MailPoet\EmailEditor\Engine\Renderer\BodyRenderer::class), + $this->diContainer->get(\MailPoet\EmailEditor\Engine\Renderer\Renderer::class), new Preprocessor( $this->diContainer->get(\MailPoet\Newsletter\Renderer\Blocks\AbandonedCartContent::class), $this->diContainer->get(\MailPoet\Newsletter\Renderer\Blocks\AutomatedLatestContentBlock::class),