diff --git a/core/lib/Drupal/Core/Mail/MailManager.php b/core/lib/Drupal/Core/Mail/MailManager.php index 3a7a93f8b193df088f01cc16fd54b00e9bd3be65..bbc0432c6da4df16c23f6f6a6127d81af1b38a06 100644 --- a/core/lib/Drupal/Core/Mail/MailManager.php +++ b/core/lib/Drupal/Core/Mail/MailManager.php @@ -2,7 +2,9 @@ namespace Drupal\Core\Mail; +use Drupal\Component\Render\MarkupInterface; use Drupal\Component\Render\PlainTextOutput; +use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Messenger\MessengerTrait; @@ -10,6 +12,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Render\Markup; use Drupal\Core\Render\RenderContext; use Drupal\Core\Render\RendererInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -277,6 +280,13 @@ public function doMail($module, $key, $to, $langcode, $params = [], $reply = NUL // Retrieve the responsible implementation for this message. $system = $this->getInstance(['module' => $module, 'key' => $key]); + // Attempt to convert relative URLs to absolute. + foreach ($message['body'] as &$body_part) { + if ($body_part instanceof MarkupInterface) { + $body_part = Markup::create(Html::transformRootRelativeUrlsToAbsolute((string) $body_part, \Drupal::request()->getSchemeAndHttpHost())); + } + } + // Format the message body. $message = $system->format($message); diff --git a/core/modules/system/tests/modules/mail_html_test/mail_html_test.info.yml b/core/modules/system/tests/modules/mail_html_test/mail_html_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..08ae1e8f0ce32868d912f55544662fc0031b4ebd --- /dev/null +++ b/core/modules/system/tests/modules/mail_html_test/mail_html_test.info.yml @@ -0,0 +1,6 @@ +name: 'HTML mail test support' +description: 'Test if HTML in mails works as expected.' +type: module +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/mail_html_test/mail_html_test.module b/core/modules/system/tests/modules/mail_html_test/mail_html_test.module new file mode 100644 index 0000000000000000000000000000000000000000..f873c89b50aa3783d9b7cb00258c67f73b19f316 --- /dev/null +++ b/core/modules/system/tests/modules/mail_html_test/mail_html_test.module @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Helper module for the html mail and url conversion tests. + */ + +/** + * Implements hook_mail(). + */ +function mail_html_test_mail($key, &$message, $params) { + switch ($key) { + case 'render_from_message_param': + $message['body'][] = \Drupal::service('renderer')->renderPlain($params['message']); + break; + } +} diff --git a/core/modules/system/tests/modules/mail_html_test/src/Plugin/Mail/TestHtmlMailCollector.php b/core/modules/system/tests/modules/mail_html_test/src/Plugin/Mail/TestHtmlMailCollector.php new file mode 100644 index 0000000000000000000000000000000000000000..43e413596d5fb00373b3942b8b6e45537076df95 --- /dev/null +++ b/core/modules/system/tests/modules/mail_html_test/src/Plugin/Mail/TestHtmlMailCollector.php @@ -0,0 +1,33 @@ +<?php + +namespace Drupal\mail_html_test\Plugin\Mail; + +use Drupal\Core\Mail\MailFormatHelper; +use Drupal\Core\Mail\Plugin\Mail\TestMailCollector; + +/** + * Defines a mail backend that captures sent HTML messages in the state system. + * + * This class is for running tests or for development and does not convert HTML + * to plaintext. + * + * @Mail( + * id = "test_html_mail_collector", + * label = @Translation("HTML mail collector"), + * description = @Translation("Does not send the message, but stores its HTML in Drupal within the state system. Used for testing.") + * ) + */ +class TestHtmlMailCollector extends TestMailCollector { + + /** + * {@inheritdoc} + */ + public function format(array $message) { + // Join the body array into one string. + $message['body'] = implode(PHP_EOL, $message['body']); + // Wrap the mail body for sending. + $message['body'] = MailFormatHelper::wrapMail($message['body']); + return $message; + } + +} diff --git a/core/modules/system/tests/src/Functional/Mail/MailTest.php b/core/modules/system/tests/src/Functional/Mail/MailTest.php index e8accddb6f4e2f2bd6fbec57e6f082e1a3c4e7f0..6f32539243f5042b57f66a9cdf1e6ecf77c3983a 100644 --- a/core/modules/system/tests/src/Functional/Mail/MailTest.php +++ b/core/modules/system/tests/src/Functional/Mail/MailTest.php @@ -2,8 +2,13 @@ namespace Drupal\Tests\system\Functional\Mail; +use Drupal\Component\Utility\Random; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Mail\MailFormatHelper; use Drupal\Core\Mail\Plugin\Mail\TestMailCollector; +use Drupal\Core\Render\Markup; +use Drupal\Core\Url; +use Drupal\file\Entity\File; use Drupal\Tests\BrowserTestBase; use Drupal\system_mail_failure_test\Plugin\Mail\TestPhpMailFailure; @@ -19,7 +24,7 @@ class MailTest extends BrowserTestBase { * * @var array */ - public static $modules = ['simpletest', 'system_mail_failure_test']; + public static $modules = ['simpletest', 'system_mail_failure_test', 'mail_html_test', 'file', 'image']; /** * Assert that the pluggable mail system is functional. @@ -104,4 +109,178 @@ public function testFromAndReplyToHeader() { $this->assertFalse(isset($sent_message['headers']['Errors-To']), 'Errors-to header must not be set, it is deprecated.'); } + /** + * Checks that relative paths in mails are converted into absolute URLs. + */ + public function testConvertRelativeUrlsIntoAbsolute() { + $language_interface = \Drupal::languageManager()->getCurrentLanguage(); + + // Use the HTML compatible state system collector mail backend. + $this->config('system.mail')->set('interface.default', 'test_html_mail_collector')->save(); + + // Fetch the hostname and port for matching against. + $http_host = \Drupal::request()->getSchemeAndHttpHost(); + + // Random generator. + $random = new Random(); + + // One random tag name. + $tag_name = strtolower($random->name(8, TRUE)); + + // Test root relative urls. + foreach (['href', 'src'] as $attribute) { + // Reset the state variable that holds sent messages. + \Drupal::state()->set('system.test_mail_collector', []); + + $html = "<$tag_name $attribute=\"/root-relative\">root relative url in mail test</$tag_name>"; + $expected_html = "<$tag_name $attribute=\"{$http_host}/root-relative\">root relative url in mail test</$tag_name>"; + + // Prepare render array. + $render = ['#markup' => Markup::create($html)]; + + // Send a test message that simpletest_mail_alter should cancel. + \Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]); + // Retrieve sent message. + $captured_emails = \Drupal::state()->get('system.test_mail_collector'); + $sent_message = end($captured_emails); + + // Wrap the expected HTML and assert. + $expected_html = MailFormatHelper::wrapMail($expected_html); + $this->assertSame($expected_html, $sent_message['body'], "Asserting that {$attribute} is properly converted for mails."); + } + + // Test protocol relative urls. + foreach (['href', 'src'] as $attribute) { + // Reset the state variable that holds sent messages. + \Drupal::state()->set('system.test_mail_collector', []); + + $html = "<$tag_name $attribute=\"//example.com/protocol-relative\">protocol relative url in mail test</$tag_name>"; + $expected_html = "<$tag_name $attribute=\"//example.com/protocol-relative\">protocol relative url in mail test</$tag_name>"; + + // Prepare render array. + $render = ['#markup' => Markup::create($html)]; + + // Send a test message that simpletest_mail_alter should cancel. + \Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]); + // Retrieve sent message. + $captured_emails = \Drupal::state()->get('system.test_mail_collector'); + $sent_message = end($captured_emails); + + // Wrap the expected HTML and assert. + $expected_html = MailFormatHelper::wrapMail($expected_html); + $this->assertSame($expected_html, $sent_message['body'], "Asserting that {$attribute} is properly converted for mails."); + } + + // Test absolute urls. + foreach (['href', 'src'] as $attribute) { + // Reset the state variable that holds sent messages. + \Drupal::state()->set('system.test_mail_collector', []); + + $html = "<$tag_name $attribute=\"http://example.com/absolute\">absolute url in mail test</$tag_name>"; + $expected_html = "<$tag_name $attribute=\"http://example.com/absolute\">absolute url in mail test</$tag_name>"; + + // Prepare render array. + $render = ['#markup' => Markup::create($html)]; + + // Send a test message that simpletest_mail_alter should cancel. + \Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]); + // Retrieve sent message. + $captured_emails = \Drupal::state()->get('system.test_mail_collector'); + $sent_message = end($captured_emails); + + // Wrap the expected HTML and assert. + $expected_html = MailFormatHelper::wrapMail($expected_html); + $this->assertSame($expected_html, $sent_message['body'], "Asserting that {$attribute} is properly converted for mails."); + } + } + + /** + * Checks that mails built from render arrays contain absolute paths. + * + * By default Drupal uses relative paths for images and links. When sending + * emails, absolute paths should be used instead. + */ + public function testRenderedElementsUseAbsolutePaths() { + $language_interface = \Drupal::languageManager()->getCurrentLanguage(); + + // Use the HTML compatible state system collector mail backend. + $this->config('system.mail')->set('interface.default', 'test_html_mail_collector')->save(); + + // Fetch the hostname and port for matching against. + $http_host = \Drupal::request()->getSchemeAndHttpHost(); + + // Random generator. + $random = new Random(); + $image_name = $random->name(); + + // Create an image file. + $file = File::create(['uri' => "public://{$image_name}.png", 'filename' => "{$image_name}.png"]); + $file->save(); + + $base_path = base_path(); + + $path_pairs = [ + 'root relative' => [$file->getFileUri(), "{$http_host}{$base_path}{$this->publicFilesDirectory}/{$image_name}.png"], + 'protocol relative' => ['//example.com/image.png', '//example.com/image.png'], + 'absolute' => ['http://example.com/image.png', 'http://example.com/image.png'], + ]; + + // Test images. + foreach ($path_pairs as $test_type => $paths) { + list($input_path, $expected_path) = $paths; + + // Reset the state variable that holds sent messages. + \Drupal::state()->set('system.test_mail_collector', []); + + // Build the render array. + $render = [ + '#theme' => 'image', + '#uri' => $input_path, + ]; + $expected_html = "<img src=\"$expected_path\" alt=\"\" />"; + + // Send a test message that simpletest_mail_alter should cancel. + \Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]); + // Retrieve sent message. + $captured_emails = \Drupal::state()->get('system.test_mail_collector'); + $sent_message = end($captured_emails); + + // Wrap the expected HTML and assert. + $expected_html = MailFormatHelper::wrapMail($expected_html); + $this->assertSame($expected_html, $sent_message['body'], "Asserting that {$test_type} paths are converted properly."); + } + + // Test links. + $path_pairs = [ + 'root relative' => [Url::fromUserInput('/path/to/something'), "{$http_host}{$base_path}path/to/something"], + 'protocol relative' => [Url::fromUri('//example.com/image.png'), '//example.com/image.png'], + 'absolute' => [Url::fromUri('http://example.com/image.png'), 'http://example.com/image.png'], + ]; + + foreach ($path_pairs as $paths) { + list($input_path, $expected_path) = $paths; + + // Reset the state variable that holds sent messages. + \Drupal::state()->set('system.test_mail_collector', []); + + // Build the render array. + $render = [ + '#title' => 'Link', + '#type' => 'link', + '#url' => $input_path, + ]; + $expected_html = "<a href=\"$expected_path\">Link</a>"; + + // Send a test message that simpletest_mail_alter should cancel. + \Drupal::service('plugin.manager.mail')->mail('mail_html_test', 'render_from_message_param', 'relative_url@example.com', $language_interface->getId(), ['message' => $render]); + // Retrieve sent message. + $captured_emails = \Drupal::state()->get('system.test_mail_collector'); + $sent_message = end($captured_emails); + + // Wrap the expected HTML and assert. + $expected_html = MailFormatHelper::wrapMail($expected_html); + $this->assertSame($expected_html, $sent_message['body']); + } + } + }