/* 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(); }