diff options
Diffstat (limited to 'dom/browser-element/BrowserElementChildPreload.js')
-rw-r--r-- | dom/browser-element/BrowserElementChildPreload.js | 1823 |
1 files changed, 1823 insertions, 0 deletions
diff --git a/dom/browser-element/BrowserElementChildPreload.js b/dom/browser-element/BrowserElementChildPreload.js new file mode 100644 index 000000000..780dfa80e --- /dev/null +++ b/dom/browser-element/BrowserElementChildPreload.js @@ -0,0 +1,1823 @@ +/* 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; + +var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/BrowserElementPromptService.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/ExtensionContent.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "acs", + "@mozilla.org/audiochannel/service;1", + "nsIAudioChannelService"); +XPCOMUtils.defineLazyModuleGetter(this, "ManifestFinder", + "resource://gre/modules/ManifestFinder.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ManifestObtainer", + "resource://gre/modules/ManifestObtainer.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 BrowserElementProxyForwarder() { +} + +BrowserElementProxyForwarder.prototype = { + init: function() { + Services.obs.addObserver(this, "browser-element-api:proxy-call", false); + addMessageListener("browser-element-api:proxy", this); + }, + + uninit: function() { + Services.obs.removeObserver(this, "browser-element-api:proxy-call", false); + removeMessageListener("browser-element-api:proxy", this); + }, + + // Observer callback receives messages from BrowserElementProxy.js + observe: function(subject, topic, stringifedData) { + if (subject !== content) { + return; + } + + // Forward it to BrowserElementParent.js + sendAsyncMessage(topic, JSON.parse(stringifedData)); + }, + + // Message manager callback receives messages from BrowserElementParent.js + receiveMessage: function(mmMsg) { + // Forward it to BrowserElementProxy.js + Services.obs.notifyObservers( + content, mmMsg.name, JSON.stringify(mmMsg.json)); + } +}; + +function BrowserElementChild() { + // Maps outer window id --> weak ref to window. Used by modal dialog code. + this._windowIDDict = {}; + + // _forcedVisible corresponds to the visibility state our owner has set on us + // (via iframe.setVisible). ownerVisible corresponds to whether the docShell + // whose window owns this element is visible. + // + // Our docShell is visible iff _forcedVisible and _ownerVisible are both + // true. + this._forcedVisible = true; + this._ownerVisible = true; + + this._nextPaintHandler = null; + + this._isContentWindowCreated = false; + this._pendingSetInputMethodActive = []; + + this.forwarder = new BrowserElementProxyForwarder(); + + this._init(); +}; + +BrowserElementChild.prototype = { + + QueryInterface: XPCOMUtils.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) { + webNavigation.sessionHistory = Cc["@mozilla.org/browser/shistory;1"] + .createInstance(Ci.nsISHistory); + } + + // This is necessary to get security web progress notifications. + var securityUI = Cc['@mozilla.org/secure_browser_ui;1'] + .createInstance(Ci.nsISecureBrowserUI); + securityUI.init(content); + + // 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, false); + }); + + this.forwarder.init(); + }, + + /** + * 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); + }); + + this._deactivateNextPaintListener(); + + 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); + }); + + this.forwarder.uninit(); + this.forwarder = null; + }, + + 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 = { + "purge-history": this._recvPurgeHistory, + "get-screenshot": this._recvGetScreenshot, + "get-contentdimensions": this._recvGetContentDimensions, + "set-visible": this._recvSetVisible, + "get-visible": this._recvVisible, + "send-mouse-event": this._recvSendMouseEvent, + "send-touch-event": this._recvSendTouchEvent, + "get-can-go-back": this._recvCanGoBack, + "get-can-go-forward": this._recvCanGoForward, + "mute": this._recvMute, + "unmute": this._recvUnmute, + "get-muted": this._recvGetMuted, + "set-volume": this._recvSetVolume, + "get-volume": this._recvGetVolume, + "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, + "activate-next-paint-listener": this._activateNextPaintListener, + "set-input-method-active": this._recvSetInputMethodActive, + "deactivate-next-paint-listener": this._deactivateNextPaintListener, + "find-all": this._recvFindAll, + "find-next": this._recvFindNext, + "clear-match": this._recvClearMatch, + "execute-script": this._recvExecuteScript, + "get-audio-channel-volume": this._recvGetAudioChannelVolume, + "set-audio-channel-volume": this._recvSetAudioChannelVolume, + "get-audio-channel-muted": this._recvGetAudioChannelMuted, + "set-audio-channel-muted": this._recvSetAudioChannelMuted, + "get-is-audio-channel-active": this._recvIsAudioChannelActive, + "get-web-manifest": this._recvGetWebManifest, + } + + 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 + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + }, + + _tryGetInnerWindowID: function(win) { + let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + try { + return utils.currentInnerWindowID; + } + catch(e) { + return null; + } + }, + + /** + * Show a modal prompt. Called by BrowserElementPromptService. + */ + showModalPrompt: function(win, args) { + let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + 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.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + 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; + + let thread = Services.tm.currentThread; + debug("Nested event loop - begin"); + while (win.modalDepth == origModalDepth && !this._shuttingDown) { + // 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."); + break; + } + + thread.processNextEvent(/* mayWait = */ true); + } + 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 }); + + }, + + _manifestChangedHandler: function(e) { + debug('Got manifestchanged: (' + e.target.href + ')'); + let manifest = { href: e.target.href }; + sendAsyncMsg('manifestchange', manifest); + + }, + + // Processes the "rel" field in <link> tags and forward to specific handlers. + _linkAddedHandler: function(e) { + let win = e.target.ownerDocument.defaultView; + // 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, + 'manifest': this._manifestChangedHandler + }; + + 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.ownerDocument.defaultView; + // 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 => + ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) || + (node instanceof Ci.nsIDOMHTMLAreaElement && node.href) || + node instanceof Ci.nsIDOMHTMLLinkElement); + + // 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; + }, + + _removeMozAfterPaintHandler: function(listener) { + removeEventListener('MozAfterPaint', listener, + /* useCapture = */ true); + }, + + _activateNextPaintListener: function(e) { + if (!this._nextPaintHandler) { + this._nextPaintHandler = this._addMozAfterPaintHandler(function () { + this._nextPaintHandler = null; + sendAsyncMsg('nextpaint'); + }.bind(this)); + } + }, + + _deactivateNextPaintListener: function(e) { + if (this._nextPaintHandler) { + this._removeMozAfterPaintHandler(this._nextPaintHandler); + this._nextPaintHandler = null; + } + }, + + _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 + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation); + 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; + // Handle pending SetInputMethodActive request. + while (this._pendingSetInputMethodActive.length > 0) { + this._recvSetInputMethodActive(this._pendingSetInputMethodActive.shift()); + } + } + }, + + _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 ((elem instanceof Ci.nsIDOMHTMLAnchorElement && elem.href) || + (elem instanceof Ci.nsIDOMHTMLAreaElement && elem.href)) { + return {uri: elem.href, + documentURI: documentURI, + text: elem.textContent.substring(0, kLongestReturnedString)}; + } + if (elem instanceof Ci.nsIImageLoadingContent && elem.currentURI) { + return {uri: elem.currentURI.spec, documentURI: documentURI}; + } + if (elem instanceof Ci.nsIDOMHTMLImageElement) { + return {uri: elem.src, documentURI: documentURI}; + } + if (elem instanceof Ci.nsIDOMHTMLMediaElement) { + let hasVideo = !(elem.readyState >= elem.HAVE_METADATA && + (elem.videoWidth == 0 || elem.videoHeight == 0)); + return {uri: elem.currentSrc || elem.src, + hasVideo: hasVideo, + documentURI: documentURI}; + } + if (elem instanceof Ci.nsIDOMHTMLInputElement && + 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 (parent instanceof Ci.nsIDOMHTMLFormElement && + 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 }); + }, + + _recvPurgeHistory: function(data) { + debug("Received purgeHistory message: (" + data.json.id + ")"); + + let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; + + try { + if (history && history.count) { + history.PurgeHistory(history.count); + } + } catch(e) {} + + sendAsyncMsg('got-purge-history', { id: data.json.id, successRv: true }); + }, + + _recvGetScreenshot: function(data) { + debug("Received getScreenshot message: (" + data.json.id + ")"); + + let self = this; + let maxWidth = data.json.args.width; + let maxHeight = data.json.args.height; + let mimeType = data.json.args.mimeType; + let domRequestID = data.json.id; + + let takeScreenshotClosure = function() { + self._takeScreenshot(maxWidth, maxHeight, mimeType, domRequestID); + }; + + let maxDelayMS = 2000; + try { + maxDelayMS = Services.prefs.getIntPref('dom.browserElement.maxScreenshotDelayMS'); + } + catch(e) {} + + // Try to wait for the event loop to go idle before we take the screenshot, + // but once we've waited maxDelayMS milliseconds, go ahead and take it + // anyway. + Cc['@mozilla.org/message-loop;1'].getService(Ci.nsIMessageLoop).postIdleTask( + takeScreenshotClosure, maxDelayMS); + }, + + _recvExecuteScript: function(data) { + debug("Received executeScript message: (" + data.json.id + ")"); + + let domRequestID = data.json.id; + + let sendError = errorMsg => sendAsyncMsg("execute-script-done", { + errorMsg, + id: domRequestID + }); + + let sendSuccess = successRv => sendAsyncMsg("execute-script-done", { + successRv, + id: domRequestID + }); + + let isJSON = obj => { + try { + JSON.stringify(obj); + } catch(e) { + return false; + } + return true; + } + + let expectedOrigin = data.json.args.options.origin; + let expectedUrl = data.json.args.options.url; + + if (expectedOrigin) { + if (expectedOrigin != content.location.origin) { + sendError("Origin mismatches"); + return; + } + } + + if (expectedUrl) { + let expectedURI + try { + expectedURI = Services.io.newURI(expectedUrl, null, null); + } catch(e) { + sendError("Malformed URL"); + return; + } + let currentURI = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI; + if (!currentURI.equalsExceptRef(expectedURI)) { + sendError("URL mismatches"); + return; + } + } + + let sandbox = new Cu.Sandbox([content], { + sandboxPrototype: content, + sandboxName: "browser-api-execute-script", + allowWaivers: false, + sameZoneAs: content + }); + + try { + let sandboxRv = Cu.evalInSandbox(data.json.args.script, sandbox, "1.8"); + if (sandboxRv instanceof sandbox.Promise) { + sandboxRv.then(rv => { + if (isJSON(rv)) { + sendSuccess(rv); + } else { + sendError("Value returned (resolve) by promise is not a valid JSON object"); + } + }, error => { + if (isJSON(error)) { + sendError(error); + } else { + sendError("Value returned (reject) by promise is not a valid JSON object"); + } + }); + } else { + if (isJSON(sandboxRv)) { + sendSuccess(sandboxRv); + } else { + sendError("Script last expression must be a promise or a JSON object"); + } + } + } catch(e) { + sendError(e.toString()); + } + }, + + _recvGetContentDimensions: function(data) { + debug("Received getContentDimensions message: (" + data.json.id + ")"); + sendAsyncMsg('got-contentdimensions', { + id: data.json.id, + successRv: this._getContentDimensions() + }); + }, + + _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"); + }, + + _getContentDimensions: function() { + return { + width: content.document.body.scrollWidth, + height: content.document.body.scrollHeight + } + }, + + /** + * Actually take a screenshot and foward the result up to our parent, given + * the desired maxWidth and maxHeight (in CSS pixels), and given the + * DOMRequest ID associated with the request from the parent. + */ + _takeScreenshot: function(maxWidth, maxHeight, mimeType, domRequestID) { + // You can think of the screenshotting algorithm as carrying out the + // following steps: + // + // - Calculate maxWidth, maxHeight, and viewport's width and height in the + // dimension of device pixels by multiply the numbers with + // window.devicePixelRatio. + // + // - Let scaleWidth be the factor by which we'd need to downscale the + // viewport pixel width so it would fit within maxPixelWidth. + // (If the viewport's pixel width is less than maxPixelWidth, let + // scaleWidth be 1.) Compute scaleHeight the same way. + // + // - Scale the viewport by max(scaleWidth, scaleHeight). Now either the + // viewport's width is no larger than maxWidth, the viewport's height is + // no larger than maxHeight, or both. + // + // - Crop the viewport so its width is no larger than maxWidth and its + // height is no larger than maxHeight. + // + // - Set mozOpaque to true and background color to solid white + // if we are taking a JPEG screenshot, keep transparent if otherwise. + // + // - Return a screenshot of the page's viewport scaled and cropped per + // above. + debug("Taking a screenshot: maxWidth=" + maxWidth + + ", maxHeight=" + maxHeight + + ", mimeType=" + mimeType + + ", domRequestID=" + domRequestID + "."); + + if (!content) { + // If content is not loaded yet, bail out since even sendAsyncMessage + // fails... + debug("No content yet!"); + return; + } + + let devicePixelRatio = content.devicePixelRatio; + + let maxPixelWidth = Math.round(maxWidth * devicePixelRatio); + let maxPixelHeight = Math.round(maxHeight * devicePixelRatio); + + let contentPixelWidth = content.innerWidth * devicePixelRatio; + let contentPixelHeight = content.innerHeight * devicePixelRatio; + + let scaleWidth = Math.min(1, maxPixelWidth / contentPixelWidth); + let scaleHeight = Math.min(1, maxPixelHeight / contentPixelHeight); + + let scale = Math.max(scaleWidth, scaleHeight); + + let canvasWidth = + Math.min(maxPixelWidth, Math.round(contentPixelWidth * scale)); + let canvasHeight = + Math.min(maxPixelHeight, Math.round(contentPixelHeight * scale)); + + let transparent = (mimeType !== 'image/jpeg'); + + var canvas = content.document + .createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + if (!transparent) + canvas.mozOpaque = true; + canvas.width = canvasWidth; + canvas.height = canvasHeight; + + let ctx = canvas.getContext("2d", { willReadFrequently: true }); + ctx.scale(scale * devicePixelRatio, scale * devicePixelRatio); + + let flags = ctx.DRAWWINDOW_DRAW_VIEW | + ctx.DRAWWINDOW_USE_WIDGET_LAYERS | + ctx.DRAWWINDOW_DO_NOT_FLUSH | + ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES; + ctx.drawWindow(content, 0, 0, content.innerWidth, content.innerHeight, + transparent ? "rgba(255,255,255,0)" : "rgb(255,255,255)", + flags); + + // Take a JPEG screenshot by default instead of PNG with alpha channel. + // This requires us to unpremultiply the alpha channel, which + // is expensive on ARM processors because they lack a hardware integer + // division instruction. + canvas.toBlob(function(blob) { + sendAsyncMsg('got-screenshot', { + id: domRequestID, + successRv: blob + }); + }, mimeType); + }, + + _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; + }, + + _recvSetVisible: function(data) { + debug("Received setVisible message: (" + data.json.visible + ")"); + if (this._forcedVisible == data.json.visible) { + return; + } + + this._forcedVisible = data.json.visible; + this._updateVisibility(); + }, + + _recvVisible: function(data) { + sendAsyncMsg('got-visible', { + id: data.json.id, + successRv: docShell.isActive + }); + }, + + /** + * Called when the window which contains this iframe becomes hidden or + * visible. + */ + _recvOwnerVisibilityChange: function(data) { + debug("Received ownerVisibilityChange: (" + data.json.visible + ")"); + this._ownerVisible = data.json.visible; + this._updateVisibility(); + }, + + _updateVisibility: function() { + var visible = this._forcedVisible && this._ownerVisible; + if (docShell && docShell.isActive !== visible) { + docShell.isActive = visible; + sendAsyncMsg('visibilitychange', {visible: 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.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + utils.sendMouseEventToWindow(json.type, json.x, json.y, json.button, + json.clickCount, json.modifiers); + }, + + _recvSendTouchEvent: function(data) { + let json = data.json; + let utils = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + utils.sendTouchEventToWindow(json.type, json.identifiers, json.touchesX, + json.touchesY, json.radiisX, json.radiisY, + json.rotationAngles, json.forces, json.count, + 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 + }); + }, + + _recvMute: function(data) { + this._windowUtils.audioMuted = true; + }, + + _recvUnmute: function(data) { + this._windowUtils.audioMuted = false; + }, + + _recvGetMuted: function(data) { + sendAsyncMsg('got-muted', { + id: data.json.id, + successRv: this._windowUtils.audioMuted + }); + }, + + _recvSetVolume: function(data) { + this._windowUtils.audioVolume = data.json.volume; + }, + + _recvGetVolume: function(data) { + sendAsyncMsg('got-volume', { + id: data.json.id, + successRv: this._windowUtils.audioVolume + }); + }, + + _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); + }, + + _recvZoom: function(data) { + docShell.contentViewer.fullZoom = data.json.zoom; + }, + + _recvGetAudioChannelVolume: function(data) { + debug("Received getAudioChannelVolume message: (" + data.json.id + ")"); + + let volume = acs.getAudioChannelVolume(content, + data.json.args.audioChannel); + sendAsyncMsg('got-audio-channel-volume', { + id: data.json.id, successRv: volume + }); + }, + + _recvSetAudioChannelVolume: function(data) { + debug("Received setAudioChannelVolume message: (" + data.json.id + ")"); + + acs.setAudioChannelVolume(content, + data.json.args.audioChannel, + data.json.args.volume); + sendAsyncMsg('got-set-audio-channel-volume', { + id: data.json.id, successRv: true + }); + }, + + _recvGetAudioChannelMuted: function(data) { + debug("Received getAudioChannelMuted message: (" + data.json.id + ")"); + + let muted = acs.getAudioChannelMuted(content, data.json.args.audioChannel); + sendAsyncMsg('got-audio-channel-muted', { + id: data.json.id, successRv: muted + }); + }, + + _recvSetAudioChannelMuted: function(data) { + debug("Received setAudioChannelMuted message: (" + data.json.id + ")"); + + acs.setAudioChannelMuted(content, data.json.args.audioChannel, + data.json.args.muted); + sendAsyncMsg('got-set-audio-channel-muted', { + id: data.json.id, successRv: true + }); + }, + + _recvIsAudioChannelActive: function(data) { + debug("Received isAudioChannelActive message: (" + data.json.id + ")"); + + let active = acs.isAudioChannelActive(content, data.json.args.audioChannel); + sendAsyncMsg('got-is-audio-channel-active', { + id: data.json.id, successRv: active + }); + }, + _recvGetWebManifest: Task.async(function* (data) { + debug(`Received GetWebManifest message: (${data.json.id})`); + let manifest = null; + let hasManifest = ManifestFinder.contentHasManifestLink(content); + if (hasManifest) { + try { + manifest = yield ManifestObtainer.contentObtainManifest(content); + } catch (e) { + sendAsyncMsg('got-web-manifest', { + id: data.json.id, + errorMsg: `Error fetching web manifest: ${e}.`, + }); + return; + } + } + sendAsyncMsg('got-web-manifest', { + id: data.json.id, + successRv: manifest + }); + }), + + _initFinder: function() { + if (!this._finder) { + let {Finder} = Components.utils.import("resource://gre/modules/Finder.jsm", {}); + this._finder = new Finder(docShell); + } + let listener = { + onMatchesCountResult: (data) => { + sendAsyncMsg("findchange", { + active: true, + searchString: this._finder.searchString, + searchLimit: this._finder.matchesCountLimit, + activeMatchOrdinal: data.current, + numberOfMatches: data.total + }); + this._finder.removeResultListener(listener); + } + }; + this._finder.addResultListener(listener); + }, + + _recvFindAll: function(data) { + this._initFinder(); + let searchString = data.json.searchString; + this._finder.caseSensitive = data.json.caseSensitive; + this._finder.fastFind(searchString, false, false); + this._finder.requestMatchesCount(searchString, this._finder.matchesCountLimit, false); + }, + + _recvFindNext: function(data) { + if (!this._finder) { + debug("findNext() called before findAll()"); + return; + } + this._initFinder(); + this._finder.findAgain(data.json.backward, false, false); + this._finder.requestMatchesCount(this._finder.searchString, this._finder.matchesCountLimit, false); + }, + + _recvClearMatch: function(data) { + if (!this._finder) { + debug("clearMach() called before findAll()"); + return; + } + this._finder.removeSelection(); + sendAsyncMsg("findchange", {active: false}); + }, + + _recvSetInputMethodActive: function(data) { + let msgData = { id: data.json.id }; + if (!this._isContentWindowCreated) { + if (data.json.args.isActive) { + // To activate the input method, we should wait before the content + // window is ready. + this._pendingSetInputMethodActive.push(data); + return; + } + msgData.successRv = null; + sendAsyncMsg('got-set-input-method-active', msgData); + return; + } + // Unwrap to access webpage content. + let nav = XPCNativeWrapper.unwrap(content.document.defaultView.navigator); + if (nav.mozInputMethod) { + // Wrap to access the chrome-only attribute setActive. + new XPCNativeWrapper(nav.mozInputMethod).setActive(data.json.args.isActive); + msgData.successRv = null; + } else { + msgData.errorMsg = 'Cannot access mozInputMethod.'; + } + sendAsyncMsg('got-set-input-method-active', msgData); + }, + + // The docShell keeps a weak reference to the progress listener, so we need + // to keep a strong ref to it ourselves. + _progressListener: { + QueryInterface: XPCOMUtils.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_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; + + 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 = null; + try { + errorPage = Services.prefs.getCharPref(CERTIFICATE_ERROR_PAGE_PREF); + } catch (e) {} + + 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, + }); + }, + + onStatusChange: function(webProgress, request, status, message) {}, + onProgressChange: function(webProgress, request, curSelfProgress, + maxSelfProgress, curTotalProgress, maxTotalProgress) {}, + }, + + // 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(); +} |