From 4a15cbd55eea37945402d9ea32ab9bff753b514a Mon Sep 17 00:00:00 2001 From: Jaro Varga Date: Tue, 13 Oct 2026 09:05:23 +0200 Subject: [PATCH] Render plain text properly in the default HTML email template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The template wrapped every line in its own paragraph without escaping it, so user-supplied text was inserted as markup, blank lines became empty paragraphs and CRLF input left carriage returns behind. Treat $content as plain text: escape each line, separate paragraphs by blank lines and turn single line breaks into
. Restrict \R to CR and LF so that the UTF-8 byte 0x85 in characters such as "Å" is not taken as a line break. Fixes #1115 --- templates/email/html/default.php | 13 ++- tests/TestCase/Mailer/DefaultTemplateTest.php | 92 +++++++++++++++++++ 2 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 tests/TestCase/Mailer/DefaultTemplateTest.php diff --git a/templates/email/html/default.php b/templates/email/html/default.php index 70a6cd4492..47421fde07 100644 --- a/templates/email/html/default.php +++ b/templates/email/html/default.php @@ -15,8 +15,11 @@ * @var string $content */ -$lines = explode("\n", $content); - -foreach ($lines as $line) : - echo '

' . $line . "

\n"; -endforeach; +// $content is plain text: blank lines separate paragraphs and single line breaks become
. +// (*BSR_ANYCRLF) limits \R to CR and LF; without /u it would also match the byte 0x85 inside UTF-8 characters such as "Å". +?> + + +

', array_map(h(...), preg_split('/(*BSR_ANYCRLF)\R/', $paragraph) ?: [])) ?>

+ + diff --git a/tests/TestCase/Mailer/DefaultTemplateTest.php b/tests/TestCase/Mailer/DefaultTemplateTest.php new file mode 100644 index 0000000000..7a543d6e70 --- /dev/null +++ b/tests/TestCase/Mailer/DefaultTemplateTest.php @@ -0,0 +1,92 @@ +viewBuilder() + ->setTemplate('default') + ->disableAutoLayout(); + + return $renderer->render($content, [Message::MESSAGE_HTML])[Message::MESSAGE_HTML]; + } + + /** + * Test blank lines separate paragraphs and single line breaks become
. + * + * @return void + */ + public function testHtmlParagraphs(): void + { + $html = $this->renderHtml("First line\r\nsecond line\r\n\r\n\r\nNext paragraph\n"); + + $this->assertSame( + '

First line
second line

Next paragraph

', + preg_replace('/>\s+<', trim($html)), + ); + } + + /** + * Test the content is escaped. + * + * @return void + */ + public function testHtmlEscapesContent(): void + { + $html = $this->renderHtml(' & more'); + + $this->assertStringContainsString('<script>alert("x")</script> & more', $html); + $this->assertStringNotContainsString('