Problem/Motivation

  1. The root cause of this mess: the fact that Drupal generates file URLs without knowledge about the context those URLs will be used in. But the place where a file URL is generated in may itself not know the context either! For example, an image may be rendered into HTML and then that HTML may happen to be sent in an e-mail (or RSS), and therefore the URL should be absolute. But that was impossible to know at the time when the image template was being rendered (for example when rendering an article to be sent via e-mail, the rendering of the article is not at all different: it's still rendered to HTML).
  2. Any time you're sending HTML in a non-web context (for example: e-mail, RSS), you have to make sure all URLs are absolute. But file_create_url() is only invoked on files referenced by code. A complete solution would already have to make all relative URLs absolute anyway, including those in user-generated content.

Proposed resolution

The only sensible solution is to solve the problem at its root. make sure all relative URLs in e-mails are made absolute.

Remaining tasks

  • Patch.
  • Test coverage.
  • Reviews.

User interface changes

None.

API changes

None.

Data model changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

Wim Leers’s picture

Assigned: Unassigned » Berdir

Talked to @Berdir in IRC, he's going to take this on. See #88183-61: Relative URLs in feeds should be converted to absolute ones for preparational work to support his work here.

Wim Leers’s picture

kylebrowning’s picture

I can take this on if it has stalled.

alexpott’s picture

I started looking into this however I actually think this is not an issue for core. The only mail plugin we have in core calls MailFormatHelper::htmlToText() which calls MailFormatHelper:: htmlToMailUrls() which ensures that any links in the mail are absolute.

Looking at MailManager::mail() I think it is the mail plugin's responsibility to ensure any links are absolute when it does:

    // Format the message body.
    $message = $system->format($message);

That said perhaps we should repurpose this issue for documentation?

Berdir’s picture

Hm, possibly. It's still an API/behavior change for everyone who sends HTML mails that happened in a patch update, though.

But if #88183: Relative URLs in feeds should be converted to absolute ones isn't critical, then I guess this isn't either.

alexpott’s picture

Status: Active » Needs review
Issue tags: -Needs tests
FileSize
641 bytes
alexpott’s picture

@Berdir yep it was an API behaviour change but given that it has occurred the question is what is the best way to go forward. Phpmail::format() takes steps to ensure all urls are absolute.

We have a few choices I guess...

  1. Call something for every plugin after the ->format() call that does something similar to #88183: Relative URLs in feeds should be converted to absolute ones
  2. Call something a method on plugins that implement it, after the ->format() call (advantage being that PhpMail does not have a performance penalty).
  3. Add the documentation suggested here

Option 1 means that we kinda revert the API breaks caused by #1494670: References to CSS, JS, and similar files should be root-relative URLs: avoids mixed content warnings & fewer bytes to send but it does NOT feel like the path to go down now.

alexpott’s picture

In discussing with @Berdir on IRC he pointed out that

not sure what you mean with option 2, but sounds like a new optional interface + maybe a trait, so contrib modules would have to change anyway. not much different to
#3
, really?
so it's probably not 2 or 3 but 2 and 3? document it and provide an easy way for them to handle this?

So maybe option two should be more like:
2. Add a new method to MailInterface to convert relative urls to absolute - plugins like core's PhpMail can no-op because they've got it covered in their ::format() (because it removes all HTML

I suggested this in IRC @berdir asked why do we want to break the SMTP module but actually that module has an option $this->smtpConfig->get('smtp_allowhtml'); if this is FALSE it does the same as core's PhpMail if it is TRUE it'll need to do something about relative urls.

The more I investigate the more I'm in favour of option 2 with an interface break so the plugin authors know they have to do something.

Berdir’s picture

As discussed in IRC, I can live with option 3 if we provide a trait or so that does the work and you just have to call it, and announce this in the release notes.

2 seems a bit much to me (breaking all mail plugins to enforce them to consider this), it would break those modules for many users that are not even sending html (and just use smtp or swiftmailer to send through smtp), and sending a mail is probably not something you'd test in a patch update?

If you feel strongly about that then I can live that, for me personally, it doesn't make a difference.. I have to fix and update swiftmailer anyway.

alexpott’s picture

Priority: Critical » Major

Discussed with @catch and we agreed on a variation of option 2 - adding a new interface and using trigger error - so that mail plugin author's can find out something is wrong but also so live sites don't break.

We also agreed that this is not critical as core's mail plugin is doing things correctly. Demoting to major since it is a regression that we've introduced to 8.x cycle and the best solution requires a soft API break.

alexpott’s picture

There might another approach for Mail plugins instead of having to fix all the links. I think I recall once ensuring that HTML emails displayed correctly using the base element. Note this does not affect the outcome here - it just offers another solution for example plugin to explore.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Berdir’s picture

Just trying out some ideas, what if we'd do something like this? No parsing overhead if the input is not actually some kind of HTML, but it would still work also for PhpMail if the markup is then converted to plaintext and contains relative links.

Also no additional methods or interfaces necessary.

dawehner’s picture

Given that it would at least not "break" any kind of existing simple strings, but it would allow modules, like simplenews, to opt into that behaviour basically.

Status: Needs review » Needs work

The last submitted patch, 14: mail-relative-to-absolute-2704597-14.patch, failed testing.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Wim Leers’s picture

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Wim Leers’s picture

TR’s picture

Neograph734’s picture

@Berdir, is the proposed solution direction of #14 still valid? I could re-roll it, but it may be that core has other methods by now?

Berdir’s picture

Yes, that is still valid, I'm not aware of any changes here. Would be awesome if you could look into tests.

Neograph734’s picture

Assigned: Berdir » Neograph734

I'll see if I can come up with something then.

Wim Leers’s picture

🎉 Thanks, @Neograph734!

Neograph734’s picture

@Berdir and @Wim Leers,

I am a bit confused on what to test. The main use cases for desiring absolute URLs in mails appears to come from #2931548: CDN and images in emails: absolute URLs necessary and #2842780: Add a token for the site logo and those are about adding images in mails.

However, the only mail backend that is provided with Drupal, is PhpMail, which uses MailFormatHelper::htmlToText to strip everything except for <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt> <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr>. Of those elements, only <a> can contain a URL, but MailFormatHelper::htmlToMailUrls should already take care of making that absolute.

I could write a simple test to ensure that relative urls in anchors are indeed converted into absolute as claimed, but that would not help #2931548: CDN and images in emails: absolute URLs necessary and #2842780: Add a token for the site logo. On the other hand, implementing a totally different mail backend that supports images seems a bit out of scope.

IMO these related issues are not as related as assumed, and their problems cannot be solved within this issue. Or am I missing something?

TR’s picture

implementing a totally different mail backend that supports images seems a bit out of scope.

You just have to stop PhpMail from converting the HTML to plain text, so here's your implementation:

use Drupal\Core\Mail\Plugin\Mail\TestMailCollector;

/**
 * Modifies the Drupal mail system to send HTML emails.
 *
 * @Mail(
 *   id = "test_html_mail",
 *   label = @Translation("Test mailer"),
 *   description = @Translation("Sends the message as HTML, using PHP's native mail() function.")
 * )
 */
class TestMail extends TestMailCollector {

  /**
   * Overrides PhpMail::format() to prevent it from stripping out the HTML.
   *
   * @param array $message
   *   A message array, as described in hook_mail_alter().
   *
   * @return string
   *   The formatted $message.
   */
  public function format(array $message) {
    $message['body'] = implode("\n\n", $message['body']);
    return $message;
  }

}

Note: TestMailCollector extends PhpMail and overrides the mail() method to capture mail in a state variable rather than trying to send it using PHP's mail() function.

Oh, and of course you have to tell your test to use the new plugin:

    \Drupal::configFactory()->getEditable('system.mail')->set('interface.default', 'test_html_mail')->save();
Berdir’s picture

Yes exactly, we need a html test mail plugin that doesn't do the HTML conversion. There is also one in #2223967: Do not decode a contact message twice that is pretty similar to the example .

Neograph734’s picture

Thanks for the guidance. This should help me to compose a working patch. I hope to have something by the end of the weekend :)

Wim Leers’s picture

Thanks, @Neograph734!

Neograph734’s picture

Oke, here we go.

I copied the mail plugin from #2223967: Do not decode a contact message twice (and cleaned up the description to better match what it does).

I have implemented hook_mail in simpletest, to include a render_from_message_param mail key, which takes the 'message' parameter and changes it into a body part. (Might be useful for other tests too.)

/**
 * Implements hook_mail().
 */
function simpletest_mail($key, &$message, $params) {
  switch ($key) {
    case 'render_from_message_param':
      $message['body'][] = render($params['message']);
      break;
  }
}

I then implemented the changes from Berdir's patch in MailManager.php, but that failed when plain strings containing urls were provided as mail body parts. Therefor I chose to remove the check for MarkupInterface (why was that required?) and implement it like this:

// Attempt to convert relative URLs to absolute.
foreach ($message['body'] as &$body_part) {
  $body_part = Markup::create(Html::transformRootRelativeUrlsToAbsolute((string) $body_part, \Drupal::request()->getSchemeAndHttpHost()));
}

Finally, I had started working on a second test to assert that rendered elements are properly converted as well. But I could not get it to generate any output. And I doubt if is necessary at all. It is attached as well, and if you believe it is good to have I can have another look.

Berdir’s picture

  1. +++ b/core/modules/simpletest/simpletest.module
    @@ -751,6 +751,17 @@ function simpletest_clean_results_table($test_id = NULL) {
    + */
    +function simpletest_mail($key, &$message, $params) {
    

    I think this should be in a test module, not in simpletest itself. I see the test already depends on a similar alter hook here, but we already converted this test to phpunit and we eventually want to get rid of (almost) everything in simpletest.module, so sooner or later, we will have to move it.

  2. +++ b/core/lib/Drupal/Core/Mail/Plugin/Mail/TestHtmlMailCollector.php
    @@ -0,0 +1,32 @@
    +class TestHtmlMailCollector extends TestMailCollector {
    

    kind of the same here.

    by being a plugin, this is picked up by e.g. mailsystem.module and the user can configure this in the UI, which is weird.

  3. +++ b/core/modules/simpletest/simpletest.module
    @@ -751,6 +751,17 @@ function simpletest_clean_results_table($test_id = NULL) {
    +      $message['body'][] = render($params['message']);
    

    render should be deprecated like drupal_render() don't know why it isn't.

    Lets use \Drupal::service('renderer')->renderPlain() because render() bubbles things up into the main context could leave things like placeholders, since this will be sent out as-is, we need to fully render it.

I then implemented the changes from Berdir's patch in MailManager.php, but that failed when plain strings containing urls were provided as mail body parts. Therefor I chose to remove the check for MarkupInterface (why was that required?) and implement it like this:

Markup stuff is tricky. If we accept any input as valid and allowed html, then something like a comment notification might result in showing user input as html in a mail client. While those have to protect themself already against arbitrary mails with html, a user might not expect something strange from a trusted system notification e-mail.

What kind of plain strings with urls did you have? HTML that's not a MarkupInterface is imho a bug. See for example \Drupal\swiftmailer\Plugin\Mail\SwiftMailer::massageMessageBody(), anything that is not (safe) Markup is escaped. Your change would completely sidetrack that as everything would then be Markup. We already fixed things in core (contact e-mails) to be compatible with that.

Neograph734’s picture

Assigned: Unassigned » Neograph734
Status: Needs review » Needs work

Thanks for the feedback, I will adjust the patch accordingly.

What kind of plain strings with urls did you have?

It did not encounter any specifically, but in the past I have seen them at least in several contrib modules. I will make sure to include the check for the MarkupInterface.

TR’s picture

Finally, I had started working on a second test to assert that rendered elements are properly converted as well. But I could not get it to generate any output. And I doubt if is necessary at all.

Yes, I do think it's necessary, because that's where you test the image URLs.

The construct that was broken by #1494670: References to CSS, JS, and similar files should be root-relative URLs: avoids mixed content warnings & fewer bytes to send and which causes a problem in #2842780: Add a token for the site logo and elsewhere, and which prompted this issue is:

    $logo = array(
      '#theme' => 'image',
      '#uri' =>  Url::fromUri($uri, ['absolute' => TRUE])->toString(),
    );

since this always produces a relative URL when $uri is a local image.

It is this usage which we have to ensure is fixed, so I think this should be tested explicitly, as you do in your second test.

Neograph734’s picture

@TR, well, yes and no (this is the cause of my confusion as well).

My reasoning against is that the coverson to absolute happens after format, meaning that all render elements should have been converted to html already right?

So if we test for the replacement in any random html tag, we should be good.

TR’s picture

In theory, if you could really test any random HTML tag, then yes, <img> tags would be covered by that.

However, testing all possible HTML tags isn't practical - your test makes assumptions, such as URLs will be found only in href or src attributes, that aren't strictly true (what about <form action="/relative/url"> ?

So I think that there should be a specific test for the bug we know about - the '#theme' => 'image', thing - AND a general test like you have which attempts to cover all other unknown cases.

The specific test proves the bug and the fix, while the general test attempts to protect us against other similar bugs being introduced.

I can contrive several ways the general test could potentially fail to catch the <img> bug that we intend to fix. For instance, the general test is not checking self-closing tags like img, so potentially the relative->absolute conversion fails on these and won't be caught. Likewise, the general test has only one tag attribute, so potentially the conversion fails if the URL that needs converting is in the second rather than first attribute, etc.

The point is, we do have a specific bug and a specific failure, so we should have a specific test for the failure and fix. Because adding a specific test is just a few extra lines of code in your current test, I don't see any reason to leave it out.

Neograph734’s picture

Thank you both for the feedback. This is the improved version.

Neograph734’s picture

Assigned: Neograph734 » Unassigned

Status: Needs review » Needs work

The last submitted patch, 37: mail-relative-to-absolute-2704597-37.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Neograph734’s picture

Added the base_path to the expected path pattern to ensure the expected path matches with the actual path. Furthermore the coding standards errors that slipped through have been resolved.

Status: Needs review » Needs work

The last submitted patch, 41: mail-relative-to-absolute-2704597-41.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Neograph734’s picture

And obviously the base path was needed for the second round of assertions as well...

Neograph734’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 43: mail-relative-to-absolute-2704597-43.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Neograph734’s picture

Status: Needs work » Needs review

That was a random fail. Retesting showed up green. What do you think?

Neograph734’s picture

@Berdir, could you have another look please?

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Berdir’s picture

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 49: mail-relative-to-absolute-2704597-49.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

izus’s picture

Status: Needs work » Needs review
FileSize
13.02 KB

Hi,
following patch is #49 + codesniffer_fixes.patch reported in #51
hope this will help it to be green :)

jonathanshaw’s picture

@izus an interdiff would make it 1000 times easier to review and rtbc your changes.

izus’s picture

FileSize
711 bytes

Hi,
@jonathanshaw
here is the interdiff between #49 and #52
Thanks

jonathanshaw’s picture

Status: Needs review » Reviewed & tested by the community
larowlan’s picture

Adding review credits for TR for helping with the test case

  • larowlan committed 530f7ac on 8.7.x
    Issue #2704597 by Neograph734, Berdir, izus, alexpott, TR: Relative URLs...

  • larowlan committed f2915d9 on 8.6.x
    Issue #2704597 by Neograph734, Berdir, izus, alexpott, TR: Relative URLs...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Committed as 530f7ac and pushed to 8.7.x.

Cherry-picked as f2915d9 and pushed to 8.6.x.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

AnnaE1990’s picture

i need patch for drupal 9.2 any off them i can use?

longwave’s picture

@AnnaE1990 This feature is already included in Drupal 9.2, if you are having trouble please open a new issue with full information on what's not working.