diff --git a/composer.json b/composer.json index d50dce4fcf708..59b21fd6d0c9a 100644 --- a/composer.json +++ b/composer.json @@ -126,22 +126,23 @@ "doctrine/data-fixtures": "^1.1", "doctrine/dbal": "^2.13.1|^3.0", "doctrine/orm": "^2.7.4", + "egulias/email-validator": "^2.1.10|^3.1", "guzzlehttp/promises": "^1.4", + "league/html-to-markdown": "^5.0", "masterminds/html5": "^2.7.2", "monolog/monolog": "^1.25.1|^2", "nyholm/psr7": "^1.0", "pda/pheanstalk": "^4.0", "php-http/httplug": "^1.0|^2.0", + "phpdocumentor/reflection-docblock": "^5.2", "phpstan/phpdoc-parser": "^1.0", "predis/predis": "~1.1", "psr/http-client": "^1.0", "psr/simple-cache": "^1.0|^2.0|^3.0", - "egulias/email-validator": "^2.1.10|^3.1", "symfony/mercure-bundle": "^0.3", "symfony/phpunit-bridge": "^5.4|^6.0", "symfony/runtime": "self.version", "symfony/security-acl": "~2.8|~3.0", - "phpdocumentor/reflection-docblock": "^5.2", "twig/cssinliner-extra": "^2.12|^3", "twig/inky-extra": "^2.12|^3", "twig/markdown-extra": "^2.12|^3" diff --git a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php index d82dca6f2d2a0..397b35f1569d2 100644 --- a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php +++ b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php @@ -11,9 +11,12 @@ namespace Symfony\Bridge\Twig\Mime; -use League\HTMLToMarkdown\HtmlConverter; +use League\HTMLToMarkdown\HtmlConverterInterface; use Symfony\Component\Mime\BodyRendererInterface; use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter; +use Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface; +use Symfony\Component\Mime\HtmlToTextConverter\LeagueHtmlToMarkdownConverter; use Symfony\Component\Mime\Message; use Twig\Environment; @@ -24,19 +27,13 @@ final class BodyRenderer implements BodyRendererInterface { private Environment $twig; private array $context; - private HtmlConverter $converter; + private HtmlToTextConverterInterface $converter; - public function __construct(Environment $twig, array $context = []) + public function __construct(Environment $twig, array $context = [], HtmlToTextConverterInterface $converter = null) { $this->twig = $twig; $this->context = $context; - if (class_exists(HtmlConverter::class)) { - $this->converter = new HtmlConverter([ - 'hard_break' => true, - 'strip_tags' => true, - 'remove_nodes' => 'head style', - ]); - } + $this->converter = $converter ?: (interface_exists(HtmlConverterInterface::class) ? new LeagueHtmlToMarkdownConverter() : new DefaultHtmlToTextConverter()); } public function render(Message $message): void @@ -74,16 +71,8 @@ public function render(Message $message): void // if text body is empty, compute one from the HTML body if (!$message->getTextBody() && null !== $html = $message->getHtmlBody()) { - $message->text($this->convertHtmlToText(\is_resource($html) ? stream_get_contents($html) : $html)); - } - } - - private function convertHtmlToText(string $html): string - { - if (isset($this->converter)) { - return $this->converter->convert($html); + $text = $this->converter->convert(\is_resource($html) ? stream_get_contents($html) : $html, $message->getHtmlCharset()); + $message->text($text, $message->getHtmlCharset()); } - - return strip_tags(preg_replace('{<(head|style)\b.*?}is', '', $html)); } } diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php index 8ff343b684b5e..231067e0365dc 100644 --- a/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php @@ -15,6 +15,8 @@ use Symfony\Bridge\Twig\Mime\BodyRenderer; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter; +use Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface; use Symfony\Component\Mime\Part\Multipart\AlternativePart; use Twig\Environment; use Twig\Loader\ArrayLoader; @@ -27,14 +29,24 @@ public function testRenderTextOnly() $this->assertEquals('Text', $email->getBody()->bodyToString()); } - public function testRenderHtmlOnly() + public function testRenderHtmlOnlyWithDefaultConverter() { - $html = 'headHTML'; - $email = $this->prepareEmail(null, $html); + $html = 'HTML'; + $email = $this->prepareEmail(null, $html, [], new DefaultHtmlToTextConverter()); $body = $email->getBody(); $this->assertInstanceOf(AlternativePart::class, $body); $this->assertEquals('HTML', $body->getParts()[0]->bodyToString()); - $this->assertEquals(str_replace('=', '=3D', $html), $body->getParts()[1]->bodyToString()); + $this->assertEquals(str_replace(['=', "\n"], ['=3D', "\r\n"], $html), $body->getParts()[1]->bodyToString()); + } + + public function testRenderHtmlOnlyWithLeagueConverter() + { + $html = 'HTML'; + $email = $this->prepareEmail(null, $html); + $body = $email->getBody(); + $this->assertInstanceOf(AlternativePart::class, $body); + $this->assertEquals('**HTML**', $body->getParts()[0]->bodyToString()); + $this->assertEquals(str_replace(['=', "\n"], ['=3D', "\r\n"], $html), $body->getParts()[1]->bodyToString()); } public function testRenderMultiLineHtmlOnly() @@ -50,7 +62,7 @@ public function testRenderMultiLineHtmlOnly() $email = $this->prepareEmail(null, $html); $body = $email->getBody(); $this->assertInstanceOf(AlternativePart::class, $body); - $this->assertEquals('HTML', str_replace(["\r", "\n"], '', $body->getParts()[0]->bodyToString())); + $this->assertEquals('**HTML**', str_replace(["\r", "\n"], '', $body->getParts()[0]->bodyToString())); $this->assertEquals(str_replace(['=', "\n"], ['=3D', "\r\n"], $html), $body->getParts()[1]->bodyToString()); } @@ -121,7 +133,7 @@ public function testRenderedOnceUnserializableContext() $this->assertEquals('Text', $email->getTextBody()); } - private function prepareEmail(?string $text, ?string $html, array $context = []): TemplatedEmail + private function prepareEmail(?string $text, ?string $html, array $context = [], HtmlToTextConverterInterface $converter = null): TemplatedEmail { $twig = new Environment(new ArrayLoader([ 'text' => $text, @@ -129,7 +141,7 @@ private function prepareEmail(?string $text, ?string $html, array $context = []) 'document.txt' => 'Some text document...', 'image.jpg' => 'Some image data', ])); - $renderer = new BodyRenderer($twig); + $renderer = new BodyRenderer($twig, [], $converter); $email = (new TemplatedEmail()) ->to('fabien@symfony.com') ->from('helene@symfony.com') diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index 09d372285b992..be7c2d2ab31ac 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -23,6 +23,7 @@ "require-dev": { "doctrine/annotations": "^1.12", "egulias/email-validator": "^2.1.10|^3", + "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/asset": "^5.4|^6.0", "symfony/dependency-injection": "^5.4|^6.0", @@ -32,7 +33,7 @@ "symfony/http-foundation": "^5.4|^6.0", "symfony/http-kernel": "^6.2", "symfony/intl": "^5.4|^6.0", - "symfony/mime": "^5.4|^6.0", + "symfony/mime": "^6.2", "symfony/polyfill-intl-icu": "~1.0", "symfony/property-info": "^5.4|^6.0", "symfony/routing": "^5.4|^6.0", @@ -59,6 +60,7 @@ "symfony/form": "<6.1", "symfony/http-foundation": "<5.4", "symfony/http-kernel": "<6.2", + "symfony/mime": "<6.2", "symfony/translation": "<5.4", "symfony/workflow": "<5.4" }, diff --git a/src/Symfony/Component/Mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php b/src/Symfony/Component/Mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php new file mode 100644 index 0000000000000..2aaf8e6c474c5 --- /dev/null +++ b/src/Symfony/Component/Mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +/** + * @author Fabien Potencier + */ +class DefaultHtmlToTextConverter implements HtmlToTextConverterInterface +{ + public function convert(string $html, string $charset): string + { + return strip_tags(preg_replace('{<(head|style)\b.*?}is', '', $html)); + } +} diff --git a/src/Symfony/Component/Mime/HtmlToTextConverter/HtmlToTextConverterInterface.php b/src/Symfony/Component/Mime/HtmlToTextConverter/HtmlToTextConverterInterface.php new file mode 100644 index 0000000000000..ed2d8f98acb35 --- /dev/null +++ b/src/Symfony/Component/Mime/HtmlToTextConverter/HtmlToTextConverterInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +/** + * @author Fabien Potencier + */ +interface HtmlToTextConverterInterface +{ + /** + * Converts and HTML representation of a Message to a text representation. + * + * The output must use the same charset as the HTML one. + */ + public function convert(string $html, string $charset): string; +} diff --git a/src/Symfony/Component/Mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php b/src/Symfony/Component/Mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php new file mode 100644 index 0000000000000..253a7b19f681d --- /dev/null +++ b/src/Symfony/Component/Mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +use League\HTMLToMarkdown\HtmlConverter; +use League\HTMLToMarkdown\HtmlConverterInterface; + +/** + * @author Fabien Potencier + */ +class LeagueHtmlToMarkdownConverter implements HtmlToTextConverterInterface +{ + public function __construct( + private HtmlConverterInterface $converter = new HtmlConverter([ + 'hard_break' => true, + 'strip_tags' => true, + 'remove_nodes' => 'head style', + ]), + ) { + } + + public function convert(string $html, string $charset): string + { + return $this->converter->convert($html); + } +} diff --git a/src/Symfony/Component/Mime/Tests/HtmlToTextConverter/DefaultHtmlToTextConverterTest.php b/src/Symfony/Component/Mime/Tests/HtmlToTextConverter/DefaultHtmlToTextConverterTest.php new file mode 100644 index 0000000000000..48b46ca207d4b --- /dev/null +++ b/src/Symfony/Component/Mime/Tests/HtmlToTextConverter/DefaultHtmlToTextConverterTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Tests\HtmlToTextConverter; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\HtmlToTextConverter\DefaultHtmlToTextConverter; + +class DefaultHtmlToTextConverterTest extends TestCase +{ + public function testConvert() + { + $converter = new DefaultHtmlToTextConverter(); + $this->assertSame('HTML', $converter->convert('HTML', 'UTF-8')); + } +} diff --git a/src/Symfony/Component/Mime/Tests/HtmlToTextConverter/LeagueHtmlToMarkdownConverterTest.php b/src/Symfony/Component/Mime/Tests/HtmlToTextConverter/LeagueHtmlToMarkdownConverterTest.php new file mode 100644 index 0000000000000..0829078608da4 --- /dev/null +++ b/src/Symfony/Component/Mime/Tests/HtmlToTextConverter/LeagueHtmlToMarkdownConverterTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Tests\HtmlToTextConverter; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Mime\HtmlToTextConverter\LeagueHtmlToMarkdownConverter; + +class LeagueHtmlToMarkdownConverterTest extends TestCase +{ + public function testConvert() + { + $converter = new LeagueHtmlToMarkdownConverter(); + $this->assertSame('**HTML**', $converter->convert('HTML', 'UTF-8')); + } +} diff --git a/src/Symfony/Component/Mime/composer.json b/src/Symfony/Component/Mime/composer.json index f487df56072b1..a09c9e274f4c4 100644 --- a/src/Symfony/Component/Mime/composer.json +++ b/src/Symfony/Component/Mime/composer.json @@ -22,6 +22,7 @@ }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1", + "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", "symfony/dependency-injection": "^5.4|^6.0", "symfony/property-access": "^5.4|^6.0",