diff options
Diffstat (limited to 'devtools/client/responsive.html/browser')
-rw-r--r-- | devtools/client/responsive.html/browser/moz.build | 11 | ||||
-rw-r--r-- | devtools/client/responsive.html/browser/swap.js | 309 | ||||
-rw-r--r-- | devtools/client/responsive.html/browser/tunnel.js | 619 | ||||
-rw-r--r-- | devtools/client/responsive.html/browser/web-navigation.js | 179 |
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; |