Bug 1602173: Capture attempts to load pages and redirect back to the browser when needed. r=Gijs draft
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 10 Dec 2019 20:58:01 +0000
changeset 2527394 211cbccebf71ee91ce7c12a7bbbb3d3d7bc00f8d
parent 2527393 60090e11b49390ab7d500c6142957c22b2d26284
child 2527395 8fa835522305be87a3e01507466c2b1aeead6cb3
push id463373
push userreviewbot
push dateTue, 10 Dec 2019 20:58:49 +0000
treeherdertry@c7876b1e9faf [default view] [failures only]
reviewersGijs
bugs1602173
milestone73.0a1
Bug 1602173: Capture attempts to load pages and redirect back to the browser when needed. r=Gijs Differential Revision: https://phabricator.services.mozilla.com/D56286 Differential Diff: PHID-DIFF-5k642oknkyvniuvt3yf7
browser/components/BrowserGlue.jsm
browser/components/ssb/SiteSpecificBrowserChild.jsm
browser/components/ssb/SiteSpecificBrowserParent.jsm
browser/components/ssb/content/ssb.js
browser/components/ssb/moz.build
browser/components/ssb/tests/browser/browser.ini
browser/components/ssb/tests/browser/browser_ssb_direct.js
browser/components/ssb/tests/browser/browser_ssb_newtab.js
browser/components/ssb/tests/browser/browser_ssb_newwindow.js
browser/components/ssb/tests/browser/browser_ssb_windowlocation.js
browser/components/ssb/tests/browser/browser_ssb_windowopen.js
browser/components/ssb/tests/browser/head.js
browser/components/ssb/tests/browser/test_page.html
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -274,16 +274,27 @@ let ACTORS = {
   SwitchDocumentDirection: {
     child: {
       moduleURI: "resource:///actors/SwitchDocumentDirectionChild.jsm",
     },
 
     allFrames: true,
   },
 
+  SiteSpecificBrowser: {
+    parent: {
+      moduleURI: "resource:///actors/SiteSpecificBrowserParent.jsm",
+    },
+    child: {
+      moduleURI: "resource:///actors/SiteSpecificBrowserChild.jsm",
+    },
+
+    allFrames: true,
+  },
+
   UITour: {
     parent: {
       moduleURI: "resource:///modules/UITourParent.jsm",
     },
     child: {
       moduleURI: "resource:///modules/UITourChild.jsm",
       events: {
         mozUITour: { wantUntrusted: true },
new file mode 100644
--- /dev/null
+++ b/browser/components/ssb/SiteSpecificBrowserChild.jsm
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["SiteSpecificBrowserChild"];
+
+const { SiteSpecificBrowserBase } = ChromeUtils.import(
+  "resource:///modules/SiteSpecificBrowserService.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { E10SUtils } = ChromeUtils.import(
+  "resource://gre/modules/E10SUtils.jsm"
+);
+
+class SiteSpecificBrowserChild extends JSWindowActorChild {
+  receiveMessage(message) {
+    switch (message.name) {
+      case "SetSSB":
+        // Get the SSB instance for this frame and use it for making decisions
+        // about navigations.
+        let ssb = SiteSpecificBrowserBase.get(message.data);
+        let browserChild = this.docShell
+          .QueryInterface(Ci.nsIInterfaceRequestor)
+          .getInterface(Ci.nsIBrowserChild);
+
+        // Note that this sets the webbrowserchrome for the top-level browser
+        // child in this page. This means that any inner-frames loading in
+        // different processes will not be handled correctly. Fixing this will
+        // happen in bug 1602849.
+        browserChild.webBrowserChrome = new WebBrowserChrome(ssb);
+        break;
+    }
+  }
+}
+
+function getActor(docShell) {
+  return docShell.domWindow
+    .getWindowGlobalChild()
+    .getActor("SiteSpecificBrowser");
+}
+
+// JS actors can't generally be XPCOM objects so we must use a separate class.
+class WebBrowserChrome {
+  constructor(ssb) {
+    this.ssb = ssb;
+  }
+
+  // nsIWebBrowserChrome3
+
+  /**
+   * This gets called when a user clicks on a link or submits a form. We can use
+   * it to see where the resulting page load will occur and if needed redirect
+   * it to a different target.
+   *
+   * @param {string}  originalTarget the target intended for the load.
+   * @param {nsIURI}  linkURI        the URI that will be loaded.
+   * @param {Node}    linkNode       the element causing the load.
+   * @param {boolean} isAppTab       whether the source docshell is marked as an
+   *                                 app tab.
+   * @return {string} the target to use for the load.
+   */
+  onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
+    // Our actor is for the top-level frame in the page while this may be being
+    // called for a navigation in an inner frame. First we have to find the
+    // browsing context for the frame doing the load.
+
+    let docShell = linkNode.ownerGlobal.docShell;
+    let bc = docShell.browsingContext;
+
+    // Which browsing context is this link targetting?
+    let target = originalTarget ? bc.findWithName(originalTarget) : bc;
+
+    if (target) {
+      // If we found a target then it must be one of the frames within this
+      // frame tree since we don't support popup windows.
+      if (target.parent) {
+        // An inner frame, continue.
+        return originalTarget;
+      }
+
+      // A top-level load. If our SSB cannot load this URI then start the
+      // process of opening it into a new tab somewhere.
+      return this.ssb.canLoad(linkURI) ? originalTarget : "_blank";
+    }
+
+    // An attempt to open a new window/tab. If the new URI can be loaded by our
+    // SSB then load it at the top-level. Note that we override the requested
+    // target so that this page can't reach the new context.
+    return this.ssb.canLoad(linkURI) ? "_top" : "_blank";
+  }
+
+  /**
+   * A load is about to occur in a frame. This is an opportunity to stop it
+   * and redirect it somewhere.
+   *
+   * @param {nsIDocShell}     docShell            the current docshell.
+   * @param {nsIURI}          uri                 the URI that will be loaded.
+   * @param {nsIReferrerInfo} referrerInfo        the referrer info.
+   * @param {boolean}         hasPostData         whether there is POST data
+   *                                              for the load.
+   * @param {nsIPrincipal}    triggeringPrincipal the triggering principal.
+   * @param {nsIContentSecurityPolicy} csp the content security policy.
+   * @return {boolean} whether the load should proceed or not.
+   */
+  shouldLoadURI(
+    docShell,
+    uri,
+    referrerInfo,
+    hasPostData,
+    triggeringPrincipal,
+    csp
+  ) {
+    // As above, our actor is for the top-level frame in the page however we
+    // are passed the docshell potentially handling the load here so we can
+    // do the right thing.
+
+    // We only police loads at the top level.
+    if (docShell.browsingContext.parent) {
+      return true;
+    }
+
+    if (!this.ssb.canLoad(uri)) {
+      // Should only have got this far for a window.location manipulation.
+
+      getActor(docShell).sendAsyncMessage("LoadURI", {
+        uri: uri.spec,
+        referrerInfo: E10SUtils.serializeReferrerInfo(referrerInfo),
+        triggeringPrincipal: E10SUtils.serializePrincipal(
+          triggeringPrincipal ||
+            Services.scriptSecurityManager.createNullPrincipal({})
+        ),
+        csp: csp ? E10SUtils.serializeCSP(csp) : null,
+      });
+
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * A simple check for whether this is the correct process to load this URI.
+   *
+   * @param {nsIURI}   uri  the URI that will be loaded.
+   * @return {boolean} whether the load should proceed or not.
+   */
+  shouldLoadURIInThisProcess(uri) {
+    return this.ssb.canLoad(uri);
+  }
+
+  /**
+   * Instructs us to start a fresh process to load this URI. Usually used for
+   * large allocation sites. SSB does not support this.
+   *
+   * @param {nsIDocShell}     docShell            the current docshell.
+   * @param {nsIURI}          uri                 the URI that will be loaded.
+   * @param {nsIReferrerInfo} referrerInfo        the referrer info.
+   * @param {nsIPrincipal}    triggeringPrincipal the triggering principal.
+   * @param {Number}          loadFlags           the load flags.
+   * @param {nsIContentSecurityPolicy} csp the content security policy.
+   * @return {boolean} whether the load should proceed or not.
+   */
+  reloadInFreshProcess(
+    docShell,
+    uri,
+    referrerInfo,
+    triggeringPrincipal,
+    loadFlags,
+    csp
+  ) {
+    return false;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/ssb/SiteSpecificBrowserParent.jsm
@@ -0,0 +1,85 @@
+/* vim: set ts=2 sw=2 sts=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["SiteSpecificBrowserParent"];
+
+const { BrowserWindowTracker } = ChromeUtils.import(
+  "resource:///modules/BrowserWindowTracker.jsm"
+);
+const { E10SUtils } = ChromeUtils.import(
+  "resource://gre/modules/E10SUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { AppConstants } = ChromeUtils.import(
+  "resource://gre/modules/AppConstants.jsm"
+);
+
+class SiteSpecificBrowserParent extends JSWindowActorParent {
+  receiveMessage(message) {
+    switch (message.name) {
+      case "LoadURI":
+        // The content process found a URI that needs to be loaded in the main
+        // browser.
+        let triggeringPrincipal = E10SUtils.deserializePrincipal(
+          message.data.triggeringPrincipal
+        );
+        let referrerInfo = E10SUtils.deserializeReferrerInfo(
+          message.data.referrerInfo
+        );
+        let csp = E10SUtils.deserializeCSP(message.data.csp);
+
+        // Attempt to find an existing window to open it in.
+        let win = BrowserWindowTracker.getTopWindow();
+        if (win) {
+          win.gBrowser.selectedTab = win.gBrowser.addTab(message.data.uri, {
+            triggeringPrincipal,
+            csp,
+            referrerInfo,
+          });
+        } else {
+          let sa = Cc["@mozilla.org/array;1"].createInstance(
+            Ci.nsIMutableArray
+          );
+
+          let wuri = Cc["@mozilla.org/supports-string;1"].createInstance(
+            Ci.nsISupportsString
+          );
+          wuri.data = message.data.uri;
+
+          let allowThirdPartyFixupSupports = Cc[
+            "@mozilla.org/supports-PRBool;1"
+          ].createInstance(Ci.nsISupportsPRBool);
+          allowThirdPartyFixupSupports.data = false;
+
+          let userContextIdSupports = Cc[
+            "@mozilla.org/supports-PRUint32;1"
+          ].createInstance(Ci.nsISupportsPRUint32);
+          userContextIdSupports.data = 0;
+
+          sa.appendElement(wuri);
+          sa.appendElement(null); // unused (bug 871161)
+          sa.appendElement(referrerInfo);
+          sa.appendElement(null); // postData
+          sa.appendElement(null); // allowThirdPartyFixup
+          sa.appendElement(null); // userContextId
+          sa.appendElement(null); // originPrincipal
+          sa.appendElement(null); // originStoragePrincipal
+          sa.appendElement(triggeringPrincipal);
+          sa.appendElement(null); // allowInheritPrincipal
+          sa.appendElement(csp);
+
+          Services.ww.openWindow(
+            null,
+            AppConstants.BROWSER_CHROME_URL,
+            null,
+            "chrome,dialog=no,all",
+            sa
+          );
+        }
+        break;
+    }
+  }
+}
--- a/browser/components/ssb/content/ssb.js
+++ b/browser/components/ssb/content/ssb.js
@@ -1,25 +1,195 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-ChromeUtils.defineModuleGetter(
-  this,
-  "SiteSpecificBrowser",
-  "resource:///modules/SiteSpecificBrowserService.jsm"
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
 );
 
+XPCOMUtils.defineLazyModuleGetters(this, {
+  BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+  SiteSpecificBrowser: "resource:///modules/SiteSpecificBrowserService.jsm",
+  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
 let gSSBBrowser = null;
 let gSSB = null;
 
 function init() {
   gSSB = SiteSpecificBrowser.get(window.arguments[0]);
 
+  window.browserDOMWindow = new BrowserDOMWindow();
+
   gSSBBrowser = document.createXULElement("browser");
   gSSBBrowser.setAttribute("id", "browser");
   gSSBBrowser.setAttribute("type", "content");
   gSSBBrowser.setAttribute("remote", "true");
+  gSSBBrowser.setAttribute("nodefaultsrc", "true");
   document.getElementById("browser-container").appendChild(gSSBBrowser);
+
+  // Give our actor the SSB's ID.
+  let actor = gSSBBrowser.browsingContext.currentWindowGlobal.getActor(
+    "SiteSpecificBrowser"
+  );
+  actor.sendAsyncMessage("SetSSB", gSSB.id);
+
   gSSBBrowser.src = gSSB.startURI.spec;
 }
 
+class BrowserDOMWindow {
+  /**
+   * Called when a page in the main process needs a new window to display a new
+   * page in.
+   *
+   * @param {nsIURI?} uri
+   * @param {Window} opener
+   * @param {Number} where
+   * @param {Number} flags
+   * @param {nsIPrincipal} triggeringPrincipal
+   * @param {nsIContentSecurityPolicy?} csp
+   * @return {BrowsingContext} the BrowsingContext the URI should be loaded in.
+   */
+  createContentWindow(uri, opener, where, flags, triggeringPrincipal, csp) {
+    console.error(
+      "createContentWindow should never be called from a remote browser"
+    );
+    throw Cr.NS_ERROR_FAILURE;
+  }
+
+  /**
+   * Called from a page in the main process to open a new URI.
+   *
+   * @param {nsIURI} uri
+   * @param {Window} opener
+   * @param {Number} where
+   * @param {Number} flags
+   * @param {nsIPrincipal} triggeringPrincipal
+   * @param {nsIContentSecurityPolicy?} csp
+   * @return {BrowsingContext} the BrowsingContext the URI should be loaded in.
+   */
+  openURI(uri, opener, where, flags, triggeringPrincipal, csp) {
+    console.error("openURI should never be called from a remote browser");
+    throw Cr.NS_ERROR_FAILURE;
+  }
+
+  /**
+   * Finds a new frame to load some content in.
+   *
+   * @param {nsIURI?} uri
+   * @param {nsIOpenURIInFrameParams} params
+   * @param {Number} where
+   * @param {Number} flags
+   * @param {Number} nextRemoteTabId
+   * @param {string} name
+   * @param {boolean} shouldOpen should the load start or not.
+   * @return {Element} the frame element the URI should be loaded in.
+   */
+  getContentWindowOrOpenURIInFrame(
+    uri,
+    params,
+    where,
+    flags,
+    nextRemoteTabId,
+    name,
+    shouldOpen
+  ) {
+    // It's been determined that this load needs to happen in a new frame.
+    // Either onBeforeLinkTraversal set this correctly or this is the result
+    // of a window.open call.
+
+    // If this ssb can load the url then just load it internally.
+    if (gSSB.canLoad(uri)) {
+      return gSSBBrowser;
+    }
+
+    // Try and find a browser window to open in.
+    let win = BrowserWindowTracker.getTopWindow({
+      private: params.isPrivate,
+      allowPopups: false,
+    });
+
+    if (win) {
+      // Just hand off to the window's handler
+      win.focus();
+      return win.browserDOMWindow.openURIInFrame(
+        shouldOpen ? uri : null,
+        params,
+        where,
+        flags,
+        nextRemoteTabId,
+        name
+      );
+    }
+
+    // We need to open a new browser window and a tab in it. That's an
+    // asychronous operation but luckily if we return null here the platform
+    // handles doing that for us.
+    return null;
+  }
+
+  /**
+   * Gets an nsFrameLoaderOwner to load some new content in.
+   *
+   * @param {nsIURI?} uri
+   * @param {nsIOpenURIInFrameParams} params
+   * @param {Number} where
+   * @param {Number} flags
+   * @param {Number} nextRemoteTabId
+   * @param {string} name
+   * @return {Element} the frame element the URI should be loaded in.
+   */
+  createContentWindowInFrame(uri, params, where, flags, nextRemoteTabId, name) {
+    return this.getContentWindowOrOpenURIInFrame(
+      uri,
+      params,
+      where,
+      flags,
+      nextRemoteTabId,
+      name,
+      false
+    );
+  }
+
+  /**
+   * Create a new nsFrameLoaderOwner and load some content into it.
+   *
+   * @param {nsIURI} uri
+   * @param {nsIOpenURIInFrameParams} params
+   * @param {Number} where
+   * @param {Number} flags
+   * @param {Number} nextRemoteTabId
+   * @param {string} name
+   * @return {Element} the frame element the URI is loading in.
+   */
+  openURIInFrame(uri, params, where, flags, nextRemoteTabId, name) {
+    return this.getContentWindowOrOpenURIInFrame(
+      uri,
+      params,
+      where,
+      flags,
+      nextRemoteTabId,
+      name,
+      true
+    );
+  }
+
+  isTabContentWindow(window) {
+    // This method is probably not needed anymore: bug 1602915
+    return gSSBBrowser.contentWindow == window;
+  }
+
+  canClose() {
+    return BrowserUtils.canCloseWindow(window);
+  }
+
+  get tabCount() {
+    return 1;
+  }
+}
+
+BrowserDOMWindow.prototype.QueryInterface = ChromeUtils.generateQI([
+  Ci.nsIBrowserDOMWindow,
+]);
+
 window.addEventListener("load", init, true);
--- a/browser/components/ssb/moz.build
+++ b/browser/components/ssb/moz.build
@@ -9,8 +9,13 @@ BROWSER_CHROME_MANIFESTS += ['tests/brow
 
 XPCOM_MANIFESTS += [
     'components.conf',
 ]
 
 EXTRA_JS_MODULES += [
     'SiteSpecificBrowserService.jsm',
 ]
+
+FINAL_TARGET_FILES.actors += [
+    'SiteSpecificBrowserChild.jsm',
+    'SiteSpecificBrowserParent.jsm',
+]
--- a/browser/components/ssb/tests/browser/browser.ini
+++ b/browser/components/ssb/tests/browser/browser.ini
@@ -1,10 +1,17 @@
 [DEFAULT]
 support-files =
   head.js
   test_page.html
+  empty_page.html
 prefs =
   browser.ssb.enabled=true
 
+[browser_ssb_direct.js]
 [browser_ssb_lasttab.js]
 [browser_ssb_menu.js]
+[browser_ssb_newtab.js]
+[browser_ssb_newwindow.js]
 [browser_ssb_open.js]
+[browser_ssb_windowlocation.js]
+[browser_ssb_windowopen.js]
+skip-if = true # It is unclear what we want to do here.
new file mode 100644
--- /dev/null
+++ b/browser/components/ssb/tests/browser/browser_ssb_direct.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function testDirectLoad(target, checker) {
+  let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
+
+  let promise = checker(ssb);
+  await BrowserTestUtils.synthesizeMouseAtCenter(
+    "#direct",
+    {},
+    getBrowser(ssb)
+  );
+
+  await promise;
+  await BrowserTestUtils.closeWindow(ssb);
+}
+
+// A link that should load inside the ssb
+add_task(async function local() {
+  await testDirectLoad(gHttpsTestRoot + "empty_page.html", async ssb => {
+    try {
+      await expectSSBLoad(ssb);
+      Assert.equal(
+        getBrowser(ssb).currentURI.spec,
+        gHttpsTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to an insecure site should load outside the ssb
+add_task(async function insecure() {
+  await testDirectLoad(gHttpTestRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to a different host should load outside the ssb
+add_task(async function external() {
+  await testDirectLoad(gHttpsOtherRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpsOtherRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/ssb/tests/browser/browser_ssb_newtab.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function testNewTabLoad(target, checker) {
+  let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
+
+  let promise = checker(ssb);
+  await BrowserTestUtils.synthesizeMouseAtCenter(
+    "#new-tab",
+    {},
+    getBrowser(ssb)
+  );
+
+  await promise;
+  await BrowserTestUtils.closeWindow(ssb);
+}
+
+// A link that should load inside the ssb
+add_task(async function local() {
+  await testNewTabLoad(gHttpsTestRoot + "empty_page.html", async ssb => {
+    try {
+      await expectSSBLoad(ssb);
+      Assert.equal(
+        getBrowser(ssb).currentURI.spec,
+        gHttpsTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to an insecure site should load outside the ssb
+add_task(async function insecure() {
+  await testNewTabLoad(gHttpTestRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to a different host should load outside the ssb
+add_task(async function external() {
+  await testNewTabLoad(gHttpsOtherRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpsOtherRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/ssb/tests/browser/browser_ssb_newwindow.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function testNewWindowLoad(target, checker) {
+  let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
+
+  let promise = checker(ssb);
+  await BrowserTestUtils.synthesizeMouseAtCenter(
+    "#new-window",
+    {},
+    getBrowser(ssb)
+  );
+
+  await promise;
+  await BrowserTestUtils.closeWindow(ssb);
+}
+
+// A link that should load inside the ssb
+add_task(async function local() {
+  await testNewWindowLoad(gHttpsTestRoot + "empty_page.html", async ssb => {
+    try {
+      await expectSSBLoad(ssb);
+      Assert.equal(
+        getBrowser(ssb).currentURI.spec,
+        gHttpsTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to an insecure site should load outside the ssb
+add_task(async function insecure() {
+  await testNewWindowLoad(gHttpTestRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to a different host should load outside the ssb
+add_task(async function external() {
+  await testNewWindowLoad(gHttpsOtherRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpsOtherRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/ssb/tests/browser/browser_ssb_windowlocation.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function testWindowLocationLoad(target, checker) {
+  let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
+
+  let promise = checker(ssb);
+  await BrowserTestUtils.synthesizeMouseAtCenter(
+    "#window-location",
+    {},
+    getBrowser(ssb)
+  );
+
+  await promise;
+  await BrowserTestUtils.closeWindow(ssb);
+}
+
+// A link that should load inside the ssb
+add_task(async function local() {
+  await testWindowLocationLoad(
+    gHttpsTestRoot + "empty_page.html",
+    async ssb => {
+      try {
+        await expectSSBLoad(ssb);
+        Assert.equal(
+          getBrowser(ssb).currentURI.spec,
+          gHttpsTestRoot + "empty_page.html",
+          "Should have loaded the right uri."
+        );
+      } catch (e) {
+        // Any error will already have logged a failure.
+      }
+    }
+  );
+});
+
+// A link to an insecure site should load outside the ssb
+add_task(async function insecure() {
+  await testWindowLocationLoad(gHttpTestRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to a different host should load outside the ssb
+add_task(async function external() {
+  await testWindowLocationLoad(
+    gHttpsOtherRoot + "empty_page.html",
+    async ssb => {
+      try {
+        let tab = await expectTabLoad(ssb);
+        Assert.equal(
+          tab.linkedBrowser.currentURI.spec,
+          gHttpsOtherRoot + "empty_page.html",
+          "Should have loaded the right uri."
+        );
+        BrowserTestUtils.removeTab(tab);
+      } catch (e) {
+        // Any error will already have logged a failure.
+      }
+    }
+  );
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/ssb/tests/browser/browser_ssb_windowopen.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function testWindowOpenLoad(target, checker) {
+  let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
+
+  let promise = checker(ssb);
+  await BrowserTestUtils.synthesizeMouseAtCenter(
+    "#window-open",
+    {},
+    getBrowser(ssb)
+  );
+
+  await promise;
+  await BrowserTestUtils.closeWindow(ssb);
+}
+
+// A link that should load inside the ssb
+add_task(async function local() {
+  await testWindowOpenLoad(gHttpsTestRoot + "empty_page.html", async ssb => {
+    try {
+      await expectSSBLoad(ssb);
+      Assert.equal(
+        getBrowser(ssb).currentURI.spec,
+        gHttpsTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to an insecure site should load outside the ssb
+add_task(async function insecure() {
+  await testWindowOpenLoad(gHttpTestRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpTestRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
+
+// A link to a different host should load outside the ssb
+add_task(async function external() {
+  await testWindowOpenLoad(gHttpsOtherRoot + "empty_page.html", async ssb => {
+    try {
+      let tab = await expectTabLoad(ssb);
+      Assert.equal(
+        tab.linkedBrowser.currentURI.spec,
+        gHttpsOtherRoot + "empty_page.html",
+        "Should have loaded the right uri."
+      );
+      BrowserTestUtils.removeTab(tab);
+    } catch (e) {
+      // Any error will already have logged a failure.
+    }
+  });
+});
--- a/browser/components/ssb/tests/browser/head.js
+++ b/browser/components/ssb/tests/browser/head.js
@@ -8,19 +8,44 @@ const gHttpTestRoot = getRootDirectory(g
 );
 
 // A secure site to use.
 const gHttpsTestRoot = getRootDirectory(gTestPath).replace(
   "chrome://mochitests/content/",
   "https://example.com/"
 );
 
+// A different secure site to use.
+const gHttpsOtherRoot = getRootDirectory(gTestPath).replace(
+  "chrome://mochitests/content/",
+  "https://example.org/"
+);
+
 // The chrome url for the SSB UI.
 const SSB_WINDOW = "chrome://browser/content/ssb/ssb.html";
 
+// Directly opens an SSB for the given URI. Resolves to the SSB DOM window after
+// the SSB content has loaded.
+async function openSSB(uri) {
+  if (!(uri instanceof Ci.nsIURI)) {
+    uri = Services.io.newURI(uri);
+  }
+
+  let openPromise = BrowserTestUtils.domWindowOpened(null, async domwin => {
+    await BrowserTestUtils.waitForEvent(domwin, "load");
+    return domwin.location.toString() == SSB_WINDOW;
+  });
+
+  SiteSpecificBrowserService.launchFromURI(uri);
+
+  let ssbwin = await openPromise;
+  await BrowserTestUtils.browserLoaded(getBrowser(ssbwin), true);
+  return ssbwin;
+}
+
 // Simulates opening a SSB from the main browser window. Resolves to the SSB
 // DOM window after the SSB content has loaded.
 async function openSSBFromBrowserWindow(win = window) {
   let doc = win.document;
   let pageActionButton = doc.getElementById("pageActionButton");
   let panel = doc.getElementById("pageActionPanel");
   let popupShown = BrowserTestUtils.waitForEvent(panel, "popupshown");
 
@@ -33,17 +58,136 @@ async function openSSBFromBrowserWindow(
 
   let openPromise = BrowserTestUtils.domWindowOpened(null, async domwin => {
     await BrowserTestUtils.waitForEvent(domwin, "load");
     return domwin.location.toString() == SSB_WINDOW;
   });
 
   EventUtils.synthesizeMouseAtCenter(openItem, {}, win);
   let ssbwin = await openPromise;
-  let browser = ssbwin.document.getElementById("browser");
-  await BrowserTestUtils.browserLoaded(browser, true);
+  await BrowserTestUtils.browserLoaded(getBrowser(ssbwin), true);
   return ssbwin;
 }
 
 // Given the SSB UI DOM window gets the browser element showing the content.
 function getBrowser(ssbwin) {
   return ssbwin.document.getElementById("browser");
 }
+
+/**
+ * Waits for a load in response to an attempt to navigate from the SSB. It
+ * listens for new tab opens in the main window, new window opens and loads in
+ * the SSB itself. It returns a promise.
+ *
+ * The `where` argument is a string saying where the load is expected to
+ * happen, "ssb", "tab" or "window". The promise rejects if the load happens
+ * somewhere else and the offending new item (tab or window) get closed. When
+ * the load is seen in the correct location different things are returned
+ * depending on `where`. For "tab" the new tab is returned (it will have
+ * finished loading), for "window" the new window is returned (it will have
+ * finished loading). The "ssb" case doesn't return anything.
+ *
+ * Generally use the methods below this as they look more obvious.
+ */
+function expectLoadSomewhere(ssb, where, win = window) {
+  return new Promise((resolve, reject) => {
+    // Listens for a new tab opening in the main window.
+    const tabListener = async ({ target: tab }) => {
+      cleanup();
+
+      await BrowserTestUtils.browserLoaded(tab.linkedBrowser, true);
+
+      if (where != "tab") {
+        Assert.ok(
+          false,
+          `Did not expect ${
+            tab.linkedBrowser.currentURI.spec
+          } to load in a new tab.`
+        );
+        BrowserTestUtils.removeTab(tab);
+        reject(new Error("Page unexpectedly loaded in a new tab."));
+        return;
+      }
+      Assert.ok(
+        true,
+        `${tab.linkedBrowser.currentURI.spec} loaded in a new tab as expected.`
+      );
+      resolve(tab);
+    };
+    win.gBrowser.tabContainer.addEventListener("TabOpen", tabListener);
+
+    // Listens for new top-level windows.
+    const winObserver = async (domwin, topic) => {
+      if (topic != "domwindowopened") {
+        return;
+      }
+
+      cleanup();
+
+      await BrowserTestUtils.waitForEvent(domwin, "load");
+
+      if (where != "window") {
+        Assert.ok(false, `Did not expect a new ${domwin.location} to open.`);
+        await BrowserTestUtils.closeWindow(domwin);
+        reject(new Error("New window unexpectedly opened."));
+        return;
+      }
+      Assert.ok(true, `${domwin.location} opened as expected.`);
+      resolve(domwin);
+    };
+    Services.ww.registerNotification(winObserver);
+
+    BrowserTestUtils.browserLoaded(getBrowser(ssb), true).then(() => {
+      cleanup();
+
+      if (where != "ssb") {
+        Assert.ok(
+          false,
+          `Did not expect ${
+            getBrowser(ssb).currentURI.spec
+          } to load in the ssb window.`
+        );
+        reject(new Error("Page unexpectedly loaded in the ssb window."));
+        return;
+      }
+      Assert.ok(
+        true,
+        `${
+          getBrowser(ssb).currentURI.spec
+        } loaded in the ssb window as expected.`
+      );
+      resolve();
+    }, reject);
+
+    // Makes sure that no notifications fire after the test is done. We assume
+    // that the SSB window will be closed between tests and so don't need to
+    // unregister the load listener for the SSB browser itself.
+    const cleanup = () => {
+      win.gBrowser.tabContainer.removeEventListener("TabOpen", tabListener);
+      Services.ww.unregisterNotification(winObserver);
+    };
+  });
+}
+
+/**
+ * Waits for a load to occur in the ssb window but rejects if a new tab or
+ * window get opened.
+ */
+function expectSSBLoad(ssb, win = window) {
+  return expectLoadSomewhere(ssb, "ssb", win);
+}
+
+/**
+ * Waits for a new tab to be opened and loaded. Rejects if a new window is
+ * opened or the ssb loads something before. Resolves with the new loaded tab.
+ */
+function expectTabLoad(ssb, win = window) {
+  return expectLoadSomewhere(ssb, "tab", win);
+}
+
+/**
+ * Waits for a new window to be opened and loaded. Rejects if a new tab is
+ * opened or the ssb loads something before. Resolves with the new loaded
+ * window.
+ */
+function expectWindowOpen(ssb, win = window) {
+  return expectLoadSomewhere(ssb, "window", win);
+}
--- a/browser/components/ssb/tests/browser/test_page.html
+++ b/browser/components/ssb/tests/browser/test_page.html
@@ -1,6 +1,45 @@
 <!DOCTYPE html>
 
 <html>
+<head>
+<script type="text/javascript">
+function initialize() {
+  let target = window.location.hash;
+  if (target.length < 2) {
+    return;
+  }
+  target = target.substring(1);
+
+  let anchor = document.getElementById("direct");
+  anchor.setAttribute("href", target);
+
+  anchor = document.getElementById("new-tab");
+  anchor.setAttribute("href", target);
+
+  anchor = document.getElementById("new-window");
+  anchor.setAttribute("href", target);
+
+  anchor = document.getElementById("window-open");
+  anchor.onclick = (e) => {
+    window.open(target, "foo", "height=300,width=400");
+    e.preventDefault();
+  };
+
+  anchor = document.getElementById("window-location");
+  anchor.onclick = (e) => {
+    window.location = target;
+    e.preventDefault();
+  };
+}
+
+window.addEventListener("load", initialize, true);
+</script>
+</head>
 <body>
+<p><a id="direct">Direct link</a></p>
+<p><a id="new-tab" target="_blank">New tab link</a></p>
+<p><a id="new-window" target="foo">New window link</a></p>
+<p><a id="window-open" href="#">window.open call</a></p>
+<p><a id="window-location" href="#">window.location manipulation</a></p>
 </body>
 </html>