bug 1174152 - crossorigin attribute for link rel=preconnect r=hsivonen
authorPatrick McManus <mcmanus@ducksong.com>
Fri, 19 Jun 2015 17:11:42 -0400
changeset 250294 a7c6156a485aa71dfcf5328894881a98d9e1f4e8
parent 250293 492d531ec4868fadb53773bb3d166879696b55d8
child 250295 7a587393c81c45774fd2720081311dbbf045a064
push id28951
push usercbook@mozilla.com
push dateFri, 26 Jun 2015 11:19:38 +0000
treeherdermozilla-central@56e207dbb3bd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewershsivonen
bugs1174152
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 1174152 - crossorigin attribute for link rel=preconnect r=hsivonen
dom/base/nsContentSink.cpp
dom/base/nsContentSink.h
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/base/nsIDocument.h
dom/html/HTMLLinkElement.cpp
netwerk/test/mochitests/rel_preconnect.sjs
netwerk/test/mochitests/test_rel_preconnect.html
parser/html/nsHtml5SpeculativeLoad.cpp
parser/html/nsHtml5SpeculativeLoad.h
parser/html/nsHtml5TreeBuilderCppSupplement.h
--- a/dom/base/nsContentSink.cpp
+++ b/dom/base/nsContentSink.cpp
@@ -441,16 +441,19 @@ nsContentSink::ProcessLinkHeader(const n
   // parse link content and call process style link
   nsAutoString href;
   nsAutoString rel;
   nsAutoString title;
   nsAutoString titleStar;
   nsAutoString type;
   nsAutoString media;
   nsAutoString anchor;
+  nsAutoString crossOrigin;
+
+  crossOrigin.SetIsVoid(true);
 
   // copy to work buffer
   nsAutoString stringList(aLinkData);
 
   // put an extra null at the end
   stringList.Append(kNullCh);
 
   char16_t* start = stringList.BeginWriting();
@@ -615,61 +618,69 @@ nsContentSink::ProcessLinkHeader(const n
               // which specifies that media queries are case insensitive.
               nsContentUtils::ASCIIToLower(media);
             }
           } else if (attr.LowerCaseEqualsLiteral("anchor")) {
             if (anchor.IsEmpty()) {
               anchor = value;
               anchor.StripWhitespace();
             }
+          } else if (attr.LowerCaseEqualsLiteral("crossorigin")) {
+            if (crossOrigin.IsVoid()) {
+              crossOrigin.SetIsVoid(false);
+              crossOrigin = value;
+              crossOrigin.StripWhitespace();
+            }
           }
         }
       }
     }
 
     if (endCh == kComma) {
       // hit a comma, process what we've got so far
 
       href.Trim(" \t\n\r\f"); // trim HTML5 whitespace
       if (!href.IsEmpty() && !rel.IsEmpty()) {
         rv = ProcessLink(anchor, href, rel,
                          // prefer RFC 5987 variant over non-I18zed version
                          titleStar.IsEmpty() ? title : titleStar,
-                         type, media);
+                         type, media, crossOrigin);
       }
 
       href.Truncate();
       rel.Truncate();
       title.Truncate();
       type.Truncate();
       media.Truncate();
       anchor.Truncate();
+      crossOrigin.SetIsVoid(true);
       
       seenParameters = false;
     }
 
     start = ++end;
   }
                 
   href.Trim(" \t\n\r\f"); // trim HTML5 whitespace
   if (!href.IsEmpty() && !rel.IsEmpty()) {
     rv = ProcessLink(anchor, href, rel,
                      // prefer RFC 5987 variant over non-I18zed version
                      titleStar.IsEmpty() ? title : titleStar,
-                     type, media);
+                     type, media, crossOrigin);
   }
 
   return rv;
 }
 
 
 nsresult
 nsContentSink::ProcessLink(const nsSubstring& aAnchor, const nsSubstring& aHref,
                            const nsSubstring& aRel, const nsSubstring& aTitle,
-                           const nsSubstring& aType, const nsSubstring& aMedia)
+                           const nsSubstring& aType, const nsSubstring& aMedia,
+                           const nsSubstring& aCrossOrigin)
 {
   uint32_t linkTypes =
     nsStyleLinkElement::ParseLinkTypes(aRel, mDocument->NodePrincipal());
 
   // The link relation may apply to a different resource, specified
   // in the anchor parameter. For the link relations supported so far,
   // we simply abort if the link applies to a resource different to the
   // one we've loaded
@@ -683,17 +694,17 @@ nsContentSink::ProcessLink(const nsSubst
     PrefetchHref(aHref, mDocument, hasPrefetch);
   }
 
   if (!aHref.IsEmpty() && (linkTypes & nsStyleLinkElement::eDNS_PREFETCH)) {
     PrefetchDNS(aHref);
   }
 
   if (!aHref.IsEmpty() && (linkTypes & nsStyleLinkElement::ePRECONNECT)) {
-    Preconnect(aHref);
+    Preconnect(aHref, aCrossOrigin);
   }
 
   // is it a stylesheet link?
   if (!(linkTypes & nsStyleLinkElement::eSTYLESHEET)) {
     return NS_OK;
   }
 
   bool isAlternate = linkTypes & nsStyleLinkElement::eALTERNATE;
@@ -870,27 +881,27 @@ nsContentSink::PrefetchDNS(const nsAStri
   }
 
   if (!hostname.IsEmpty() && nsHTMLDNSPrefetch::IsAllowed(mDocument)) {
     nsHTMLDNSPrefetch::PrefetchLow(hostname);
   }
 }
 
 void
-nsContentSink::Preconnect(const nsAString &aHref)
+nsContentSink::Preconnect(const nsAString& aHref, const nsAString& aCrossOrigin)
 {
   // construct URI using document charset
   const nsACString& charset = mDocument->GetDocumentCharacterSet();
   nsCOMPtr<nsIURI> uri;
   NS_NewURI(getter_AddRefs(uri), aHref,
             charset.IsEmpty() ? nullptr : PromiseFlatCString(charset).get(),
             mDocument->GetDocBaseURI());
 
   if (uri && mDocument) {
-    mDocument->MaybePreconnect(uri);
+    mDocument->MaybePreconnect(uri, dom::Element::StringToCORSMode(aCrossOrigin));
   }
 }
 
 nsresult
 nsContentSink::SelectDocAppCache(nsIApplicationCache *aLoadApplicationCache,
                                  nsIURI *aManifestURI,
                                  bool aFetchedWithHTTPGetOrEquiv,
                                  CacheSelectionAction *aAction)
--- a/dom/base/nsContentSink.h
+++ b/dom/base/nsContentSink.h
@@ -146,17 +146,17 @@ protected:
 
   nsresult ProcessHTTPHeaders(nsIChannel* aChannel);
   nsresult ProcessHeaderData(nsIAtom* aHeader, const nsAString& aValue,
                              nsIContent* aContent = nullptr);
   nsresult ProcessLinkHeader(const nsAString& aLinkData);
   nsresult ProcessLink(const nsSubstring& aAnchor,
                        const nsSubstring& aHref, const nsSubstring& aRel,
                        const nsSubstring& aTitle, const nsSubstring& aType,
-                       const nsSubstring& aMedia);
+                       const nsSubstring& aMedia, const nsSubstring& aCrossOrigin);
 
   virtual nsresult ProcessStyleLink(nsIContent* aElement,
                                     const nsSubstring& aHref,
                                     bool aAlternate,
                                     const nsSubstring& aTitle,
                                     const nsSubstring& aType,
                                     const nsSubstring& aMedia);
 
@@ -220,17 +220,17 @@ public:
   void ProcessOfflineManifest(const nsAString& aManifestSpec);
 
   // Extracts the manifest attribute from the element if it is the root 
   // element and calls the above method.
   void ProcessOfflineManifest(nsIContent *aElement);
 
   // For Preconnect() aHref can either be the usual
   // URI format or of the form "//www.hostname.com" without a scheme.
-  void Preconnect(const nsAString &aHref);
+  void Preconnect(const nsAString& aHref, const nsAString& aCrossOrigin);
 
 protected:
   // Tries to scroll to the URI's named anchor. Once we've successfully
   // done that, further calls to this method will be ignored.
   void ScrollToRef();
 
   // Start layout.  If aIgnorePendingSheets is true, this will happen even if
   // we still have stylesheet loads pending.  Otherwise, we'll wait until the
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -9758,30 +9758,52 @@ nsDocument::MaybePreLoadImage(nsIURI* ur
   // the "real" load occurs. Unpinned in DispatchContentLoadedEvents and
   // unlink
   if (NS_SUCCEEDED(rv)) {
     mPreloadingImages.Put(uri, request.forget());
   }
 }
 
 void
-nsDocument::MaybePreconnect(nsIURI* uri)
-{
+nsDocument::MaybePreconnect(nsIURI* aOrigURI, mozilla::CORSMode aCORSMode)
+{
+  nsCOMPtr<nsIURI> uri;
+  if (NS_FAILED(aOrigURI->Clone(getter_AddRefs(uri)))) {
+      return;
+  }
+
+  // The URI created here is used in 2 contexts. One is nsISpeculativeConnect
+  // which ignores the path and uses only the origin. The other is for the
+  // document mPreloadedPreconnects de-duplication hash. Anonymous vs
+  // non-Anonymous preconnects create different connections on the wire and
+  // therefore should not be considred duplicates of each other and we
+  // normalize the path before putting it in the hash to accomplish that.
+
+  if (aCORSMode == CORS_ANONYMOUS) {
+    uri->SetPath(NS_LITERAL_CSTRING("/anonymous"));
+  } else {
+    uri->SetPath(NS_LITERAL_CSTRING("/"));
+  }
+
   if (mPreloadedPreconnects.Contains(uri)) {
     return;
   }
   mPreloadedPreconnects.Put(uri, true);
 
   nsCOMPtr<nsISpeculativeConnect>
     speculator(do_QueryInterface(nsContentUtils::GetIOService()));
   if (!speculator) {
     return;
   }
 
-  speculator->SpeculativeConnect(uri, nullptr);
+  if (aCORSMode == CORS_ANONYMOUS) {
+    speculator->SpeculativeAnonymousConnect(uri, nullptr);
+  } else {
+    speculator->SpeculativeConnect(uri, nullptr);
+  }
 }
 
 void
 nsDocument::ForgetImagePreload(nsIURI* aURI)
 {
   // Checking count is faster than hashing the URI in the common
   // case of empty table.
   if (mPreloadingImages.Count() != 0) {
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -1136,17 +1136,18 @@ public:
                         const nsAString& aSrcsetAttr,
                         const nsAString& aSizesAttr) override;
 
   virtual void MaybePreLoadImage(nsIURI* uri,
                                  const nsAString &aCrossOriginAttr,
                                  ReferrerPolicy aReferrerPolicy) override;
   virtual void ForgetImagePreload(nsIURI* aURI) override;
 
-  virtual void MaybePreconnect(nsIURI* uri) override;
+  virtual void MaybePreconnect(nsIURI* uri,
+                               mozilla::CORSMode aCORSMode) override;
 
   virtual void PreloadStyle(nsIURI* uri, const nsAString& charset,
                             const nsAString& aCrossOriginAttr,
                             ReferrerPolicy aReferrerPolicy) override;
 
   virtual nsresult LoadChromeSheetSync(nsIURI* uri, bool isAgentSheet,
                                        mozilla::CSSStyleSheet** sheet) override;
 
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -26,16 +26,17 @@
 #include "nsWeakReference.h"
 #include "mozilla/dom/DocumentBinding.h"
 #include "mozilla/WeakPtr.h"
 #include "Units.h"
 #include "nsExpirationTracker.h"
 #include "nsClassHashtable.h"
 #include "prclist.h"
 #include "mozilla/UniquePtr.h"
+#include "mozilla/CORSMode.h"
 #include <bitset>                        // for member
 
 class imgIRequest;
 class nsAString;
 class nsBindingManager;
 class nsIDocShell;
 class nsDocShell;
 class nsDOMNavigationTiming;
@@ -2032,17 +2033,18 @@ public:
    * so once can know whether a document is expected to be rendered left-to-right
    * or right-to-left.
    */
   virtual bool IsDocumentRightToLeft() { return false; }
 
   /**
    * Called by Parser for link rel=preconnect
    */
-  virtual void MaybePreconnect(nsIURI* uri) = 0;
+  virtual void MaybePreconnect(nsIURI* uri,
+                               mozilla::CORSMode aCORSMode) = 0;
 
   enum DocumentTheme {
     Doc_Theme_Uninitialized, // not determined yet
     Doc_Theme_None,
     Doc_Theme_Neutral,
     Doc_Theme_Dark,
     Doc_Theme_Bright
   };
--- a/dom/html/HTMLLinkElement.cpp
+++ b/dom/html/HTMLLinkElement.cpp
@@ -311,17 +311,17 @@ HTMLLinkElement::UpdatePreconnect()
   if (!(linkTypes & ePRECONNECT)) {
     return;
   }
 
   nsIDocument *owner = OwnerDoc();
   if (owner) {
     nsCOMPtr<nsIURI> uri = GetHrefURI();
     if (uri) {
-      owner->MaybePreconnect(uri);
+        owner->MaybePreconnect(uri, GetCORSMode());
     }
   }
 }
 
 nsresult
 HTMLLinkElement::SetAttr(int32_t aNameSpaceID, nsIAtom* aName,
                          nsIAtom* aPrefix, const nsAString& aValue,
                          bool aNotify)
--- a/netwerk/test/mochitests/rel_preconnect.sjs
+++ b/netwerk/test/mochitests/rel_preconnect.sjs
@@ -1,12 +1,15 @@
 // Generate response header "Link: <HREF>; rel=preconnect"
 // HREF is provided by the request header X-Link
 
 function handleRequest(request, response)
 {
     response.setHeader("Cache-Control", "no-cache", false);
     response.setHeader("Link", "<" + 
                        request.getHeader('X-Link') +
-                       ">; rel=preconnect");
+                       ">; rel=preconnect" + ", " +
+                        "<" + 
+                       request.getHeader('X-Link') +
+                       ">; rel=preconnect; crossOrigin=anonymous");
     response.write("check that header");
 }
 
--- a/netwerk/test/mochitests/test_rel_preconnect.html
+++ b/netwerk/test/mochitests/test_rel_preconnect.html
@@ -17,81 +17,92 @@ function TestServer1(nextTest) {
     this.listener= Cc["@mozilla.org/network/server-socket;1"]
                    .createInstance(Ci.nsIServerSocket);
     this.listener.init(-1, true, -1);
     this.listener.asyncListen(SpecialPowers.wrapCallbackObject(this));
     this.nextTest = nextTest;
 }
 
 TestServer1.prototype = {
+    remainder : 2,
+
     QueryInterface: function(iid) {
       iid = SpecialPowers.wrap(iid);
         if (iid.equals(Ci.nsIServerSocketListener) ||
             iid.equals(Ci.nsISupports))
             return this;
         throw Cr.NS_ERROR_NO_INTERFACE;
     },
     onSocketAccepted: function(socket, trans) {
         try { socket.close(); } catch(e) {}
         try { trans.close(); } catch(e) {}
-        ok(true, "received connect");
-        srv1.listener.close();
-        if(srv1.nextTest != null) {
-         setTimeout(srv1.nextTest, 0);
+        this.remainder--;
+        ok(true, "received connect remainder = " + this.remainder);
+        if (!this.remainder) {
+          srv1.listener.close();
+          setTimeout(srv1.nextTest, 0);
         }
-        srv1.nextTest = null;
     },
     onStopListening: function(socket) {}
 };
 
 function TestServer2(nextTest) {
     this.listener= Cc["@mozilla.org/network/server-socket;1"]
                    .createInstance(Ci.nsIServerSocket);
     this.listener.init(-1, true, -1);
     this.listener.asyncListen(SpecialPowers.wrapCallbackObject(this));
     this.nextTest = nextTest;
 }
 
 TestServer2.prototype = {
+    remainder : 2,
+
     QueryInterface: function(iid) {
       iid = SpecialPowers.wrap(iid);
         if (iid.equals(Ci.nsIServerSocketListener) ||
             iid.equals(Ci.nsISupports))
             return this;
         throw Cr.NS_ERROR_NO_INTERFACE;
     },
     onSocketAccepted: function(socket, trans) {
         try { socket.close(); } catch(e) {}
         try { trans.close(); } catch(e) {}
-        ok(true, "received connect");
-        srv2.listener.close();
-        if(srv2.nextTest != null) {
-         setTimeout(srv2.nextTest, 0);
+        this.remainder--;
+        ok(true, "received connect srv2 remainder = " + this.remainder);
+        if (!this.remainder) {
+          srv2.listener.close();
+          setTimeout(srv2.nextTest, 0);
         }
-        srv2.nextTest = null;
     },
     onStopListening: function(socket) {}
 };
 
 var originalLimit = SpecialPowers.getIntPref("network.http.speculative-parallel-limit");
 
 function testElement()
 {
-  // test the link rel=preconnect element in the head
+  // test the link rel=preconnect element in the head for both normal
+  // and crossOrigin=anonymous
   srv1 = new TestServer1(testHeader);
-  SpecialPowers.setIntPref("network.http.speculative-parallel-limit", 1);
+  SpecialPowers.setIntPref("network.http.speculative-parallel-limit", 2);
   var link = document.createElement("link");
   link.rel = "preconnect";
   link.href = "//localhost:" +  srv1.listener.port;
   document.head.appendChild(link);
+  link = document.createElement("link");
+  link.rel = "preconnect";
+  link.href = "//localhost:" +  srv1.listener.port;
+  link.crossOrigin = "anonymous";
+  document.head.appendChild(link);
 }
 
 function testHeader()
 {
-  // test the http link response header
+  // test the http link response header - the test contains both a
+  // normal and anonymous preconnect link header
   srv2 = new TestServer2(testDone);
   var xhr = new XMLHttpRequest();
   xhr.open("GET", 'rel_preconnect.sjs', false);
   xhr.setRequestHeader("X-Link", "//localhost:" + srv2.listener.port);
   xhr.send();
   is(xhr.status, 200, 'xhr cool');
 }
 
--- a/parser/html/nsHtml5SpeculativeLoad.cpp
+++ b/parser/html/nsHtml5SpeculativeLoad.cpp
@@ -63,15 +63,15 @@ nsHtml5SpeculativeLoad::Perform(nsHtml5T
         NS_ASSERTION(mTypeOrCharsetSource.Length() == 1,
             "Unexpected charset source string");
         int32_t intSource = (int32_t)mTypeOrCharsetSource.First();
         aExecutor->SetDocumentCharsetAndSource(narrowName,
                                                intSource);
       }
       break;
     case eSpeculativeLoadPreconnect:
-      aExecutor->Preconnect(mUrl);
+      aExecutor->Preconnect(mUrl, mCrossOrigin);
       break;
     default:
       NS_NOTREACHED("Bogus speculative load.");
       break;
   }
 }
--- a/parser/html/nsHtml5SpeculativeLoad.h
+++ b/parser/html/nsHtml5SpeculativeLoad.h
@@ -159,22 +159,24 @@ class nsHtml5SpeculativeLoad {
     {
       NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
                       "Trying to reinitialize a speculative load!");
       mOpCode = eSpeculativeLoadSetDocumentCharset;
       CopyUTF8toUTF16(aCharset, mCharset);
       mTypeOrCharsetSource.Assign((char16_t)aCharsetSource);
     }
 
-    inline void InitPreconnect(const nsAString& aUrl)
+    inline void InitPreconnect(const nsAString& aUrl,
+                               const nsAString& aCrossOrigin)
     {
       NS_PRECONDITION(mOpCode == eSpeculativeLoadUninitialized,
                       "Trying to reinitialize a speculative load!");
       mOpCode = eSpeculativeLoadPreconnect;
       mUrl.Assign(aUrl);
+      mCrossOrigin.Assign(aCrossOrigin);
     }
 
     void Perform(nsHtml5TreeOpExecutor* aExecutor);
 
   private:
     eHtml5SpeculativeLoad mOpCode;
     nsString mUrl;
     nsString mMetaReferrerPolicy;
@@ -188,19 +190,19 @@ class nsHtml5SpeculativeLoad {
     /**
      * If mOpCode is eSpeculativeLoadSetDocumentCharset, this is a
      * one-character string whose single character's code point is to be
      * interpreted as a charset source integer. Otherwise, it is empty or
      * the value of the type attribute.
      */
     nsString mTypeOrCharsetSource;
     /**
-     * If mOpCode is eSpeculativeLoadImage or eSpeculativeLoadScript[FromHead],
-     * this is the value of the "crossorigin" attribute.  If the
-     * attribute is not set, this will be a void string.
+     * If mOpCode is eSpeculativeLoadImage or eSpeculativeLoadScript[FromHead]
+     * or eSpeculativeLoadPreconnect this is the value of the "crossorigin"
+     * attribute.  If the attribute is not set, this will be a void string.
      */
     nsString mCrossOrigin;
     /**
      * If mOpCode is eSpeculativeLoadImage or eSpeculativeLoadPictureSource,
      * this is the value of "srcset" attribute.  If the attribute is not set,
      * this will be a void string.
      */
     nsString mSrcset;
--- a/parser/html/nsHtml5TreeBuilderCppSupplement.h
+++ b/parser/html/nsHtml5TreeBuilderCppSupplement.h
@@ -186,18 +186,20 @@ nsHtml5TreeBuilder::createElement(int32_
                 mSpeculativeLoadQueue.AppendElement()->
                   InitStyle(*url,
                             (charset) ? *charset : EmptyString(),
                             (crossOrigin) ? *crossOrigin : NullString());
               }
             } else if (rel->LowerCaseEqualsASCII("preconnect")) {
               nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_HREF);
               if (url) {
+                nsString* crossOrigin =
+                  aAttributes->getValue(nsHtml5AttributeName::ATTR_CROSSORIGIN);
                 mSpeculativeLoadQueue.AppendElement()->
-                  InitPreconnect(*url);
+                  InitPreconnect(*url, (crossOrigin) ? *crossOrigin : NullString());
               }
             }
           }
         } else if (nsHtml5Atoms::video == aName) {
           nsString* url = aAttributes->getValue(nsHtml5AttributeName::ATTR_POSTER);
           if (url) {
             mSpeculativeLoadQueue.AppendElement()->InitImage(*url, NullString(),
                                                              NullString(),