зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1585084 - Remove browser frame scripts that were used by old-RDM. r=gl
Depends on D82559 Differential Revision: https://phabricator.services.mozilla.com/D82563
This commit is contained in:
Родитель
70e2aea069
Коммит
07337fae67
|
@ -1,229 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
/* global content, docShell, addEventListener, addMessageListener,
|
||||
removeEventListener, removeMessageListener, sendAsyncMessage, Services */
|
||||
|
||||
var global = this;
|
||||
|
||||
// Guard against loading this frame script mutiple times
|
||||
(function() {
|
||||
if (global.responsiveFrameScriptLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gDeviceSizeWasPageSize = docShell.deviceSizeIsPageSize;
|
||||
const gFloatingScrollbarsStylesheet = Services.io.newURI(
|
||||
"chrome://devtools/skin/floating-scrollbars-responsive-design.css"
|
||||
);
|
||||
|
||||
let requiresFloatingScrollbars;
|
||||
let active = false;
|
||||
let resizeNotifications = false;
|
||||
|
||||
addMessageListener("ResponsiveMode:Start", startResponsiveMode);
|
||||
addMessageListener("ResponsiveMode:Stop", stopResponsiveMode);
|
||||
addMessageListener("ResponsiveMode:IsActive", isActive);
|
||||
|
||||
function debug(msg) {
|
||||
// dump(`RDM CHILD: ${msg}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by tests to verify the state of responsive mode.
|
||||
*/
|
||||
function isActive() {
|
||||
sendAsyncMessage("ResponsiveMode:IsActive:Done", { active });
|
||||
}
|
||||
|
||||
function startResponsiveMode({ data }) {
|
||||
debug("START");
|
||||
if (active) {
|
||||
debug("ALREADY STARTED");
|
||||
sendAsyncMessage("ResponsiveMode:Start:Done");
|
||||
return;
|
||||
}
|
||||
addMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
|
||||
const webProgress = docShell
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebProgress);
|
||||
webProgress.addProgressListener(
|
||||
WebProgressListener,
|
||||
Ci.nsIWebProgress.NOTIFY_ALL
|
||||
);
|
||||
docShell.deviceSizeIsPageSize = true;
|
||||
requiresFloatingScrollbars = data.requiresFloatingScrollbars;
|
||||
if (data.notifyOnResize) {
|
||||
startOnResize();
|
||||
}
|
||||
|
||||
// At this point, a content viewer might not be loaded for this
|
||||
// docshell. makeScrollbarsFloating will be triggered by onLocationChange.
|
||||
if (docShell.contentViewer) {
|
||||
makeScrollbarsFloating();
|
||||
}
|
||||
active = true;
|
||||
sendAsyncMessage("ResponsiveMode:Start:Done");
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
// Send both a content-resize event and a viewport-resize event, since both
|
||||
// may have changed.
|
||||
let { width, height } = content.screen;
|
||||
debug(`EMIT CONTENTRESIZE: ${width} x ${height}`);
|
||||
sendAsyncMessage("ResponsiveMode:OnContentResize", {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
width = content.innerWidth;
|
||||
height = content.innerHeight;
|
||||
debug(`EMIT RESIZEVIEWPORT: ${width} x ${height}`);
|
||||
sendAsyncMessage("ResponsiveMode:OnResizeViewport", {
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
function bindOnResize() {
|
||||
content.addEventListener("resize", onResize);
|
||||
}
|
||||
|
||||
function startOnResize() {
|
||||
debug("START ON RESIZE");
|
||||
if (resizeNotifications) {
|
||||
return;
|
||||
}
|
||||
resizeNotifications = true;
|
||||
bindOnResize();
|
||||
addEventListener("DOMWindowCreated", bindOnResize, false);
|
||||
}
|
||||
|
||||
function stopOnResize() {
|
||||
debug("STOP ON RESIZE");
|
||||
if (!resizeNotifications) {
|
||||
return;
|
||||
}
|
||||
resizeNotifications = false;
|
||||
content.removeEventListener("resize", onResize);
|
||||
removeEventListener("DOMWindowCreated", bindOnResize, false);
|
||||
}
|
||||
|
||||
function stopResponsiveMode() {
|
||||
debug("STOP");
|
||||
if (!active) {
|
||||
debug("ALREADY STOPPED, ABORT");
|
||||
return;
|
||||
}
|
||||
active = false;
|
||||
removeMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
|
||||
const webProgress = docShell
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebProgress);
|
||||
webProgress.removeProgressListener(WebProgressListener);
|
||||
docShell.deviceSizeIsPageSize = gDeviceSizeWasPageSize;
|
||||
restoreScrollbars();
|
||||
stopOnResize();
|
||||
sendAsyncMessage("ResponsiveMode:Stop:Done");
|
||||
}
|
||||
|
||||
function makeScrollbarsFloating() {
|
||||
if (!requiresFloatingScrollbars) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allDocShells = [docShell];
|
||||
|
||||
for (let i = 0; i < docShell.childCount; i++) {
|
||||
const child = docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell);
|
||||
allDocShells.push(child);
|
||||
}
|
||||
|
||||
for (const d of allDocShells) {
|
||||
const win = d.contentViewer.DOMDocument.defaultView;
|
||||
const winUtils = win.windowUtils;
|
||||
try {
|
||||
winUtils.loadSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
flushStyle();
|
||||
}
|
||||
|
||||
function restoreScrollbars() {
|
||||
const allDocShells = [docShell];
|
||||
for (let i = 0; i < docShell.childCount; i++) {
|
||||
allDocShells.push(docShell.getChildAt(i).QueryInterface(Ci.nsIDocShell));
|
||||
}
|
||||
for (const d of allDocShells) {
|
||||
const win = d.contentViewer.DOMDocument.defaultView;
|
||||
const winUtils = win.windowUtils;
|
||||
try {
|
||||
winUtils.removeSheet(gFloatingScrollbarsStylesheet, win.AGENT_SHEET);
|
||||
} catch (e) {}
|
||||
}
|
||||
flushStyle();
|
||||
}
|
||||
|
||||
function flushStyle() {
|
||||
// Force presContext destruction
|
||||
const isSticky = docShell.contentViewer.sticky;
|
||||
docShell.contentViewer.sticky = false;
|
||||
docShell.contentViewer.hide();
|
||||
docShell.contentViewer.show();
|
||||
docShell.contentViewer.sticky = isSticky;
|
||||
}
|
||||
|
||||
function screenshot() {
|
||||
const canvas = content.document.createElementNS(
|
||||
"http://www.w3.org/1999/xhtml",
|
||||
"canvas"
|
||||
);
|
||||
const ratio = content.devicePixelRatio;
|
||||
const width = content.innerWidth * ratio;
|
||||
const height = content.innerHeight * ratio;
|
||||
canvas.mozOpaque = true;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.scale(ratio, ratio);
|
||||
ctx.drawWindow(
|
||||
content,
|
||||
content.scrollX,
|
||||
content.scrollY,
|
||||
width,
|
||||
height,
|
||||
"#fff"
|
||||
);
|
||||
sendAsyncMessage(
|
||||
"ResponsiveMode:RequestScreenshot:Done",
|
||||
canvas.toDataURL()
|
||||
);
|
||||
}
|
||||
|
||||
const WebProgressListener = {
|
||||
onLocationChange(webProgress, request, URI, flags) {
|
||||
if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
|
||||
return;
|
||||
}
|
||||
// Notify the Responsive UI manager to set orientation state on a location change.
|
||||
// This is necessary since we want to ensure that the RDM Document's orientation
|
||||
// state persists throughout while RDM is opened.
|
||||
sendAsyncMessage("ResponsiveMode:OnLocationChange", {
|
||||
width: content.innerWidth,
|
||||
height: content.innerHeight,
|
||||
});
|
||||
makeScrollbarsFloating();
|
||||
},
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
"nsIWebProgressListener",
|
||||
"nsISupportsWeakReference",
|
||||
]),
|
||||
};
|
||||
})();
|
||||
|
||||
global.responsiveFrameScriptLoaded = true;
|
||||
sendAsyncMessage("ResponsiveMode:ChildScriptReady");
|
|
@ -1,12 +0,0 @@
|
|||
# -*- 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(
|
||||
'content.js',
|
||||
'swap.js',
|
||||
'tunnel.js',
|
||||
'web-navigation.js',
|
||||
)
|
|
@ -1,484 +0,0 @@
|
|||
/* 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 { E10SUtils } = require("resource://gre/modules/E10SUtils.jsm");
|
||||
const {
|
||||
tunnelToInnerBrowser,
|
||||
} = require("devtools/client/responsive/browser/tunnel");
|
||||
|
||||
function debug(msg) {
|
||||
// console.log(`RDM swap: ${msg}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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 browserWindow = tab.ownerGlobal;
|
||||
let gBrowser = browserWindow.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.js 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.
|
||||
const dispatchDevToolsBrowserSwap = (from, to) => {
|
||||
const CustomEvent = browserWindow.CustomEvent;
|
||||
const event = new CustomEvent("DevTools:BrowserSwap", {
|
||||
detail: to,
|
||||
bubbles: true,
|
||||
});
|
||||
from.dispatchEvent(event);
|
||||
};
|
||||
|
||||
// A version of `gBrowser.addTab` that absorbs the `TabOpen` event.
|
||||
// The swap process uses a temporary tab, and there's no real need for others to hear
|
||||
// about it. This hides the temporary tab from things like WebExtensions.
|
||||
const addTabSilently = (uri, options) => {
|
||||
browserWindow.addEventListener(
|
||||
"TabOpen",
|
||||
event => {
|
||||
event.stopImmediatePropagation();
|
||||
},
|
||||
{ capture: true, once: true }
|
||||
);
|
||||
options.triggeringPrincipal = Services.scriptSecurityManager.createNullPrincipal(
|
||||
{
|
||||
userContextId: options.userContextId,
|
||||
}
|
||||
);
|
||||
return gBrowser.addWebTab(uri, options);
|
||||
};
|
||||
|
||||
// A version of `gBrowser.swapBrowsersAndCloseOther` that absorbs the `TabClose` event.
|
||||
// The swap process uses a temporary tab, and there's no real need for others to hear
|
||||
// about it. This hides the temporary tab from things like WebExtensions.
|
||||
const swapBrowsersAndCloseOtherSilently = (ourTab, otherTab) => {
|
||||
browserWindow.addEventListener(
|
||||
"TabClose",
|
||||
event => {
|
||||
event.stopImmediatePropagation();
|
||||
},
|
||||
{ capture: true, once: true }
|
||||
);
|
||||
gBrowser.swapBrowsersAndCloseOther(ourTab, otherTab);
|
||||
};
|
||||
|
||||
// It is possible for the frame loader swap within `gBrowser._swapBrowserDocShells` to
|
||||
// fail when various frame state is either not ready yet or doesn't match between the
|
||||
// two browsers you're trying to swap. However, such errors are currently caught and
|
||||
// silenced in the browser, because they are apparently expected in certain cases.
|
||||
// So, here we do our own check to verify that the swap actually did in fact take place,
|
||||
// making it much easier to track such errors when they happen.
|
||||
const swapBrowserDocShells = (ourTab, otherBrowser) => {
|
||||
// The verification step here assumes both browsers are remote.
|
||||
if (
|
||||
!ourTab.linkedBrowser.isRemoteBrowser ||
|
||||
!otherBrowser.isRemoteBrowser
|
||||
) {
|
||||
throw new Error("Both browsers should be remote before swapping.");
|
||||
}
|
||||
const contentTabId = ourTab.linkedBrowser.frameLoader.remoteTab.tabId;
|
||||
gBrowser._swapBrowserDocShells(ourTab, otherBrowser);
|
||||
if (otherBrowser.frameLoader.remoteTab.tabId != contentTabId) {
|
||||
// Bug 1408602: Try to unwind to save tab content from being lost.
|
||||
throw new Error("Swapping tab content between browsers failed.");
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for a browser to load into a new frame loader.
|
||||
function loadURIWithNewFrameLoader(browser, uri, options) {
|
||||
return new Promise(resolve => {
|
||||
gBrowser.addEventListener("XULFrameLoaderCreated", resolve, {
|
||||
once: true,
|
||||
});
|
||||
browser.loadURI(uri, options);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
async start() {
|
||||
// In some cases, such as a preloaded browser used for about:newtab, browser code
|
||||
// will force a new frameloader on next navigation to remote content to ensure
|
||||
// balanced process assignment. If this case will happen here, navigate to
|
||||
// about:blank first to get this out of way so that we stay within one process while
|
||||
// RDM is open. Some process selection rules are specific to remote content, so we
|
||||
// use `http://example.com` as a test for what a remote navigation would cause.
|
||||
const {
|
||||
requiredRemoteType,
|
||||
mustChangeProcess,
|
||||
newFrameloader,
|
||||
} = E10SUtils.shouldLoadURIInBrowser(
|
||||
tab.linkedBrowser,
|
||||
"http://example.com"
|
||||
);
|
||||
if (newFrameloader) {
|
||||
debug(
|
||||
`Tab will force a new frameloader on navigation, load about:blank first`
|
||||
);
|
||||
await loadURIWithNewFrameLoader(tab.linkedBrowser, "about:blank", {
|
||||
flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
|
||||
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
|
||||
{}
|
||||
),
|
||||
});
|
||||
}
|
||||
// When the separate privileged content process is enabled, about:home and
|
||||
// about:newtab will load in it, and we'll need to switch away if the user
|
||||
// ever browses to a new URL. To avoid that, when the privileged process is
|
||||
// enabled, we do the process flip immediately before entering RDM mode. The
|
||||
// trade-off is that about:newtab can't be inspected in RDM, but it allows
|
||||
// users to start RDM on that page and keep it open.
|
||||
//
|
||||
// The other trade is that sometimes users will be viewing the local file
|
||||
// URI process, and will want to view the page in RDM. We allow this without
|
||||
// blanking out the page, but we trade that for closing RDM if browsing ever
|
||||
// causes them to flip processes.
|
||||
//
|
||||
// Bug 1510806 has been filed to fix this properly, by making RDM resilient
|
||||
// to process flips.
|
||||
if (
|
||||
mustChangeProcess &&
|
||||
tab.linkedBrowser.remoteType == "privilegedabout"
|
||||
) {
|
||||
debug(
|
||||
`Tab must flip away from the privileged content process ` +
|
||||
`on navigation`
|
||||
);
|
||||
gBrowser.updateBrowserRemoteness(tab.linkedBrowser, {
|
||||
remoteType: requiredRemoteType,
|
||||
});
|
||||
}
|
||||
|
||||
tab.isResponsiveDesignMode = true;
|
||||
|
||||
// Hide the browser content temporarily while things move around to avoid displaying
|
||||
// strange intermediate states.
|
||||
tab.linkedBrowser.style.visibility = "hidden";
|
||||
|
||||
// Freeze navigation temporarily to avoid "blinking" in the location bar.
|
||||
freezeNavigationState(tab);
|
||||
|
||||
// 1. Create a temporary, hidden tab to load the tool UI.
|
||||
debug("Add blank tool tab");
|
||||
const containerTab = addTabSilently("about:blank", {
|
||||
skipAnimation: true,
|
||||
forceNotRemote: true,
|
||||
userContextId: tab.userContextId,
|
||||
});
|
||||
gBrowser.hideTab(containerTab);
|
||||
const containerBrowser = containerTab.linkedBrowser;
|
||||
// Even though we load the `containerURL` with `LOAD_FLAGS_BYPASS_HISTORY` below,
|
||||
// `SessionHistory.jsm` has a fallback path for tabs with no history which
|
||||
// fabricates a history entry by reading the current URL, and this can cause the
|
||||
// container URL to be recorded in the session store. To avoid this, we send a
|
||||
// bogus `epoch` value to our container tab, which causes all future history
|
||||
// messages to be ignored. (Actual navigations are still correctly recorded because
|
||||
// this only affects the container frame, not the content.) A better fix would be
|
||||
// to just not load the `content-sessionStore.js` frame script at all in the
|
||||
// container tab, but it's loaded for all tab browsers, so this seems a bit harder
|
||||
// to achieve in a nice way.
|
||||
containerBrowser.messageManager.sendAsyncMessage("SessionStore:flush", {
|
||||
epoch: -1,
|
||||
});
|
||||
// Prevent the `containerURL` from ending up in the tab's history.
|
||||
debug("Load container URL");
|
||||
containerBrowser.loadURI(containerURL, {
|
||||
flags: Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
|
||||
// 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.
|
||||
const 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.
|
||||
debug("Set container docShell active");
|
||||
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.
|
||||
debug("Wait until container tab loaded");
|
||||
await tabLoaded(containerTab);
|
||||
debug("Wait until inner browser available");
|
||||
innerBrowser = await getInnerBrowser(containerBrowser);
|
||||
|
||||
Object.defineProperty(innerBrowser, "outerBrowser", {
|
||||
get() {
|
||||
return tab.linkedBrowser;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
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);
|
||||
debug("Swap content to inner browser");
|
||||
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.
|
||||
debug("Flip original tab to remote false");
|
||||
gBrowser.updateBrowserRemoteness(tab.linkedBrowser, {
|
||||
remoteType: E10SUtils.NOT_REMOTE,
|
||||
});
|
||||
|
||||
// 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`.
|
||||
debug("Swap tool UI to original tab");
|
||||
swapBrowsersAndCloseOtherSilently(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);
|
||||
debug("Wait until tunnel start");
|
||||
await 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)) {
|
||||
const findBar = gBrowser.getCachedFindBar(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);
|
||||
|
||||
// Show the browser content again now that the move is done.
|
||||
tab.linkedBrowser.style.visibility = "";
|
||||
debug("Exit swap start");
|
||||
},
|
||||
|
||||
stop() {
|
||||
// Hide the browser content temporarily while things move around to avoid displaying
|
||||
// strange intermediate states.
|
||||
tab.linkedBrowser.style.visibility = "hidden";
|
||||
|
||||
// 1. Stop the tunnel between outer and inner browsers.
|
||||
tunnel.stop();
|
||||
tunnel = null;
|
||||
|
||||
// 2. Create a temporary, hidden tab to hold the content.
|
||||
const contentTab = addTabSilently("about:blank", {
|
||||
skipAnimation: true,
|
||||
userContextId: tab.userContextId,
|
||||
});
|
||||
gBrowser.hideTab(contentTab);
|
||||
const 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);
|
||||
swapBrowserDocShells(contentTab, innerBrowser);
|
||||
innerBrowser = null;
|
||||
|
||||
// Copy tab listener state flags to content tab. See similar comment in `start`
|
||||
// above for more details.
|
||||
const 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, {
|
||||
remoteType: contentBrowser.remoteType,
|
||||
});
|
||||
|
||||
// 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);
|
||||
swapBrowsersAndCloseOtherSilently(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)) {
|
||||
const findBar = gBrowser.getCachedFindBar(tab);
|
||||
findBar.browser = tab.linkedBrowser;
|
||||
if (!findBar.hidden) {
|
||||
// Force the find bar to activate again, restoring the search string.
|
||||
findBar.onFindCommand();
|
||||
}
|
||||
}
|
||||
|
||||
gBrowser = null;
|
||||
browserWindow = 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;
|
||||
|
||||
// Show the browser content again now that the move is done.
|
||||
tab.linkedBrowser.style.visibility = "";
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (const property of NAVIGATION_PROPERTIES) {
|
||||
const 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 (const 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.remoteType == undefined) {
|
||||
Object.defineProperty(browser, "remoteType", {
|
||||
get() {
|
||||
return this.messageManager.remoteType;
|
||||
},
|
||||
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.js'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._remoteWebNavigation == undefined) {
|
||||
browser._remoteWebNavigation = {
|
||||
swapBrowser() {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function tabLoaded(tab) {
|
||||
return new Promise(resolve => {
|
||||
function handle(event) {
|
||||
if (
|
||||
event.originalTarget != tab.linkedBrowser.contentDocument ||
|
||||
event.target.location.href == "about:blank"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
tab.linkedBrowser.removeEventListener("load", handle, true);
|
||||
resolve(event);
|
||||
}
|
||||
|
||||
tab.linkedBrowser.addEventListener("load", handle, true);
|
||||
});
|
||||
}
|
||||
|
||||
exports.swapToInnerBrowser = swapToInnerBrowser;
|
|
@ -1,708 +0,0 @@
|
|||
/* 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 } = require("chrome");
|
||||
const ChromeUtils = require("ChromeUtils");
|
||||
const Services = require("Services");
|
||||
const {
|
||||
BrowserElementWebNavigation,
|
||||
} = require("devtools/client/responsive/browser/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");
|
||||
// Export for use in tests.
|
||||
exports.OUTER_FRAME_LOADER_SYMBOL = FRAME_LOADER;
|
||||
|
||||
function debug(msg) {
|
||||
// console.log(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties swapped between browsers by browser.js's `swapDocShells`.
|
||||
*/
|
||||
const SWAPPED_BROWSER_STATE = [
|
||||
"_remoteFinder",
|
||||
"_securityUI",
|
||||
"_documentURI",
|
||||
"_documentContentType",
|
||||
"_characterSet",
|
||||
"_contentPrincipal",
|
||||
"_isSyntheticDocument",
|
||||
];
|
||||
|
||||
/**
|
||||
* Various parts of the Firefox code base expect to access properties on the browser
|
||||
* window in response to events (by reaching for the window via the event's target).
|
||||
*
|
||||
* When RDM is enabled, these bits of code instead reach the RDM tool's window instead of
|
||||
* the browser window, which won't have the properties they are looking for. At the
|
||||
* moment, we address this by exposing them from the browser window on RDM's window as
|
||||
* needed.
|
||||
*/
|
||||
const PROPERTIES_FROM_BROWSER_WINDOW = [
|
||||
// This is used by PermissionUI.jsm for permission doorhangers.
|
||||
"PopupNotifications",
|
||||
// This is used by various event handlers, typically to call `getTabForBrowser` to map
|
||||
// a browser back to a tab.
|
||||
"gBrowser",
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 a Custom Element defined for such
|
||||
* elements in browser.js, 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;
|
||||
|
||||
// Mirror the state updates from the outer <xul:browser> to the inner
|
||||
// <iframe mozbrowser>.
|
||||
const mirroringProgressListener = {
|
||||
onStateChange: (webProgress, request, stateFlags, status) => {
|
||||
if (webProgress?.isTopLevel) {
|
||||
inner._characterSet = outer._characterSet;
|
||||
inner._documentURI = outer._documentURI;
|
||||
inner._documentContentType = outer._documentContentType;
|
||||
}
|
||||
},
|
||||
|
||||
onLocationChange: (webProgress, request, location, flags) => {
|
||||
if (webProgress?.isTopLevel) {
|
||||
inner._documentURI = outer._documentURI;
|
||||
inner._documentContentType = outer._documentContentType;
|
||||
inner._characterSet = outer._characterSet;
|
||||
inner._isSyntheticDocument = outer._isSyntheticDocument;
|
||||
inner._remoteWebNavigation._currentURI =
|
||||
outer._remoteWebNavigation._currentURI;
|
||||
// mozbrowser elements do not support the `contentPrincipal` property.
|
||||
// Because of this, we copy the outer browser's (xul:browser)
|
||||
// `contentPrincipal` here. We need to do this because some event
|
||||
// listeners on the browser tab try to access this property
|
||||
// directly off the browser element.
|
||||
inner.contentPrincipal = outer.contentPrincipal;
|
||||
}
|
||||
},
|
||||
|
||||
// We do not need an onSecurityChange handler since the remote security UI
|
||||
// has been copied from the inner (remote) browser to the outer (non-remote)
|
||||
// browser and they share it.
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsIWebProgressListener,
|
||||
]),
|
||||
};
|
||||
|
||||
return {
|
||||
async start() {
|
||||
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 browser.js
|
||||
// * `preserveLayers` from browser.js
|
||||
// * `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() {
|
||||
const 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 Custom Element getter `browser.messageManager` is overridden. This
|
||||
// getter is always used 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);
|
||||
|
||||
// Clear out any cached state that references the Custom Element's non-remote state,
|
||||
// such as form fill controllers. Otherwise they will remain in place and leak the
|
||||
// outer docshell.
|
||||
outer.destroy();
|
||||
|
||||
// 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.
|
||||
// By setting this attribute and then forcibly reinitializing the binding state,
|
||||
// we start using the remote browser message manager which is used for many actions
|
||||
// in the UI. This 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.
|
||||
// The content within is not reloaded.
|
||||
outer.setAttribute("remote", "true");
|
||||
outer.construct();
|
||||
|
||||
Object.defineProperty(outer, "remoteType", {
|
||||
get() {
|
||||
return inner.remoteType;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
// Verify that we indeed have the correct binding.
|
||||
if (!outer.isRemoteBrowser) {
|
||||
throw new Error("Browser failed to switch to remote browser binding");
|
||||
}
|
||||
|
||||
// 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 binding creates. We do not care about it's original value
|
||||
// because stop() will remove the browser binding and these will no longer bee
|
||||
// used.
|
||||
const webNavigation = new BrowserElementWebNavigation(inner);
|
||||
webNavigation.copyStateFrom(inner._remoteWebNavigation);
|
||||
outer._remoteWebNavigation = webNavigation;
|
||||
|
||||
// Now that we've flipped to the remote browser mode, 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 mode
|
||||
// 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.
|
||||
const tab = gBrowser.getTabForBrowser(outer);
|
||||
const filteredProgressListener = gBrowser._tabFilters.get(tab);
|
||||
outer.webProgress.addProgressListener(
|
||||
filteredProgressListener,
|
||||
Ci.nsIWebProgress.NOTIFY_ALL
|
||||
);
|
||||
outer.webProgress.addProgressListener(
|
||||
mirroringProgressListener,
|
||||
Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION
|
||||
);
|
||||
|
||||
// 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 (const property of SWAPPED_BROWSER_STATE) {
|
||||
outer[property] = inner[property];
|
||||
}
|
||||
|
||||
// Expose various properties from the browser window on the RDM tool's global. This
|
||||
// aids various bits of code that expect to find a browser window, such as event
|
||||
// handlers that reach for the window via the event's target.
|
||||
for (const property of PROPERTIES_FROM_BROWSER_WINDOW) {
|
||||
Object.defineProperty(inner.ownerGlobal, property, {
|
||||
get() {
|
||||
return outer.ownerGlobal[property];
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Add mozbrowser event handlers
|
||||
inner.addEventListener("mozbrowseropenwindow", this);
|
||||
inner.addEventListener("mozbrowsershowmodalprompt", this);
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "mozbrowseropenwindow":
|
||||
this.handleOpenWindowEvent(event);
|
||||
break;
|
||||
case "mozbrowsershowmodalprompt":
|
||||
this.handleModalPromptEvent(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
handleOpenWindowEvent(event) {
|
||||
// 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.
|
||||
const { detail } = event;
|
||||
event.preventDefault();
|
||||
const uri = Services.io.newURI(detail.url);
|
||||
let flags = Ci.nsIBrowserDOMWindow.OPEN_NEWTAB;
|
||||
if (detail.forceNoReferrer) {
|
||||
flags |= Ci.nsIBrowserDOMWindow.OPEN_NO_REFERRER;
|
||||
}
|
||||
// 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,
|
||||
flags,
|
||||
Ci.nsIBrowserDOMWindow.OPEN_NEW,
|
||||
outer.contentPrincipal
|
||||
);
|
||||
},
|
||||
|
||||
handleModalPromptEvent({ detail }) {
|
||||
// Relay window.alert(), window.prompt() and window.confirm() dialogs through the
|
||||
// outer window and make sure the return value is passed back to the inner window.
|
||||
// When this event handler is called, the inner iframe is spinning in a nested event
|
||||
// loop waiting to be unblocked.
|
||||
// If we were calling preventDefault() here, then we would have to call
|
||||
// detail.unblock() to unblock the inner iframe.
|
||||
// But since we aren't the inner iframe will be unblocked automatically as soon as
|
||||
// the mozbrowsershowmodalprompt event is done dispatching (i.e. as soon as this
|
||||
// handler completes).
|
||||
// See _handleShowModelPrompt in /dom/browser-element/BrowserElementParent.js
|
||||
|
||||
if (!["alert", "prompt", "confirm"].includes(detail.promptType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promptFunction = outer.contentWindow[detail.promptType];
|
||||
// Passing the initial value is useful for window.prompt() and doesn't hurt
|
||||
// window.alert() and window.confirm(). See the Window webidl:
|
||||
// https://searchfox.org/mozilla-central/source/dom/webidl/Window.webidl#77-80
|
||||
detail.returnValue = promptFunction(detail.message, detail.initialValue);
|
||||
},
|
||||
|
||||
stop() {
|
||||
const tab = gBrowser.getTabForBrowser(outer);
|
||||
const 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 (const 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);
|
||||
outer.webProgress.removeProgressListener(mirroringProgressListener);
|
||||
|
||||
// Reset the Custom Element back to the original state.
|
||||
outer.destroy();
|
||||
|
||||
// Reset @remote since this is now back to a regular, non-remote browser
|
||||
outer.setAttribute("remote", "false");
|
||||
outer.removeAttribute("remoteType");
|
||||
|
||||
// Stop forwarding remoteType to the inner browser
|
||||
delete outer.remoteType;
|
||||
|
||||
outer.construct();
|
||||
|
||||
// Delete browser window properties exposed on content's owner global
|
||||
for (const property of PROPERTIES_FROM_BROWSER_WINDOW) {
|
||||
delete inner.ownerGlobal[property];
|
||||
}
|
||||
|
||||
// Remove mozbrowser event handlers
|
||||
inner.removeEventListener("mozbrowseropenwindow", this);
|
||||
inner.removeEventListener("mozbrowsershowmodalprompt", 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.outerRef = Cu.getWeakReference(outer);
|
||||
this.innerRef = Cu.getWeakReference(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: [
|
||||
"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 browser.js
|
||||
"Browser:PurgeSessionHistory",
|
||||
"InPermitUnload",
|
||||
"PermitUnload",
|
||||
// Messages sent from browser.js
|
||||
"PageStyle:Disable",
|
||||
"PageStyle:Switch",
|
||||
// Messages sent from SessionStore.jsm
|
||||
"SessionStore:flush",
|
||||
"SessionStore:restoreHistory",
|
||||
"SessionStore:restoreTabContent",
|
||||
],
|
||||
|
||||
INNER_TO_OUTER_MESSAGES: [
|
||||
// Messages sent to browser.js
|
||||
"PageStyle:StyleSheets",
|
||||
// Messages sent to browser.js
|
||||
"InPermitUnload",
|
||||
"PermitUnload",
|
||||
// Messages sent to SessionStore.jsm
|
||||
"SessionStore:update",
|
||||
// Messages sent to BrowserTestUtils.jsm
|
||||
"browser-test-utils:loadEvent",
|
||||
],
|
||||
|
||||
OUTER_TO_INNER_MESSAGE_PREFIXES: [
|
||||
// Messages sent from browser.js
|
||||
"Autoscroll:",
|
||||
// Messages sent from DevTools
|
||||
"debug:",
|
||||
// Messages sent from RemoteFinder.jsm
|
||||
"Finder:",
|
||||
// Messages sent from InlineSpellChecker.jsm
|
||||
"MessageChannel:",
|
||||
// Messages sent from printUtils.js
|
||||
"Printing:",
|
||||
],
|
||||
|
||||
INNER_TO_OUTER_MESSAGE_PREFIXES: [
|
||||
// Messages sent to browser.js
|
||||
"Autoscroll:",
|
||||
// Messages sent to DevTools
|
||||
"debug:",
|
||||
// Messages sent to RemoteFinder.jsm
|
||||
"Finder:",
|
||||
// Messages sent to MessageChannel.jsm
|
||||
"MessageChannel:",
|
||||
// Messages sent to printUtils.js
|
||||
"Printing:",
|
||||
],
|
||||
|
||||
OUTER_TO_INNER_FRAME_SCRIPTS: [
|
||||
// DevTools server for OOP frames
|
||||
"resource://devtools/server/startup/frame.js",
|
||||
],
|
||||
|
||||
get outer() {
|
||||
return this.outerRef.get();
|
||||
},
|
||||
|
||||
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.
|
||||
const docShell = this.outer[FRAME_LOADER].docShell;
|
||||
return docShell.messageManager;
|
||||
},
|
||||
|
||||
get inner() {
|
||||
return this.innerRef.get();
|
||||
},
|
||||
|
||||
get innerParentMM() {
|
||||
if (!this.inner.frameLoader) {
|
||||
return null;
|
||||
}
|
||||
return this.inner.frameLoader.messageManager;
|
||||
},
|
||||
|
||||
init() {
|
||||
for (const method of this.PASS_THROUGH_METHODS) {
|
||||
this[method] = (...args) => {
|
||||
if (!this.outerParentMM) {
|
||||
return null;
|
||||
}
|
||||
return this.outerParentMM[method](...args);
|
||||
};
|
||||
}
|
||||
|
||||
for (const name of this.INNER_TO_OUTER_MESSAGES) {
|
||||
this.innerParentMM.addMessageListener(name, this);
|
||||
this.tunneledMessageNames.add(name);
|
||||
}
|
||||
|
||||
Services.obs.addObserver(this, "message-manager-close");
|
||||
|
||||
// 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 (const 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 (const method of this.OVERRIDDEN_METHODS) {
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Expose the inner frame's value for `processMessageManager`. This is done mainly to
|
||||
* allow Browser Content Toolbox (which needs to find a tab's process) to work for RDM
|
||||
* tabs. (The property is quite rarely used in general.)
|
||||
*/
|
||||
get processMessageManager() {
|
||||
return this.innerParentMM.processMessageManager;
|
||||
},
|
||||
|
||||
get remoteType() {
|
||||
return this.innerParentMM.remoteType;
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
this.outerChildMM.sendAsyncMessage(name, data);
|
||||
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)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
toString() {
|
||||
return "[object MessageManagerTunnel]";
|
||||
},
|
||||
};
|
|
@ -1,181 +0,0 @@
|
|||
/* 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 { Cc, Ci, Cr, components: Components } = require("chrome");
|
||||
const ChromeUtils = require("ChromeUtils");
|
||||
const Services = require("Services");
|
||||
|
||||
/**
|
||||
* 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 actor 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 WebNavigation
|
||||
* actor to perform all actions.
|
||||
*/
|
||||
function BrowserElementWebNavigation(browser) {
|
||||
this._browser = browser;
|
||||
}
|
||||
|
||||
BrowserElementWebNavigation.prototype = {
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebNavigation]),
|
||||
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
|
||||
goBack() {
|
||||
const cancelContentJSEpoch = this.maybeCancelContentJSExecution(
|
||||
Ci.nsIRemoteTab.NAVIGATE_BACK
|
||||
);
|
||||
this._browser.browsingContext.goBack(cancelContentJSEpoch);
|
||||
},
|
||||
|
||||
goForward() {
|
||||
const cancelContentJSEpoch = this.maybeCancelContentJSExecution(
|
||||
Ci.nsIRemoteTab.NAVIGATE_FORWARD
|
||||
);
|
||||
this._browser.browsingContext.goForward(cancelContentJSEpoch);
|
||||
},
|
||||
|
||||
maybeCancelContentJSExecution(navigationType, options = {}) {
|
||||
const epoch = this._cancelContentJSEpoch++;
|
||||
this._browser.frameLoader.remoteTab.maybeCancelContentJSExecution(
|
||||
navigationType,
|
||||
{ ...options, epoch }
|
||||
);
|
||||
return epoch;
|
||||
},
|
||||
|
||||
gotoIndex(index) {
|
||||
// No equivalent in the current BrowserElement API
|
||||
this._browser.browsingContext.gotoIndex(index);
|
||||
},
|
||||
|
||||
loadURI(uri, flags, referrer, postData, headers) {
|
||||
// No equivalent in the current BrowserElement API
|
||||
this.loadURIWithOptions(
|
||||
uri,
|
||||
flags,
|
||||
referrer,
|
||||
Ci.nsIReferrerInfo.EMPTY,
|
||||
postData,
|
||||
headers,
|
||||
null,
|
||||
Services.scriptSecurityManager.createNullPrincipal({})
|
||||
);
|
||||
},
|
||||
|
||||
loadURIWithOptions(
|
||||
uri,
|
||||
flags,
|
||||
referrer,
|
||||
referrerPolicy,
|
||||
postData,
|
||||
headers,
|
||||
baseURI,
|
||||
triggeringPrincipal
|
||||
) {
|
||||
// No equivalent in the current BrowserElement API
|
||||
const referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance(
|
||||
Ci.nsIReferrerInfo
|
||||
);
|
||||
referrerInfo.init(referrerPolicy, true, referrer);
|
||||
|
||||
this._browser.browsingContext.loadURI(uri, {
|
||||
loadFlags: flags,
|
||||
referrerInfo,
|
||||
postData,
|
||||
headers,
|
||||
baseURI,
|
||||
triggeringPrincipal,
|
||||
});
|
||||
},
|
||||
|
||||
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) {
|
||||
this._browser.browsingContext.stop(flags);
|
||||
},
|
||||
|
||||
get document() {
|
||||
return this._browser.contentDocument;
|
||||
},
|
||||
|
||||
_currentURI: null,
|
||||
get currentURI() {
|
||||
if (!this._currentURI) {
|
||||
this._currentURI = Services.io.newURI("about:blank");
|
||||
}
|
||||
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 Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
set sessionHistory(value) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
|
||||
swapBrowser(browser) {
|
||||
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
||||
},
|
||||
|
||||
copyStateFrom(otherWebNavigation) {
|
||||
const state = ["canGoBack", "canGoForward", "_currentURI"];
|
||||
for (const 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 (const flag of FLAGS) {
|
||||
BrowserElementWebNavigation.prototype[flag] = Ci.nsIWebNavigation[flag];
|
||||
}
|
||||
|
||||
exports.BrowserElementWebNavigation = BrowserElementWebNavigation;
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
DIRS += [
|
||||
'actions',
|
||||
'browser',
|
||||
'components',
|
||||
'images',
|
||||
'reducers',
|
||||
|
|
Загрузка…
Ссылка в новой задаче