Bug 1222516 part 4. Implement support for rel=noopener on links. r=mconley
authorBoris Zbarsky <bzbarsky@mit.edu>
Thu, 20 Oct 2016 16:52:39 -0400
changeset 361747 bdb691bd6d3dbc2771841a97940c8512808e6755
parent 361746 7c0cd94d563605d1c28b4d1f16947d4885747155
child 361748 e151b7a9b0c7d327d5800004d7a5d90474317235
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-beta@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1222516
milestone52.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 1222516 part 4. Implement support for rel=noopener on links. r=mconley
docshell/base/nsDocShell.cpp
dom/html/HTMLAnchorElement.cpp
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/html/semantics/links/links-created-by-a-and-area-elements/htmlanchorelement_noopener.html
testing/web-platform/tests/html/semantics/links/links-created-by-a-and-area-elements/support/noopener-popup.html
testing/web-platform/tests/html/semantics/links/links-created-by-a-and-area-elements/support/noopener-target-1.html
testing/web-platform/tests/html/semantics/links/links-created-by-a-and-area-elements/support/noopener-target-2.html
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -13958,21 +13958,26 @@ nsDocShell::OnLinkClickSync(nsIContent* 
 
   uint32_t flags = INTERNAL_LOAD_FLAGS_NONE;
   if (IsElementAnchor(aContent)) {
     MOZ_ASSERT(aContent->IsHTMLElement());
     nsAutoString referrer;
     aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::rel, referrer);
     nsWhitespaceTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tok(referrer);
     while (tok.hasMoreTokens()) {
-      if (tok.nextToken().LowerCaseEqualsLiteral("noreferrer")) {
+      const nsAString& token = tok.nextToken();
+      if (token.LowerCaseEqualsLiteral("noreferrer")) {
         flags |= INTERNAL_LOAD_FLAGS_DONT_SEND_REFERRER |
                  INTERNAL_LOAD_FLAGS_NO_OPENER;
+        // We now have all the flags we could possibly have, so just stop.
         break;
       }
+      if (token.LowerCaseEqualsLiteral("noopener")) {
+        flags |= INTERNAL_LOAD_FLAGS_NO_OPENER;
+      }
     }
   }
 
   // Get the owner document of the link that was clicked, this will be
   // the document that the link is in, or the last document that the
   // link was in. From that document, we'll get the URI to use as the
   // referer, since the current URI in this docshell may be a
   // new document that we're in the process of loading.
--- a/dom/html/HTMLAnchorElement.cpp
+++ b/dom/html/HTMLAnchorElement.cpp
@@ -37,16 +37,17 @@ enum {
 
 ASSERT_NODE_FLAGS_SPACE(ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 2);
 
 #undef ANCHOR_ELEMENT_FLAG_BIT
 
 // static
 const DOMTokenListSupportedToken HTMLAnchorElement::sSupportedRelValues[] = {
   "noreferrer",
+  "noopener",
   nullptr
 };
 
 HTMLAnchorElement::~HTMLAnchorElement()
 {
 }
 
 bool
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -37655,16 +37655,22 @@
           }
         ],
         "html/semantics/forms/the-textarea-element/cloning-steps.html": [
           {
             "path": "html/semantics/forms/the-textarea-element/cloning-steps.html",
             "url": "/html/semantics/forms/the-textarea-element/cloning-steps.html"
           }
         ],
+        "html/semantics/links/links-created-by-a-and-area-elements/htmlanchorelement_noopener.html": [
+          {
+            "path": "html/semantics/links/links-created-by-a-and-area-elements/htmlanchorelement_noopener.html",
+            "url": "/html/semantics/links/links-created-by-a-and-area-elements/htmlanchorelement_noopener.html"
+          }
+        ],
         "html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-1.html": [
           {
             "path": "html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-1.html",
             "url": "/html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-1.html"
           }
         ],
         "html/semantics/scripting-1/the-script-element/script-onerror-insertion-point-2.html": [
           {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/links/links-created-by-a-and-area-elements/htmlanchorelement_noopener.html
@@ -0,0 +1,78 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test behavior of rel="noopener" links</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<iframe name="oursubframe"></iframe>
+<a href="support/noopener-target-2.html" rel="noopener" target="ourpopup"></a>
+<a href="support/noopener-target-2.html" rel="noopener" target="oursubframe"></a>
+<script>
+var tests = [];
+// First test the special targets
+function target1Loaded(win) {
+  // Find the relevant test
+  var test = tests.find((t) => t.openedWindow == win);
+  test.step(function() {
+    assert_equals(win.opener, window);
+    win.close();
+    test.done();
+  });
+}
+/**
+ * Test that <a rel="noopener"> targeted at one of _self, _parent, _top does the
+ * load in the appropriate existing browsing context instead of opening a new
+ * one.  The test is run in a separate popup window we open and which we can
+ * navigate without causing the test harness going into conniptions.
+ */
+for (var target of ["self", "parent", "top"]) {
+  var t = async_test("Check that rel=noopener with target=_" + target + " does a normal load");
+  tests.push(t);
+  t.openedWindow = window.open("support/noopener-popup.html");
+  t.targetName = target;
+  t.openedWindow.onload = t.step_func(function() {
+    this.openedWindow.findLink(this.targetName).click();
+  });
+}
+
+/**
+ * And now check that a noopener load targeted at something other than one of
+ * the three special targets above in fact ignores existing things with the
+ * given name.  We do this in two ways.  First, by opening a window named
+ * "ourpopup" and then doing a load via <a rel="noopener" target="ourpopup"> and
+ * verifying that the load happens in a window with a null opener, etc, while
+ * the opener of the thing we opened is not modified.  And second, by targeting
+ * <a rel="noopener"> at a name that an existing subframe has, and ensuring that
+ * this subframe is not navigated.
+ */
+var t1 = async_test("Check that targeting of rel=noopener with a given name ignores an existing window with that name");
+var w;
+t1.add_cleanup(function() { w.close(); });
+var channel = new BroadcastChannel("ourpopup");
+channel.onmessage = t1.step_func_done(function(e) {
+  var data = e.data;
+  assert_false(data.hasOpener);
+  assert_false(data.hasParent);
+  assert_equals(data.name, "ourpopup");
+  assert_equals(w.opener, window);
+  assert_equals(w.location.href, "about:blank");
+});
+t1.step(function() {
+  w = window.open("", "ourpopup");
+  assert_equals(w.opener, window);
+  document.querySelectorAll("a")[0].click();
+});
+
+var t2 = async_test("Check that targeting of rel=noopener with a given name ignores an existing subframe with that name");
+var channel = new BroadcastChannel("oursubframe");
+channel.onmessage = t2.step_func_done(function(e) {
+  var data = e.data;
+  assert_false(data.hasOpener);
+  assert_false(data.hasParent);
+  assert_equals(data.name, "oursubframe");
+  assert_equals(document.querySelector("iframe").contentWindow.location.href,
+                "about:blank");
+});
+t2.step(function() {
+  document.querySelectorAll("a")[1].click();
+});
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/links/links-created-by-a-and-area-elements/support/noopener-popup.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<script>
+  function findLink(arg) {
+    var doc;
+    if (arg == "self") {
+      doc = document;
+    } else {
+      doc = frames[0].document;
+    }
+    return doc.getElementById(arg + "target");
+  }
+</script>
+<a rel="noopener" target="_self" id="selftarget"
+   href="noopener-target-1.html"></a>
+<iframe srcdoc='
+        <a rel="noopener" target="_parent" id="parenttarget"
+           href="noopener-target-1.html"></a>
+        <a rel="noopener" target="_top" id="toptarget"
+           href="noopener-target-1.html"></a>'></iframe>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/links/links-created-by-a-and-area-elements/support/noopener-target-1.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+  opener.target1Loaded(this);
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/semantics/links/links-created-by-a-and-area-elements/support/noopener-target-2.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<script>
+  var channel = new BroadcastChannel(this.name);
+  channel.postMessage({ hasOpener: opener !== null ,
+                        hasParent: parent != this,
+                        name: window.name });
+  window.close();
+</script>