import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -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',
)

View file

@ -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;

View file

@ -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));
},
};

View file

@ -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;