Bug 1267339 part 3. Add support for the noopener window feature in windowwatcher. r=mconley
authorBoris Zbarsky <bzbarsky@mit.edu>
Thu, 20 Oct 2016 16:52:38 -0400
changeset 361742 3d73ae4c960ff6539e640e5ef2f28a120efb99b7
parent 361741 7f0d60789d0b8d8202dd51879b0654fe9df7fdc9
child 361743 50ea003d750ddc6d086d35dbad3ebc2b51c724b4
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
bugs1267339
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 1267339 part 3. Add support for the noopener window feature in windowwatcher. r=mconley
dom/base/nsGlobalWindow.cpp
dom/base/nsGlobalWindow.h
dom/workers/ServiceWorkerClients.cpp
embedding/components/windowwatcher/nsPIWindowWatcher.idl
embedding/components/windowwatcher/nsWindowWatcher.cpp
embedding/components/windowwatcher/nsWindowWatcher.h
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/html/browsers/the-window-object/support/noopener-target.html
testing/web-platform/tests/html/browsers/the-window-object/window-open-noopener.html
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -48,16 +48,17 @@
 #include "nsFrameSelection.h"
 #include "nsNetUtil.h"
 #include "nsVariant.h"
 
 // Helper Classes
 #include "nsJSUtils.h"
 #include "jsapi.h"              // for JSAutoRequest
 #include "jswrapper.h"
+#include "nsCharSeparatedTokenizer.h"
 #include "nsReadableUtils.h"
 #include "nsDOMClassInfo.h"
 #include "nsJSEnvironment.h"
 #include "ScriptSettings.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Likely.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/Unused.h"
@@ -6052,21 +6053,28 @@ GetCallerDocShellTreeItem()
   nsCOMPtr<nsIWebNavigation> callerWebNav = do_GetInterface(GetEntryGlobal());
   nsCOMPtr<nsIDocShellTreeItem> callerItem = do_QueryInterface(callerWebNav);
 
   return callerItem.forget();
 }
 
 bool
 nsGlobalWindow::WindowExists(const nsAString& aName,
+                             bool aForceNoOpener,
                              bool aLookForCallerOnJSStack)
 {
   NS_PRECONDITION(IsOuterWindow(), "Must be outer window");
   NS_PRECONDITION(mDocShell, "Must have docshell");
 
+  if (aForceNoOpener) {
+    return aName.LowerCaseEqualsLiteral("_self") ||
+           aName.LowerCaseEqualsLiteral("_top") ||
+           aName.LowerCaseEqualsLiteral("_parent");
+  }
+
   nsCOMPtr<nsIDocShellTreeItem> caller;
   if (aLookForCallerOnJSStack) {
     caller = GetCallerDocShellTreeItem();
   }
 
   if (!caller) {
     caller = mDocShell;
   }
@@ -11817,22 +11825,34 @@ nsGlobalWindow::OpenInternal(const nsASt
 
   // Popups from apps are never blocked.
   bool isApp = false;
   if (mDoc) {
     isApp = mDoc->NodePrincipal()->GetAppStatus() >=
               nsIPrincipal::APP_STATUS_INSTALLED;
   }
 
+  bool forceNoOpener = false;
+  // Unlike other window flags, "noopener" comes from splitting on commas with
+  // HTML whitespace trimming...
+  nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tok(
+    aOptions, ',');
+  while (tok.hasMoreTokens()) {
+    if (tok.nextToken().EqualsLiteral("noopener")) {
+      forceNoOpener = true;
+      break;
+    }
+  }
+
   // XXXbz When this gets fixed to not use LegacyIsCallerNativeCode()
   // (indirectly) maybe we can nix the AutoJSAPI usage OnLinkClickEvent::Run.
   // But note that if you change this to GetEntryGlobal(), say, then
   // OnLinkClickEvent::Run will need a full-blown AutoEntryScript.
   const bool checkForPopup = !nsContentUtils::LegacyIsCallerChromeOrNativeCode() &&
-    !isApp && !aDialog && !WindowExists(aName, !aCalledNoScript);
+    !isApp && !aDialog && !WindowExists(aName, forceNoOpener, !aCalledNoScript);
 
   // Note: it's very important that this be an nsXPIDLCString, since we want
   // .get() on it to return nullptr until we write stuff to it.  The window
   // watcher expects a null URL string if there is no URL to load.
   nsXPIDLCString url;
   nsresult rv = NS_OK;
 
   // It's important to do this security check before determining whether this
@@ -11910,16 +11930,17 @@ nsGlobalWindow::OpenInternal(const nsASt
 
     if (!aCalledNoScript) {
       // We asserted at the top of this function that aNavigate is true for
       // !aCalledNoScript.
       rv = pwwatch->OpenWindow2(AsOuter(), url.get(), name_ptr,
                                 options_ptr, /* aCalledFromScript = */ true,
                                 aDialog, aNavigate, argv,
                                 isPopupSpamWindow,
+                                forceNoOpener,
                                 getter_AddRefs(domReturn));
     } else {
       // Force a system caller here so that the window watcher won't screw us
       // up.  We do NOT want this case looking at the JS context on the stack
       // when searching.  Compare comments on
       // nsIDOMWindow::OpenWindow and nsIWindowWatcher::OpenWindow.
 
       // Note: Because nsWindowWatcher is so broken, it's actually important
@@ -11930,16 +11951,17 @@ nsGlobalWindow::OpenInternal(const nsASt
       if (!aContentModal) {
         nojsapi.emplace();
       }
 
       rv = pwwatch->OpenWindow2(AsOuter(), url.get(), name_ptr,
                                 options_ptr, /* aCalledFromScript = */ false,
                                 aDialog, aNavigate, aExtraArgument,
                                 isPopupSpamWindow,
+                                forceNoOpener,
                                 getter_AddRefs(domReturn));
 
     }
   }
 
   NS_ENSURE_SUCCESS(rv, rv);
 
   // success!
--- a/dom/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -1595,17 +1595,18 @@ public:
   {
     return GetParentInternal() != nullptr;
   }
 
   // Outer windows only.
   // If aLookForCallerOnJSStack is true, this method will look at the JS stack
   // to determine who the caller is.  If it's false, it'll use |this| as the
   // caller.
-  bool WindowExists(const nsAString& aName, bool aLookForCallerOnJSStack);
+  bool WindowExists(const nsAString& aName, bool aForceNoOpener,
+                    bool aLookForCallerOnJSStack);
 
   already_AddRefed<nsIWidget> GetMainWidget();
   nsIWidget* GetNearestWidget() const;
 
   void Freeze()
   {
     NS_ASSERTION(!IsFrozen(), "Double-freezing?");
     mIsFrozen = true;
--- a/dom/workers/ServiceWorkerClients.cpp
+++ b/dom/workers/ServiceWorkerClients.cpp
@@ -604,16 +604,20 @@ private:
       nsCOMPtr<mozIDOMWindowProxy> newWindow;
       pwwatch->OpenWindow2(nullptr,
                            spec.get(),
                            nullptr,
                            nullptr,
                            false, false, true, nullptr,
                            // Not a spammy popup; we got permission, we swear!
                            /* aIsPopupSpam = */ false,
+                           // Don't force noopener.  We're not passing in an
+                           // opener anyway, and we _do_ want the returned
+                           // window.
+                           /* aForceNoOpener = */ false,
                            getter_AddRefs(newWindow));
       nsCOMPtr<nsPIDOMWindowOuter> pwindow = nsPIDOMWindowOuter::From(newWindow);
       pwindow.forget(aWindow);
       return NS_OK;
     }
 
     // Find the most recent browser window and open a new tab in it.
     nsCOMPtr<nsPIDOMWindowOuter> browserWindow =
--- a/embedding/components/windowwatcher/nsPIWindowWatcher.idl
+++ b/embedding/components/windowwatcher/nsPIWindowWatcher.idl
@@ -52,33 +52,39 @@ interface nsPIWindowWatcher : nsISupport
       @param aFeatures window features from JS window.open. can be null.
       @param aCalledFromScript true if we were called from script.
       @param aDialog use dialog defaults (see nsIDOMWindow::openDialog)
       @param aNavigate true if we should navigate the new window to the
              specified URL.
       @param aArgs Window argument
       @param aIsPopupSpam true if the window is a popup spam window; used for
                           popup blocker internals.
+      @param aForceNoOpener If true, force noopener behavior.  This means not
+                            looking for existing windows with the given name,
+                            not setting an opener on the newly opened window,
+                            and returning null from this method.
+
       @return the new window
 
       @note This method may examine the JS context stack for purposes of
             determining the security context to use for the search for a given
             window named aName.
       @note This method should try to set the default charset for the new
             window to the default charset of the document in the calling window
             (which is determined based on the JS stack and the value of
             aParent).  This is not guaranteed, however.
   */
   mozIDOMWindowProxy openWindow2(in mozIDOMWindowProxy aParent, in string aUrl,
                                  in string aName, in string aFeatures,
                                  in boolean aCalledFromScript,
                                  in boolean aDialog,
                                  in boolean aNavigate,
                                  in nsISupports aArgs,
-                                 in boolean aIsPopupSpam);
+                                 in boolean aIsPopupSpam,
+                                 in boolean aForceNoOpener);
 
   /**
    * Opens a new window using the most recent non-private browser
    * window as its parent.
    *
    * @return the nsITabParent of the initial browser for the newly opened
    *         window.
    */
--- a/embedding/components/windowwatcher/nsWindowWatcher.cpp
+++ b/embedding/components/windowwatcher/nsWindowWatcher.cpp
@@ -369,16 +369,17 @@ nsWindowWatcher::OpenWindow(mozIDOMWindo
     argv->GetLength(&argc);
   }
   bool dialog = (argc != 0);
 
   return OpenWindowInternal(aParent, aUrl, aName, aFeatures,
                             /* calledFromJS = */ false, dialog,
                             /* navigate = */ true, argv,
                             /* aIsPopupSpam = */ false,
+                            /* aForceNoOpener = */ false,
                             aResult);
 }
 
 struct SizeSpec
 {
   SizeSpec()
     : mLeft(0)
     , mTop(0)
@@ -433,16 +434,17 @@ nsWindowWatcher::OpenWindow2(mozIDOMWind
                              const char* aUrl,
                              const char* aName,
                              const char* aFeatures,
                              bool aCalledFromScript,
                              bool aDialog,
                              bool aNavigate,
                              nsISupports* aArguments,
                              bool aIsPopupSpam,
+                             bool aForceNoOpener,
                              mozIDOMWindowProxy** aResult)
 {
   nsCOMPtr<nsIArray> argv = ConvertArgsToArray(aArguments);
 
   uint32_t argc = 0;
   if (argv) {
     argv->GetLength(&argc);
   }
@@ -453,17 +455,17 @@ nsWindowWatcher::OpenWindow2(mozIDOMWind
   bool dialog = aDialog;
   if (!aCalledFromScript) {
     dialog = argc > 0;
   }
 
   return OpenWindowInternal(aParent, aUrl, aName, aFeatures,
                             aCalledFromScript, dialog,
                             aNavigate, argv, aIsPopupSpam,
-                            aResult);
+                            aForceNoOpener, aResult);
 }
 
 // This static function checks if the aDocShell uses an UserContextId equal to
 // the userContextId of subjectPrincipal, if not null.
 static bool
 CheckUserContextCompatibility(nsIDocShell* aDocShell)
 {
   MOZ_ASSERT(aDocShell);
@@ -690,16 +692,17 @@ nsWindowWatcher::OpenWindowInternal(mozI
                                     const char* aUrl,
                                     const char* aName,
                                     const char* aFeatures,
                                     bool aCalledFromJS,
                                     bool aDialog,
                                     bool aNavigate,
                                     nsIArray* aArgv,
                                     bool aIsPopupSpam,
+                                    bool aForceNoOpener,
                                     mozIDOMWindowProxy** aResult)
 {
   nsresult rv = NS_OK;
   bool isNewToplevelWindow = false;
   bool windowIsNew = false;
   bool windowNeedsName = false;
   bool windowIsModal = false;
   bool uriToLoadIsChrome = false;
@@ -747,17 +750,18 @@ nsWindowWatcher::OpenWindowInternal(mozI
   if (aFeatures) {
     features.Assign(aFeatures);
     features.StripWhitespace();
   } else {
     features.SetIsVoid(true);
   }
 
   // try to find an extant window with the given name
-  nsCOMPtr<nsPIDOMWindowOuter> foundWindow = SafeGetWindowByName(name, aParent);
+  nsCOMPtr<nsPIDOMWindowOuter> foundWindow =
+    SafeGetWindowByName(name, aForceNoOpener, aParent);
   GetWindowTreeItem(foundWindow, getter_AddRefs(newDocShellItem));
 
   // Do sandbox checks here, instead of waiting until nsIDocShell::LoadURI.
   // The state of the window can change before this call and if we are blocked
   // because of sandboxing, we wouldn't want that to happen.
   nsCOMPtr<nsPIDOMWindowOuter> parentWindow =
     aParent ? nsPIDOMWindowOuter::From(aParent) : nullptr;
   nsCOMPtr<nsIDocShell> parentDocShell;
@@ -1063,17 +1067,18 @@ nsWindowWatcher::OpenWindowInternal(mozI
   // Copy sandbox flags to the new window if activeDocsSandboxFlags says to do
   // so.  Note that it's only nonzero if the window is new, so clobbering
   // sandbox flags on the window makes sense in that case.
   if (activeDocsSandboxFlags &
         SANDBOX_PROPAGATES_TO_AUXILIARY_BROWSING_CONTEXTS) {
     newDocShell->SetSandboxFlags(activeDocsSandboxFlags);
   }
 
-  rv = ReadyOpenedDocShellItem(newDocShellItem, parentWindow, windowIsNew, aResult);
+  rv = ReadyOpenedDocShellItem(newDocShellItem, parentWindow, windowIsNew,
+                               aForceNoOpener, aResult);
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   if (isNewToplevelWindow) {
     nsCOMPtr<nsIDocShellTreeOwner> newTreeOwner;
     newDocShellItem->GetTreeOwner(getter_AddRefs(newTreeOwner));
     MaybeDisablePersistence(features, newTreeOwner);
@@ -1311,16 +1316,20 @@ nsWindowWatcher::OpenWindowInternal(mozI
       // events about the dialog, to prevent the current state from
       // being active the whole time a modal dialog is open.
       nsAutoPopupStatePusher popupStatePusher(openAbused);
 
       newChrome->ShowAsModal();
     }
   }
 
+  if (aForceNoOpener && windowIsNew) {
+    NS_RELEASE(*aResult);
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsWindowWatcher::RegisterNotification(nsIObserver* aObserver)
 {
   // just a convenience method; it delegates to nsIObserverService
 
@@ -2138,18 +2147,28 @@ nsWindowWatcher::GetCallerTreeItem(nsIDo
     callerItem = aParentItem;
   }
 
   return callerItem.forget();
 }
 
 nsPIDOMWindowOuter*
 nsWindowWatcher::SafeGetWindowByName(const nsAString& aName,
+                                     bool aForceNoOpener,
                                      mozIDOMWindowProxy* aCurrentWindow)
 {
+  if (aForceNoOpener) {
+    if (!aName.LowerCaseEqualsLiteral("_self") &&
+        !aName.LowerCaseEqualsLiteral("_top") &&
+        !aName.LowerCaseEqualsLiteral("_parent")) {
+      // Ignore all other names in the noopener case.
+      return nullptr;
+    }
+  }
+
   nsCOMPtr<nsIDocShellTreeItem> startItem;
   GetWindowTreeItem(aCurrentWindow, getter_AddRefs(startItem));
 
   nsCOMPtr<nsIDocShellTreeItem> callerItem = GetCallerTreeItem(startItem);
 
   const nsAFlatString& flatName = PromiseFlatString(aName);
 
   nsCOMPtr<nsIDocShellTreeItem> foundItem;
@@ -2168,27 +2187,30 @@ nsWindowWatcher::SafeGetWindowByName(con
    This forces the creation of a script context, if one has not already
    been created. Note it also sets the window's opener to the parent,
    if applicable -- because it's just convenient, that's all. null aParent
    is acceptable. */
 nsresult
 nsWindowWatcher::ReadyOpenedDocShellItem(nsIDocShellTreeItem* aOpenedItem,
                                          nsPIDOMWindowOuter* aParent,
                                          bool aWindowIsNew,
+                                         bool aForceNoOpener,
                                          mozIDOMWindowProxy** aOpenedWindow)
 {
   nsresult rv = NS_ERROR_FAILURE;
 
   NS_ENSURE_ARG(aOpenedWindow);
 
   *aOpenedWindow = 0;
   nsCOMPtr<nsPIDOMWindowOuter> piOpenedWindow = aOpenedItem->GetWindow();
   if (piOpenedWindow) {
     if (aParent) {
-      piOpenedWindow->SetOpenerWindow(aParent, aWindowIsNew); // damnit
+      if (!aForceNoOpener) {
+        piOpenedWindow->SetOpenerWindow(aParent, aWindowIsNew); // damnit
+      }
 
       if (aWindowIsNew) {
 #ifdef DEBUG
         // Assert that we're not loading things right now.  If we are, when
         // that load completes it will clobber whatever principals we set up
         // on this new window!
         nsCOMPtr<nsIDocumentLoader> docloader = do_QueryInterface(aOpenedItem);
         NS_ASSERTION(docloader, "How can we not have a docloader here?");
--- a/embedding/components/windowwatcher/nsWindowWatcher.h
+++ b/embedding/components/windowwatcher/nsWindowWatcher.h
@@ -66,30 +66,33 @@ protected:
 
   // Get the caller tree item.  Look on the JS stack, then fall back
   // to the parent if there's nothing there.
   already_AddRefed<nsIDocShellTreeItem> GetCallerTreeItem(
     nsIDocShellTreeItem* aParentItem);
 
   // Unlike GetWindowByName this will look for a caller on the JS
   // stack, and then fall back on aCurrentWindow if it can't find one.
+  // It also knows to not look for things if aForceNoOpener is set.
   nsPIDOMWindowOuter* SafeGetWindowByName(const nsAString& aName,
+                                          bool aForceNoOpener,
                                           mozIDOMWindowProxy* aCurrentWindow);
 
   // Just like OpenWindowJS, but knows whether it got called via OpenWindowJS
   // (which means called from script) or called via OpenWindow.
   nsresult OpenWindowInternal(mozIDOMWindowProxy* aParent,
                               const char* aUrl,
                               const char* aName,
                               const char* aFeatures,
                               bool aCalledFromJS,
                               bool aDialog,
                               bool aNavigate,
                               nsIArray* aArgv,
                               bool aIsPopupSpam,
+                              bool aForceNoOpener,
                               mozIDOMWindowProxy** aResult);
 
   static nsresult URIfromURL(const char* aURL,
                              mozIDOMWindowProxy* aParent,
                              nsIURI** aURI);
 
   static uint32_t CalculateChromeFlagsForChild(const nsACString& aFeaturesStr);
 
@@ -102,16 +105,17 @@ protected:
 
   static int32_t WinHasOption(const nsACString& aOptions, const char* aName,
                               int32_t aDefault, bool* aPresenceFlag);
   /* Compute the right SizeSpec based on aFeatures */
   static void CalcSizeSpec(const nsACString& aFeatures, SizeSpec& aResult);
   static nsresult ReadyOpenedDocShellItem(nsIDocShellTreeItem* aOpenedItem,
                                           nsPIDOMWindowOuter* aParent,
                                           bool aWindowIsNew,
+                                          bool aForceNoOpener,
                                           mozIDOMWindowProxy** aOpenedWindow);
   static void SizeOpenedWindow(nsIDocShellTreeOwner* aTreeOwner,
                                mozIDOMWindowProxy* aParent,
                                bool aIsCallerChrome,
                                const SizeSpec& aSizeSpec,
                                mozilla::Maybe<float> aOpenerFullZoom =
                                  mozilla::Nothing());
   static void GetWindowTreeItem(mozIDOMWindowProxy* aWindow,
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -37607,16 +37607,22 @@
           }
         ],
         "html/browsers/history/the-location-interface/location-prototype-setting.html": [
           {
             "path": "html/browsers/history/the-location-interface/location-prototype-setting.html",
             "url": "/html/browsers/history/the-location-interface/location-prototype-setting.html"
           }
         ],
+        "html/browsers/the-window-object/window-open-noopener.html": [
+          {
+            "path": "html/browsers/the-window-object/window-open-noopener.html",
+            "url": "/html/browsers/the-window-object/window-open-noopener.html"
+          }
+        ],
         "html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-1.html": [
           {
             "path": "html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-1.html",
             "url": "/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-1.html"
           }
         ],
         "html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html": [
           {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/support/noopener-target.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<script>
+  var channelName = location.search.substr(1);
+  var channel = new BroadcastChannel(channelName);
+  channel.postMessage({ name: window.name,
+                        haveOpener: window.opener !== null });
+  window.close();
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/html/browsers/the-window-object/window-open-noopener.html
@@ -0,0 +1,105 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>window.open() with "noopener" tests</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+var testData = [
+  { testDescription: "window.open() with 'noopener' should not reuse existing target",
+    secondWindowFeatureString: "noopener",
+    shouldReuse: false },
+  { testDescription: "noopener needs to be present as a token on its own",
+    secondWindowFeatureString: "noopener=1",
+    shouldReuse: true },
+  { testDescription: "noopener needs to be present as a token on its own again",
+    secondWindowFeatureString: "noopener=0",
+    shouldReuse: true },
+  { testDescription: "noopener needs to be present as a token on its own yet again",
+    secondWindowFeatureString: "make me noopener",
+    shouldReuse: true },
+  { testDescription: "Trailing noopener should work",
+    secondWindowFeatureString: "abc def,  \n\r noopener",
+    shouldReuse: false },
+  { testDescription: "Leading noopener should work",
+    secondWindowFeatureString: "noopener \f\t , hey, there",
+    shouldReuse: false },
+  { testDescription: "Interior noopener should work",
+    secondWindowFeatureString: "and now, noopener   , hey, there",
+    shouldReuse: false },
+];
+
+var tests = [];
+/**
+ * Loop over our testData array and kick off an async test for each entry.  Each
+ * async test opens a window using window.open() with some per-test unique name,
+ * then tries to do a second window.open() call with the same name and the
+ * test-specific feature string.  It then checks whether that second
+ * window.open() call reuses the existing window, whether the return value of
+ * the second window.open() call is correct (it should be null in the noopener
+ * cases and non-null in the cases when the existing window gets reused) and so
+ * forth.
+ */
+for (var i = 0; i < testData.length; ++i) {
+  var test = testData[i];
+  var t = async_test(test.testDescription);
+  tests.push(t);
+  t.secondWindowFeatureString = test.secondWindowFeatureString;
+  t.windowName = "someuniquename" + i;
+
+  if (test.shouldReuse) {
+    t.step(function() {
+      var windowName = this.windowName;
+
+      var w1 = window.open("", windowName);
+      this.add_cleanup(function() { w1.close(); });
+
+      assert_equals(w1.opener, window);
+
+      var w2 = window.open("", windowName, this.secondWindowFeatureString);
+      assert_equals(w2, w1);
+      assert_equals(w2.opener, w1.opener);
+      assert_equals(w2.opener, window);
+      this.done();
+    });
+  } else {
+    t.step(function() {
+      var w1;
+      this.add_cleanup(function() { w1.close(); });
+
+      var windowName = this.windowName;
+      var channel = new BroadcastChannel(windowName);
+
+      channel.onmessage = this.step_func_done(function(e) {
+        var data = e.data;
+        assert_equals(data.name, windowName, "Should have the right name");
+        assert_equals(data.haveOpener, false, "Should not have opener");
+        assert_equals(w1.opener, window);
+        assert_equals(w1.location.href, "about:blank");
+      });
+
+      w1 = window.open("", windowName);
+      assert_equals(w1.opener, window);
+
+      var w2 = window.open("support/noopener-target.html?" + windowName,
+                           windowName, this.secondWindowFeatureString);
+      assert_equals(w2, null);
+
+      assert_equals(w1.opener, window);
+    });
+  }
+}
+
+/**
+ * Loop over the special targets that ignore noopener and check that doing a
+ * window.open() with those targets correctly reuses the existing window.
+ */
+for (var target of ["_self", "_parent", "_top"]) {
+  var t = async_test("noopener window.open targeting " + target);
+  tests.push(t);
+  t.openedWindow = window.open(`javascript:var w2 = window.open("", "${target}", "noopener"); this.checkValues(w2); this.close(); void(0);`);
+  assert_equals(t.openedWindow.opener, window);
+  t.openedWindow.checkValues = t.step_func_done(function(win) {
+    assert_equals(win, this.openedWindow);
+  });
+}
+</script>