summaryrefslogtreecommitdiffstats
path: root/devtools/client/responsive.html/browser
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/responsive.html/browser')
-rw-r--r--devtools/client/responsive.html/browser/moz.build11
-rw-r--r--devtools/client/responsive.html/browser/swap.js309
-rw-r--r--devtools/client/responsive.html/browser/tunnel.js619
-rw-r--r--devtools/client/responsive.html/browser/web-navigation.js179
4 files changed, 1118 insertions, 0 deletions
diff --git a/devtools/client/responsive.html/browser/moz.build b/devtools/client/responsive.html/browser/moz.build
new file mode 100644
index 000000000..f99bbc443
--- /dev/null
+++ b/devtools/client/responsive.html/browser/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'swap.js',
+ 'tunnel.js',
+ 'web-navigation.js',
+)
diff --git a/devtools/client/responsive.html/browser/swap.js b/devtools/client/responsive.html/browser/swap.js
new file mode 100644
index 000000000..7ab028065
--- /dev/null
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -0,0 +1,309 @@
+/* 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 { Ci } = require("chrome");
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const { tunnelToInnerBrowser } = require("./tunnel");
+
+/**
+ * Swap page content from an existing tab into a new browser within a container
+ * page. Page state is preserved by using `swapFrameLoaders`, just like when
+ * you move a tab to a new window. This provides a seamless transition for the
+ * user since the page is not reloaded.
+ *
+ * See /devtools/docs/responsive-design-mode.md for a high level overview of how
+ * this is used in RDM. The steps described there are copied into the code
+ * below.
+ *
+ * For additional low level details about swapping browser content,
+ * see /devtools/client/responsive.html/docs/browser-swap.md.
+ *
+ * @param tab
+ * A browser tab with content to be swapped.
+ * @param containerURL
+ * URL to a page that holds an inner browser.
+ * @param getInnerBrowser
+ * Function that returns a Promise to the inner browser within the
+ * container page. It is called with the outer browser that loaded the
+ * container page.
+ */
+function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
+ let gBrowser = tab.ownerDocument.defaultView.gBrowser;
+ let innerBrowser;
+ let tunnel;
+
+ // Dispatch a custom event each time the _viewport content_ is swapped from one browser
+ // to another. DevTools server code uses this to follow the content if there is an
+ // active DevTools connection. While browser.xml does dispatch it's own SwapDocShells
+ // event, this one is easier for DevTools to follow because it's only emitted once per
+ // transition, instead of twice like SwapDocShells.
+ let dispatchDevToolsBrowserSwap = (from, to) => {
+ let CustomEvent = tab.ownerDocument.defaultView.CustomEvent;
+ let event = new CustomEvent("DevTools:BrowserSwap", {
+ detail: to,
+ bubbles: true,
+ });
+ from.dispatchEvent(event);
+ };
+
+ return {
+
+ start: Task.async(function* () {
+ tab.isResponsiveDesignMode = true;
+
+ // Freeze navigation temporarily to avoid "blinking" in the location bar.
+ freezeNavigationState(tab);
+
+ // 1. Create a temporary, hidden tab to load the tool UI.
+ let containerTab = gBrowser.addTab("about:blank", {
+ skipAnimation: true,
+ forceNotRemote: true,
+ });
+ gBrowser.hideTab(containerTab);
+ let containerBrowser = containerTab.linkedBrowser;
+ // Prevent the `containerURL` from ending up in the tab's history.
+ containerBrowser.loadURIWithFlags(containerURL, {
+ flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
+ });
+
+ // Copy tab listener state flags to container tab. Each tab gets its own tab
+ // listener and state flags which cache document loading progress. The state flags
+ // are checked when switching tabs to update the browser UI. The later step of
+ // `swapBrowsersAndCloseOther` will fold the state back into the main tab.
+ let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags;
+ gBrowser._tabListeners.get(containerTab).mStateFlags = stateFlags;
+
+ // 2. Mark the tool tab browser's docshell as active so the viewport frame
+ // is created eagerly and will be ready to swap.
+ // This line is crucial when the tool UI is loaded into a background tab.
+ // Without it, the viewport browser's frame is created lazily, leading to
+ // a multi-second delay before it would be possible to `swapFrameLoaders`.
+ // Even worse than the delay, there appears to be no obvious event fired
+ // after the frame is set lazily, so it's unclear how to know that work
+ // has finished.
+ containerBrowser.docShellIsActive = true;
+
+ // 3. Create the initial viewport inside the tool UI.
+ // The calling application will use container page loaded into the tab to
+ // do whatever it needs to create the inner browser.
+ yield tabLoaded(containerTab);
+ innerBrowser = yield getInnerBrowser(containerBrowser);
+ addXULBrowserDecorations(innerBrowser);
+ if (innerBrowser.isRemoteBrowser != tab.linkedBrowser.isRemoteBrowser) {
+ throw new Error("The inner browser's remoteness must match the " +
+ "original tab.");
+ }
+
+ // 4. Swap tab content from the regular browser tab to the browser within
+ // the viewport in the tool UI, preserving all state via
+ // `gBrowser._swapBrowserDocShells`.
+ dispatchDevToolsBrowserSwap(tab.linkedBrowser, innerBrowser);
+ gBrowser._swapBrowserDocShells(tab, innerBrowser);
+
+ // 5. Force the original browser tab to be non-remote since the tool UI
+ // must be loaded in the parent process, and we're about to swap the
+ // tool UI into this tab.
+ gBrowser.updateBrowserRemoteness(tab.linkedBrowser, false);
+
+ // 6. Swap the tool UI (with viewport showing the content) into the
+ // original browser tab and close the temporary tab used to load the
+ // tool via `swapBrowsersAndCloseOther`.
+ gBrowser.swapBrowsersAndCloseOther(tab, containerTab);
+
+ // 7. Start a tunnel from the tool tab's browser to the viewport browser
+ // so that some browser UI functions, like navigation, are connected to
+ // the content in the viewport, instead of the tool page.
+ tunnel = tunnelToInnerBrowser(tab.linkedBrowser, innerBrowser);
+ yield tunnel.start();
+
+ // Swapping browsers disconnects the find bar UI from the browser.
+ // If the find bar has been initialized, reconnect it.
+ if (gBrowser.isFindBarInitialized(tab)) {
+ let findBar = gBrowser.getFindBar(tab);
+ findBar.browser = tab.linkedBrowser;
+ if (!findBar.hidden) {
+ // Force the find bar to activate again, restoring the search string.
+ findBar.onFindCommand();
+ }
+ }
+
+ // Force the browser UI to match the new state of the tab and browser.
+ thawNavigationState(tab);
+ gBrowser.setTabTitle(tab);
+ gBrowser.updateCurrentBrowser(true);
+ }),
+
+ stop() {
+ // 1. Stop the tunnel between outer and inner browsers.
+ tunnel.stop();
+ tunnel = null;
+
+ // 2. Create a temporary, hidden tab to hold the content.
+ let contentTab = gBrowser.addTab("about:blank", {
+ skipAnimation: true,
+ });
+ gBrowser.hideTab(contentTab);
+ let contentBrowser = contentTab.linkedBrowser;
+
+ // 3. Mark the content tab browser's docshell as active so the frame
+ // is created eagerly and will be ready to swap.
+ contentBrowser.docShellIsActive = true;
+
+ // 4. Swap tab content from the browser within the viewport in the tool UI
+ // to the regular browser tab, preserving all state via
+ // `gBrowser._swapBrowserDocShells`.
+ dispatchDevToolsBrowserSwap(innerBrowser, contentBrowser);
+ gBrowser._swapBrowserDocShells(contentTab, innerBrowser);
+ innerBrowser = null;
+
+ // Copy tab listener state flags to content tab. See similar comment in `start`
+ // above for more details.
+ let stateFlags = gBrowser._tabListeners.get(tab).mStateFlags;
+ gBrowser._tabListeners.get(contentTab).mStateFlags = stateFlags;
+
+ // 5. Force the original browser tab to be remote since web content is
+ // loaded in the child process, and we're about to swap the content
+ // into this tab.
+ gBrowser.updateBrowserRemoteness(tab.linkedBrowser, true);
+
+ // 6. Swap the content into the original browser tab and close the
+ // temporary tab used to hold the content via
+ // `swapBrowsersAndCloseOther`.
+ dispatchDevToolsBrowserSwap(contentBrowser, tab.linkedBrowser);
+ gBrowser.swapBrowsersAndCloseOther(tab, contentTab);
+
+ // Swapping browsers disconnects the find bar UI from the browser.
+ // If the find bar has been initialized, reconnect it.
+ if (gBrowser.isFindBarInitialized(tab)) {
+ let findBar = gBrowser.getFindBar(tab);
+ findBar.browser = tab.linkedBrowser;
+ if (!findBar.hidden) {
+ // Force the find bar to activate again, restoring the search string.
+ findBar.onFindCommand();
+ }
+ }
+
+ gBrowser = null;
+
+ // The focus manager seems to get a little dizzy after all this swapping. If a
+ // content element had been focused inside the viewport before stopping, it will
+ // have lost focus. Activate the frame to restore expected focus.
+ tab.linkedBrowser.frameLoader.activateRemoteFrame();
+
+ delete tab.isResponsiveDesignMode;
+ },
+
+ };
+}
+
+/**
+ * Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
+ * location bar, etc. caused by the containerURL peeking through before the swap is
+ * complete.
+ */
+const NAVIGATION_PROPERTIES = [
+ "currentURI",
+ "contentTitle",
+ "securityUI",
+];
+
+function freezeNavigationState(tab) {
+ // Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
+ // location bar, etc. caused by the containerURL peeking through before the swap is
+ // complete.
+ for (let property of NAVIGATION_PROPERTIES) {
+ let value = tab.linkedBrowser[property];
+ Object.defineProperty(tab.linkedBrowser, property, {
+ get() {
+ return value;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+}
+
+function thawNavigationState(tab) {
+ // Thaw out the properties we froze at the beginning now that the swap is complete.
+ for (let property of NAVIGATION_PROPERTIES) {
+ delete tab.linkedBrowser[property];
+ }
+}
+
+/**
+ * Browser elements that are passed to `gBrowser._swapBrowserDocShells` are
+ * expected to have certain properties that currently exist only on
+ * <xul:browser> elements. In particular, <iframe mozbrowser> elements don't
+ * have them.
+ *
+ * Rather than duplicate the swapping code used by the browser to work around
+ * this, we stub out the missing properties needed for the swap to complete.
+ */
+function addXULBrowserDecorations(browser) {
+ if (browser.isRemoteBrowser == undefined) {
+ Object.defineProperty(browser, "isRemoteBrowser", {
+ get() {
+ return this.getAttribute("remote") == "true";
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ if (browser.messageManager == undefined) {
+ Object.defineProperty(browser, "messageManager", {
+ get() {
+ return this.frameLoader.messageManager;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ if (browser.outerWindowID == undefined) {
+ Object.defineProperty(browser, "outerWindowID", {
+ get() {
+ return browser._outerWindowID;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+
+ // It's not necessary for these to actually do anything. These properties are
+ // swapped between browsers in browser.xml's `swapDocShells`, and then their
+ // `swapBrowser` methods are called, so we define them here for that to work
+ // without errors. During the swap process above, these will move from the
+ // the new inner browser to the original tab's browser (step 4) and then to
+ // the temporary container tab's browser (step 7), which is then closed.
+ if (browser._remoteWebNavigationImpl == undefined) {
+ browser._remoteWebNavigationImpl = {
+ swapBrowser() {},
+ };
+ }
+ if (browser._remoteWebProgressManager == undefined) {
+ browser._remoteWebProgressManager = {
+ swapBrowser() {},
+ };
+ }
+}
+
+function tabLoaded(tab) {
+ let deferred = promise.defer();
+
+ function handle(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", handle, true);
+ deferred.resolve(event);
+ }
+
+ tab.linkedBrowser.addEventListener("load", handle, true);
+ return deferred.promise;
+}
+
+exports.swapToInnerBrowser = swapToInnerBrowser;
diff --git a/devtools/client/responsive.html/browser/tunnel.js b/devtools/client/responsive.html/browser/tunnel.js
new file mode 100644
index 000000000..fdbfe8918
--- /dev/null
+++ b/devtools/client/responsive.html/browser/tunnel.js
@@ -0,0 +1,619 @@
+/* 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 { Ci } = require("chrome");
+const Services = require("Services");
+const { Task } = require("devtools/shared/task");
+const { BrowserElementWebNavigation } = require("./web-navigation");
+const { getStack } = require("devtools/shared/platform/stack");
+
+// A symbol used to hold onto the frame loader from the outer browser while tunneling.
+const FRAME_LOADER = Symbol("devtools/responsive/frame-loader");
+
+function debug(msg) {
+ // console.log(msg);
+}
+
+/**
+ * Properties swapped between browsers by browser.xml's `swapDocShells`. See also the
+ * list at /devtools/client/responsive.html/docs/browser-swap.md.
+ */
+const SWAPPED_BROWSER_STATE = [
+ "_remoteFinder",
+ "_securityUI",
+ "_documentURI",
+ "_documentContentType",
+ "_contentTitle",
+ "_characterSet",
+ "_contentPrincipal",
+ "_imageDocument",
+ "_fullZoom",
+ "_textZoom",
+ "_isSyntheticDocument",
+ "_innerWindowID",
+ "_manifestURI",
+];
+
+/**
+ * This module takes an "outer" <xul:browser> from a browser tab as described by
+ * Firefox's tabbrowser.xml and wires it up to an "inner" <iframe mozbrowser>
+ * browser element containing arbitrary page content of interest.
+ *
+ * The inner <iframe mozbrowser> element is _just_ the page content. It is not
+ * enough to to replace <xul:browser> on its own. <xul:browser> comes along
+ * with lots of associated functionality via XBL bindings defined for such
+ * elements in browser.xml and remote-browser.xml, and the Firefox UI depends on
+ * these various things to make the UI function.
+ *
+ * By mapping various methods, properties, and messages from the outer browser
+ * to the inner browser, we can control the content inside the inner browser
+ * using the standard Firefox UI elements for navigation, reloading, and more.
+ *
+ * The approaches used in this module were chosen to avoid needing changes to
+ * the core browser for this specialized use case. If we start to increase
+ * usage of <iframe mozbrowser> in the core browser, we should avoid this module
+ * and instead refactor things to work with mozbrowser directly.
+ *
+ * For the moment though, this serves as a sufficient path to connect the
+ * Firefox UI to a mozbrowser.
+ *
+ * @param outer
+ * A <xul:browser> from a regular browser tab.
+ * @param inner
+ * A <iframe mozbrowser> containing page content to be wired up to the
+ * primary browser UI via the outer browser.
+ */
+function tunnelToInnerBrowser(outer, inner) {
+ let browserWindow = outer.ownerDocument.defaultView;
+ let gBrowser = browserWindow.gBrowser;
+ let mmTunnel;
+
+ return {
+
+ start: Task.async(function* () {
+ if (outer.isRemoteBrowser) {
+ throw new Error("The outer browser must be non-remote.");
+ }
+ if (!inner.isRemoteBrowser) {
+ throw new Error("The inner browser must be remote.");
+ }
+
+ // Various browser methods access the `frameLoader` property, including:
+ // * `saveBrowser` from contentAreaUtils.js
+ // * `docShellIsActive` from remote-browser.xml
+ // * `hasContentOpener` from remote-browser.xml
+ // * `preserveLayers` from remote-browser.xml
+ // * `receiveMessage` from SessionStore.jsm
+ // In general, these methods are interested in the `frameLoader` for the content,
+ // so we redirect them to the inner browser's `frameLoader`.
+ outer[FRAME_LOADER] = outer.frameLoader;
+ Object.defineProperty(outer, "frameLoader", {
+ get() {
+ let stack = getStack();
+ // One exception is `receiveMessage` from SessionStore.jsm. SessionStore
+ // expects data updates to come in as messages targeted to a <xul:browser>.
+ // In addition, it verifies[1] correctness by checking that the received
+ // message's `targetFrameLoader` property matches the `frameLoader` of the
+ // <xul:browser>. To keep SessionStore functioning as expected, we give it the
+ // outer `frameLoader` as if nothing has changed.
+ // [1]: https://dxr.mozilla.org/mozilla-central/rev/b1b18f25c0ea69d9ee57c4198d577dfcd0129ce1/browser/components/sessionstore/SessionStore.jsm#716
+ if (stack.caller.filename.endsWith("SessionStore.jsm")) {
+ return outer[FRAME_LOADER];
+ }
+ return inner.frameLoader;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // The `outerWindowID` of the content is used by browser actions like view source
+ // and print. They send the ID down to the client to find the right content frame
+ // to act on.
+ Object.defineProperty(outer, "outerWindowID", {
+ get() {
+ return inner.outerWindowID;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // The `permanentKey` property on a <xul:browser> is used to index into various maps
+ // held by the session store. When you swap content around with
+ // `_swapBrowserDocShells`, these keys are also swapped so they follow the content.
+ // This means the key that matches the content is on the inner browser. Since we
+ // want the browser UI to believe the page content is part of the outer browser, we
+ // copy the content's `permanentKey` up to the outer browser.
+ debug("Copy inner permanentKey to outer browser");
+ outer.permanentKey = inner.permanentKey;
+
+ // Replace the outer browser's native messageManager with a message manager tunnel
+ // which we can use to route messages of interest to the inner browser instead.
+ // Note: The _actual_ messageManager accessible from
+ // `browser.frameLoader.messageManager` is not overridable and is left unchanged.
+ // Only the XBL getter `browser.messageManager` is overridden. Browser UI code
+ // always uses this getter instead of `browser.frameLoader.messageManager` directly,
+ // so this has the effect of overriding the message manager for browser UI code.
+ mmTunnel = new MessageManagerTunnel(outer, inner);
+
+ // We are tunneling to an inner browser with a specific remoteness, so it is simpler
+ // for the logic of the browser UI to assume this tab has taken on that remoteness,
+ // even though it's not true. Since the actions the browser UI performs are sent
+ // down to the inner browser by this tunnel, the tab's remoteness effectively is the
+ // remoteness of the inner browser.
+ outer.setAttribute("remote", "true");
+
+ // Clear out any cached state that references the current non-remote XBL binding,
+ // such as form fill controllers. Otherwise they will remain in place and leak the
+ // outer docshell.
+ outer.destroy();
+ // The XBL binding for remote browsers uses the message manager for many actions in
+ // the UI and that works well here, since it gives us one main thing we need to
+ // route to the inner browser (the messages), instead of having to tweak many
+ // different browser properties. It is safe to alter a XBL binding dynamically.
+ // The content within is not reloaded.
+ outer.style.MozBinding = "url(chrome://browser/content/tabbrowser.xml" +
+ "#tabbrowser-remote-browser)";
+
+ // The constructor of the new XBL binding is run asynchronously and there is no
+ // event to signal its completion. Spin an event loop to watch for properties that
+ // are set by the contructor.
+ while (!outer._remoteWebNavigation) {
+ Services.tm.currentThread.processNextEvent(true);
+ }
+
+ // Replace the `webNavigation` object with our own version which tries to use
+ // mozbrowser APIs where possible. This replaces the webNavigation object that the
+ // remote-browser.xml binding creates. We do not care about it's original value
+ // because stop() will remove the remote-browser.xml binding and these will no
+ // longer be used.
+ let webNavigation = new BrowserElementWebNavigation(inner);
+ webNavigation.copyStateFrom(inner._remoteWebNavigationImpl);
+ outer._remoteWebNavigation = webNavigation;
+ outer._remoteWebNavigationImpl = webNavigation;
+
+ // Now that we've flipped to the remote browser XBL binding, add `progressListener`
+ // onto the remote version of `webProgress`. Normally tabbrowser.xml does this step
+ // when it creates a new browser, etc. Since we manually changed the XBL binding
+ // above, it caused a fresh webProgress object to be created which does not have any
+ // listeners added. So, we get the listener that gBrowser is using for the tab and
+ // reattach it here.
+ let tab = gBrowser.getTabForBrowser(outer);
+ let filteredProgressListener = gBrowser._tabFilters.get(tab);
+ outer.webProgress.addProgressListener(filteredProgressListener);
+
+ // Add the inner browser to tabbrowser's WeakMap from browser to tab. This assists
+ // with tabbrowser's processing of some events such as MozLayerTreeReady which
+ // bubble up from the remote content frame and trigger tabbrowser to lookup the tab
+ // associated with the browser that triggered the event.
+ gBrowser._tabForBrowser.set(inner, tab);
+
+ // All of the browser state from content was swapped onto the inner browser. Pull
+ // this state up to the outer browser.
+ for (let property of SWAPPED_BROWSER_STATE) {
+ outer[property] = inner[property];
+ }
+
+ // Expose `PopupNotifications` on the content's owner global.
+ // This is used by PermissionUI.jsm for permission doorhangers.
+ // Note: This pollutes the responsive.html tool UI's global.
+ Object.defineProperty(inner.ownerGlobal, "PopupNotifications", {
+ get() {
+ return outer.ownerGlobal.PopupNotifications;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // Expose `whereToOpenLink` on the content's owner global.
+ // This is used by ContentClick.jsm when opening links in ways other than just
+ // navigating the viewport.
+ // Note: This pollutes the responsive.html tool UI's global.
+ Object.defineProperty(inner.ownerGlobal, "whereToOpenLink", {
+ get() {
+ return outer.ownerGlobal.whereToOpenLink;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // Add mozbrowser event handlers
+ inner.addEventListener("mozbrowseropenwindow", this);
+ }),
+
+ handleEvent(event) {
+ if (event.type != "mozbrowseropenwindow") {
+ return;
+ }
+
+ // Minimal support for <a target/> and window.open() which just ensures we at
+ // least open them somewhere (in a new tab). The following things are ignored:
+ // * Specific target names (everything treated as _blank)
+ // * Window features
+ // * window.opener
+ // These things are deferred for now, since content which does depend on them seems
+ // outside the main focus of RDM.
+ let { detail } = event;
+ event.preventDefault();
+ let uri = Services.io.newURI(detail.url, null, null);
+ // This API is used mainly because it's near the path used for <a target/> with
+ // regular browser tabs (which calls `openURIInFrame`). The more elaborate APIs
+ // that support openers, window features, etc. didn't seem callable from JS and / or
+ // this event doesn't give enough info to use them.
+ browserWindow.browserDOMWindow
+ .openURI(uri, null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
+ Ci.nsIBrowserDOMWindow.OPEN_NEW);
+ },
+
+ stop() {
+ let tab = gBrowser.getTabForBrowser(outer);
+ let filteredProgressListener = gBrowser._tabFilters.get(tab);
+
+ // The browser's state has changed over time while the tunnel was active. Push the
+ // the current state down to the inner browser, so that it follows the content in
+ // case that browser will be swapped elsewhere.
+ for (let property of SWAPPED_BROWSER_STATE) {
+ inner[property] = outer[property];
+ }
+
+ // Remove the inner browser from the WeakMap from browser to tab.
+ gBrowser._tabForBrowser.delete(inner);
+
+ // Remove the progress listener we added manually.
+ outer.webProgress.removeProgressListener(filteredProgressListener);
+
+ // Reset the XBL binding back to the default.
+ outer.destroy();
+ outer.style.MozBinding = "";
+
+ // Reset @remote since this is now back to a regular, non-remote browser
+ outer.setAttribute("remote", "false");
+
+ // Delete browser window properties exposed on content's owner global
+ delete inner.ownerGlobal.PopupNotifications;
+ delete inner.ownerGlobal.whereToOpenLink;
+
+ // Remove mozbrowser event handlers
+ inner.removeEventListener("mozbrowseropenwindow", this);
+
+ mmTunnel.destroy();
+ mmTunnel = null;
+
+ // Reset overridden XBL properties and methods. Deleting the override
+ // means it will fallback to the original XBL binding definitions which
+ // are on the prototype.
+ delete outer.frameLoader;
+ delete outer[FRAME_LOADER];
+ delete outer.outerWindowID;
+
+ // Invalidate outer's permanentKey so that SessionStore stops associating
+ // things that happen to the outer browser with the content inside in the
+ // inner browser.
+ outer.permanentKey = { id: "zombie" };
+
+ browserWindow = null;
+ gBrowser = null;
+ },
+
+ };
+}
+
+exports.tunnelToInnerBrowser = tunnelToInnerBrowser;
+
+/**
+ * This module allows specific messages of interest to be directed from the
+ * outer browser to the inner browser (and vice versa) in a targetted fashion
+ * without having to touch the original code paths that use them.
+ */
+function MessageManagerTunnel(outer, inner) {
+ if (outer.isRemoteBrowser) {
+ throw new Error("The outer browser must be non-remote.");
+ }
+ this.outer = outer;
+ this.inner = inner;
+ this.tunneledMessageNames = new Set();
+ this.init();
+}
+
+MessageManagerTunnel.prototype = {
+
+ /**
+ * Most message manager methods are left alone and are just passed along to
+ * the outer browser's real message manager.
+ */
+ PASS_THROUGH_METHODS: [
+ "killChild",
+ "assertPermission",
+ "assertContainApp",
+ "assertAppHasPermission",
+ "assertAppHasStatus",
+ "removeDelayedFrameScript",
+ "getDelayedFrameScripts",
+ "loadProcessScript",
+ "removeDelayedProcessScript",
+ "getDelayedProcessScripts",
+ "addWeakMessageListener",
+ "removeWeakMessageListener",
+ ],
+
+ /**
+ * The following methods are overridden with special behavior while tunneling.
+ */
+ OVERRIDDEN_METHODS: [
+ "loadFrameScript",
+ "addMessageListener",
+ "removeMessageListener",
+ "sendAsyncMessage",
+ ],
+
+ OUTER_TO_INNER_MESSAGES: [
+ // Messages sent from remote-browser.xml
+ "Browser:PurgeSessionHistory",
+ "InPermitUnload",
+ "PermitUnload",
+ // Messages sent from browser.js
+ "Browser:Reload",
+ // Messages sent from SelectParentHelper.jsm
+ "Forms:DismissedDropDown",
+ "Forms:MouseOut",
+ "Forms:MouseOver",
+ "Forms:SelectDropDownItem",
+ // Messages sent from SessionStore.jsm
+ "SessionStore:flush",
+ ],
+
+ INNER_TO_OUTER_MESSAGES: [
+ // Messages sent to RemoteWebProgress.jsm
+ "Content:LoadURIResult",
+ "Content:LocationChange",
+ "Content:ProgressChange",
+ "Content:SecurityChange",
+ "Content:StateChange",
+ "Content:StatusChange",
+ // Messages sent to remote-browser.xml
+ "DOMTitleChanged",
+ "ImageDocumentLoaded",
+ "Forms:ShowDropDown",
+ "Forms:HideDropDown",
+ "InPermitUnload",
+ "PermitUnload",
+ // Messages sent to tabbrowser.xml
+ "contextmenu",
+ // Messages sent to SelectParentHelper.jsm
+ "Forms:UpdateDropDown",
+ // Messages sent to browser.js
+ "PageVisibility:Hide",
+ "PageVisibility:Show",
+ // Messages sent to SessionStore.jsm
+ "SessionStore:update",
+ // Messages sent to BrowserTestUtils.jsm
+ "browser-test-utils:loadEvent",
+ ],
+
+ OUTER_TO_INNER_MESSAGE_PREFIXES: [
+ // Messages sent from nsContextMenu.js
+ "ContextMenu:",
+ // Messages sent from DevTools
+ "debug:",
+ // Messages sent from findbar.xml
+ "Findbar:",
+ // Messages sent from RemoteFinder.jsm
+ "Finder:",
+ // Messages sent from InlineSpellChecker.jsm
+ "InlineSpellChecker:",
+ // Messages sent from pageinfo.js
+ "PageInfo:",
+ // Messages sent from printUtils.js
+ "Printing:",
+ // Messages sent from browser-social.js
+ "Social:",
+ "PageMetadata:",
+ // Messages sent from viewSourceUtils.js
+ "ViewSource:",
+ ],
+
+ INNER_TO_OUTER_MESSAGE_PREFIXES: [
+ // Messages sent to nsContextMenu.js
+ "ContextMenu:",
+ // Messages sent to DevTools
+ "debug:",
+ // Messages sent to findbar.xml
+ "Findbar:",
+ // Messages sent to RemoteFinder.jsm
+ "Finder:",
+ // Messages sent to pageinfo.js
+ "PageInfo:",
+ // Messages sent to printUtils.js
+ "Printing:",
+ // Messages sent to browser-social.js
+ "Social:",
+ "PageMetadata:",
+ // Messages sent to viewSourceUtils.js
+ "ViewSource:",
+ ],
+
+ OUTER_TO_INNER_FRAME_SCRIPTS: [
+ // DevTools server for OOP frames
+ "resource://devtools/server/child.js"
+ ],
+
+ get outerParentMM() {
+ if (!this.outer[FRAME_LOADER]) {
+ return null;
+ }
+ return this.outer[FRAME_LOADER].messageManager;
+ },
+
+ get outerChildMM() {
+ // This is only possible because we require the outer browser to be
+ // non-remote, so we're able to reach into its window and use the child
+ // side message manager there.
+ let docShell = this.outer[FRAME_LOADER].docShell;
+ return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ },
+
+ get innerParentMM() {
+ if (!this.inner.frameLoader) {
+ return null;
+ }
+ return this.inner.frameLoader.messageManager;
+ },
+
+ init() {
+ for (let method of this.PASS_THROUGH_METHODS) {
+ // Workaround bug 449811 to ensure a fresh binding each time through the loop
+ let _method = method;
+ this[_method] = (...args) => {
+ if (!this.outerParentMM) {
+ return null;
+ }
+ return this.outerParentMM[_method](...args);
+ };
+ }
+
+ for (let name of this.INNER_TO_OUTER_MESSAGES) {
+ this.innerParentMM.addMessageListener(name, this);
+ this.tunneledMessageNames.add(name);
+ }
+
+ Services.obs.addObserver(this, "message-manager-close", false);
+
+ // Replace the outer browser's messageManager with this tunnel
+ Object.defineProperty(this.outer, "messageManager", {
+ value: this,
+ writable: false,
+ configurable: true,
+ enumerable: true,
+ });
+ },
+
+ destroy() {
+ if (this.destroyed) {
+ return;
+ }
+ this.destroyed = true;
+ debug("Destroy tunnel");
+
+ // Watch for the messageManager to close. In most cases, the caller will stop the
+ // tunnel gracefully before this, but when the browser window closes or application
+ // exits, we may not see the high-level close events.
+ Services.obs.removeObserver(this, "message-manager-close");
+
+ // Reset the messageManager. Deleting the override means it will fallback to the
+ // original XBL binding definitions which are on the prototype.
+ delete this.outer.messageManager;
+
+ for (let name of this.tunneledMessageNames) {
+ this.innerParentMM.removeMessageListener(name, this);
+ }
+
+ // Some objects may have cached this tunnel as the messageManager for a frame. To
+ // ensure it keeps working after tunnel close, rewrite the overidden methods as pass
+ // through methods.
+ for (let method of this.OVERRIDDEN_METHODS) {
+ // Workaround bug 449811 to ensure a fresh binding each time through the loop
+ let _method = method;
+ this[_method] = (...args) => {
+ if (!this.outerParentMM) {
+ return null;
+ }
+ return this.outerParentMM[_method](...args);
+ };
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic != "message-manager-close") {
+ return;
+ }
+ if (subject == this.innerParentMM) {
+ debug("Inner messageManager has closed");
+ this.destroy();
+ }
+ if (subject == this.outerParentMM) {
+ debug("Outer messageManager has closed");
+ this.destroy();
+ }
+ },
+
+ loadFrameScript(url, ...args) {
+ debug(`Calling loadFrameScript for ${url}`);
+
+ if (!this.OUTER_TO_INNER_FRAME_SCRIPTS.includes(url)) {
+ debug(`Should load ${url} into inner?`);
+ this.outerParentMM.loadFrameScript(url, ...args);
+ return;
+ }
+
+ debug(`Load ${url} into inner`);
+ this.innerParentMM.loadFrameScript(url, ...args);
+ },
+
+ addMessageListener(name, ...args) {
+ debug(`Calling addMessageListener for ${name}`);
+
+ debug(`Add outer listener for ${name}`);
+ // Add an outer listener, just like a simple pass through
+ this.outerParentMM.addMessageListener(name, ...args);
+
+ // If the message name is part of a prefix we're tunneling, we also need to add the
+ // tunnel as an inner listener.
+ if (this.INNER_TO_OUTER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix))) {
+ debug(`Add inner listener for ${name}`);
+ this.innerParentMM.addMessageListener(name, this);
+ this.tunneledMessageNames.add(name);
+ }
+ },
+
+ removeMessageListener(name, ...args) {
+ debug(`Calling removeMessageListener for ${name}`);
+
+ debug(`Remove outer listener for ${name}`);
+ // Remove an outer listener, just like a simple pass through
+ this.outerParentMM.removeMessageListener(name, ...args);
+
+ // Leave the tunnel as an inner listener for the case of prefix messages to avoid
+ // tracking counts of add calls. The inner listener will get removed on destroy.
+ },
+
+ sendAsyncMessage(name, ...args) {
+ debug(`Calling sendAsyncMessage for ${name}`);
+
+ if (!this._shouldTunnelOuterToInner(name)) {
+ debug(`Should ${name} go to inner?`);
+ this.outerParentMM.sendAsyncMessage(name, ...args);
+ return;
+ }
+
+ debug(`${name} outer -> inner`);
+ this.innerParentMM.sendAsyncMessage(name, ...args);
+ },
+
+ receiveMessage({ name, data, objects, principal, sync }) {
+ if (!this._shouldTunnelInnerToOuter(name)) {
+ debug(`Received unexpected message ${name}`);
+ return undefined;
+ }
+
+ debug(`${name} inner -> outer, sync: ${sync}`);
+ if (sync) {
+ return this.outerChildMM.sendSyncMessage(name, data, objects, principal);
+ }
+ this.outerChildMM.sendAsyncMessage(name, data, objects, principal);
+ return undefined;
+ },
+
+ _shouldTunnelOuterToInner(name) {
+ return this.OUTER_TO_INNER_MESSAGES.includes(name) ||
+ this.OUTER_TO_INNER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix));
+ },
+
+ _shouldTunnelInnerToOuter(name) {
+ return this.INNER_TO_OUTER_MESSAGES.includes(name) ||
+ this.INNER_TO_OUTER_MESSAGE_PREFIXES.some(prefix => name.startsWith(prefix));
+ },
+
+};
diff --git a/devtools/client/responsive.html/browser/web-navigation.js b/devtools/client/responsive.html/browser/web-navigation.js
new file mode 100644
index 000000000..4519df0bd
--- /dev/null
+++ b/devtools/client/responsive.html/browser/web-navigation.js
@@ -0,0 +1,179 @@
+/* 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 { Ci, Cu, Cr } = require("chrome");
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+const Services = require("Services");
+const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
+
+function readInputStreamToString(stream) {
+ return NetUtil.readInputStreamToString(stream, stream.available());
+}
+
+/**
+ * This object aims to provide the nsIWebNavigation interface for mozbrowser elements.
+ * nsIWebNavigation is one of the interfaces expected on <xul:browser>s, so this wrapper
+ * helps mozbrowser elements support this.
+ *
+ * It attempts to use the mozbrowser API wherever possible, however some methods don't
+ * exist yet, so we fallback to the WebNavigation frame script messages in those cases.
+ * Ideally the mozbrowser API would eventually be extended to cover all properties and
+ * methods used here.
+ *
+ * This is largely copied from RemoteWebNavigation.js, which uses the message manager to
+ * perform all actions.
+ */
+function BrowserElementWebNavigation(browser) {
+ this._browser = browser;
+}
+
+BrowserElementWebNavigation.prototype = {
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebNavigation,
+ Ci.nsISupports
+ ]),
+
+ get _mm() {
+ return this._browser.frameLoader.messageManager;
+ },
+
+ canGoBack: false,
+ canGoForward: false,
+
+ goBack() {
+ this._browser.goBack();
+ },
+
+ goForward() {
+ this._browser.goForward();
+ },
+
+ gotoIndex(index) {
+ // No equivalent in the current BrowserElement API
+ this._sendMessage("WebNavigation:GotoIndex", { index });
+ },
+
+ loadURI(uri, flags, referrer, postData, headers) {
+ // No equivalent in the current BrowserElement API
+ this.loadURIWithOptions(uri, flags, referrer,
+ Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
+ postData, headers, null);
+ },
+
+ loadURIWithOptions(uri, flags, referrer, referrerPolicy, postData, headers,
+ baseURI) {
+ // No equivalent in the current BrowserElement API
+ this._sendMessage("WebNavigation:LoadURI", {
+ uri,
+ flags,
+ referrer: referrer ? referrer.spec : null,
+ referrerPolicy: referrerPolicy,
+ postData: postData ? readInputStreamToString(postData) : null,
+ headers: headers ? readInputStreamToString(headers) : null,
+ baseURI: baseURI ? baseURI.spec : null,
+ });
+ },
+
+ setOriginAttributesBeforeLoading(originAttributes) {
+ // No equivalent in the current BrowserElement API
+ this._sendMessage("WebNavigation:SetOriginAttributes", {
+ originAttributes,
+ });
+ },
+
+ reload(flags) {
+ let hardReload = false;
+ if (flags & this.LOAD_FLAGS_BYPASS_PROXY ||
+ flags & this.LOAD_FLAGS_BYPASS_CACHE) {
+ hardReload = true;
+ }
+ this._browser.reload(hardReload);
+ },
+
+ stop(flags) {
+ // No equivalent in the current BrowserElement API
+ this._sendMessage("WebNavigation:Stop", { flags });
+ },
+
+ get document() {
+ return this._browser.contentDocument;
+ },
+
+ _currentURI: null,
+ get currentURI() {
+ if (!this._currentURI) {
+ this._currentURI = Services.io.newURI("about:blank", null, null);
+ }
+ return this._currentURI;
+ },
+ set currentURI(uri) {
+ this._browser.src = uri.spec;
+ },
+
+ referringURI: null,
+
+ // Bug 1233803 - accessing the sessionHistory of remote browsers should be
+ // done in content scripts.
+ get sessionHistory() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ set sessionHistory(value) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ _sendMessage(message, data) {
+ try {
+ this._mm.sendAsyncMessage(message, data);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ swapBrowser(browser) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ copyStateFrom(otherWebNavigation) {
+ const state = [
+ "canGoBack",
+ "canGoForward",
+ "_currentURI",
+ ];
+ for (let property of state) {
+ this[property] = otherWebNavigation[property];
+ }
+ },
+
+};
+
+const FLAGS = [
+ "LOAD_FLAGS_MASK",
+ "LOAD_FLAGS_NONE",
+ "LOAD_FLAGS_IS_REFRESH",
+ "LOAD_FLAGS_IS_LINK",
+ "LOAD_FLAGS_BYPASS_HISTORY",
+ "LOAD_FLAGS_REPLACE_HISTORY",
+ "LOAD_FLAGS_BYPASS_CACHE",
+ "LOAD_FLAGS_BYPASS_PROXY",
+ "LOAD_FLAGS_CHARSET_CHANGE",
+ "LOAD_FLAGS_STOP_CONTENT",
+ "LOAD_FLAGS_FROM_EXTERNAL",
+ "LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP",
+ "LOAD_FLAGS_FIRST_LOAD",
+ "LOAD_FLAGS_ALLOW_POPUPS",
+ "LOAD_FLAGS_BYPASS_CLASSIFIER",
+ "LOAD_FLAGS_FORCE_ALLOW_COOKIES",
+ "STOP_NETWORK",
+ "STOP_CONTENT",
+ "STOP_ALL",
+];
+
+for (let flag of FLAGS) {
+ BrowserElementWebNavigation.prototype[flag] = Ci.nsIWebNavigation[flag];
+}
+
+exports.BrowserElementWebNavigation = BrowserElementWebNavigation;