gecko-dev/dom/browser-element/BrowserElementChildPreload.js

1263 строки
40 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";
function debug(msg) {
// dump("BrowserElementChildPreload - " + msg + "\n");
}
debug("loaded");
var BrowserElementIsReady;
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/BrowserElementPromptService.jsm");
var kLongestReturnedString = 128;
var Timer = Components.Constructor("@mozilla.org/timer;1",
"nsITimer",
"initWithCallback");
function sendAsyncMsg(msg, data) {
// Ensure that we don't send any messages before BrowserElementChild.js
// finishes loading.
if (!BrowserElementIsReady) {
return;
}
if (!data) {
data = { };
}
data.msg_name = msg;
sendAsyncMessage('browser-element-api:call', data);
}
function sendSyncMsg(msg, data) {
// Ensure that we don't send any messages before BrowserElementChild.js
// finishes loading.
if (!BrowserElementIsReady) {
return;
}
if (!data) {
data = { };
}
data.msg_name = msg;
return sendSyncMessage('browser-element-api:call', data);
}
var CERTIFICATE_ERROR_PAGE_PREF = 'security.alternate_certificate_error_page';
var OBSERVED_EVENTS = [
'xpcom-shutdown',
'audio-playback',
'activity-done',
'will-launch-app'
];
var LISTENED_EVENTS = [
{ type: "DOMTitleChanged", useCapture: true, wantsUntrusted: false },
{ type: "DOMLinkAdded", useCapture: true, wantsUntrusted: false },
{ type: "MozScrolledAreaChanged", useCapture: true, wantsUntrusted: false },
{ type: "MozDOMFullscreen:Request", useCapture: true, wantsUntrusted: false },
{ type: "MozDOMFullscreen:NewOrigin", useCapture: true, wantsUntrusted: false },
{ type: "MozDOMFullscreen:Exit", useCapture: true, wantsUntrusted: false },
{ type: "DOMMetaAdded", useCapture: true, wantsUntrusted: false },
{ type: "DOMMetaChanged", useCapture: true, wantsUntrusted: false },
{ type: "DOMMetaRemoved", useCapture: true, wantsUntrusted: false },
{ type: "scrollviewchange", useCapture: true, wantsUntrusted: false },
{ type: "click", useCapture: false, wantsUntrusted: false },
// This listens to unload events from our message manager, but /not/ from
// the |content| window. That's because the window's unload event doesn't
// bubble, and we're not using a capturing listener. If we'd used
// useCapture == true, we /would/ hear unload events from the window, which
// is not what we want!
{ type: "unload", useCapture: false, wantsUntrusted: false },
];
// We are using the system group for those events so if something in the
// content called .stopPropagation() this will still be called.
var LISTENED_SYSTEM_EVENTS = [
{ type: "DOMWindowClose", useCapture: false },
{ type: "DOMWindowCreated", useCapture: false },
{ type: "DOMWindowResize", useCapture: false },
{ type: "contextmenu", useCapture: false },
{ type: "scroll", useCapture: false },
];
/**
* The BrowserElementChild implements one half of <iframe mozbrowser>.
* (The other half is, unsurprisingly, BrowserElementParent.)
*
* This script is injected into an <iframe mozbrowser> via
* nsIMessageManager::LoadFrameScript().
*
* Our job here is to listen for events within this frame and bubble them up to
* the parent process.
*/
var global = this;
function BrowserElementChild() {
// Maps outer window id --> weak ref to window. Used by modal dialog code.
this._windowIDDict = {};
this._isContentWindowCreated = false;
this._init();
};
BrowserElementChild.prototype = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
_init: function() {
debug("Starting up.");
BrowserElementPromptService.mapWindowToBrowserElementChild(content, this);
docShell.QueryInterface(Ci.nsIWebProgress)
.addProgressListener(this._progressListener,
Ci.nsIWebProgress.NOTIFY_LOCATION |
Ci.nsIWebProgress.NOTIFY_SECURITY |
Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
if (!webNavigation.sessionHistory) {
// XXX(nika): I don't think this code should ever be hit? We should run
// TabChild::Init before we run this code which will perform this setup
// for us.
docShell.initSessionHistory();
}
// This is necessary to get security web progress notifications.
var securityUI = Cc['@mozilla.org/secure_browser_ui;1']
.createInstance(Ci.nsISecureBrowserUI);
securityUI.init(docShell);
// A cache of the menuitem dom objects keyed by the id we generate
// and pass to the embedder
this._ctxHandlers = {};
// Counter of contextmenu events fired
this._ctxCounter = 0;
this._shuttingDown = false;
LISTENED_EVENTS.forEach(event => {
addEventListener(event.type, this, event.useCapture, event.wantsUntrusted);
});
// Registers a MozAfterPaint handler for the very first paint.
this._addMozAfterPaintHandler(function () {
sendAsyncMsg('firstpaint');
});
addMessageListener("browser-element-api:call", this);
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
LISTENED_SYSTEM_EVENTS.forEach(event => {
els.addSystemEventListener(global, event.type, this, event.useCapture);
});
OBSERVED_EVENTS.forEach((aTopic) => {
Services.obs.addObserver(this, aTopic);
});
},
/**
* Shut down the frame's side of the browser API. This is called when:
* - our TabChildGlobal starts to die
* - the content is moved to frame without the browser API
* This is not called when the page inside |content| unloads.
*/
destroy: function() {
debug("Destroying");
this._shuttingDown = true;
BrowserElementPromptService.unmapWindowToBrowserElementChild(content);
docShell.QueryInterface(Ci.nsIWebProgress)
.removeProgressListener(this._progressListener);
LISTENED_EVENTS.forEach(event => {
removeEventListener(event.type, this, event.useCapture, event.wantsUntrusted);
});
removeMessageListener("browser-element-api:call", this);
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
LISTENED_SYSTEM_EVENTS.forEach(event => {
els.removeSystemEventListener(global, event.type, this, event.useCapture);
});
OBSERVED_EVENTS.forEach((aTopic) => {
Services.obs.removeObserver(this, aTopic);
});
},
handleEvent: function(event) {
switch (event.type) {
case "DOMTitleChanged":
this._titleChangedHandler(event);
break;
case "DOMLinkAdded":
this._linkAddedHandler(event);
break;
case "MozScrolledAreaChanged":
this._mozScrollAreaChanged(event);
break;
case "MozDOMFullscreen:Request":
this._mozRequestedDOMFullscreen(event);
break;
case "MozDOMFullscreen:NewOrigin":
this._mozFullscreenOriginChange(event);
break;
case "MozDOMFullscreen:Exit":
this._mozExitDomFullscreen(event);
break;
case "DOMMetaAdded":
this._metaChangedHandler(event);
break;
case "DOMMetaChanged":
this._metaChangedHandler(event);
break;
case "DOMMetaRemoved":
this._metaChangedHandler(event);
break;
case "scrollviewchange":
this._ScrollViewChangeHandler(event);
break;
case "click":
this._ClickHandler(event);
break;
case "unload":
this.destroy(event);
break;
case "DOMWindowClose":
this._windowCloseHandler(event);
break;
case "DOMWindowCreated":
this._windowCreatedHandler(event);
break;
case "DOMWindowResize":
this._windowResizeHandler(event);
break;
case "contextmenu":
this._contextmenuHandler(event);
break;
case "scroll":
this._scrollEventHandler(event);
break;
}
},
receiveMessage: function(message) {
let self = this;
let mmCalls = {
"send-mouse-event": this._recvSendMouseEvent,
"get-can-go-back": this._recvCanGoBack,
"get-can-go-forward": this._recvCanGoForward,
"go-back": this._recvGoBack,
"go-forward": this._recvGoForward,
"reload": this._recvReload,
"stop": this._recvStop,
"zoom": this._recvZoom,
"unblock-modal-prompt": this._recvStopWaiting,
"fire-ctx-callback": this._recvFireCtxCallback,
"owner-visibility-change": this._recvOwnerVisibilityChange,
"entered-fullscreen": this._recvEnteredFullscreen,
"exit-fullscreen": this._recvExitFullscreen,
}
if (message.data.msg_name in mmCalls) {
return mmCalls[message.data.msg_name].apply(self, arguments);
}
},
_paintFrozenTimer: null,
observe: function(subject, topic, data) {
// Ignore notifications not about our document. (Note that |content| /can/
// be null; see bug 874900.)
if (topic !== 'activity-done' &&
topic !== 'audio-playback' &&
topic !== 'will-launch-app' &&
(!content || subject !== content.document)) {
return;
}
if (topic == 'activity-done' && docShell !== subject)
return;
switch (topic) {
case 'activity-done':
sendAsyncMsg('activitydone', { success: (data == 'activity-success') });
break;
case 'audio-playback':
if (subject === content) {
sendAsyncMsg('audioplaybackchange', { _payload_: data });
}
break;
case 'xpcom-shutdown':
this._shuttingDown = true;
break;
case 'will-launch-app':
// If the launcher is not visible, let's ignore the message.
if (!docShell.isActive) {
return;
}
// If this is not a content process, let's not freeze painting.
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_CONTENT) {
return;
}
docShell.contentViewer.pausePainting();
this._paintFrozenTimer && this._paintFrozenTimer.cancel();
this._paintFrozenTimer = new Timer(this, 3000, Ci.nsITimer.TYPE_ONE_SHOT);
break;
}
},
notify: function(timer) {
docShell.contentViewer.resumePainting();
this._paintFrozenTimer.cancel();
this._paintFrozenTimer = null;
},
get _windowUtils() {
return content.document.defaultView.windowUtils;
},
_tryGetInnerWindowID: function(win) {
let utils = win.windowUtils;
try {
return utils.currentInnerWindowID;
}
catch(e) {
return null;
}
},
/**
* Show a modal prompt. Called by BrowserElementPromptService.
*/
showModalPrompt: function(win, args) {
let utils = win.windowUtils;
args.windowID = { outer: utils.outerWindowID,
inner: this._tryGetInnerWindowID(win) };
sendAsyncMsg('showmodalprompt', args);
let returnValue = this._waitForResult(win);
if (args.promptType == 'prompt' ||
args.promptType == 'confirm' ||
args.promptType == 'custom-prompt') {
return returnValue;
}
},
/**
* Spin in a nested event loop until we receive a unblock-modal-prompt message for
* this window.
*/
_waitForResult: function(win) {
debug("_waitForResult(" + win + ")");
let utils = win.windowUtils;
let outerWindowID = utils.outerWindowID;
let innerWindowID = this._tryGetInnerWindowID(win);
if (innerWindowID === null) {
// I have no idea what waiting for a result means when there's no inner
// window, so let's just bail.
debug("_waitForResult: No inner window. Bailing.");
return;
}
this._windowIDDict[outerWindowID] = Cu.getWeakReference(win);
debug("Entering modal state (outerWindowID=" + outerWindowID + ", " +
"innerWindowID=" + innerWindowID + ")");
utils.enterModalState();
// We'll decrement win.modalDepth when we receive a unblock-modal-prompt message
// for the window.
if (!win.modalDepth) {
win.modalDepth = 0;
}
win.modalDepth++;
let origModalDepth = win.modalDepth;
debug("Nested event loop - begin");
Services.tm.spinEventLoopUntil(() => {
// Bail out of the loop if the inner window changed; that means the
// window navigated. Bail out when we're shutting down because otherwise
// we'll leak our window.
if (this._tryGetInnerWindowID(win) !== innerWindowID) {
debug("_waitForResult: Inner window ID changed " +
"while in nested event loop.");
return true;
}
return win.modalDepth !== origModalDepth || this._shuttingDown;
});
debug("Nested event loop - finish");
if (win.modalDepth == 0) {
delete this._windowIDDict[outerWindowID];
}
// If we exited the loop because the inner window changed, then bail on the
// modal prompt.
if (innerWindowID !== this._tryGetInnerWindowID(win)) {
throw Components.Exception("Modal state aborted by navigation",
Cr.NS_ERROR_NOT_AVAILABLE);
}
let returnValue = win.modalReturnValue;
delete win.modalReturnValue;
if (!this._shuttingDown) {
utils.leaveModalState();
}
debug("Leaving modal state (outerID=" + outerWindowID + ", " +
"innerID=" + innerWindowID + ")");
return returnValue;
},
_recvStopWaiting: function(msg) {
let outerID = msg.json.windowID.outer;
let innerID = msg.json.windowID.inner;
let returnValue = msg.json.returnValue;
debug("recvStopWaiting(outer=" + outerID + ", inner=" + innerID +
", returnValue=" + returnValue + ")");
if (!this._windowIDDict[outerID]) {
debug("recvStopWaiting: No record of outer window ID " + outerID);
return;
}
let win = this._windowIDDict[outerID].get();
if (!win) {
debug("recvStopWaiting, but window is gone\n");
return;
}
if (innerID !== this._tryGetInnerWindowID(win)) {
debug("recvStopWaiting, but inner ID has changed\n");
return;
}
debug("recvStopWaiting " + win);
win.modalReturnValue = returnValue;
win.modalDepth--;
},
_recvEnteredFullscreen: function() {
if (!this._windowUtils.handleFullscreenRequests() &&
!content.document.fullscreenElement) {
// If we don't actually have any pending fullscreen request
// to handle, neither we have been in fullscreen, tell the
// parent to just exit.
sendAsyncMsg("exit-dom-fullscreen");
}
},
_recvExitFullscreen: function() {
this._windowUtils.exitFullscreen();
},
_titleChangedHandler: function(e) {
debug("Got titlechanged: (" + e.target.title + ")");
var win = e.target.defaultView;
// Ignore titlechanges which don't come from the top-level
// <iframe mozbrowser> window.
if (win == content) {
sendAsyncMsg('titlechange', { _payload_: e.target.title });
}
else {
debug("Not top level!");
}
},
_maybeCopyAttribute: function(src, target, attribute) {
if (src.getAttribute(attribute)) {
target[attribute] = src.getAttribute(attribute);
}
},
_iconChangedHandler: function(e) {
debug('Got iconchanged: (' + e.target.href + ')');
let icon = { href: e.target.href };
this._maybeCopyAttribute(e.target, icon, 'sizes');
this._maybeCopyAttribute(e.target, icon, 'rel');
sendAsyncMsg('iconchange', icon);
},
_openSearchHandler: function(e) {
debug('Got opensearch: (' + e.target.href + ')');
if (e.target.type !== "application/opensearchdescription+xml") {
return;
}
sendAsyncMsg('opensearch', { title: e.target.title,
href: e.target.href });
},
// Processes the "rel" field in <link> tags and forward to specific handlers.
_linkAddedHandler: function(e) {
let win = e.target.ownerGlobal;
// Ignore links which don't come from the top-level
// <iframe mozbrowser> window.
if (win != content) {
debug('Not top level!');
return;
}
let handlers = {
'icon': this._iconChangedHandler.bind(this),
'apple-touch-icon': this._iconChangedHandler.bind(this),
'apple-touch-icon-precomposed': this._iconChangedHandler.bind(this),
'search': this._openSearchHandler,
};
debug('Got linkAdded: (' + e.target.href + ') ' + e.target.rel);
e.target.rel.split(' ').forEach(function(x) {
let token = x.toLowerCase();
if (handlers[token]) {
handlers[token](e);
}
}, this);
},
_metaChangedHandler: function(e) {
let win = e.target.ownerGlobal;
// Ignore metas which don't come from the top-level
// <iframe mozbrowser> window.
if (win != content) {
debug('Not top level!');
return;
}
var name = e.target.name;
var property = e.target.getAttributeNS(null, "property");
if (!name && !property) {
return;
}
debug('Got metaChanged: (' + (name || property) + ') ' +
e.target.content);
let handlers = {
'viewmode': this._genericMetaHandler,
'theme-color': this._genericMetaHandler,
'theme-group': this._genericMetaHandler,
'application-name': this._applicationNameChangedHandler
};
let handler = handlers[name];
if ((property || name).match(/^og:/)) {
name = property || name;
handler = this._genericMetaHandler;
}
if (handler) {
handler(name, e.type, e.target);
}
},
_applicationNameChangedHandler: function(name, eventType, target) {
if (eventType !== 'DOMMetaAdded') {
// Bug 1037448 - Decide what to do when <meta name="application-name">
// changes
return;
}
let meta = { name: name,
content: target.content };
let lang;
let elm;
for (elm = target;
!lang && elm && elm.nodeType == target.ELEMENT_NODE;
elm = elm.parentNode) {
if (elm.hasAttribute('lang')) {
lang = elm.getAttribute('lang');
continue;
}
if (elm.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')) {
lang = elm.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang');
continue;
}
}
// No lang has been detected.
if (!lang && elm.nodeType == target.DOCUMENT_NODE) {
lang = elm.contentLanguage;
}
if (lang) {
meta.lang = lang;
}
sendAsyncMsg('metachange', meta);
},
_ScrollViewChangeHandler: function(e) {
e.stopPropagation();
let detail = {
state: e.state,
};
sendAsyncMsg('scrollviewchange', detail);
},
_ClickHandler: function(e) {
let isHTMLLink = node =>
((ChromeUtils.getClassName(node) === "HTMLAnchorElement" && node.href) ||
(ChromeUtils.getClassName(node) === "HTMLAreaElement" && node.href) ||
ChromeUtils.getClassName(node) === "HTMLLinkElement");
// Open in a new tab if middle click or ctrl/cmd-click,
// and e.target is a link or inside a link.
if ((Services.appinfo.OS == 'Darwin' && e.metaKey) ||
(Services.appinfo.OS != 'Darwin' && e.ctrlKey) ||
e.button == 1) {
let node = e.target;
while (node && !isHTMLLink(node)) {
node = node.parentNode;
}
if (node) {
sendAsyncMsg('opentab', {url: node.href});
}
}
},
_genericMetaHandler: function(name, eventType, target) {
let meta = {
name: name,
content: target.content,
type: eventType.replace('DOMMeta', '').toLowerCase()
};
sendAsyncMsg('metachange', meta);
},
_addMozAfterPaintHandler: function(callback) {
function onMozAfterPaint() {
let uri = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
if (uri.spec != "about:blank") {
debug("Got afterpaint event: " + uri.spec);
removeEventListener('MozAfterPaint', onMozAfterPaint,
/* useCapture = */ true);
callback();
}
}
addEventListener('MozAfterPaint', onMozAfterPaint, /* useCapture = */ true);
return onMozAfterPaint;
},
_windowCloseHandler: function(e) {
let win = e.target;
if (win != content || e.defaultPrevented) {
return;
}
debug("Closing window " + win);
sendAsyncMsg('close');
// Inform the window implementation that we handled this close ourselves.
e.preventDefault();
},
_windowCreatedHandler: function(e) {
let targetDocShell = e.target.defaultView.docShell;
if (targetDocShell != docShell) {
return;
}
let uri = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
debug("Window created: " + uri.spec);
if (uri.spec != "about:blank") {
this._addMozAfterPaintHandler(function () {
sendAsyncMsg('documentfirstpaint');
});
this._isContentWindowCreated = true;
}
},
_windowResizeHandler: function(e) {
let win = e.target;
if (win != content || e.defaultPrevented) {
return;
}
debug("resizing window " + win);
sendAsyncMsg('resize', { width: e.detail.width, height: e.detail.height });
// Inform the window implementation that we handled this resize ourselves.
e.preventDefault();
},
_contextmenuHandler: function(e) {
debug("Got contextmenu");
if (e.defaultPrevented) {
return;
}
this._ctxCounter++;
this._ctxHandlers = {};
var elem = e.target;
var menuData = {systemTargets: [], contextmenu: null};
var ctxMenuId = null;
var clipboardPlainTextOnly = Services.prefs.getBoolPref('clipboard.plainTextOnly');
var copyableElements = {
image: false,
link: false,
hasElements: function() {
return this.image || this.link;
}
};
// Set the event target as the copy image command needs it to
// determine what was context-clicked on.
docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit).setCommandNode(elem);
while (elem && elem.parentNode) {
var ctxData = this._getSystemCtxMenuData(elem);
if (ctxData) {
menuData.systemTargets.push({
nodeName: elem.nodeName,
data: ctxData
});
}
if (!ctxMenuId && 'hasAttribute' in elem && elem.hasAttribute('contextmenu')) {
ctxMenuId = elem.getAttribute('contextmenu');
}
// Enable copy image/link option
if (elem.nodeName == 'IMG') {
copyableElements.image = !clipboardPlainTextOnly;
} else if (elem.nodeName == 'A') {
copyableElements.link = true;
}
elem = elem.parentNode;
}
if (ctxMenuId || copyableElements.hasElements()) {
var menu = null;
if (ctxMenuId) {
menu = e.target.ownerDocument.getElementById(ctxMenuId);
}
menuData.contextmenu = this._buildMenuObj(menu, '', copyableElements);
}
// Pass along the position where the context menu should be located
menuData.clientX = e.clientX;
menuData.clientY = e.clientY;
menuData.screenX = e.screenX;
menuData.screenY = e.screenY;
// The value returned by the contextmenu sync call is true if the embedder
// called preventDefault() on its contextmenu event.
//
// We call preventDefault() on our contextmenu event if the embedder called
// preventDefault() on /its/ contextmenu event. This way, if the embedder
// ignored the contextmenu event, TabChild will fire a click.
if (sendSyncMsg('contextmenu', menuData)[0]) {
e.preventDefault();
} else {
this._ctxHandlers = {};
}
},
_getSystemCtxMenuData: function(elem) {
let documentURI =
docShell.QueryInterface(Ci.nsIWebNavigation).currentURI.spec;
if ((ChromeUtils.getClassName(elem) === "HTMLAnchorElement" && elem.href) ||
(ChromeUtils.getClassName(elem) === "HTMLAreaElement" && elem.href)) {
return {uri: elem.href,
documentURI: documentURI,
text: elem.textContent.substring(0, kLongestReturnedString)};
}
if (elem instanceof Ci.nsIImageLoadingContent &&
(elem.currentRequestFinalURI || elem.currentURI)) {
let uri = elem.currentRequestFinalURI || elem.currentURI;
return {uri: uri.spec, documentURI: documentURI};
}
if (ChromeUtils.getClassName(elem) === "HTMLImageElement") {
return {uri: elem.src, documentURI: documentURI};
}
if (ChromeUtils.getClassName(elem) === "HTMLVideoElement" ||
ChromeUtils.getClassName(elem) === "HTMLAudioElement") {
let hasVideo = !(elem.readyState >= elem.HAVE_METADATA &&
(elem.videoWidth == 0 || elem.videoHeight == 0));
return {uri: elem.currentSrc || elem.src,
hasVideo: hasVideo,
documentURI: documentURI};
}
if (ChromeUtils.getClassName(elem) === "HTMLInputElement" &&
elem.hasAttribute("name")) {
// For input elements, we look for a parent <form> and if there is
// one we return the form's method and action uri.
let parent = elem.parentNode;
while (parent) {
if (ChromeUtils.getClassName(parent) === "HTMLFormElement" &&
parent.hasAttribute("action")) {
let actionHref = docShell.QueryInterface(Ci.nsIWebNavigation)
.currentURI
.resolve(parent.getAttribute("action"));
let method = parent.hasAttribute("method")
? parent.getAttribute("method").toLowerCase()
: "get";
return {
documentURI: documentURI,
action: actionHref,
method: method,
name: elem.getAttribute("name"),
}
}
parent = parent.parentNode;
}
}
return false;
},
_scrollEventHandler: function(e) {
let win = e.target.defaultView;
if (win != content) {
return;
}
debug("scroll event " + win);
sendAsyncMsg("scroll", { top: win.scrollY, left: win.scrollX });
},
_mozScrollAreaChanged: function(e) {
sendAsyncMsg('scrollareachanged', {
width: e.width,
height: e.height
});
},
_mozRequestedDOMFullscreen: function(e) {
sendAsyncMsg("requested-dom-fullscreen");
},
_mozFullscreenOriginChange: function(e) {
sendAsyncMsg("fullscreen-origin-change", {
originNoSuffix: e.target.nodePrincipal.originNoSuffix
});
},
_mozExitDomFullscreen: function(e) {
sendAsyncMsg("exit-dom-fullscreen");
},
_recvFireCtxCallback: function(data) {
debug("Received fireCtxCallback message: (" + data.json.menuitem + ")");
let doCommandIfEnabled = (command) => {
if (docShell.isCommandEnabled(command)) {
docShell.doCommand(command);
}
};
if (data.json.menuitem == 'copy-image') {
doCommandIfEnabled('cmd_copyImage');
} else if (data.json.menuitem == 'copy-link') {
doCommandIfEnabled('cmd_copyLink');
} else if (data.json.menuitem in this._ctxHandlers) {
this._ctxHandlers[data.json.menuitem].click();
this._ctxHandlers = {};
} else {
// We silently ignore if the embedder uses an incorrect id in the callback
debug("Ignored invalid contextmenu invocation");
}
},
_buildMenuObj: function(menu, idPrefix, copyableElements) {
var menuObj = {type: 'menu', customized: false, items: []};
// Customized context menu
if (menu) {
this._maybeCopyAttribute(menu, menuObj, 'label');
for (var i = 0, child; child = menu.children[i++];) {
if (child.nodeName === 'MENU') {
menuObj.items.push(this._buildMenuObj(child, idPrefix + i + '_', false));
} else if (child.nodeName === 'MENUITEM') {
var id = this._ctxCounter + '_' + idPrefix + i;
var menuitem = {id: id, type: 'menuitem'};
this._maybeCopyAttribute(child, menuitem, 'label');
this._maybeCopyAttribute(child, menuitem, 'icon');
this._ctxHandlers[id] = child;
menuObj.items.push(menuitem);
}
}
if (menuObj.items.length > 0) {
menuObj.customized = true;
}
}
// Note: Display "Copy Link" first in order to make sure "Copy Image" is
// put together with other image options if elem is an image link.
// "Copy Link" menu item
if (copyableElements.link) {
menuObj.items.push({id: 'copy-link'});
}
// "Copy Image" menu item
if (copyableElements.image) {
menuObj.items.push({id: 'copy-image'});
}
return menuObj;
},
/**
* Called when the window which contains this iframe becomes hidden or
* visible.
*/
_recvOwnerVisibilityChange: function(data) {
debug("Received ownerVisibilityChange: (" + data.json.visible + ")");
var visible = data.json.visible;
if (docShell && docShell.isActive !== visible) {
docShell.isActive = visible;
// Ensure painting is not frozen if the app goes visible.
if (visible && this._paintFrozenTimer) {
this.notify();
}
}
},
_recvSendMouseEvent: function(data) {
let json = data.json;
let utils = content.windowUtils;
utils.sendMouseEventToWindow(json.type, json.x, json.y, json.button,
json.clickCount, json.modifiers);
},
_recvCanGoBack: function(data) {
var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
sendAsyncMsg('got-can-go-back', {
id: data.json.id,
successRv: webNav.canGoBack
});
},
_recvCanGoForward: function(data) {
var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
sendAsyncMsg('got-can-go-forward', {
id: data.json.id,
successRv: webNav.canGoForward
});
},
_recvGoBack: function(data) {
try {
docShell.QueryInterface(Ci.nsIWebNavigation).goBack();
} catch(e) {
// Silently swallow errors; these happen when we can't go back.
}
},
_recvGoForward: function(data) {
try {
docShell.QueryInterface(Ci.nsIWebNavigation).goForward();
} catch(e) {
// Silently swallow errors; these happen when we can't go forward.
}
},
_recvReload: function(data) {
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
let reloadFlags = data.json.hardReload ?
webNav.LOAD_FLAGS_BYPASS_PROXY | webNav.LOAD_FLAGS_BYPASS_CACHE :
webNav.LOAD_FLAGS_NONE;
try {
webNav.reload(reloadFlags);
} catch(e) {
// Silently swallow errors; these can happen if a used cancels reload
}
},
_recvStop: function(data) {
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.stop(webNav.STOP_NETWORK);
},
// The docShell keeps a weak reference to the progress listener, so we need
// to keep a strong ref to it ourselves.
_progressListener: {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener,
Ci.nsISupportsWeakReference]),
_seenLoadStart: false,
onLocationChange: function(webProgress, request, location, flags) {
// We get progress events from subshells here, which is kind of weird.
if (webProgress != docShell) {
return;
}
// Ignore locationchange events which occur before the first loadstart.
// These are usually about:blank loads we don't care about.
if (!this._seenLoadStart) {
return;
}
// Remove password and wyciwyg from uri.
location = Cc["@mozilla.org/docshell/urifixup;1"]
.getService(Ci.nsIURIFixup).createExposableURI(location);
var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
sendAsyncMsg('locationchange', { url: location.spec,
canGoBack: webNav.canGoBack,
canGoForward: webNav.canGoForward });
},
onStateChange: function(webProgress, request, stateFlags, status) {
if (webProgress != docShell) {
return;
}
if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
this._seenLoadStart = true;
sendAsyncMsg('loadstart');
}
if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
let bgColor = 'transparent';
try {
bgColor = content.getComputedStyle(content.document.body)
.getPropertyValue('background-color');
} catch (e) {}
sendAsyncMsg('loadend', {backgroundColor: bgColor});
switch (status) {
case Cr.NS_OK :
case Cr.NS_BINDING_ABORTED :
// Ignoring NS_BINDING_ABORTED, which is set when loading page is
// stopped.
case Cr.NS_ERROR_PARSED_DATA_CACHED:
return;
// TODO See nsDocShell::DisplayLoadError to see what extra
// information we should be annotating this first block of errors
// with. Bug 1107091.
case Cr.NS_ERROR_UNKNOWN_PROTOCOL :
sendAsyncMsg('error', { type: 'unknownProtocolFound' });
return;
case Cr.NS_ERROR_FILE_NOT_FOUND :
sendAsyncMsg('error', { type: 'fileNotFound' });
return;
case Cr.NS_ERROR_UNKNOWN_HOST :
sendAsyncMsg('error', { type: 'dnsNotFound' });
return;
case Cr.NS_ERROR_CONNECTION_REFUSED :
sendAsyncMsg('error', { type: 'connectionFailure' });
return;
case Cr.NS_ERROR_NET_INTERRUPT :
sendAsyncMsg('error', { type: 'netInterrupt' });
return;
case Cr.NS_ERROR_NET_TIMEOUT :
sendAsyncMsg('error', { type: 'netTimeout' });
return;
case Cr.NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION :
sendAsyncMsg('error', { type: 'cspBlocked' });
return;
case Cr.NS_ERROR_PHISHING_URI :
sendAsyncMsg('error', { type: 'deceptiveBlocked' });
return;
case Cr.NS_ERROR_MALWARE_URI :
sendAsyncMsg('error', { type: 'malwareBlocked' });
return;
case Cr.NS_ERROR_HARMFUL_URI :
sendAsyncMsg('error', { type: 'harmfulBlocked' });
return;
case Cr.NS_ERROR_UNWANTED_URI :
sendAsyncMsg('error', { type: 'unwantedBlocked' });
return;
case Cr.NS_ERROR_FORBIDDEN_URI :
sendAsyncMsg('error', { type: 'forbiddenBlocked' });
return;
case Cr.NS_ERROR_OFFLINE :
sendAsyncMsg('error', { type: 'offline' });
return;
case Cr.NS_ERROR_MALFORMED_URI :
sendAsyncMsg('error', { type: 'malformedURI' });
return;
case Cr.NS_ERROR_REDIRECT_LOOP :
sendAsyncMsg('error', { type: 'redirectLoop' });
return;
case Cr.NS_ERROR_UNKNOWN_SOCKET_TYPE :
sendAsyncMsg('error', { type: 'unknownSocketType' });
return;
case Cr.NS_ERROR_NET_RESET :
sendAsyncMsg('error', { type: 'netReset' });
return;
case Cr.NS_ERROR_DOCUMENT_NOT_CACHED :
sendAsyncMsg('error', { type: 'notCached' });
return;
case Cr.NS_ERROR_DOCUMENT_IS_PRINTMODE :
sendAsyncMsg('error', { type: 'isprinting' });
return;
case Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED :
sendAsyncMsg('error', { type: 'deniedPortAccess' });
return;
case Cr.NS_ERROR_UNKNOWN_PROXY_HOST :
sendAsyncMsg('error', { type: 'proxyResolveFailure' });
return;
case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED :
sendAsyncMsg('error', { type: 'proxyConnectFailure' });
return;
case Cr.NS_ERROR_INVALID_CONTENT_ENCODING :
sendAsyncMsg('error', { type: 'contentEncodingFailure' });
return;
case Cr.NS_ERROR_REMOTE_XUL :
sendAsyncMsg('error', { type: 'remoteXUL' });
return;
case Cr.NS_ERROR_UNSAFE_CONTENT_TYPE :
sendAsyncMsg('error', { type: 'unsafeContentType' });
return;
case Cr.NS_ERROR_CORRUPTED_CONTENT :
sendAsyncMsg('error', { type: 'corruptedContentErrorv2' });
return;
case Cr.NS_ERROR_BLOCKED_BY_POLICY :
sendAsyncMsg('error', { type: 'blockedByPolicy' });
return;
default:
// getErrorClass() will throw if the error code passed in is not a NSS
// error code.
try {
let nssErrorsService = Cc['@mozilla.org/nss_errors_service;1']
.getService(Ci.nsINSSErrorsService);
if (nssErrorsService.getErrorClass(status)
== Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
// XXX Is there a point firing the event if the error page is not
// certerror? If yes, maybe we should add a property to the
// event to to indicate whether there is a custom page. That would
// let the embedder have more control over the desired behavior.
let errorPage = Services.prefs.getCharPref(CERTIFICATE_ERROR_PAGE_PREF, "");
if (errorPage == 'certerror') {
sendAsyncMsg('error', { type: 'certerror' });
return;
}
}
} catch (e) {}
sendAsyncMsg('error', { type: 'other' });
return;
}
}
},
onSecurityChange: function(webProgress, request, state) {
if (webProgress != docShell) {
return;
}
var securityStateDesc;
if (state & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
securityStateDesc = 'secure';
}
else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
securityStateDesc = 'broken';
}
else if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
securityStateDesc = 'insecure';
}
else {
debug("Unexpected securitychange state!");
securityStateDesc = '???';
}
var trackingStateDesc;
if (state & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) {
trackingStateDesc = 'loaded_tracking_content';
}
else if (state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
trackingStateDesc = 'blocked_tracking_content';
}
var mixedStateDesc;
if (state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
mixedStateDesc = 'blocked_mixed_active_content';
}
else if (state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
// Note that STATE_LOADED_MIXED_ACTIVE_CONTENT implies STATE_IS_BROKEN
mixedStateDesc = 'loaded_mixed_active_content';
}
var isEV = !!(state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL);
var isTrackingContent = !!(state &
(Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT |
Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT));
var isMixedContent = !!(state &
(Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT |
Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT));
sendAsyncMsg('securitychange', {
state: securityStateDesc,
trackingState: trackingStateDesc,
mixedState: mixedStateDesc,
extendedValidation: isEV,
trackingContent: isTrackingContent,
mixedContent: isMixedContent,
});
},
},
// Expose the message manager for WebApps and others.
_messageManagerPublic: {
sendAsyncMessage: global.sendAsyncMessage.bind(global),
sendSyncMessage: global.sendSyncMessage.bind(global),
addMessageListener: global.addMessageListener.bind(global),
removeMessageListener: global.removeMessageListener.bind(global)
},
get messageManager() {
return this._messageManagerPublic;
}
};
var api = null;
if ('DoPreloadPostfork' in this && typeof this.DoPreloadPostfork === 'function') {
// If we are preloaded, instantiate BrowserElementChild after a content
// process is forked.
this.DoPreloadPostfork(function() {
api = new BrowserElementChild();
});
} else {
api = new BrowserElementChild();
}