 core/core.services.yml                             |  4 ++
 core/lib/Drupal/Component/Utility/Html.php         | 39 +++++++++++
 .../RssResponseRelativeUrlFilter.php               | 76 ++++++++++++++++++++++
 .../file/src/Tests/FileFieldRSSContentTest.php     |  6 +-
 core/modules/node/src/Tests/NodeRSSContentTest.php | 49 ++++++++++++++
 core/modules/taxonomy/src/Tests/RssTest.php        |  2 +-
 core/modules/views/src/Tests/Wizard/BasicTest.php  |  2 +-
 7 files changed, 173 insertions(+), 5 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 6a31a44..dfc6288 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1598,3 +1598,7 @@ services:
     arguments: ['@current_user', '@path.current', '@path.matcher', '@language_manager']
     tags:
       - { name: event_subscriber }
+  response_filter.rss.relative_url:
+    class: Drupal\Core\EventSubscriber\RssResponseRelativeUrlFilter
+    tags:
+      - { name: event_subscriber }
diff --git a/core/lib/Drupal/Component/Utility/Html.php b/core/lib/Drupal/Component/Utility/Html.php
index 758a053..a6f449e 100644
--- a/core/lib/Drupal/Component/Utility/Html.php
+++ b/core/lib/Drupal/Component/Utility/Html.php
@@ -1,6 +1,7 @@
 <?php
 
 namespace Drupal\Component\Utility;
+use Symfony\Component\HttpFoundation\Request;
 
 /**
  * Provides DOMDocument helpers for parsing and serializing HTML strings.
@@ -37,6 +38,15 @@ class Html {
   protected static $isAjax = FALSE;
 
   /**
+   * All attributes that may contain URIs.
+   *
+   * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
+   *
+   * @var string[]
+   */
+  protected static $uriAttributes = ['href', 'poster', 'src', 'cite'];
+
+  /**
    * Prepares a string for use as a valid class name.
    *
    * Do not pass one string containing multiple classes as they will be
@@ -402,4 +412,33 @@ public static function escape($text) {
     return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
   }
 
+  /**
+   * Converts all root-relative URLs to absolute URLs.
+   *
+   * Does not change any existing protocol-relative or absolute URLs.
+   *
+   * Necessary for HTML that is served outside of a website: RSS, e-mail …
+   *
+   * @param string $html.
+   *   The partial (X)HTML snippet to load. Invalid markup will be corrected on
+   *   import.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return string
+   *   The updated (X)HTML snippet.
+   */
+  public static function transformRelativeUrlsToAbsolute($html, Request $request) {
+    $html_dom = Html::load($html);
+    $xpath = new \DOMXpath($html_dom);
+
+    // Update all relative URLs to absolute URLs in the given HTML.
+    foreach (static::$uriAttributes as $attr) {
+      foreach ($xpath->query("//*[starts-with(@$attr, '/') and not(starts-with(@$attr, '//'))]") as $node) {
+        $node->setAttribute($attr, $request->getSchemeAndHttpHost() . $node->getAttribute($attr));
+      }
+    }
+    return Html::serialize($html_dom);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/EventSubscriber/RssResponseRelativeUrlFilter.php b/core/lib/Drupal/Core/EventSubscriber/RssResponseRelativeUrlFilter.php
new file mode 100644
index 0000000..98c5ea9
--- /dev/null
+++ b/core/lib/Drupal/Core/EventSubscriber/RssResponseRelativeUrlFilter.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\EventSubscriber\RssResponseLinkFilter.
+ */
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Component\Utility\Html;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Subscribes to filter RSS responses, to make relative URIs absolute.
+ */
+class RssResponseRelativeUrlFilter implements EventSubscriberInterface {
+
+  /**
+   * Converts relative URLs to absolute URLs.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The response event.
+   */
+  public function onResponse(FilterResponseEvent $event) {
+    // Only care about RSS responses.
+    if (stripos($event->getResponse()->headers->get('Content-Type'), 'application/rss+xml') === FALSE) {
+      return;
+    }
+
+    $response = $event->getResponse();
+    $response->setContent($this->rssRel2abs($response->getContent(), $event->getRequest()));
+  }
+
+  /**
+   * Converts all root-relative URLs to absolute URLs.
+   *
+   * Does not change any existing protocol-relative or absolute URLs.
+   *
+   * @param string $rss_markup.
+   *   The RSS markup to update.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return string
+   *   The updated RSS markup.
+   */
+  protected function rssRel2abs($rss_markup, $request) {
+    $rss_dom = new \DOMDocument();
+    $rss_dom->loadXml($rss_markup);
+
+    // Invoke htmlRel2abs() on all HTML content embedded in this RSS feed.
+    foreach ($rss_dom->getElementsByTagName('description') as $node) {
+      $html_markup = $node->nodeValue;
+      if (!empty($html_markup)) {
+        $node->nodeValue = Html::transformRelativeUrlsToAbsolute($html_markup, $request);
+      }
+    }
+
+    return $rss_dom->saveXML();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Should run after any other response subscriber that modifies the markup.
+    // @see \Drupal\Core\EventSubscriber\ActiveLinkResponseFilter
+    $events[KernelEvents::RESPONSE][] = ['onResponse', -512];
+
+    return $events;
+  }
+
+}
diff --git a/core/modules/file/src/Tests/FileFieldRSSContentTest.php b/core/modules/file/src/Tests/FileFieldRSSContentTest.php
index 1fc4177..f472087 100644
--- a/core/modules/file/src/Tests/FileFieldRSSContentTest.php
+++ b/core/modules/file/src/Tests/FileFieldRSSContentTest.php
@@ -56,12 +56,12 @@ function testFileFieldRSSContent() {
     // Check that the RSS enclosure appears in the RSS feed.
     $this->drupalGet('rss.xml');
     $uploaded_filename = str_replace('public://', '', $node_file->getFileUri());
-    $test_element = sprintf(
-      '<enclosure url="%s" length="%s" type="%s" />',
+    $selector = sprintf(
+      'enclosure[url="%s"][length="%s"][type="%s"]',
       file_create_url("public://$uploaded_filename", array('absolute' => TRUE)),
       $node_file->getSize(),
       $node_file->getMimeType()
     );
-    $this->assertRaw($test_element, 'File field RSS enclosure is displayed when viewing the RSS feed.');
+    $this->assertTrue(!empty($this->cssSelect($selector)), 'File field RSS enclosure is displayed when viewing the RSS feed.');
   }
 }
diff --git a/core/modules/node/src/Tests/NodeRSSContentTest.php b/core/modules/node/src/Tests/NodeRSSContentTest.php
index 27870bd..a420efd 100644
--- a/core/modules/node/src/Tests/NodeRSSContentTest.php
+++ b/core/modules/node/src/Tests/NodeRSSContentTest.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\node\Tests;
 
+use Drupal\Core\Url;
+use Drupal\filter\Entity\FilterFormat;
+
 /**
  * Ensures that data added to nodes by other modules appears in RSS feeds.
  *
@@ -60,4 +63,50 @@ function testNodeRSSContent() {
     $this->assertNoText($rss_only_content, 'Node content designed for RSS does not appear when viewing node.');
   }
 
+  /**
+   * Tests relative, root-relative, protocol-relative and absolute URLs.
+   */
+  function testUrlHandling() {
+    global $base_url;
+    $uri_parts = parse_url($base_url);
+
+    // Only the plain_text text format is available by default, which escapes
+    // all HTML.
+    FilterFormat::create([
+      'format' => 'full_html',
+      'name' => 'Full HTML',
+      'filters' => [],
+    ])->save();
+
+    $defaults = [
+      'type' => 'article',
+      'promote' => 1,
+    ];
+    $this->drupalCreateNode($defaults + [
+      'body' => [
+        'value' => '<p><a href="/root-relative">Root-relative URL</a></p>',
+        'format' => 'full_html',
+      ],
+    ]);
+    $protocol_relative_url = '//' . $uri_parts['host'] . '/protocol-relative';
+    $this->drupalCreateNode($defaults + [
+      'body' => [
+        'value' => '<p><a href="' . $protocol_relative_url . '">Protocol-relative URL</a></p>',
+        'format' => 'full_html',
+      ],
+    ]);
+    $absolute_url = $base_url . '/absolute';
+    $this->drupalCreateNode($defaults + [
+      'body' => [
+        'value' => '<p><a href="' . $absolute_url . '">Absolute URL</a></p>',
+        'format' => 'full_html',
+      ],
+    ]);
+
+    $this->drupalGet('rss.xml');
+    $this->assertRaw(Url::fromUri('base:root-relative', ['absolute' => TRUE])->toString(TRUE)->getGeneratedUrl(), 'Root-relative URL is transformed to absolute.');
+    $this->assertRaw($protocol_relative_url, 'Protocol-relative URL is left untouched.');
+    $this->assertRaw($absolute_url, 'Absolute URL is left untouched.');
+  }
+
 }
diff --git a/core/modules/taxonomy/src/Tests/RssTest.php b/core/modules/taxonomy/src/Tests/RssTest.php
index 3350b33..300e0c1 100644
--- a/core/modules/taxonomy/src/Tests/RssTest.php
+++ b/core/modules/taxonomy/src/Tests/RssTest.php
@@ -105,7 +105,7 @@ function testTaxonomyRss() {
 
     // Test that the feed page exists for the term.
     $this->drupalGet("taxonomy/term/{$term1->id()}/feed");
-    $this->assertRaw('<rss version="2.0"', "Feed page is RSS.");
+    $this->assertTrue(!empty($this->cssSelect('rss[version="2.0"]')), "Feed page is RSS.");
 
     // Check that the "Exception value" is disabled by default.
     $this->drupalGet('taxonomy/term/all/feed');
diff --git a/core/modules/views/src/Tests/Wizard/BasicTest.php b/core/modules/views/src/Tests/Wizard/BasicTest.php
index e558cde..1a20f17 100644
--- a/core/modules/views/src/Tests/Wizard/BasicTest.php
+++ b/core/modules/views/src/Tests/Wizard/BasicTest.php
@@ -80,7 +80,7 @@ function testViewsWizardAndListing() {
     $elements = $this->cssSelect('link[href="' . Url::fromRoute('view.' . $view2['id'] . '.feed_1', [], ['absolute' => TRUE])->toString() . '"]');
     $this->assertEqual(count($elements), 1, 'Feed found.');
     $this->drupalGet($view2['page[feed_properties][path]']);
-    $this->assertRaw('<rss version="2.0"');
+    $this->assertTrue(!empty($this->cssSelect('rss[version="2.0"]')));
     // The feed should have the same title and nodes as the page.
     $this->assertText($view2['page[title]']);
     $this->assertRaw($node1->url('canonical', ['absolute' => TRUE]));
