forked from mirrors/gecko-dev
This is more consistent with other setters, and lets us handle the null frameLoader case a bit more simply. Differential Revision: https://phabricator.services.mozilla.com/D16370 --HG-- extra : moz-landing-system : lando
1852 lines
57 KiB
JavaScript
1852 lines
57 KiB
JavaScript
/* 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 elementsToDestroyOnUnload = new Set();
|
|
|
|
window.addEventListener("unload", () => {
|
|
for (let element of elementsToDestroyOnUnload.values()) {
|
|
element.destroy();
|
|
}
|
|
elementsToDestroyOnUnload.clear();
|
|
}, { mozSystemGroup: true, once: true });
|
|
|
|
class MozBrowser extends MozElementMixin(XULFrameElement) {
|
|
static get observedAttributes() {
|
|
return ["remote"];
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
// When we have already been set up via connectedCallback and the
|
|
// and the [remote] value changes, we need to start over. This used
|
|
// to happen due to a XBL binding change.
|
|
if (name === "remote" && oldValue != newValue && this.isConnectedAndReady) {
|
|
this.destroy();
|
|
this.construct();
|
|
}
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.onPageHide = this.onPageHide.bind(this);
|
|
|
|
/**
|
|
* These are managed by the tabbrowser:
|
|
*/
|
|
this.droppedLinkHandler = null;
|
|
this.mIconURL = null;
|
|
this.lastURI = null;
|
|
|
|
this.addEventListener("keypress", (event) => {
|
|
if (event.keyCode != KeyEvent.DOM_VK_F7) {
|
|
return;
|
|
}
|
|
|
|
if (event.defaultPrevented || !event.isTrusted) {
|
|
return;
|
|
}
|
|
|
|
const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled";
|
|
const kPrefWarnOnEnable = "accessibility.warn_on_browsewithcaret";
|
|
const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
|
|
|
|
var isEnabled = this.mPrefs.getBoolPref(kPrefShortcutEnabled);
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
// Toggle browse with caret mode
|
|
var browseWithCaretOn = this.mPrefs.getBoolPref(kPrefCaretBrowsingOn, false);
|
|
var warn = this.mPrefs.getBoolPref(kPrefWarnOnEnable, true);
|
|
if (warn && !browseWithCaretOn) {
|
|
var checkValue = { value: false };
|
|
var promptService = Services.prompt;
|
|
|
|
var buttonPressed = promptService.confirmEx(window,
|
|
this.mStrBundle.GetStringFromName("browsewithcaret.checkWindowTitle"),
|
|
this.mStrBundle.GetStringFromName("browsewithcaret.checkLabel"),
|
|
// Make "No" the default:
|
|
promptService.STD_YES_NO_BUTTONS | promptService.BUTTON_POS_1_DEFAULT,
|
|
null, null, null, this.mStrBundle.GetStringFromName("browsewithcaret.checkMsg"),
|
|
checkValue);
|
|
if (buttonPressed != 0) {
|
|
if (checkValue.value) {
|
|
try {
|
|
this.mPrefs.setBoolPref(kPrefShortcutEnabled, false);
|
|
} catch (ex) {}
|
|
}
|
|
return;
|
|
}
|
|
if (checkValue.value) {
|
|
try {
|
|
this.mPrefs.setBoolPref(kPrefWarnOnEnable, false);
|
|
} catch (ex) {}
|
|
}
|
|
}
|
|
|
|
// Toggle the pref
|
|
try {
|
|
this.mPrefs.setBoolPref(kPrefCaretBrowsingOn, !browseWithCaretOn);
|
|
} catch (ex) {}
|
|
}, { mozSystemGroup: true });
|
|
|
|
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 {
|
|
// 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([
|
|
Ci.nsIObserver,
|
|
Ci.nsISupportsWeakReference,
|
|
]),
|
|
};
|
|
|
|
|
|
this._documentURI = null;
|
|
|
|
this._documentContentType = null;
|
|
|
|
/**
|
|
* Weak reference to an optional frame loader that can be used to influence
|
|
* process selection for this browser.
|
|
* See nsIBrowser.sameProcessAsFrameLoader.
|
|
*/
|
|
this._sameProcessAsFrameLoader = null;
|
|
|
|
this._loadContext = null;
|
|
|
|
this._imageDocument = null;
|
|
|
|
this._webBrowserFind = null;
|
|
|
|
this._finder = null;
|
|
|
|
this._remoteFinder = null;
|
|
|
|
this._fastFind = null;
|
|
|
|
this._outerWindowID = null;
|
|
|
|
this._innerWindowID = null;
|
|
|
|
this._browsingContextId = null;
|
|
|
|
this._lastSearchString = null;
|
|
|
|
this._controller = null;
|
|
|
|
this._selectParentHelper = null;
|
|
|
|
this._remoteWebNavigation = null;
|
|
|
|
this._remoteWebProgress = null;
|
|
|
|
this._contentTitle = "";
|
|
|
|
this._characterSet = "";
|
|
|
|
this._mayEnableCharacterEncodingMenu = null;
|
|
|
|
this._contentPrincipal = null;
|
|
|
|
this._contentRequestContextID = null;
|
|
|
|
this._fullZoom = 1;
|
|
|
|
this._textZoom = 1;
|
|
|
|
this._isSyntheticDocument = false;
|
|
|
|
this.mPrefs = Services.prefs;
|
|
|
|
this._mStrBundle = null;
|
|
|
|
this.blockedPopups = null;
|
|
|
|
this._audioMuted = false;
|
|
|
|
this._hasAnyPlayingMediaBeenBlocked = false;
|
|
|
|
/**
|
|
* Only send the message "Browser:UnselectedTabHover" when someone requests
|
|
* for the message, which can reduce non-necessary communication.
|
|
*/
|
|
this._shouldSendUnselectedTabHover = false;
|
|
|
|
this._unselectedTabHoverMessageListenerCount = 0;
|
|
|
|
this._securityUI = null;
|
|
|
|
this.urlbarChangeTracker = {
|
|
_startedLoadSinceLastUserTyping: false,
|
|
|
|
startedLoad() {
|
|
this._startedLoadSinceLastUserTyping = true;
|
|
},
|
|
finishedLoad() {
|
|
this._startedLoadSinceLastUserTyping = false;
|
|
},
|
|
userTyped() {
|
|
this._startedLoadSinceLastUserTyping = false;
|
|
},
|
|
};
|
|
|
|
this._userTypedValue = null;
|
|
|
|
this._AUTOSCROLL_SNAP = 10;
|
|
|
|
this._scrolling = false;
|
|
|
|
this._startX = null;
|
|
|
|
this._startY = null;
|
|
|
|
this._autoScrollPopup = null;
|
|
|
|
this._autoScrollNeedsCleanup = false;
|
|
|
|
/**
|
|
* These IDs identify the scroll frame being autoscrolled.
|
|
*/
|
|
this._autoScrollScrollId = null;
|
|
|
|
this._autoScrollPresShellId = null;
|
|
|
|
this._permitUnloadId = 0;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
get currentURI() {
|
|
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 sameProcessAsFrameLoader(val) {
|
|
this._sameProcessAsFrameLoader = Cu.getWeakReference(val);
|
|
}
|
|
|
|
get sameProcessAsFrameLoader() {
|
|
return this._sameProcessAsFrameLoader && this._sameProcessAsFrameLoader.get();
|
|
}
|
|
|
|
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"));
|
|
}
|
|
|
|
get dateTimePicker() {
|
|
return document.getElementById(this.getAttribute("datetimepicker"));
|
|
}
|
|
|
|
set docShellIsActive(val) {
|
|
if (this.isRemoteBrowser) {
|
|
let { frameLoader } = this;
|
|
if (frameLoader && frameLoader.tabParent) {
|
|
frameLoader.tabParent.docShellIsActive = val;
|
|
}
|
|
} else if (this.docShell) {
|
|
this.docShell.isActive = val;
|
|
}
|
|
}
|
|
|
|
get docShellIsActive() {
|
|
if (this.isRemoteBrowser) {
|
|
let { frameLoader } = this;
|
|
if (frameLoader && frameLoader.tabParent) {
|
|
return frameLoader.tabParent.docShellIsActive;
|
|
}
|
|
return false;
|
|
}
|
|
return this.docShell && this.docShell.isActive;
|
|
}
|
|
|
|
set renderLayers(val) {
|
|
if (this.isRemoteBrowser) {
|
|
let { frameLoader } = this;
|
|
if (frameLoader && frameLoader.tabParent) {
|
|
frameLoader.tabParent.renderLayers = val;
|
|
}
|
|
} else {
|
|
this.docShellIsActive = val;
|
|
}
|
|
}
|
|
|
|
get renderLayers() {
|
|
if (this.isRemoteBrowser) {
|
|
let { frameLoader } = this;
|
|
if (frameLoader && frameLoader.tabParent) {
|
|
return frameLoader.tabParent.renderLayers;
|
|
}
|
|
return false;
|
|
}
|
|
return this.docShellIsActive;
|
|
}
|
|
|
|
get hasLayers() {
|
|
if (this.isRemoteBrowser) {
|
|
let { frameLoader } = this;
|
|
if (frameLoader && frameLoader.tabParent) {
|
|
return frameLoader.tabParent.hasLayers;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return this.docShellIsActive;
|
|
}
|
|
|
|
get imageDocument() {
|
|
if (this.isRemoteBrowser) {
|
|
return this._imageDocument;
|
|
}
|
|
var document = this.contentDocument;
|
|
if (!document || !(document instanceof Ci.nsIImageDocument))
|
|
return null;
|
|
|
|
try {
|
|
return { width: document.imageRequest.image.width, height: document.imageRequest.image.height };
|
|
} catch (e) {}
|
|
return null;
|
|
}
|
|
|
|
get isRemoteBrowser() {
|
|
return (this.getAttribute("remote") == "true");
|
|
}
|
|
|
|
get remoteType() {
|
|
if (!this.isRemoteBrowser) {
|
|
return null;
|
|
}
|
|
|
|
let remoteType = this.getAttribute("remoteType");
|
|
if (remoteType) {
|
|
return remoteType;
|
|
}
|
|
|
|
let E10SUtils = ChromeUtils.import("resource://gre/modules/E10SUtils.jsm", {}).E10SUtils;
|
|
return E10SUtils.DEFAULT_REMOTE_TYPE;
|
|
}
|
|
|
|
get messageManager() {
|
|
if (this.frameLoader) {
|
|
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) {
|
|
// Don't attempt to create the remote finder if the
|
|
// messageManager has already gone away
|
|
if (!this.messageManager)
|
|
return null;
|
|
|
|
let jsm = "resource://gre/modules/FinderParent.jsm";
|
|
let { FinderParent } = ChromeUtils.import(jsm, {});
|
|
this._remoteFinder = new FinderParent(this);
|
|
}
|
|
return this._remoteFinder;
|
|
}
|
|
if (!this._finder) {
|
|
if (!this.docShell)
|
|
return null;
|
|
|
|
let Finder = ChromeUtils.import("resource://gre/modules/Finder.jsm", {}).Finder;
|
|
this._finder = new 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() {
|
|
if (this.isRemoteBrowser) {
|
|
return this._outerWindowID;
|
|
}
|
|
return this.docShell.outerWindowID;
|
|
}
|
|
|
|
get innerWindowID() {
|
|
if (this.isRemoteBrowser) {
|
|
return this._innerWindowID;
|
|
}
|
|
try {
|
|
return this.contentWindow.windowUtils.currentInnerWindowID;
|
|
} catch (e) {
|
|
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
|
|
throw e;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
get browsingContext() {
|
|
if (!this.isRemoteBrowser) {
|
|
return this.docShell.browsingContext;
|
|
}
|
|
|
|
return ChromeUtils.getBrowsingContext(this._browsingContextId);
|
|
}
|
|
/**
|
|
* 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.isRemoteBrowser ? this._remoteWebProgress : this.docShell && this.docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
|
|
}
|
|
|
|
get sessionHistory() {
|
|
return this.webNavigation.sessionHistory;
|
|
}
|
|
|
|
get markupDocumentViewer() {
|
|
return this.docShell.contentViewer;
|
|
}
|
|
|
|
get contentTitle() {
|
|
return this.isRemoteBrowser ? this._contentTitle : this.contentDocument.title;
|
|
}
|
|
|
|
set characterSet(val) {
|
|
if (this.isRemoteBrowser) {
|
|
this.messageManager.sendAsyncMessage("UpdateCharacterSet", { value: val });
|
|
this._characterSet = val;
|
|
} else {
|
|
this.docShell.charset = val;
|
|
this.docShell.gatherCharsetMenuTelemetry();
|
|
}
|
|
}
|
|
|
|
get characterSet() {
|
|
return this.isRemoteBrowser ? this._characterSet : this.docShell.charset;
|
|
}
|
|
|
|
get mayEnableCharacterEncodingMenu() {
|
|
return this.isRemoteBrowser ? this._mayEnableCharacterEncodingMenu : this.docShell.mayEnableCharacterEncodingMenu;
|
|
}
|
|
|
|
get contentPrincipal() {
|
|
return this.isRemoteBrowser ? this._contentPrincipal : this.contentDocument.nodePrincipal;
|
|
}
|
|
|
|
get contentRequestContextID() {
|
|
if (this.isRemoteBrowser) {
|
|
return this._contentRequestContextID;
|
|
}
|
|
try {
|
|
return this.contentDocument.documentLoadGroup
|
|
.requestContextID;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
set showWindowResizer(val) {
|
|
if (val) {
|
|
this.setAttribute("showresizer", "true");
|
|
} else {
|
|
this.removeAttribute("showresizer");
|
|
}
|
|
}
|
|
|
|
get showWindowResizer() {
|
|
return this.getAttribute("showresizer") == "true";
|
|
}
|
|
|
|
set fullZoom(val) {
|
|
if (this.isRemoteBrowser) {
|
|
let changed = val.toFixed(2) != this._fullZoom.toFixed(2);
|
|
|
|
if (changed) {
|
|
this._fullZoom = val;
|
|
try {
|
|
this.messageManager.sendAsyncMessage("FullZoom", { value: val });
|
|
} catch (ex) {}
|
|
|
|
let event = new Event("FullZoomChange", { bubbles: true });
|
|
this.dispatchEvent(event);
|
|
}
|
|
} else {
|
|
this.markupDocumentViewer.fullZoom = val;
|
|
}
|
|
}
|
|
|
|
get fullZoom() {
|
|
if (this.isRemoteBrowser) {
|
|
return this._fullZoom;
|
|
}
|
|
return this.markupDocumentViewer.fullZoom;
|
|
}
|
|
|
|
set textZoom(val) {
|
|
if (this.isRemoteBrowser) {
|
|
let changed = val.toFixed(2) != this._textZoom.toFixed(2);
|
|
|
|
if (changed) {
|
|
this._textZoom = val;
|
|
try {
|
|
this.messageManager.sendAsyncMessage("TextZoom", { value: val });
|
|
} catch (ex) {}
|
|
|
|
let event = new Event("TextZoomChange", { bubbles: true });
|
|
this.dispatchEvent(event);
|
|
}
|
|
} else {
|
|
this.markupDocumentViewer.textZoom = val;
|
|
}
|
|
|
|
}
|
|
|
|
get textZoom() {
|
|
if (this.isRemoteBrowser) {
|
|
return this._textZoom;
|
|
}
|
|
return this.markupDocumentViewer.textZoom;
|
|
}
|
|
|
|
get isSyntheticDocument() {
|
|
if (this.isRemoteBrowser) {
|
|
return this._isSyntheticDocument;
|
|
}
|
|
return this.contentDocument.mozSyntheticDocument;
|
|
}
|
|
|
|
get hasContentOpener() {
|
|
if (this.isRemoteBrowser) {
|
|
return this.frameLoader.tabParent.hasContentOpener;
|
|
}
|
|
return !!this.contentWindow.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._shouldSendUnselectedTabHover;
|
|
}
|
|
|
|
get securityUI() {
|
|
if (this.isRemoteBrowser) {
|
|
if (!this._securityUI) {
|
|
// Don't attempt to create the remote web progress if the
|
|
// messageManager has already gone away
|
|
if (!this.messageManager)
|
|
return null;
|
|
|
|
let jsm = "resource://gre/modules/RemoteSecurityUI.jsm";
|
|
let RemoteSecurityUI = ChromeUtils.import(jsm, {}).RemoteSecurityUI;
|
|
this._securityUI = new RemoteSecurityUI();
|
|
}
|
|
|
|
// We want to double-wrap the JS implemented interface, so that QI and instanceof works.
|
|
var ptr = Cc["@mozilla.org/supports-interface-pointer;1"]
|
|
.createInstance(Ci.nsISupportsInterfacePointer);
|
|
ptr.data = this._securityUI;
|
|
return ptr.data.QueryInterface(Ci.nsISecureBrowserUI);
|
|
}
|
|
|
|
if (!this.docShell.securityUI) {
|
|
const SECUREBROWSERUI_CONTRACTID = "@mozilla.org/secure_browser_ui;1";
|
|
var securityUI = Cc[SECUREBROWSERUI_CONTRACTID]
|
|
.createInstance(Ci.nsISecureBrowserUI);
|
|
securityUI.init(this.docShell);
|
|
}
|
|
|
|
return this.docShell.securityUI;
|
|
}
|
|
|
|
set userTypedValue(val) {
|
|
this.urlbarChangeTracker.userTyped();
|
|
this._userTypedValue = val;
|
|
}
|
|
|
|
get userTypedValue() {
|
|
return this._userTypedValue;
|
|
}
|
|
|
|
get dontPromptAndDontUnload() {
|
|
return 1;
|
|
}
|
|
|
|
get dontPromptAndUnload() {
|
|
return 2;
|
|
}
|
|
|
|
_wrapURIChangeCall(fn) {
|
|
if (!this.isRemoteBrowser) {
|
|
this.inLoadURI = true;
|
|
try {
|
|
fn();
|
|
} finally {
|
|
this.inLoadURI = false;
|
|
}
|
|
} else {
|
|
fn();
|
|
}
|
|
}
|
|
|
|
goBack() {
|
|
var webNavigation = this.webNavigation;
|
|
if (webNavigation.canGoBack)
|
|
this._wrapURIChangeCall(() => webNavigation.goBack());
|
|
}
|
|
|
|
goForward() {
|
|
var webNavigation = this.webNavigation;
|
|
if (webNavigation.canGoForward)
|
|
this._wrapURIChangeCall(() => webNavigation.goForward());
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* throws exception for unknown schemes
|
|
*/
|
|
loadURI(aURI, aParams) {
|
|
if (!aURI) {
|
|
aURI = "about:blank";
|
|
}
|
|
|
|
let {
|
|
flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
|
|
referrerURI,
|
|
referrerPolicy = Ci.nsIHttpChannel.REFERRER_POLICY_UNSET,
|
|
triggeringPrincipal,
|
|
postData,
|
|
} = aParams || {};
|
|
let loadURIOptions = {
|
|
triggeringPrincipal,
|
|
referrerURI,
|
|
loadFlags: flags,
|
|
referrerPolicy,
|
|
postData,
|
|
};
|
|
this._wrapURIChangeCall(() =>
|
|
this.webNavigation.loadURI(aURI, loadURIOptions));
|
|
}
|
|
|
|
gotoIndex(aIndex) {
|
|
this._wrapURIChangeCall(() => this.webNavigation.gotoIndex(aIndex));
|
|
}
|
|
|
|
/**
|
|
* Used by session restore to ensure that currentURI is set so
|
|
* that switch-to-tab works before the tab is fully
|
|
* restored. This function also invokes onLocationChanged
|
|
* listeners in tabbrowser.xml.
|
|
*/
|
|
_setCurrentURI(aURI) {
|
|
if (this.isRemoteBrowser) {
|
|
this._remoteWebProgressManager.setCurrentURI(aURI);
|
|
} else {
|
|
this.docShell.setCurrentURI(aURI);
|
|
}
|
|
}
|
|
|
|
preserveLayers(preserve) {
|
|
if (!this.isRemoteBrowser) {
|
|
return;
|
|
}
|
|
let { frameLoader } = this;
|
|
if (frameLoader.tabParent) {
|
|
frameLoader.tabParent.preserveLayers(preserve);
|
|
}
|
|
}
|
|
|
|
deprioritize() {
|
|
if (!this.isRemoteBrowser) {
|
|
return;
|
|
}
|
|
let { frameLoader } = this;
|
|
if (frameLoader.tabParent) {
|
|
frameLoader.tabParent.deprioritize();
|
|
}
|
|
}
|
|
|
|
forceRepaint() {
|
|
if (!this.isRemoteBrowser) {
|
|
return;
|
|
}
|
|
let { frameLoader } = this;
|
|
if (frameLoader && frameLoader.tabParent) {
|
|
frameLoader.tabParent.forceRepaint();
|
|
}
|
|
}
|
|
|
|
getTabBrowser() {
|
|
if (this.ownerGlobal.gBrowser &&
|
|
this.ownerGlobal.gBrowser.getTabForBrowser &&
|
|
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 (!this.docShell || !this.fastFind)
|
|
return;
|
|
var tabBrowser = this.getTabBrowser();
|
|
if (!tabBrowser || !("fastFind" in tabBrowser) ||
|
|
tabBrowser.selectedBrowser == this)
|
|
this.fastFind.setDocShell(this.docShell);
|
|
}
|
|
|
|
updateBlockedPopups() {
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("DOMUpdateBlockedPopups", true, true);
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
retrieveListOfBlockedPopups() {
|
|
this.messageManager.sendAsyncMessage("PopupBlocking:GetBlockedPopupList", null);
|
|
return new Promise(resolve => {
|
|
let self = this;
|
|
this.messageManager.addMessageListener("PopupBlocking:ReplyGetBlockedPopupList",
|
|
function replyReceived(msg) {
|
|
self.messageManager.removeMessageListener("PopupBlocking:ReplyGetBlockedPopupList",
|
|
replyReceived);
|
|
resolve(msg.data.popupData);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
unblockPopup(aPopupIndex) {
|
|
this.messageManager.sendAsyncMessage("PopupBlocking:UnblockPopup", { index: aPopupIndex });
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
notifyGloballyAutoplayBlocked() {
|
|
let event = document.createEvent("CustomEvent");
|
|
event.initCustomEvent("GloballyAutoplayBlocked", true, false, {
|
|
url: this.documentURI,
|
|
});
|
|
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;
|
|
}
|
|
this.messageManager.sendAsyncMessage("AudioPlayback", { type: "mute" });
|
|
}
|
|
|
|
unmute() {
|
|
this._audioMuted = false;
|
|
this.messageManager.sendAsyncMessage("AudioPlayback", { type: "unmute" });
|
|
}
|
|
|
|
pauseMedia(disposable) {
|
|
let suspendedReason;
|
|
if (disposable) {
|
|
suspendedReason = "mediaControlPaused";
|
|
} else {
|
|
suspendedReason = "lostAudioFocusTransiently";
|
|
}
|
|
|
|
this.messageManager.sendAsyncMessage("AudioPlayback", { type: suspendedReason });
|
|
}
|
|
|
|
stopMedia() {
|
|
this.messageManager.sendAsyncMessage("AudioPlayback", { type: "mediaControlStopped" });
|
|
}
|
|
|
|
resumeMedia() {
|
|
this.messageManager.sendAsyncMessage("AudioPlayback", { type: "resumeMedia" });
|
|
if (this._hasAnyPlayingMediaBeenBlocked) {
|
|
this._hasAnyPlayingMediaBeenBlocked = false;
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("DOMAudioPlaybackBlockStopped", true, false);
|
|
this.dispatchEvent(event);
|
|
}
|
|
}
|
|
|
|
unselectedTabHover(hovered) {
|
|
if (!this._shouldSendUnselectedTabHover) {
|
|
return;
|
|
}
|
|
this.messageManager.sendAsyncMessage("Browser:UnselectedTabHover", { hovered });
|
|
}
|
|
|
|
didStartLoadSinceLastUserTyping() {
|
|
return !this.inLoadURI &&
|
|
this.urlbarChangeTracker._startedLoadSinceLastUserTyping;
|
|
}
|
|
|
|
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 = Cc["@mozilla.org/remote-web-navigation;1"]
|
|
.createInstance(Ci.nsIWebNavigation);
|
|
this._remoteWebNavigationImpl = this._remoteWebNavigation.wrappedJSObject;
|
|
this._remoteWebNavigationImpl.swapBrowser(this);
|
|
|
|
// Initialize contentPrincipal to the about:blank principal for this loadcontext
|
|
let { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
|
|
let aboutBlank = Services.io.newURI("about:blank");
|
|
let ssm = Services.scriptSecurityManager;
|
|
this._contentPrincipal = ssm.getLoadContextCodebasePrincipal(aboutBlank, this.loadContext);
|
|
|
|
this.messageManager.addMessageListener("Browser:Init", this);
|
|
this.messageManager.addMessageListener("DOMTitleChanged", this);
|
|
this.messageManager.addMessageListener("ImageDocumentLoaded", this);
|
|
this.messageManager.addMessageListener("FullZoomChange", this);
|
|
this.messageManager.addMessageListener("TextZoomChange", this);
|
|
this.messageManager.addMessageListener("ZoomChangeUsingMouseWheel", this);
|
|
|
|
// browser-child messages, such as Content:LocationChange, are handled in
|
|
// RemoteWebProgress, ensure it is loaded and ready.
|
|
let jsm = "resource://gre/modules/RemoteWebProgress.jsm";
|
|
let { RemoteWebProgressManager } = ChromeUtils.import(jsm, {});
|
|
|
|
let oldManager = this._remoteWebProgressManager;
|
|
this._remoteWebProgressManager = new RemoteWebProgressManager(this);
|
|
if (oldManager) {
|
|
// We're transitioning from one remote type to another. This means that
|
|
// the RemoteWebProgress listener is listening to the old message manager,
|
|
// and needs to be pointed at the new one.
|
|
this._remoteWebProgressManager.swapListeners(oldManager);
|
|
}
|
|
|
|
this._remoteWebProgress = this._remoteWebProgressManager.topLevelWebProgress;
|
|
|
|
this.messageManager.loadFrameScript("chrome://global/content/browser-child.js", true);
|
|
|
|
if (this.hasAttribute("selectmenulist")) {
|
|
this.messageManager.addMessageListener("Forms:ShowDropDown", this);
|
|
this.messageManager.addMessageListener("Forms:HideDropDown", this);
|
|
}
|
|
|
|
if (!this.hasAttribute("disablehistory")) {
|
|
Services.obs.addObserver(this.observer, "browser:purge-session-history", true);
|
|
}
|
|
|
|
let rc_js = "resource://gre/modules/RemoteController.js";
|
|
let scope = {};
|
|
Services.scriptloader.loadSubScript(rc_js, scope);
|
|
let RemoteController = scope.RemoteController;
|
|
this._controller = new RemoteController(this);
|
|
this.controllers.appendController(this._controller);
|
|
}
|
|
|
|
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.useGlobalHistory = true;
|
|
} catch (ex) {
|
|
// This can occur if the Places database is locked
|
|
Cu.reportError("Error enabling browser global history: " + ex);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
}
|
|
try {
|
|
// Ensures the securityUI is initialized.
|
|
var securityUI = this.securityUI; // eslint-disable-line no-unused-vars
|
|
} catch (e) {}
|
|
|
|
// tabbrowser.xml sets "sameProcessAsFrameLoader" as a direct property
|
|
// on some browsers before they are put into a DOM (and get a
|
|
// binding). This hack makes sure that we hold a weak reference to
|
|
// the other browser (and go through the proper getter and setter).
|
|
if (this.hasOwnProperty("sameProcessAsFrameLoader")) {
|
|
var sameProcessAsFrameLoader = this.sameProcessAsFrameLoader;
|
|
delete this.sameProcessAsFrameLoader;
|
|
this.sameProcessAsFrameLoader = sameProcessAsFrameLoader;
|
|
}
|
|
|
|
if (!this.isRemoteBrowser) {
|
|
// If we've transitioned from remote to non-remote, we'll give up trying to
|
|
// keep the web progress listeners persisted during the transition.
|
|
delete this._remoteWebProgressManager;
|
|
delete this._remoteWebProgress;
|
|
|
|
this.addEventListener("pagehide", this.onPageHide, true);
|
|
}
|
|
|
|
if (this.messageManager) {
|
|
this.messageManager.addMessageListener("PopupBlocking:UpdateBlockedPopups", this);
|
|
this.messageManager.addMessageListener("Autoscroll:Start", this);
|
|
this.messageManager.addMessageListener("Autoscroll:Cancel", this);
|
|
this.messageManager.addMessageListener("AudioPlayback:Start", this);
|
|
this.messageManager.addMessageListener("AudioPlayback:Stop", this);
|
|
this.messageManager.addMessageListener("AudioPlayback:ActiveMediaBlockStart", this);
|
|
this.messageManager.addMessageListener("AudioPlayback:ActiveMediaBlockStop", this);
|
|
this.messageManager.addMessageListener("UnselectedTabHover:Toggle", this);
|
|
this.messageManager.addMessageListener("GloballyAutoplayBlocked", this);
|
|
|
|
if (this.hasAttribute("selectmenulist")) {
|
|
this.messageManager.addMessageListener("Forms:ShowDropDown", this);
|
|
this.messageManager.addMessageListener("Forms:HideDropDown", this);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is necessary because the destructor doesn't always get called when
|
|
* we are removed from a tabbrowser. This will be explicitly called by tabbrowser.
|
|
*/
|
|
destroy() {
|
|
elementsToDestroyOnUnload.delete(this);
|
|
|
|
// Make sure that any open select is closed.
|
|
if (this._selectParentHelper) {
|
|
let menulist = document.getElementById(this.getAttribute("selectmenulist"));
|
|
this._selectParentHelper.hide(menulist, this);
|
|
}
|
|
|
|
this.resetFields();
|
|
|
|
if (!this.mInitialized)
|
|
return;
|
|
|
|
this.mInitialized = false;
|
|
|
|
if (this.isRemoteBrowser) {
|
|
try {
|
|
this.controllers.removeController(this._controller);
|
|
} catch (ex) {
|
|
// This can fail when this browser element is not attached to a
|
|
// BrowserDOMWindow.
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.lastURI = null;
|
|
|
|
if (!this.isRemoteBrowser) {
|
|
this.removeEventListener("pagehide", this.onPageHide, true);
|
|
}
|
|
|
|
if (this._autoScrollNeedsCleanup) {
|
|
// we polluted the global scope, so clean it up
|
|
this._autoScrollPopup.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We call this _receiveMessage (and alias receiveMessage to it) so that
|
|
* bindings that inherit from this one can delegate to it.
|
|
*/
|
|
_receiveMessage(aMessage) {
|
|
let data = aMessage.data;
|
|
switch (aMessage.name) {
|
|
case "PopupBlocking:UpdateBlockedPopups":
|
|
{
|
|
this.blockedPopups = {
|
|
length: data.count,
|
|
reported: !data.freshPopup,
|
|
};
|
|
|
|
this.updateBlockedPopups();
|
|
break;
|
|
}
|
|
case "Autoscroll:Start":
|
|
{
|
|
if (!this.autoscrollEnabled) {
|
|
return { autoscrollEnabled: false, usingApz: false };
|
|
}
|
|
this.startScroll(data.scrolldir, data.screenX, data.screenY);
|
|
let usingApz = false;
|
|
if (this.isRemoteBrowser && data.scrollId != null &&
|
|
this.mPrefs.getBoolPref("apz.autoscroll.enabled", false)) {
|
|
let { tabParent } = this.frameLoader;
|
|
if (tabParent) {
|
|
// 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.
|
|
var os = Services.obs;
|
|
os.addObserver(this.observer, "apz:cancel-autoscroll", true);
|
|
|
|
usingApz = tabParent.startApzAutoscroll(
|
|
data.screenX, data.screenY,
|
|
data.scrollId, data.presShellId);
|
|
}
|
|
// Save the IDs for later
|
|
this._autoScrollScrollId = data.scrollId;
|
|
this._autoScrollPresShellId = data.presShellId;
|
|
}
|
|
return { autoscrollEnabled: true, usingApz };
|
|
}
|
|
case "Autoscroll:Cancel":
|
|
this._autoScrollPopup.hidePopup();
|
|
break;
|
|
case "AudioPlayback:Start":
|
|
this.audioPlaybackStarted();
|
|
break;
|
|
case "AudioPlayback:Stop":
|
|
this.audioPlaybackStopped();
|
|
break;
|
|
case "AudioPlayback:ActiveMediaBlockStart":
|
|
this.activeMediaBlockStarted();
|
|
break;
|
|
case "AudioPlayback:ActiveMediaBlockStop":
|
|
this.activeMediaBlockStopped();
|
|
break;
|
|
case "UnselectedTabHover:Toggle":
|
|
this._shouldSendUnselectedTabHover = data.enable ?
|
|
++this._unselectedTabHoverMessageListenerCount > 0 :
|
|
--this._unselectedTabHoverMessageListenerCount == 0;
|
|
break;
|
|
case "GloballyAutoplayBlocked":
|
|
this.notifyGloballyAutoplayBlocked();
|
|
break;
|
|
case "Forms:ShowDropDown":
|
|
{
|
|
if (!this._selectParentHelper) {
|
|
this._selectParentHelper =
|
|
ChromeUtils.import("resource://gre/modules/SelectParentHelper.jsm", {}).SelectParentHelper;
|
|
}
|
|
|
|
let menulist = document.getElementById(this.getAttribute("selectmenulist"));
|
|
menulist.menupopup.style.direction = data.direction;
|
|
this._selectParentHelper.populate(menulist, data.options, data.selectedIndex, this._fullZoom,
|
|
data.uaSelectBackgroundColor, data.uaSelectColor,
|
|
data.selectBackgroundColor, data.selectColor, data.selectTextShadow);
|
|
this._selectParentHelper.open(this, menulist, data.rect, data.isOpenedViaTouch);
|
|
break;
|
|
}
|
|
|
|
case "Forms:HideDropDown":
|
|
{
|
|
if (this._selectParentHelper) {
|
|
let menulist = document.getElementById(this.getAttribute("selectmenulist"));
|
|
this._selectParentHelper.hide(menulist, this);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
receiveMessage(aMessage) {
|
|
if (!this.isRemoteBrowser) {
|
|
return this._receiveMessage(aMessage);
|
|
}
|
|
|
|
let data = aMessage.data;
|
|
switch (aMessage.name) {
|
|
case "Browser:Init":
|
|
this._outerWindowID = data.outerWindowID;
|
|
this._browsingContextId = data.browsingContextId;
|
|
break;
|
|
case "DOMTitleChanged":
|
|
this._contentTitle = data.title;
|
|
break;
|
|
case "ImageDocumentLoaded":
|
|
this._imageDocument = {
|
|
width: data.width,
|
|
height: data.height,
|
|
};
|
|
break;
|
|
|
|
case "Forms:ShowDropDown":
|
|
{
|
|
if (!this._selectParentHelper) {
|
|
this._selectParentHelper =
|
|
ChromeUtils.import("resource://gre/modules/SelectParentHelper.jsm", {}).SelectParentHelper;
|
|
}
|
|
|
|
let menulist = document.getElementById(this.getAttribute("selectmenulist"));
|
|
menulist.menupopup.style.direction = data.direction;
|
|
|
|
let zoom = Services.prefs.getBoolPref("browser.zoom.full") ||
|
|
this.isSyntheticDocument ? this._fullZoom : this._textZoom;
|
|
this._selectParentHelper.populate(menulist, data.options, data.selectedIndex,
|
|
zoom,
|
|
data.uaSelectBackgroundColor, data.uaSelectColor,
|
|
data.selectBackgroundColor, data.selectColor, data.selectTextShadow);
|
|
this._selectParentHelper.open(this, menulist, data.rect, data.isOpenedViaTouch);
|
|
break;
|
|
}
|
|
|
|
case "FullZoomChange":
|
|
{
|
|
this._fullZoom = data.value;
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("FullZoomChange", true, false);
|
|
this.dispatchEvent(event);
|
|
break;
|
|
}
|
|
|
|
case "TextZoomChange":
|
|
{
|
|
this._textZoom = data.value;
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("TextZoomChange", true, false);
|
|
this.dispatchEvent(event);
|
|
break;
|
|
}
|
|
|
|
case "ZoomChangeUsingMouseWheel":
|
|
{
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("ZoomChangeUsingMouseWheel", true, false);
|
|
this.dispatchEvent(event);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
return this._receiveMessage(aMessage);
|
|
}
|
|
return undefined;
|
|
|
|
}
|
|
|
|
enableDisableCommandsRemoteOnly(aAction, aEnabledLength, aEnabledCommands, aDisabledLength, aDisabledCommands) {
|
|
if (this._controller) {
|
|
this._controller.enableDisableCommands(aAction,
|
|
aEnabledLength, aEnabledCommands,
|
|
aDisabledLength, aDisabledCommands);
|
|
}
|
|
}
|
|
|
|
purgeSessionHistory() {
|
|
if (this.isRemoteBrowser) {
|
|
try {
|
|
this.messageManager.sendAsyncMessage("Browser:PurgeSessionHistory");
|
|
} catch (ex) {
|
|
// This can throw if the browser has started to go away.
|
|
if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
|
|
throw ex;
|
|
}
|
|
}
|
|
this._remoteWebNavigationImpl.canGoBack = false;
|
|
this._remoteWebNavigationImpl.canGoForward = false;
|
|
return;
|
|
}
|
|
this.messageManager.sendAsyncMessage("Browser:PurgeSessionHistory");
|
|
}
|
|
|
|
createAboutBlankContentViewer(aPrincipal) {
|
|
if (this.isRemoteBrowser) {
|
|
// Ensure that the content process has the permissions which are
|
|
// needed to create a document with the given principal.
|
|
let permissionPrincipal =
|
|
BrowserUtils.principalWithMatchingOA(aPrincipal, this.contentPrincipal);
|
|
this.frameLoader.tabParent.transmitPermissionsForPrincipal(permissionPrincipal);
|
|
|
|
// Create the about blank content viewer in the content process
|
|
this.messageManager.sendAsyncMessage("Browser:CreateAboutBlank", aPrincipal);
|
|
return;
|
|
}
|
|
let principal = BrowserUtils.principalWithMatchingOA(aPrincipal, this.contentPrincipal);
|
|
this.docShell.createAboutBlankContentViewer(principal);
|
|
}
|
|
|
|
stopScroll() {
|
|
if (this._scrolling) {
|
|
this._scrolling = false;
|
|
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);
|
|
this.messageManager.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.isRemoteBrowser && this._autoScrollScrollId != null) {
|
|
let { tabParent } = this.frameLoader;
|
|
if (tabParent) {
|
|
tabParent.stopApzAutoscroll(this._autoScrollScrollId,
|
|
this._autoScrollPresShellId);
|
|
}
|
|
this._autoScrollScrollId = null;
|
|
this._autoScrollPresShellId = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
_createAutoScrollPopup() {
|
|
var popup = document.createXULElement("panel");
|
|
popup.className = "autoscroller";
|
|
// We set this attribute on the element so that mousemove
|
|
// events can be handled by browser-content.js.
|
|
popup.setAttribute("mousethrough", "always");
|
|
popup.setAttribute("consumeoutsideclicks", "true");
|
|
popup.setAttribute("rolluponmousewheel", "true");
|
|
popup.setAttribute("hidden", "true");
|
|
return popup;
|
|
}
|
|
|
|
startScroll(scrolldir, screenX, screenY) {
|
|
const POPUP_SIZE = 32;
|
|
if (!this._autoScrollPopup) {
|
|
if (this.hasAttribute("autoscrollpopup")) {
|
|
// our creator provided a popup to share
|
|
this._autoScrollPopup = document.getElementById(this.getAttribute("autoscrollpopup"));
|
|
} else {
|
|
// we weren't provided a popup; we have to use the global scope
|
|
this._autoScrollPopup = this._createAutoScrollPopup();
|
|
document.documentElement.appendChild(this._autoScrollPopup);
|
|
this._autoScrollNeedsCleanup = true;
|
|
}
|
|
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";
|
|
}
|
|
|
|
let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
|
|
.getService(Ci.nsIScreenManager);
|
|
let screen = screenManager.screenForRect(screenX, screenY, 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", /Win|Mac/.test(navigator.platform));
|
|
}
|
|
|
|
this._autoScrollPopup.setAttribute("scrolldir", scrolldir);
|
|
this._autoScrollPopup.addEventListener("popuphidden", this, true);
|
|
|
|
// 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. Unfortunately `window.screen.availLeft` can be negative
|
|
// on Windows in multi-monitor setups, so we use nsIScreenManager instead:
|
|
let left = {},
|
|
top = {},
|
|
width = {},
|
|
height = {};
|
|
screen.GetAvailRect(left, top, width, height);
|
|
|
|
// We need to get screen CSS-pixel (rather than display-pixel) coordinates.
|
|
// With 175% DPI, the actual ratio of display pixels to CSS pixels is
|
|
// 1.7647 because of rounding inside gecko. Unfortunately defaultCSSScaleFactor
|
|
// returns the original 1.75 dpi factor. While window.devicePixelRatio would
|
|
// get us the correct ratio, if the window is split between 2 screens,
|
|
// window.devicePixelRatio isn't guaranteed to match the screen we're
|
|
// autoscrolling on. So instead we do the same math as Gecko.
|
|
const scaleFactor = 60 / Math.round(60 / screen.defaultCSSScaleFactor);
|
|
let minX = left.value / scaleFactor + 0.5 * POPUP_SIZE;
|
|
let maxX = (left.value + width.value) / scaleFactor - 0.5 * POPUP_SIZE;
|
|
let minY = top.value / scaleFactor + 0.5 * POPUP_SIZE;
|
|
let maxY = (top.value + height.value) / scaleFactor - 0.5 * POPUP_SIZE;
|
|
let popupX = Math.max(minX, Math.min(maxX, screenX));
|
|
let popupY = Math.max(minY, Math.min(maxY, screenY));
|
|
this._autoScrollPopup.openPopupAtScreen(popupX, popupY);
|
|
this._ignoreMouseEvents = true;
|
|
this._scrolling = true;
|
|
this._startX = screenX;
|
|
this._startY = screenY;
|
|
|
|
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);
|
|
}
|
|
|
|
handleEvent(aEvent) {
|
|
if (this._scrolling) {
|
|
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":
|
|
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":
|
|
{
|
|
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.
|
|
}
|
|
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, aFlags) {
|
|
// 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, aFlags);
|
|
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 swpDocShells 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",
|
|
];
|
|
|
|
if (this.isRemoteBrowser) {
|
|
fieldsToSwap.push(...[
|
|
"_remoteWebNavigation",
|
|
"_remoteWebNavigationImpl",
|
|
"_remoteWebProgressManager",
|
|
"_remoteWebProgress",
|
|
"_remoteFinder",
|
|
"_securityUI",
|
|
"_documentURI",
|
|
"_documentContentType",
|
|
"_contentTitle",
|
|
"_characterSet",
|
|
"_mayEnableCharacterEncodingMenu",
|
|
"_contentPrincipal",
|
|
"_imageDocument",
|
|
"_fullZoom",
|
|
"_textZoom",
|
|
"_isSyntheticDocument",
|
|
"_innerWindowID",
|
|
]);
|
|
}
|
|
|
|
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._remoteWebNavigationImpl.swapBrowser(this);
|
|
aOtherBrowser._remoteWebNavigationImpl.swapBrowser(aOtherBrowser);
|
|
|
|
if (this._remoteWebProgressManager && aOtherBrowser._remoteWebProgressManager) {
|
|
this._remoteWebProgressManager.swapBrowser(this);
|
|
aOtherBrowser._remoteWebProgressManager.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 id = this._permitUnloadId++;
|
|
let mm = this.messageManager;
|
|
mm.sendAsyncMessage("InPermitUnload", { id });
|
|
mm.addMessageListener("InPermitUnload", function listener(msg) {
|
|
if (msg.data.id != id) {
|
|
return;
|
|
}
|
|
aCallback(msg.data.inPermitUnload);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!this.docShell || !this.docShell.contentViewer) {
|
|
aCallback(false);
|
|
return;
|
|
}
|
|
aCallback(this.docShell.contentViewer.inPermitUnload);
|
|
}
|
|
|
|
permitUnload(aPermitUnloadFlags) {
|
|
if (this.isRemoteBrowser) {
|
|
let { tabParent } = this.frameLoader;
|
|
|
|
if (!tabParent.hasBeforeUnload) {
|
|
return { permitUnload: true, timedOut: false };
|
|
}
|
|
|
|
const kTimeout = 1000;
|
|
|
|
let finished = false;
|
|
let responded = false;
|
|
let permitUnload;
|
|
let id = this._permitUnloadId++;
|
|
let mm = this.messageManager;
|
|
let Services = ChromeUtils.import("resource://gre/modules/Services.jsm", {}).Services;
|
|
|
|
let msgListener = msg => {
|
|
if (msg.data.id != id) {
|
|
return;
|
|
}
|
|
if (msg.data.kind == "start") {
|
|
responded = true;
|
|
return;
|
|
}
|
|
done(msg.data.permitUnload);
|
|
};
|
|
|
|
let observer = subject => {
|
|
if (subject == mm) {
|
|
done(true);
|
|
}
|
|
};
|
|
|
|
function done(result) {
|
|
finished = true;
|
|
permitUnload = result;
|
|
mm.removeMessageListener("PermitUnload", msgListener);
|
|
Services.obs.removeObserver(observer, "message-manager-close");
|
|
}
|
|
|
|
mm.sendAsyncMessage("PermitUnload", { id, aPermitUnloadFlags });
|
|
mm.addMessageListener("PermitUnload", msgListener);
|
|
Services.obs.addObserver(observer, "message-manager-close");
|
|
|
|
let timedOut = false;
|
|
|
|
function timeout() {
|
|
if (!responded) {
|
|
timedOut = true;
|
|
}
|
|
|
|
// Dispatch something to ensure that the main thread wakes up.
|
|
Services.tm.dispatchToMainThread(function() {});
|
|
}
|
|
|
|
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
timer.initWithCallback(timeout, kTimeout, timer.TYPE_ONE_SHOT);
|
|
|
|
while (!finished && !timedOut) {
|
|
Services.tm.currentThread.processNextEvent(true);
|
|
}
|
|
|
|
return { permitUnload, timedOut };
|
|
}
|
|
|
|
if (!this.docShell || !this.docShell.contentViewer) {
|
|
return { permitUnload: true, timedOut: false };
|
|
}
|
|
return {
|
|
permitUnload: this.docShell.contentViewer.permitUnload(aPermitUnloadFlags),
|
|
timedOut: false,
|
|
};
|
|
}
|
|
|
|
print(aOuterWindowID, aPrintSettings, aPrintProgressListener) {
|
|
if (!this.frameLoader) {
|
|
throw Components.Exception("No frame loader.",
|
|
Cr.NS_ERROR_FAILURE);
|
|
}
|
|
|
|
this.frameLoader.print(aOuterWindowID, aPrintSettings,
|
|
aPrintProgressListener);
|
|
}
|
|
|
|
drawSnapshot(x, y, w, h, scale, backgroundColor) {
|
|
if (!this.frameLoader) {
|
|
throw Components.Exception("No frame loader.",
|
|
Cr.NS_ERROR_FAILURE);
|
|
}
|
|
return this.frameLoader.drawSnapshot(x, y, w, h, scale, backgroundColor);
|
|
}
|
|
|
|
dropLinks(aLinksCount, aLinks, aTriggeringPrincipal) {
|
|
if (!this.droppedLinkHandler) {
|
|
return false;
|
|
}
|
|
let links = [];
|
|
for (let i = 0; i < aLinksCount; i += 3) {
|
|
links.push({
|
|
url: aLinks[i],
|
|
name: aLinks[i + 1],
|
|
type: aLinks[i + 2],
|
|
});
|
|
}
|
|
this.droppedLinkHandler(null, links, aTriggeringPrincipal);
|
|
return true;
|
|
}
|
|
|
|
getContentBlockingLog() {
|
|
if (this.isRemoteBrowser) {
|
|
return this.frameLoader.tabParent.getContentBlockingLog();
|
|
}
|
|
return this.docShell ?
|
|
this.docShell.getContentBlockingLog() :
|
|
Promise.reject("docshell isn't available");
|
|
}
|
|
}
|
|
|
|
MozXULElement.implementCustomInterface(MozBrowser, [Ci.nsIBrowser, Ci.nsIFrameLoaderOwner]);
|
|
customElements.define("browser", MozBrowser);
|
|
|
|
}
|