From cddee382e9c25ae2725348a40520c66ee4eae812 Mon Sep 17 00:00:00 2001
From: Mac_Weber <Mac_Weber@789986.no-reply.drupal.org>
Date: Fri, 18 Mar 2016 20:01:09 -0300
Subject: [PATCH] Issue #2573635 by Mac_Weber, dawehner: Link field should
 accept protocol-relative URLs

---
 core/lib/Drupal/Core/Url.php                                   | 10 +++++++++-
 .../link/src/Plugin/Field/FieldFormatter/LinkFormatter.php     |  5 +++++
 core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php  |  2 +-
 .../Constraint/LinkExternalProtocolsConstraintValidator.php    |  2 +-
 core/modules/link/src/Tests/LinkFieldTest.php                  |  2 +-
 .../LinkExternalProtocolsConstraintValidatorTest.php           |  2 ++
 .../menu_link_content/src/Tests/MenuLinkContentFormTest.php    |  2 +-
 core/tests/Drupal/Tests/Core/UnroutedUrlTest.php               |  3 ++-
 8 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php
index 34b4cea..ec32448 100644
--- a/core/lib/Drupal/Core/Url.php
+++ b/core/lib/Drupal/Core/Url.php
@@ -302,7 +302,15 @@ public static function fromUri($uri, $options = []) {
     if ($uri_parts === FALSE) {
       throw new \InvalidArgumentException("The URI '$uri' is malformed.");
     }
-    if (empty($uri_parts['scheme'])) {
+    // We support protocol relative URLs.
+    if (strpos($uri, 'internal://') === 0) {
+      $uri = substr($uri, strlen('internal:'));
+      $uri_parts['scheme'] = '';
+    }
+    elseif (strpos($uri, '//') === 0) {
+      $uri_parts['scheme'] = '';
+    }
+    elseif (empty($uri_parts['scheme'])) {
       throw new \InvalidArgumentException("The URI '$uri' is invalid. You must use a valid URI scheme.");
     }
     $uri_parts += ['path' => ''];
diff --git a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
index 9245ba3..ef94ca5 100644
--- a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
+++ b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
@@ -184,6 +184,11 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
       $url = $this->buildUrl($item);
       $link_title = $url->toString();
 
+      // If the external URL starts with '//', trim it.
+      if ($url->isExternal() && (strpos($url->getUri(), '//') == 0)) {
+        $link_title = ltrim($link_title, '//');
+      }
+
       // If the title field value is available, use it for the link text.
       if (empty($settings['url_only']) && !empty($item->title)) {
         // Unsanitized token replacement here because the entire link title
diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
index 9c97caa..98b1ce5 100644
--- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
+++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
@@ -143,7 +143,7 @@ public static function validateUriElement($element, FormStateInterface $form_sta
     // @todo '<front>' is valid input for BC reasons, may be removed by
     //   https://www.drupal.org/node/2421941
     if (parse_url($uri, PHP_URL_SCHEME) === 'internal' && !in_array($element['#value'][0], ['/', '?', '#'], TRUE) && substr($element['#value'], 0, 7) !== '<front>') {
-      $form_state->setError($element, t('Manually entered paths should start with /, ? or #.'));
+      $form_state->setError($element, t('Manually entered internal paths should start with /, ? or #. External paths should start with //, or a protocol such as https://'));
       return;
     }
   }
diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkExternalProtocolsConstraintValidator.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkExternalProtocolsConstraintValidator.php
index fad7b6d..7a3e4de 100644
--- a/core/modules/link/src/Plugin/Validation/Constraint/LinkExternalProtocolsConstraintValidator.php
+++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkExternalProtocolsConstraintValidator.php
@@ -45,7 +45,7 @@ public function validate($value, Constraint $constraint) {
         return;
       }
       // Disallow external URLs using untrusted protocols.
-      if ($url->isExternal() && !in_array(parse_url($url->getUri(), PHP_URL_SCHEME), UrlHelper::getAllowedProtocols())) {
+      if ($url->isExternal() && (strpos($url->getUri(), '//') != 0 && !in_array(parse_url($url->getUri(), PHP_URL_SCHEME), UrlHelper::getAllowedProtocols()))) {
         $this->context->addViolation($constraint->message, array('@uri' => $value->uri));
       }
     }
diff --git a/core/modules/link/src/Tests/LinkFieldTest.php b/core/modules/link/src/Tests/LinkFieldTest.php
index 2d00cfa..05df1ee 100644
--- a/core/modules/link/src/Tests/LinkFieldTest.php
+++ b/core/modules/link/src/Tests/LinkFieldTest.php
@@ -146,7 +146,7 @@ function testURLValidation() {
 
     // Define some invalid URLs.
     $validation_error_1 = "The path '@link_path' is invalid.";
-    $validation_error_2 = 'Manually entered paths should start with /, ? or #.';
+    $validation_error_2 = 'Manually entered internal paths should start with /, ? or #. External paths should start with //, or a protocol such as https://';
     $validation_error_3 = "The path '@link_path' is inaccessible.";
     $invalid_external_entries = array(
       // Invalid protocol
diff --git a/core/modules/link/tests/src/Unit/Plugin/Validation/Constraint/LinkExternalProtocolsConstraintValidatorTest.php b/core/modules/link/tests/src/Unit/Plugin/Validation/Constraint/LinkExternalProtocolsConstraintValidatorTest.php
index 7a9c19c..f94cdda 100644
--- a/core/modules/link/tests/src/Unit/Plugin/Validation/Constraint/LinkExternalProtocolsConstraintValidatorTest.php
+++ b/core/modules/link/tests/src/Unit/Plugin/Validation/Constraint/LinkExternalProtocolsConstraintValidatorTest.php
@@ -55,6 +55,8 @@ public function providerValidate() {
     $data[] = ['http://www.drupal.org', TRUE];
     $data[] = ['https://www.drupal.org', TRUE];
     $data[] = ['magnet:?xt=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C', TRUE];
+    // Protocol relative URLs
+    $data[] = ['//www.drupal.org', TRUE];
 
     // Invalid protocols.
     $data[] = ['ftp://ftp.funet.fi/pub/standards/RFC/rfc959.txt', FALSE];
diff --git a/core/modules/menu_link_content/src/Tests/MenuLinkContentFormTest.php b/core/modules/menu_link_content/src/Tests/MenuLinkContentFormTest.php
index 9395d66..5babfc6 100644
--- a/core/modules/menu_link_content/src/Tests/MenuLinkContentFormTest.php
+++ b/core/modules/menu_link_content/src/Tests/MenuLinkContentFormTest.php
@@ -66,6 +66,6 @@ public function testMenuLinkContentFormValidation() {
       ),
       t('Save')
     );
-    $this->assertText(t('Manually entered paths should start with /, ? or #.'));
+    $this->assertText(t('Manually entered internal paths should start with /, ? or #. External paths should start with //, or a protocol such as https://'));
   }
 }
diff --git a/core/tests/Drupal/Tests/Core/UnroutedUrlTest.php b/core/tests/Drupal/Tests/Core/UnroutedUrlTest.php
index a040685..2faa649 100644
--- a/core/tests/Drupal/Tests/Core/UnroutedUrlTest.php
+++ b/core/tests/Drupal/Tests/Core/UnroutedUrlTest.php
@@ -87,6 +87,8 @@ public function providerFromUri() {
       // [$uri, $is_external]
       // An external URI.
       ['https://www.drupal.org', TRUE],
+      // A protocol relative URL.
+      ['//www.drupal.org', TRUE],
       // An internal, unrouted, base-relative URI.
       ['base:robots.txt', FALSE],
       // Base-relative URIs with special characters.
@@ -119,7 +121,6 @@ public function providerFromInvalidUri() {
       // Schemeless paths.
       ['test'],
       ['/test'],
-      ['//test'],
       // Schemeless path with a query string.
       ['foo?bar'],
       // Only a query string.
-- 
2.1.4

