summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/browser-custom-element.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/content/widgets/browser-custom-element.js
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/content/widgets/browser-custom-element.js')
-rw-r--r--toolkit/content/widgets/browser-custom-element.js1971
1 files changed, 1971 insertions, 0 deletions
diff --git a/toolkit/content/widgets/browser-custom-element.js b/toolkit/content/widgets/browser-custom-element.js
new file mode 100644
index 0000000000..8eee31b7c9
--- /dev/null
+++ b/toolkit/content/widgets/browser-custom-element.js
@@ -0,0 +1,1971 @@
+/* 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";
+
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+ const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+ );
+
+ let lazy = {};
+
+ ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+ Finder: "resource://gre/modules/Finder.sys.mjs",
+ FinderParent: "resource://gre/modules/FinderParent.sys.mjs",
+ PopupBlocker: "resource://gre/actors/PopupBlockingParent.sys.mjs",
+ SelectParentHelper: "resource://gre/actors/SelectParent.sys.mjs",
+ RemoteWebNavigation: "resource://gre/modules/RemoteWebNavigation.sys.mjs",
+ });
+
+ XPCOMUtils.defineLazyGetter(lazy, "blankURI", () =>
+ Services.io.newURI("about:blank")
+ );
+
+ let lazyPrefs = {};
+ XPCOMUtils.defineLazyPreferenceGetter(
+ lazyPrefs,
+ "unloadTimeoutMs",
+ "dom.beforeunload_timeout_ms"
+ );
+ Object.defineProperty(lazy, "ProcessHangMonitor", {
+ configurable: true,
+ get() {
+ // Import if we can - this is a browser/ module so it may not be
+ // available, in which case we return null. We replace this getter
+ // when the module becomes available (should be on delayed startup
+ // when the first browser window loads, via BrowserGlue.sys.mjs).
+ const kURL = "resource:///modules/ProcessHangMonitor.jsm";
+ if (Cu.isModuleLoaded(kURL)) {
+ let { ProcessHangMonitor } = ChromeUtils.import(kURL);
+ // eslint-disable-next-line mozilla/valid-lazy
+ Object.defineProperty(lazy, "ProcessHangMonitor", {
+ value: ProcessHangMonitor,
+ });
+ return ProcessHangMonitor;
+ }
+ return null;
+ },
+ });
+
+ // Get SessionStore module in the same as ProcessHangMonitor above.
+ Object.defineProperty(lazy, "SessionStore", {
+ configurable: true,
+ get() {
+ const kURL = "resource:///modules/sessionstore/SessionStore.sys.mjs";
+ if (Cu.isESModuleLoaded(kURL)) {
+ let { SessionStore } = ChromeUtils.importESModule(kURL);
+ // eslint-disable-next-line mozilla/valid-lazy
+ Object.defineProperty(lazy, "SessionStore", {
+ value: SessionStore,
+ });
+ return SessionStore;
+ }
+ return null;
+ },
+ });
+
+ const elementsToDestroyOnUnload = new Set();
+
+ window.addEventListener(
+ "unload",
+ () => {
+ for (let element of elementsToDestroyOnUnload.values()) {
+ element.destroy();
+ }
+ elementsToDestroyOnUnload.clear();
+ },
+ { mozSystemGroup: true, once: true }
+ );
+
+ class MozBrowser extends MozElements.MozElementMixin(XULFrameElement) {
+ static get observedAttributes() {
+ return ["remote"];
+ }
+
+ constructor() {
+ super();
+
+ this.onPageHide = this.onPageHide.bind(this);
+
+ this.isNavigating = false;
+
+ this._documentURI = null;
+ this._characterSet = null;
+ this._documentContentType = null;
+
+ this._inPermitUnload = new WeakSet();
+
+ this._originalURI = null;
+ this._searchTerms = "";
+ // When we open a prompt in reaction to a 401, if this 401 comes from
+ // a different base domain, the url of that site will be stored here
+ // and will be used for auth prompt spoofing protections.
+ // See bug 791594 for reference.
+ this._currentAuthPromptURI = null;
+ /**
+ * These are managed by the tabbrowser:
+ */
+ this.droppedLinkHandler = null;
+ this.mIconURL = null;
+ this.lastURI = null;
+
+ XPCOMUtils.defineLazyGetter(this, "popupBlocker", () => {
+ return new lazy.PopupBlocker(this);
+ });
+
+ this.addEventListener(
+ "dragover",
+ event => {
+ if (!this.droppedLinkHandler || event.defaultPrevented) {
+ return;
+ }
+
+ // For drags that appear to be internal text (for example, tab drags),
+ // set the dropEffect to 'none'. This prevents the drop even if some
+ // other listener cancelled the event.
+ var types = event.dataTransfer.types;
+ if (
+ types.includes("text/x-moz-text-internal") &&
+ !types.includes("text/plain")
+ ) {
+ event.dataTransfer.dropEffect = "none";
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ // No need to handle "dragover" in e10s, since nsDocShellTreeOwner.cpp in the child process
+ // handles that case using "@mozilla.org/content/dropped-link-handler;1" service.
+ if (this.isRemoteBrowser) {
+ return;
+ }
+
+ let linkHandler = Services.droppedLinkHandler;
+ if (linkHandler.canDropLink(event, false)) {
+ event.preventDefault();
+ }
+ },
+ { mozSystemGroup: true }
+ );
+
+ this.addEventListener(
+ "drop",
+ event => {
+ // No need to handle "drop" in e10s, since nsDocShellTreeOwner.cpp in the child process
+ // handles that case using "@mozilla.org/content/dropped-link-handler;1" service.
+ if (
+ !this.droppedLinkHandler ||
+ event.defaultPrevented ||
+ this.isRemoteBrowser
+ ) {
+ return;
+ }
+
+ let linkHandler = Services.droppedLinkHandler;
+ try {
+ if (!linkHandler.canDropLink(event, false)) {
+ return;
+ }
+
+ // Pass true to prevent the dropping of javascript:/data: URIs
+ var links = linkHandler.dropLinks(event, true);
+ } catch (ex) {
+ return;
+ }
+
+ if (links.length) {
+ let triggeringPrincipal = linkHandler.getTriggeringPrincipal(event);
+ this.droppedLinkHandler(event, links, triggeringPrincipal);
+ }
+ },
+ { mozSystemGroup: true }
+ );
+
+ this.addEventListener("dragstart", event => {
+ // If we're a remote browser dealing with a dragstart, stop it
+ // from propagating up, since our content process should be dealing
+ // with the mouse movement.
+ if (this.isRemoteBrowser) {
+ event.stopPropagation();
+ }
+ });
+ }
+
+ resetFields() {
+ if (this.observer) {
+ try {
+ Services.obs.removeObserver(
+ this.observer,
+ "browser:purge-session-history"
+ );
+ } catch (ex) {
+ // It's not clear why this sometimes throws an exception.
+ }
+ this.observer = null;
+ }
+
+ let browser = this;
+ this.observer = {
+ observe(aSubject, aTopic, aState) {
+ if (aTopic == "browser:purge-session-history") {
+ browser.purgeSessionHistory();
+ } else if (aTopic == "apz:cancel-autoscroll") {
+ if (aState == browser._autoScrollScrollId) {
+ // Set this._autoScrollScrollId to null, so in stopScroll() we
+ // don't call stopApzAutoscroll() (since it's APZ that
+ // initiated the stopping).
+ browser._autoScrollScrollId = null;
+ browser._autoScrollPresShellId = null;
+
+ browser._autoScrollPopup.hidePopup();
+ }
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ this._documentURI = null;
+
+ this._originalURI = null;
+
+ this._currentAuthPromptURI = null;
+
+ this._searchTerms = "";
+
+ this._documentContentType = null;
+
+ this._loadContext = null;
+
+ this._webBrowserFind = null;
+
+ this._finder = null;
+
+ this._remoteFinder = null;
+
+ this._fastFind = null;
+
+ this._lastSearchString = null;
+
+ this._characterSet = "";
+
+ this._mayEnableCharacterEncodingMenu = null;
+
+ this._contentPrincipal = null;
+
+ this._contentPartitionedPrincipal = null;
+
+ this._csp = null;
+
+ this._referrerInfo = null;
+
+ this._contentRequestContextID = null;
+
+ this._rdmFullZoom = 1.0;
+
+ this._isSyntheticDocument = false;
+
+ this.mPrefs = Services.prefs;
+
+ this._mStrBundle = null;
+
+ this._audioMuted = false;
+
+ this._hasAnyPlayingMediaBeenBlocked = false;
+
+ this._unselectedTabHoverMessageListenerCount = 0;
+
+ this.urlbarChangeTracker = {
+ _startedLoadSinceLastUserTyping: false,
+
+ startedLoad() {
+ this._startedLoadSinceLastUserTyping = true;
+ },
+ finishedLoad() {
+ this._startedLoadSinceLastUserTyping = false;
+ },
+ userTyped() {
+ this._startedLoadSinceLastUserTyping = false;
+ },
+ };
+
+ this._userTypedValue = null;
+
+ this._AUTOSCROLL_SNAP = 10;
+
+ this._autoScrollBrowsingContext = null;
+
+ this._startX = null;
+
+ this._startY = null;
+
+ this._autoScrollPopup = null;
+
+ /**
+ * These IDs identify the scroll frame being autoscrolled.
+ */
+ this._autoScrollScrollId = null;
+
+ this._autoScrollPresShellId = null;
+ }
+
+ connectedCallback() {
+ // We typically use this to avoid running JS that triggers a layout during parse
+ // (see comment on the delayConnectedCallback implementation). In this case, we
+ // are using it to avoid a leak - see https://bugzilla.mozilla.org/show_bug.cgi?id=1441935#c20.
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.construct();
+ }
+
+ disconnectedCallback() {
+ this.destroy();
+ }
+
+ get autoscrollEnabled() {
+ if (this.getAttribute("autoscroll") == "false") {
+ return false;
+ }
+
+ return this.mPrefs.getBoolPref("general.autoScroll", true);
+ }
+
+ get canGoBack() {
+ return this.webNavigation.canGoBack;
+ }
+
+ get canGoForward() {
+ return this.webNavigation.canGoForward;
+ }
+
+ // While an auth prompt from a base domain different than the current sites is open, we want to display the url of the cross domain site.
+ // This is to prevent possible auth spoofing scenarios.
+ // The URL of the requesting origin is provided by 'currentAuthPromptURI', this will only be non null while an auth prompt is open.
+ // See bug 791594 for reference.
+ get currentURI() {
+ if (this.currentAuthPromptURI) {
+ return this.currentAuthPromptURI;
+ }
+ if (this.webNavigation) {
+ return this.webNavigation.currentURI;
+ }
+ return null;
+ }
+
+ get documentURI() {
+ return this.isRemoteBrowser
+ ? this._documentURI
+ : this.contentDocument?.documentURIObject;
+ }
+
+ get documentContentType() {
+ if (this.isRemoteBrowser) {
+ return this._documentContentType;
+ }
+ return this.contentDocument ? this.contentDocument.contentType : null;
+ }
+
+ set documentContentType(aContentType) {
+ if (aContentType != null) {
+ if (this.isRemoteBrowser) {
+ this._documentContentType = aContentType;
+ } else {
+ this.contentDocument.documentContentType = aContentType;
+ }
+ }
+ }
+
+ get loadContext() {
+ if (this._loadContext) {
+ return this._loadContext;
+ }
+
+ let { frameLoader } = this;
+ if (!frameLoader) {
+ return null;
+ }
+ this._loadContext = frameLoader.loadContext;
+ return this._loadContext;
+ }
+
+ get autoCompletePopup() {
+ return document.getElementById(this.getAttribute("autocompletepopup"));
+ }
+
+ set suspendMediaWhenInactive(val) {
+ this.browsingContext.suspendMediaWhenInactive = val;
+ }
+
+ get suspendMediaWhenInactive() {
+ return !!this.browsingContext?.suspendMediaWhenInactive;
+ }
+
+ set docShellIsActive(val) {
+ this.browsingContext.isActive = val;
+ if (this.isRemoteBrowser) {
+ let remoteTab = this.frameLoader?.remoteTab;
+ if (remoteTab) {
+ remoteTab.renderLayers = val;
+ }
+ }
+ }
+
+ get docShellIsActive() {
+ return !!this.browsingContext?.isActive;
+ }
+
+ set renderLayers(val) {
+ if (this.isRemoteBrowser) {
+ let remoteTab = this.frameLoader?.remoteTab;
+ if (remoteTab) {
+ remoteTab.renderLayers = val;
+ }
+ } else {
+ this.docShellIsActive = val;
+ }
+ }
+
+ get renderLayers() {
+ if (this.isRemoteBrowser) {
+ return !!this.frameLoader?.remoteTab?.renderLayers;
+ }
+ return this.docShellIsActive;
+ }
+
+ get hasLayers() {
+ if (this.isRemoteBrowser) {
+ return !!this.frameLoader?.remoteTab?.hasLayers;
+ }
+ return this.docShellIsActive;
+ }
+
+ get isRemoteBrowser() {
+ return this.getAttribute("remote") == "true";
+ }
+
+ get remoteType() {
+ return this.browsingContext?.currentRemoteType;
+ }
+
+ get isCrashed() {
+ if (!this.isRemoteBrowser || !this.frameLoader) {
+ return false;
+ }
+
+ return !this.frameLoader.remoteTab;
+ }
+
+ get messageManager() {
+ // Bug 1524084 - Trying to get at the message manager while in the crashed state will
+ // create a new message manager that won't shut down properly when the crashed browser
+ // is removed from the DOM. We work around that right now by returning null if we're
+ // in the crashed state.
+ if (this.frameLoader && !this.isCrashed) {
+ return this.frameLoader.messageManager;
+ }
+ return null;
+ }
+
+ get webBrowserFind() {
+ if (!this._webBrowserFind) {
+ this._webBrowserFind = this.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebBrowserFind);
+ }
+ return this._webBrowserFind;
+ }
+
+ get finder() {
+ if (this.isRemoteBrowser) {
+ if (!this._remoteFinder) {
+ this._remoteFinder = new lazy.FinderParent(this);
+ }
+ return this._remoteFinder;
+ }
+ if (!this._finder) {
+ if (!this.docShell) {
+ return null;
+ }
+
+ this._finder = new lazy.Finder(this.docShell);
+ }
+ return this._finder;
+ }
+
+ get fastFind() {
+ if (!this._fastFind) {
+ if (!("@mozilla.org/typeaheadfind;1" in Cc)) {
+ return null;
+ }
+
+ var tabBrowser = this.getTabBrowser();
+ if (tabBrowser && "fastFind" in tabBrowser) {
+ return (this._fastFind = tabBrowser.fastFind);
+ }
+
+ if (!this.docShell) {
+ return null;
+ }
+
+ this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(
+ Ci.nsITypeAheadFind
+ );
+ this._fastFind.init(this.docShell);
+ }
+ return this._fastFind;
+ }
+
+ get outerWindowID() {
+ return this.browsingContext?.currentWindowGlobal?.outerWindowId;
+ }
+
+ get innerWindowID() {
+ return this.browsingContext?.currentWindowGlobal?.innerWindowId || null;
+ }
+
+ get browsingContext() {
+ if (this.frameLoader) {
+ return this.frameLoader.browsingContext;
+ }
+ return null;
+ }
+ /**
+ * Note that this overrides webNavigation on XULFrameElement, and duplicates the return value for the non-remote case
+ */
+ get webNavigation() {
+ return this.isRemoteBrowser
+ ? this._remoteWebNavigation
+ : this.docShell && this.docShell.QueryInterface(Ci.nsIWebNavigation);
+ }
+
+ get webProgress() {
+ return this.browsingContext?.webProgress;
+ }
+
+ get sessionHistory() {
+ return this.webNavigation.sessionHistory;
+ }
+
+ get contentTitle() {
+ return this.isRemoteBrowser
+ ? this.browsingContext?.currentWindowGlobal?.documentTitle
+ : this.contentDocument.title;
+ }
+
+ forceEncodingDetection() {
+ if (this.isRemoteBrowser) {
+ this.sendMessageToActor("ForceEncodingDetection", {}, "BrowserTab");
+ } else {
+ this.docShell.forceEncodingDetection();
+ }
+ }
+
+ get characterSet() {
+ return this.isRemoteBrowser ? this._characterSet : this.docShell.charset;
+ }
+
+ get mayEnableCharacterEncodingMenu() {
+ return this.isRemoteBrowser
+ ? this._mayEnableCharacterEncodingMenu
+ : this.docShell.mayEnableCharacterEncodingMenu;
+ }
+
+ set mayEnableCharacterEncodingMenu(aMayEnable) {
+ if (this.isRemoteBrowser) {
+ this._mayEnableCharacterEncodingMenu = aMayEnable;
+ }
+ }
+
+ get contentPrincipal() {
+ return this.isRemoteBrowser
+ ? this._contentPrincipal
+ : this.contentDocument.nodePrincipal;
+ }
+
+ get contentPartitionedPrincipal() {
+ return this.isRemoteBrowser
+ ? this._contentPartitionedPrincipal
+ : this.contentDocument.partitionedPrincipal;
+ }
+
+ get cookieJarSettings() {
+ return this.isRemoteBrowser
+ ? this.browsingContext?.currentWindowGlobal?.cookieJarSettings
+ : this.contentDocument.cookieJarSettings;
+ }
+
+ get csp() {
+ return this.isRemoteBrowser ? this._csp : this.contentDocument.csp;
+ }
+
+ get contentRequestContextID() {
+ if (this.isRemoteBrowser) {
+ return this._contentRequestContextID;
+ }
+ try {
+ return this.contentDocument.documentLoadGroup.requestContextID;
+ } catch (e) {
+ return null;
+ }
+ }
+
+ get referrerInfo() {
+ return this.isRemoteBrowser
+ ? this._referrerInfo
+ : this.contentDocument.referrerInfo;
+ }
+
+ set fullZoom(val) {
+ if (val.toFixed(2) == this.fullZoom.toFixed(2)) {
+ return;
+ }
+ if (this.browsingContext.inRDMPane) {
+ this._rdmFullZoom = val;
+ let event = document.createEvent("Events");
+ event.initEvent("FullZoomChange", true, false);
+ this.dispatchEvent(event);
+ } else {
+ this.browsingContext.fullZoom = val;
+ }
+ }
+
+ get fullZoom() {
+ if (this.browsingContext.inRDMPane) {
+ return this._rdmFullZoom;
+ }
+ return this.browsingContext.fullZoom;
+ }
+
+ set textZoom(val) {
+ if (val.toFixed(2) == this.textZoom.toFixed(2)) {
+ return;
+ }
+ this.browsingContext.textZoom = val;
+ }
+
+ get textZoom() {
+ return this.browsingContext.textZoom;
+ }
+
+ enterResponsiveMode() {
+ if (this.browsingContext.inRDMPane) {
+ return;
+ }
+ this.browsingContext.inRDMPane = true;
+ this._rdmFullZoom = this.browsingContext.fullZoom;
+ this.browsingContext.fullZoom = 1.0;
+ }
+
+ leaveResponsiveMode() {
+ if (!this.browsingContext.inRDMPane) {
+ return;
+ }
+ this.browsingContext.inRDMPane = false;
+ this.browsingContext.fullZoom = this._rdmFullZoom;
+ }
+
+ get isSyntheticDocument() {
+ if (this.isRemoteBrowser) {
+ return this._isSyntheticDocument;
+ }
+ return this.contentDocument.mozSyntheticDocument;
+ }
+
+ get hasContentOpener() {
+ return !!this.browsingContext.opener;
+ }
+
+ get mStrBundle() {
+ if (!this._mStrBundle) {
+ // need to create string bundle manually instead of using <xul:stringbundle/>
+ // see bug 63370 for details
+ this._mStrBundle = Services.strings.createBundle(
+ "chrome://global/locale/browser.properties"
+ );
+ }
+ return this._mStrBundle;
+ }
+
+ get audioMuted() {
+ return this._audioMuted;
+ }
+
+ get shouldHandleUnselectedTabHover() {
+ return this._unselectedTabHoverMessageListenerCount > 0;
+ }
+
+ set shouldHandleUnselectedTabHover(value) {
+ this._unselectedTabHoverMessageListenerCount += value ? 1 : -1;
+ }
+
+ get securityUI() {
+ return this.browsingContext.secureBrowserUI;
+ }
+
+ set userTypedValue(val) {
+ this.urlbarChangeTracker.userTyped();
+ this._userTypedValue = val;
+ }
+
+ get userTypedValue() {
+ return this._userTypedValue;
+ }
+
+ get dontPromptAndDontUnload() {
+ return 1;
+ }
+
+ get dontPromptAndUnload() {
+ return 2;
+ }
+
+ set originalURI(aURI) {
+ if (aURI instanceof Ci.nsIURI) {
+ this._originalURI = aURI;
+ }
+ }
+
+ get originalURI() {
+ return this._originalURI;
+ }
+
+ set searchTerms(val) {
+ this._searchTerms = val;
+ }
+
+ get searchTerms() {
+ return this._searchTerms;
+ }
+
+ set currentAuthPromptURI(aURI) {
+ this._currentAuthPromptURI = aURI;
+ }
+
+ get currentAuthPromptURI() {
+ return this._currentAuthPromptURI;
+ }
+ _wrapURIChangeCall(fn) {
+ if (!this.isRemoteBrowser) {
+ this.isNavigating = true;
+ try {
+ fn();
+ } finally {
+ this.isNavigating = false;
+ }
+ } else {
+ fn();
+ }
+ }
+
+ goBack(
+ requireUserInteraction = lazy.BrowserUtils
+ .navigationRequireUserInteraction
+ ) {
+ var webNavigation = this.webNavigation;
+ if (webNavigation.canGoBack) {
+ this._wrapURIChangeCall(() =>
+ webNavigation.goBack(requireUserInteraction)
+ );
+ }
+ }
+
+ goForward(
+ requireUserInteraction = lazy.BrowserUtils
+ .navigationRequireUserInteraction
+ ) {
+ var webNavigation = this.webNavigation;
+ if (webNavigation.canGoForward) {
+ this._wrapURIChangeCall(() =>
+ webNavigation.goForward(requireUserInteraction)
+ );
+ }
+ }
+
+ reload() {
+ const nsIWebNavigation = Ci.nsIWebNavigation;
+ const flags = nsIWebNavigation.LOAD_FLAGS_NONE;
+ this.reloadWithFlags(flags);
+ }
+
+ reloadWithFlags(aFlags) {
+ this.webNavigation.reload(aFlags);
+ }
+
+ stop() {
+ const nsIWebNavigation = Ci.nsIWebNavigation;
+ const flags = nsIWebNavigation.STOP_ALL;
+ this.webNavigation.stop(flags);
+ }
+
+ _fixLoadParamsToLoadURIOptions(params) {
+ let loadFlags =
+ params.loadFlags || params.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ delete params.flags;
+ params.loadFlags = loadFlags;
+ }
+
+ /**
+ * throws exception for unknown schemes
+ */
+ loadURI(uri, params = {}) {
+ if (!uri) {
+ uri = lazy.blankURI;
+ }
+ this._fixLoadParamsToLoadURIOptions(params);
+ this._wrapURIChangeCall(() => this.webNavigation.loadURI(uri, params));
+ }
+
+ /**
+ * throws exception for unknown schemes
+ */
+ fixupAndLoadURIString(uriString, params = {}) {
+ if (!uriString) {
+ this.loadURI(null, params);
+ return;
+ }
+ this._fixLoadParamsToLoadURIOptions(params);
+ this._wrapURIChangeCall(() =>
+ this.webNavigation.fixupAndLoadURIString(uriString, params)
+ );
+ }
+
+ gotoIndex(aIndex) {
+ this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(aIndex));
+ }
+
+ preserveLayers(preserve) {
+ if (!this.isRemoteBrowser) {
+ return;
+ }
+ let { frameLoader } = this;
+ if (frameLoader.remoteTab) {
+ frameLoader.remoteTab.preserveLayers(preserve);
+ }
+ }
+
+ deprioritize() {
+ if (!this.isRemoteBrowser) {
+ return;
+ }
+ let { remoteTab } = this.frameLoader;
+ if (remoteTab) {
+ remoteTab.priorityHint = false;
+ remoteTab.deprioritize();
+ }
+ }
+
+ getTabBrowser() {
+ if (this?.ownerGlobal?.gBrowser?.getTabForBrowser(this)) {
+ return this.ownerGlobal.gBrowser;
+ }
+ return null;
+ }
+
+ addProgressListener(aListener, aNotifyMask) {
+ if (!aNotifyMask) {
+ aNotifyMask = Ci.nsIWebProgress.NOTIFY_ALL;
+ }
+
+ this.webProgress.addProgressListener(aListener, aNotifyMask);
+ }
+
+ removeProgressListener(aListener) {
+ this.webProgress.removeProgressListener(aListener);
+ }
+
+ onPageHide(aEvent) {
+ // If we're browsing from the tab crashed UI to a URI that keeps
+ // this browser non-remote, we'll handle that here.
+ lazy.SessionStore?.maybeExitCrashedState(this);
+
+ if (!this.docShell || !this.fastFind) {
+ return;
+ }
+ var tabBrowser = this.getTabBrowser();
+ if (
+ !tabBrowser ||
+ !("fastFind" in tabBrowser) ||
+ tabBrowser.selectedBrowser == this
+ ) {
+ this.fastFind.setDocShell(this.docShell);
+ }
+ }
+
+ audioPlaybackStarted() {
+ if (this._audioMuted) {
+ return;
+ }
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackStarted", true, false);
+ this.dispatchEvent(event);
+ }
+
+ audioPlaybackStopped() {
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackStopped", true, false);
+ this.dispatchEvent(event);
+ }
+
+ /**
+ * When the pref "media.block-autoplay-until-in-foreground" is on,
+ * Gecko delays starting playback of media resources in tabs until the
+ * tab has been in the foreground or resumed by tab's play tab icon.
+ * - When Gecko delays starting playback of a media resource in a window,
+ * it sends a message to call activeMediaBlockStarted(). This causes the
+ * tab audio indicator to show.
+ * - When a tab is foregrounded, Gecko starts playing all delayed media
+ * resources in that tab, and sends a message to call
+ * activeMediaBlockStopped(). This causes the tab audio indicator to hide.
+ */
+ activeMediaBlockStarted() {
+ this._hasAnyPlayingMediaBeenBlocked = true;
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackBlockStarted", true, false);
+ this.dispatchEvent(event);
+ }
+
+ activeMediaBlockStopped() {
+ if (!this._hasAnyPlayingMediaBeenBlocked) {
+ return;
+ }
+ this._hasAnyPlayingMediaBeenBlocked = false;
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackBlockStopped", true, false);
+ this.dispatchEvent(event);
+ }
+
+ mute(transientState) {
+ if (!transientState) {
+ this._audioMuted = true;
+ }
+ let context = this.frameLoader.browsingContext;
+ context.notifyMediaMutedChanged(true);
+ }
+
+ unmute() {
+ this._audioMuted = false;
+ let context = this.frameLoader.browsingContext;
+ context.notifyMediaMutedChanged(false);
+ }
+
+ resumeMedia() {
+ this.frameLoader.browsingContext.notifyStartDelayedAutoplayMedia();
+ if (this._hasAnyPlayingMediaBeenBlocked) {
+ this._hasAnyPlayingMediaBeenBlocked = false;
+ let event = document.createEvent("Events");
+ event.initEvent("DOMAudioPlaybackBlockStopped", true, false);
+ this.dispatchEvent(event);
+ }
+ }
+
+ unselectedTabHover(hovered) {
+ if (!this.shouldHandleUnselectedTabHover) {
+ return;
+ }
+ this.sendMessageToActor(
+ "Browser:UnselectedTabHover",
+ {
+ hovered,
+ },
+ "UnselectedTabHover",
+ "roots"
+ );
+ }
+
+ didStartLoadSinceLastUserTyping() {
+ return (
+ !this.isNavigating &&
+ this.urlbarChangeTracker._startedLoadSinceLastUserTyping
+ );
+ }
+
+ constrainPopup(popup) {
+ if (this.getAttribute("constrainpopups") != "false") {
+ let constraintRect = this.getBoundingClientRect();
+ constraintRect = new DOMRect(
+ constraintRect.left + window.mozInnerScreenX,
+ constraintRect.top + window.mozInnerScreenY,
+ constraintRect.width,
+ constraintRect.height
+ );
+ popup.setConstraintRect(constraintRect);
+ } else {
+ popup.setConstraintRect(new DOMRect(0, 0, 0, 0));
+ }
+ }
+
+ construct() {
+ elementsToDestroyOnUnload.add(this);
+ this.resetFields();
+ this.mInitialized = true;
+ if (this.isRemoteBrowser) {
+ /*
+ * Don't try to send messages from this function. The message manager for
+ * the <browser> element may not be initialized yet.
+ */
+
+ this._remoteWebNavigation = new lazy.RemoteWebNavigation(this);
+
+ // Initialize contentPrincipal to the about:blank principal for this loadcontext
+ let aboutBlank = Services.io.newURI("about:blank");
+ let ssm = Services.scriptSecurityManager;
+ this._contentPrincipal = ssm.getLoadContextContentPrincipal(
+ aboutBlank,
+ this.loadContext
+ );
+ this._contentPartitionedPrincipal = this._contentPrincipal;
+ // CSP for about:blank is null; if we ever change _contentPrincipal above,
+ // we should re-evaluate the CSP here.
+ this._csp = null;
+
+ if (!this.hasAttribute("disablehistory")) {
+ Services.obs.addObserver(
+ this.observer,
+ "browser:purge-session-history",
+ true
+ );
+ }
+ }
+
+ try {
+ // |webNavigation.sessionHistory| will have been set by the frame
+ // loader when creating the docShell as long as this xul:browser
+ // doesn't have the 'disablehistory' attribute set.
+ if (this.docShell && this.webNavigation.sessionHistory) {
+ Services.obs.addObserver(
+ this.observer,
+ "browser:purge-session-history",
+ true
+ );
+
+ // enable global history if we weren't told otherwise
+ if (
+ !this.hasAttribute("disableglobalhistory") &&
+ !this.isRemoteBrowser
+ ) {
+ try {
+ this.docShell.browsingContext.useGlobalHistory = true;
+ } catch (ex) {
+ // This can occur if the Places database is locked
+ console.error("Error enabling browser global history: ", ex);
+ }
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ try {
+ // Ensures the securityUI is initialized.
+ var securityUI = this.securityUI; // eslint-disable-line no-unused-vars
+ } catch (e) {}
+
+ if (!this.isRemoteBrowser) {
+ this._remoteWebNavigation = null;
+ this.addEventListener("pagehide", this.onPageHide, true);
+ }
+ }
+
+ /**
+ * This is necessary because custom elements don't have a "real" destructor.
+ * This method is called explicitly by tabbrowser, when changing remoteness,
+ * and when we're disconnected or the window unloads.
+ */
+ destroy() {
+ elementsToDestroyOnUnload.delete(this);
+
+ // If we're browsing from the tab crashed UI to a URI that causes the tab
+ // to go remote again, we catch this here, because swapping out the
+ // non-remote browser for a remote one doesn't cause the pagehide event
+ // to be fired. Previously, we used to do this in the frame script's
+ // unload handler.
+ lazy.SessionStore?.maybeExitCrashedState(this);
+
+ // Make sure that any open select is closed.
+ let menulist = document.getElementById("ContentSelectDropdown");
+ if (menulist?.open) {
+ lazy.SelectParentHelper.hide(menulist, this);
+ }
+
+ this.resetFields();
+
+ if (!this.mInitialized) {
+ return;
+ }
+
+ this.mInitialized = false;
+ this.lastURI = null;
+
+ if (!this.isRemoteBrowser) {
+ this.removeEventListener("pagehide", this.onPageHide, true);
+ }
+ }
+
+ updateForStateChange(aCharset, aDocumentURI, aContentType) {
+ if (this.isRemoteBrowser && this.messageManager) {
+ if (aCharset != null) {
+ this._characterSet = aCharset;
+ }
+
+ if (aDocumentURI != null) {
+ this._documentURI = aDocumentURI;
+ }
+
+ if (aContentType != null) {
+ this._documentContentType = aContentType;
+ }
+ }
+ }
+
+ updateWebNavigationForLocationChange(aCanGoBack, aCanGoForward) {
+ if (
+ this.isRemoteBrowser &&
+ this.messageManager &&
+ !Services.appinfo.sessionHistoryInParent
+ ) {
+ this._remoteWebNavigation._canGoBack = aCanGoBack;
+ this._remoteWebNavigation._canGoForward = aCanGoForward;
+ }
+ }
+
+ updateForLocationChange(
+ aLocation,
+ aCharset,
+ aMayEnableCharacterEncodingMenu,
+ aDocumentURI,
+ aTitle,
+ aContentPrincipal,
+ aContentPartitionedPrincipal,
+ aCSP,
+ aReferrerInfo,
+ aIsSynthetic,
+ aHaveRequestContextID,
+ aRequestContextID,
+ aContentType
+ ) {
+ if (this.isRemoteBrowser && this.messageManager) {
+ if (aCharset != null) {
+ this._characterSet = aCharset;
+ this._mayEnableCharacterEncodingMenu =
+ aMayEnableCharacterEncodingMenu;
+ }
+
+ if (aContentType != null) {
+ this._documentContentType = aContentType;
+ }
+
+ this._remoteWebNavigation._currentURI = aLocation;
+ this._documentURI = aDocumentURI;
+ this._contentPrincipal = aContentPrincipal;
+ this._contentPartitionedPrincipal = aContentPartitionedPrincipal;
+ this._csp = aCSP;
+ this._referrerInfo = aReferrerInfo;
+ this._isSyntheticDocument = aIsSynthetic;
+ this._contentRequestContextID = aHaveRequestContextID
+ ? aRequestContextID
+ : null;
+ }
+ }
+
+ purgeSessionHistory() {
+ if (this.isRemoteBrowser && !Services.appinfo.sessionHistoryInParent) {
+ this._remoteWebNavigation._canGoBack = false;
+ this._remoteWebNavigation._canGoForward = false;
+ }
+
+ try {
+ if (Services.appinfo.sessionHistoryInParent) {
+ let sessionHistory = this.browsingContext?.sessionHistory;
+ if (!sessionHistory) {
+ return;
+ }
+
+ // place the entry at current index at the end of the history list, so it won't get removed
+ if (sessionHistory.index < sessionHistory.count - 1) {
+ let indexEntry = sessionHistory.getEntryAtIndex(
+ sessionHistory.index
+ );
+ sessionHistory.addEntry(indexEntry, true);
+ }
+
+ let purge = sessionHistory.count;
+ if (
+ this.browsingContext.currentWindowGlobal.documentURI !=
+ "about:blank"
+ ) {
+ --purge; // Don't remove the page the user's staring at from shistory
+ }
+
+ if (purge > 0) {
+ sessionHistory.purgeHistory(purge);
+ }
+
+ return;
+ }
+
+ this.sendMessageToActor(
+ "Browser:PurgeSessionHistory",
+ {},
+ "PurgeSessionHistory",
+ "roots"
+ );
+ } catch (ex) {
+ // This can throw if the browser has started to go away.
+ if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
+ throw ex;
+ }
+ }
+ }
+
+ createAboutBlankContentViewer(aPrincipal, aPartitionedPrincipal) {
+ let principal = lazy.BrowserUtils.principalWithMatchingOA(
+ aPrincipal,
+ this.contentPrincipal
+ );
+ let partitionedPrincipal = lazy.BrowserUtils.principalWithMatchingOA(
+ aPartitionedPrincipal,
+ this.contentPartitionedPrincipal
+ );
+
+ if (this.isRemoteBrowser) {
+ this.frameLoader.remoteTab.createAboutBlankContentViewer(
+ principal,
+ partitionedPrincipal
+ );
+ } else {
+ this.docShell.createAboutBlankContentViewer(
+ principal,
+ partitionedPrincipal
+ );
+ }
+ }
+
+ _acquireAutoScrollWakeLock() {
+ const pm = Cc["@mozilla.org/power/powermanagerservice;1"].getService(
+ Ci.nsIPowerManagerService
+ );
+ this._autoScrollWakelock = pm.newWakeLock("autoscroll", window);
+ }
+
+ _releaseAutoScrollWakeLock() {
+ if (this._autoScrollWakelock) {
+ try {
+ this._autoScrollWakelock.unlock();
+ } catch (e) {
+ // Ignore error since wake lock is already unlocked
+ }
+ this._autoScrollWakelock = null;
+ }
+ }
+
+ stopScroll() {
+ if (this._autoScrollBrowsingContext) {
+ window.removeEventListener("mousemove", this, true);
+ window.removeEventListener("mousedown", this, true);
+ window.removeEventListener("mouseup", this, true);
+ window.removeEventListener("DOMMouseScroll", this, true);
+ window.removeEventListener("contextmenu", this, true);
+ window.removeEventListener("keydown", this, true);
+ window.removeEventListener("keypress", this, true);
+ window.removeEventListener("keyup", this, true);
+
+ let autoScrollWnd = this._autoScrollBrowsingContext.currentWindowGlobal;
+ if (autoScrollWnd) {
+ autoScrollWnd
+ .getActor("AutoScroll")
+ .sendAsyncMessage("Autoscroll:Stop", {});
+ }
+
+ try {
+ Services.obs.removeObserver(this.observer, "apz:cancel-autoscroll");
+ } catch (ex) {
+ // It's not clear why this sometimes throws an exception
+ }
+
+ if (this._autoScrollScrollId != null) {
+ this._autoScrollBrowsingContext.stopApzAutoscroll(
+ this._autoScrollScrollId,
+ this._autoScrollPresShellId
+ );
+
+ this._autoScrollScrollId = null;
+ this._autoScrollPresShellId = null;
+ }
+
+ this._autoScrollBrowsingContext = null;
+ this._releaseAutoScrollWakeLock();
+ }
+ }
+
+ _getAndMaybeCreateAutoScrollPopup() {
+ let autoscrollPopup = document.getElementById("autoscroller");
+ if (!autoscrollPopup) {
+ autoscrollPopup = document.createXULElement("panel");
+ autoscrollPopup.className = "autoscroller";
+ autoscrollPopup.setAttribute("consumeoutsideclicks", "true");
+ autoscrollPopup.setAttribute("rolluponmousewheel", "true");
+ autoscrollPopup.id = "autoscroller";
+ }
+
+ return autoscrollPopup;
+ }
+
+ startScroll({
+ scrolldir,
+ screenXDevPx,
+ screenYDevPx,
+ scrollId,
+ presShellId,
+ browsingContext,
+ }) {
+ if (!this.autoscrollEnabled) {
+ return { autoscrollEnabled: false, usingApz: false };
+ }
+
+ // The popup size is 32px for the circle plus space for a 4px box-shadow
+ // on each side.
+ const POPUP_SIZE = 40;
+ if (!this._autoScrollPopup) {
+ this._autoScrollPopup = this._getAndMaybeCreateAutoScrollPopup();
+ document.documentElement.appendChild(this._autoScrollPopup);
+ this._autoScrollPopup.removeAttribute("hidden");
+ this._autoScrollPopup.setAttribute("noautofocus", "true");
+ this._autoScrollPopup.style.height = POPUP_SIZE + "px";
+ this._autoScrollPopup.style.width = POPUP_SIZE + "px";
+ this._autoScrollPopup.style.margin = -POPUP_SIZE / 2 + "px";
+ }
+
+ // In desktop pixels.
+ let screenXDesktopPx = screenXDevPx / window.desktopToDeviceScale;
+ let screenYDesktopPx = screenYDevPx / window.desktopToDeviceScale;
+
+ let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(
+ Ci.nsIScreenManager
+ );
+ let screen = screenManager.screenForRect(
+ screenXDesktopPx,
+ screenYDesktopPx,
+ 1,
+ 1
+ );
+
+ // we need these attributes so themers don't need to create per-platform packages
+ if (screen.colorDepth > 8) {
+ // need high color for transparency
+ // Exclude second-rate platforms
+ this._autoScrollPopup.setAttribute(
+ "transparent",
+ !/BeOS|OS\/2/.test(navigator.appVersion)
+ );
+ // Enable translucency on Windows and Mac
+ this._autoScrollPopup.setAttribute(
+ "translucent",
+ AppConstants.platform == "win" || AppConstants.platform == "macosx"
+ );
+ }
+
+ this._autoScrollPopup.setAttribute("scrolldir", scrolldir);
+ this._autoScrollPopup.addEventListener("popuphidden", this, true);
+
+ // In CSS pixels
+ let popupX;
+ let popupY;
+ {
+ let cssToDesktopScale =
+ window.devicePixelRatio / window.desktopToDeviceScale;
+
+ // Sanitize screenX/screenY for available screen size with half the size
+ // of the popup removed. The popup uses negative margins to center on the
+ // coordinates we pass. Use desktop pixels to deal correctly with
+ // multi-monitor / multi-dpi scenarios.
+ let left = {},
+ top = {},
+ width = {},
+ height = {};
+ screen.GetAvailRectDisplayPix(left, top, width, height);
+
+ let popupSizeDesktopPx = POPUP_SIZE * cssToDesktopScale;
+ let minX = left.value + 0.5 * popupSizeDesktopPx;
+ let maxX = left.value + width.value - 0.5 * popupSizeDesktopPx;
+ let minY = top.value + 0.5 * popupSizeDesktopPx;
+ let maxY = top.value + height.value - 0.5 * popupSizeDesktopPx;
+
+ popupX =
+ Math.max(minX, Math.min(maxX, screenXDesktopPx)) / cssToDesktopScale;
+ popupY =
+ Math.max(minY, Math.min(maxY, screenYDesktopPx)) / cssToDesktopScale;
+ }
+
+ // In CSS pixels.
+ let screenX = screenXDevPx / window.devicePixelRatio;
+ let screenY = screenYDevPx / window.devicePixelRatio;
+
+ this._autoScrollPopup.openPopupAtScreen(popupX, popupY);
+ this._ignoreMouseEvents = true;
+ this._startX = screenX;
+ this._startY = screenY;
+ this._autoScrollBrowsingContext = browsingContext;
+ this._acquireAutoScrollWakeLock();
+
+ window.addEventListener("mousemove", this, true);
+ window.addEventListener("mousedown", this, true);
+ window.addEventListener("mouseup", this, true);
+ window.addEventListener("DOMMouseScroll", this, true);
+ window.addEventListener("contextmenu", this, true);
+ window.addEventListener("keydown", this, true);
+ window.addEventListener("keypress", this, true);
+ window.addEventListener("keyup", this, true);
+
+ let usingApz = false;
+
+ if (
+ scrollId != null &&
+ this.mPrefs.getBoolPref("apz.autoscroll.enabled", false)
+ ) {
+ // If APZ is handling the autoscroll, it may decide to cancel
+ // it of its own accord, so register an observer to allow it
+ // to notify us of that.
+ Services.obs.addObserver(this.observer, "apz:cancel-autoscroll", true);
+
+ usingApz = browsingContext.startApzAutoscroll(
+ screenXDevPx,
+ screenYDevPx,
+ scrollId,
+ presShellId
+ );
+
+ // Save the IDs for later
+ this._autoScrollScrollId = scrollId;
+ this._autoScrollPresShellId = presShellId;
+ }
+
+ return { autoscrollEnabled: true, usingApz };
+ }
+
+ cancelScroll() {
+ this._autoScrollPopup.hidePopup();
+ }
+
+ handleEvent(aEvent) {
+ if (this._autoScrollBrowsingContext) {
+ switch (aEvent.type) {
+ case "mousemove": {
+ var x = aEvent.screenX - this._startX;
+ var y = aEvent.screenY - this._startY;
+
+ if (
+ x > this._AUTOSCROLL_SNAP ||
+ x < -this._AUTOSCROLL_SNAP ||
+ y > this._AUTOSCROLL_SNAP ||
+ y < -this._AUTOSCROLL_SNAP
+ ) {
+ this._ignoreMouseEvents = false;
+ }
+ break;
+ }
+ case "mouseup":
+ case "mousedown":
+ // The following mouse click/auxclick event on the autoscroller
+ // shouldn't be fired in web content for compatibility with Chrome.
+ aEvent.preventClickEvent();
+ // fallthrough
+ case "contextmenu": {
+ if (!this._ignoreMouseEvents) {
+ // Use a timeout to prevent the mousedown from opening the popup again.
+ // Ideally, we could use preventDefault here, but contenteditable
+ // and middlemouse paste don't interact well. See bug 1188536.
+ setTimeout(() => this._autoScrollPopup.hidePopup(), 0);
+ }
+ this._ignoreMouseEvents = false;
+ break;
+ }
+ case "DOMMouseScroll": {
+ this._autoScrollPopup.hidePopup();
+ aEvent.preventDefault();
+ break;
+ }
+ case "popuphidden": {
+ // TODO: When the autoscroller is closed by clicking outside of it,
+ // we need to prevent following click event for compatibility
+ // with Chrome. However, there is no way to do that for now.
+ this._autoScrollPopup.removeEventListener(
+ "popuphidden",
+ this,
+ true
+ );
+ this.stopScroll();
+ break;
+ }
+ case "keydown": {
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+ // the escape key will be processed by
+ // nsXULPopupManager::KeyDown and the panel will be closed.
+ // So, don't consume the key event here.
+ break;
+ }
+ // don't break here. we need to eat keydown events.
+ }
+ // fall through
+ case "keypress":
+ case "keyup": {
+ // All keyevents should be eaten here during autoscrolling.
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ }
+ }
+ }
+ }
+
+ closeBrowser() {
+ // The request comes from a XPCOM component, we'd want to redirect
+ // the request to tabbrowser.
+ let tabbrowser = this.getTabBrowser();
+ if (tabbrowser) {
+ let tab = tabbrowser.getTabForBrowser(this);
+ if (tab) {
+ tabbrowser.removeTab(tab);
+ return;
+ }
+ }
+
+ throw new Error(
+ "Closing a browser which was not attached to a tabbrowser is unsupported."
+ );
+ }
+
+ swapBrowsers(aOtherBrowser) {
+ // The request comes from a XPCOM component, we'd want to redirect
+ // the request to tabbrowser so tabbrowser will be setup correctly,
+ // and it will eventually call swapDocShells.
+ let ourTabBrowser = this.getTabBrowser();
+ let otherTabBrowser = aOtherBrowser.getTabBrowser();
+ if (ourTabBrowser && otherTabBrowser) {
+ let ourTab = ourTabBrowser.getTabForBrowser(this);
+ let otherTab = otherTabBrowser.getTabForBrowser(aOtherBrowser);
+ ourTabBrowser.swapBrowsers(ourTab, otherTab);
+ return;
+ }
+
+ // One of us is not connected to a tabbrowser, so just swap.
+ this.swapDocShells(aOtherBrowser);
+ }
+
+ swapDocShells(aOtherBrowser) {
+ if (this.isRemoteBrowser != aOtherBrowser.isRemoteBrowser) {
+ throw new Error(
+ "Can only swap docshells between browsers in the same process."
+ );
+ }
+
+ // Give others a chance to swap state.
+ // IMPORTANT: Since a swapDocShells call does not swap the messageManager
+ // instances attached to a browser to aOtherBrowser, others
+ // will need to add the message listeners to the new
+ // messageManager.
+ // This is not a bug in swapDocShells or the FrameLoader,
+ // merely a design decision: If message managers were swapped,
+ // so that no new listeners were needed, the new
+ // aOtherBrowser.messageManager would have listeners pointing
+ // to the JS global of the current browser, which would rather
+ // easily create leaks while swapping.
+ // IMPORTANT2: When the current browser element is removed from DOM,
+ // which is quite common after a swapDocShells call, its
+ // frame loader is destroyed, and that destroys the relevant
+ // message manager, which will remove the listeners.
+ let event = new CustomEvent("SwapDocShells", { detail: aOtherBrowser });
+ this.dispatchEvent(event);
+ event = new CustomEvent("SwapDocShells", { detail: this });
+ aOtherBrowser.dispatchEvent(event);
+
+ // We need to swap fields that are tied to our docshell or related to
+ // the loaded page
+ // Fields which are built as a result of notifactions (pageshow/hide,
+ // DOMLinkAdded/Removed, onStateChange) should not be swapped here,
+ // because these notifications are dispatched again once the docshells
+ // are swapped.
+ var fieldsToSwap = ["_webBrowserFind", "_rdmFullZoom"];
+
+ if (this.isRemoteBrowser) {
+ fieldsToSwap.push(
+ ...[
+ "_remoteWebNavigation",
+ "_remoteFinder",
+ "_documentURI",
+ "_documentContentType",
+ "_characterSet",
+ "_mayEnableCharacterEncodingMenu",
+ "_contentPrincipal",
+ "_contentPartitionedPrincipal",
+ "_isSyntheticDocument",
+ "_originalURI",
+ ]
+ );
+ }
+
+ var ourFieldValues = {};
+ var otherFieldValues = {};
+ for (let field of fieldsToSwap) {
+ ourFieldValues[field] = this[field];
+ otherFieldValues[field] = aOtherBrowser[field];
+ }
+
+ if (window.PopupNotifications) {
+ PopupNotifications._swapBrowserNotifications(aOtherBrowser, this);
+ }
+
+ try {
+ this.swapFrameLoaders(aOtherBrowser);
+ } catch (ex) {
+ // This may not be implemented for browser elements that are not
+ // attached to a BrowserDOMWindow.
+ }
+
+ for (let field of fieldsToSwap) {
+ this[field] = otherFieldValues[field];
+ aOtherBrowser[field] = ourFieldValues[field];
+ }
+
+ if (!this.isRemoteBrowser) {
+ // Null the current nsITypeAheadFind instances so that they're
+ // lazily re-created on access. We need to do this because they
+ // might have attached the wrong docShell.
+ this._fastFind = aOtherBrowser._fastFind = null;
+ } else {
+ // Rewire the remote listeners
+ this._remoteWebNavigation.swapBrowser(this);
+ aOtherBrowser._remoteWebNavigation.swapBrowser(aOtherBrowser);
+
+ if (this._remoteFinder) {
+ this._remoteFinder.swapBrowser(this);
+ }
+ if (aOtherBrowser._remoteFinder) {
+ aOtherBrowser._remoteFinder.swapBrowser(aOtherBrowser);
+ }
+ }
+
+ event = new CustomEvent("EndSwapDocShells", { detail: aOtherBrowser });
+ this.dispatchEvent(event);
+ event = new CustomEvent("EndSwapDocShells", { detail: this });
+ aOtherBrowser.dispatchEvent(event);
+ }
+
+ getInPermitUnload(aCallback) {
+ if (this.isRemoteBrowser) {
+ let { remoteTab } = this.frameLoader;
+ if (!remoteTab) {
+ // If we're crashed, we're definitely not in this state anymore.
+ aCallback(false);
+ return;
+ }
+
+ aCallback(
+ this._inPermitUnload.has(this.browsingContext.currentWindowGlobal)
+ );
+ return;
+ }
+
+ if (!this.docShell || !this.docShell.contentViewer) {
+ aCallback(false);
+ return;
+ }
+ aCallback(this.docShell.contentViewer.inPermitUnload);
+ }
+
+ async asyncPermitUnload(action) {
+ let wgp = this.browsingContext.currentWindowGlobal;
+ if (this._inPermitUnload.has(wgp)) {
+ throw new Error("permitUnload is already running for this tab.");
+ }
+
+ this._inPermitUnload.add(wgp);
+ try {
+ let permitUnload = await wgp.permitUnload(
+ action,
+ lazyPrefs.unloadTimeoutMs
+ );
+ return { permitUnload };
+ } finally {
+ this._inPermitUnload.delete(wgp);
+ }
+ }
+
+ get hasBeforeUnload() {
+ function hasBeforeUnload(bc) {
+ if (bc.currentWindowContext?.hasBeforeUnload) {
+ return true;
+ }
+ return bc.children.some(hasBeforeUnload);
+ }
+ return hasBeforeUnload(this.browsingContext);
+ }
+
+ permitUnload(action) {
+ if (this.isRemoteBrowser) {
+ if (!this.hasBeforeUnload) {
+ return { permitUnload: true };
+ }
+
+ // Don't bother asking if this browser is hung:
+ if (
+ lazy.ProcessHangMonitor?.findActiveReport(this) ||
+ lazy.ProcessHangMonitor?.findPausedReport(this)
+ ) {
+ return { permitUnload: true };
+ }
+
+ let result;
+ let success;
+
+ this.asyncPermitUnload(action).then(
+ val => {
+ result = val;
+ success = true;
+ },
+ err => {
+ result = err;
+ success = false;
+ }
+ );
+
+ // The permitUnload() promise will, alas, not call its resolution
+ // callbacks after the browser window the promise lives in has closed,
+ // so we have to check for that case explicitly.
+ Services.tm.spinEventLoopUntilOrQuit(
+ "browser-custom-element.js:permitUnload",
+ () => window.closed || success !== undefined
+ );
+ if (success) {
+ return result;
+ }
+ throw result;
+ }
+
+ if (!this.docShell || !this.docShell.contentViewer) {
+ return { permitUnload: true };
+ }
+ return {
+ permitUnload: this.docShell.contentViewer.permitUnload(),
+ };
+ }
+
+ /**
+ * Gets a screenshot of this browser as an ImageBitmap.
+ *
+ * @param {Number} x
+ * The x coordinate of the region from the underlying document to capture
+ * as a screenshot. This is ignored if fullViewport is true.
+ * @param {Number} y
+ * The y coordinate of the region from the underlying document to capture
+ * as a screenshot. This is ignored if fullViewport is true.
+ * @param {Number} w
+ * The width of the region from the underlying document to capture as a
+ * screenshot. This is ignored if fullViewport is true.
+ * @param {Number} h
+ * The height of the region from the underlying document to capture as a
+ * screenshot. This is ignored if fullViewport is true.
+ * @param {Number} scale
+ * The scale factor for the captured screenshot. See the documentation for
+ * WindowGlobalParent.drawSnapshot for more detail.
+ * @param {String} backgroundColor
+ * The default background color for the captured screenshot. See the
+ * documentation for WindowGlobalParent.drawSnapshot for more detail.
+ * @param {boolean|undefined} fullViewport
+ * True if the viewport rect should be captured. If this is true, the
+ * x, y, w and h parameters are ignored. Defaults to false.
+ * @returns {Promise}
+ * @resolves {ImageBitmap}
+ */
+ async drawSnapshot(
+ x,
+ y,
+ w,
+ h,
+ scale,
+ backgroundColor,
+ fullViewport = false
+ ) {
+ let rect = fullViewport ? null : new DOMRect(x, y, w, h);
+ try {
+ return this.browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ scale,
+ backgroundColor
+ );
+ } catch (e) {
+ return false;
+ }
+ }
+
+ dropLinks(aLinks, aTriggeringPrincipal) {
+ if (!this.droppedLinkHandler) {
+ return false;
+ }
+ let links = [];
+ for (let i = 0; i < aLinks.length; i += 3) {
+ links.push({
+ url: aLinks[i],
+ name: aLinks[i + 1],
+ type: aLinks[i + 2],
+ });
+ }
+ this.droppedLinkHandler(null, links, aTriggeringPrincipal);
+ return true;
+ }
+
+ getContentBlockingLog() {
+ let windowGlobal = this.browsingContext.currentWindowGlobal;
+ if (!windowGlobal) {
+ return null;
+ }
+ return windowGlobal.contentBlockingLog;
+ }
+
+ getContentBlockingEvents() {
+ let windowGlobal = this.browsingContext.currentWindowGlobal;
+ if (!windowGlobal) {
+ return 0;
+ }
+ return windowGlobal.contentBlockingEvents;
+ }
+
+ // Send an asynchronous message to the remote child via an actor.
+ // Note: use this only for messages through an actor. For old-style
+ // messages, use the message manager.
+ // The value of the scope argument determines which browsing contexts
+ // are sent to:
+ // 'all' - send to actors associated with all descendant child frames.
+ // 'roots' - send only to actors associated with process roots.
+ // undefined/'' - send only to the top-level actor and not any descendants.
+ sendMessageToActor(messageName, args, actorName, scope) {
+ if (!this.frameLoader) {
+ return;
+ }
+
+ function sendToChildren(browsingContext, childScope) {
+ let windowGlobal = browsingContext.currentWindowGlobal;
+ // If 'roots' is set, only send if windowGlobal.isProcessRoot is true.
+ if (
+ windowGlobal &&
+ (childScope != "roots" || windowGlobal.isProcessRoot)
+ ) {
+ windowGlobal.getActor(actorName).sendAsyncMessage(messageName, args);
+ }
+
+ // Iterate as long as scope in assigned. Note that we use the original
+ // passed in scope, not childScope here.
+ if (scope) {
+ for (let context of browsingContext.children) {
+ sendToChildren(context, scope);
+ }
+ }
+ }
+
+ // Pass no second argument to always send to the top-level browsing context.
+ sendToChildren(this.browsingContext);
+ }
+
+ enterModalState() {
+ this.sendMessageToActor("EnterModalState", {}, "BrowserElement", "roots");
+ }
+
+ leaveModalState() {
+ this.sendMessageToActor(
+ "LeaveModalState",
+ { forceLeave: true },
+ "BrowserElement",
+ "roots"
+ );
+ }
+
+ /**
+ * Can be called for a window with or without modal state.
+ * If the window is not in modal state, this is a no-op.
+ */
+ maybeLeaveModalState() {
+ this.sendMessageToActor(
+ "LeaveModalState",
+ { forceLeave: false },
+ "BrowserElement",
+ "roots"
+ );
+ }
+
+ getDevicePermissionOrigins(key) {
+ if (typeof key !== "string" || key.length === 0) {
+ throw new Error("Key must be non empty string.");
+ }
+ if (!this._devicePermissionOrigins) {
+ this._devicePermissionOrigins = new Map();
+ }
+ let origins = this._devicePermissionOrigins.get(key);
+ if (!origins) {
+ origins = new Set();
+ this._devicePermissionOrigins.set(key, origins);
+ }
+ return origins;
+ }
+
+ // This method is replaced by frontend code in order to delay performing the
+ // process switch until some async operatin is completed.
+ //
+ // This is used by tabbrowser to flush SessionStore before a process switch.
+ async prepareToChangeRemoteness() {
+ /* no-op unless replaced */
+ }
+
+ // This method is replaced by frontend code in order to handle restoring
+ // remote session history
+ //
+ // Called immediately after changing remoteness. If this method returns
+ // `true`, Gecko will assume frontend handled resuming the load, and will
+ // not attempt to resume the load itself.
+ afterChangeRemoteness(browser, redirectLoadSwitchId) {
+ /* no-op unless replaced */
+ return false;
+ }
+
+ // Called by Gecko before the remoteness change happens, allowing for
+ // listeners, etc. to be stashed before the process switch.
+ beforeChangeRemoteness() {
+ // Fire the `WillChangeBrowserRemoteness` event, which may be hooked by
+ // frontend code for custom behaviour.
+ let event = document.createEvent("Events");
+ event.initEvent("WillChangeBrowserRemoteness", true, false);
+ this.dispatchEvent(event);
+
+ // Destroy ourselves to unregister from observer notifications
+ // FIXME: Can we get away with something less destructive here?
+ this.destroy();
+ }
+
+ finishChangeRemoteness(redirectLoadSwitchId) {
+ // Re-construct ourselves after the destroy in `beforeChangeRemoteness`.
+ this.construct();
+
+ // Fire the `DidChangeBrowserRemoteness` event, which may be hooked by
+ // frontend code for custom behaviour.
+ let event = document.createEvent("Events");
+ event.initEvent("DidChangeBrowserRemoteness", true, false);
+ this.dispatchEvent(event);
+
+ // Call into frontend code which may want to handle the load (e.g. to
+ // while restoring session state).
+ return this.afterChangeRemoteness(redirectLoadSwitchId);
+ }
+ }
+
+ MozXULElement.implementCustomInterface(MozBrowser, [Ci.nsIBrowser]);
+ customElements.define("browser", MozBrowser);
+}