/* 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"; var Cu = Components.utils; var Ci = Components.interfaces; var Cc = Components.classes; var Cr = Components.results; /* BrowserElementParent injects script to listen for certain events in the * child. We then listen to messages from the child script and take * appropriate action here in the parent. */ Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/BrowserElementPromptService.jsm"); function debug(msg) { //dump("BrowserElementParent - " + msg + "\n"); } function handleWindowEvent(e) { if (this._browserElementParents) { let beps = ThreadSafeChromeUtils.nondeterministicGetWeakMapKeys(this._browserElementParents); beps.forEach(bep => bep._handleOwnerEvent(e)); } } function defineNoReturnMethod(fn) { return function method() { if (!this._domRequestReady) { // Remote browser haven't been created, we just queue the API call. let args = Array.slice(arguments); args.unshift(this); this._pendingAPICalls.push(method.bind.apply(fn, args)); return; } if (this._isAlive()) { fn.apply(this, arguments); } }; } function defineDOMRequestMethod(msgName) { return function() { return this._sendDOMRequest(msgName); }; } function BrowserElementParentProxyCallHandler() { } BrowserElementParentProxyCallHandler.prototype = { _frameElement: null, _mm: null, MOZBROWSER_EVENT_NAMES: Object.freeze([ "loadstart", "loadend", "close", "error", "firstpaint", "documentfirstpaint", "audioplaybackchange", "contextmenu", "securitychange", "locationchange", "iconchange", "scrollareachanged", "titlechange", "opensearch", "manifestchange", "metachange", "resize", "scrollviewchange", "caretstatechanged", "activitydone", "scroll", "opentab"]), init: function(frameElement, mm) { this._frameElement = frameElement; this._mm = mm; this.innerWindowIDSet = new Set(); mm.addMessageListener("browser-element-api:proxy-call", this); }, // Message manager callback receives messages from BrowserElementProxy.js receiveMessage: function(mmMsg) { let data = mmMsg.json; let mm; try { mm = mmMsg.target.QueryInterface(Ci.nsIFrameLoaderOwner) .frameLoader.messageManager; } catch(e) { mm = mmMsg.target; } if (!mm.assertPermission("browser:embedded-system-app")) { dump("BrowserElementParent.js: Method call " + data.methodName + " from a content process with no 'browser:embedded-system-app'" + " privileges.\n"); return; } switch (data.methodName) { case '_proxyInstanceInit': if (!this.innerWindowIDSet.size) { this._attachEventListeners(); } this.innerWindowIDSet.add(data.innerWindowID); break; case '_proxyInstanceUninit': this.innerWindowIDSet.delete(data.innerWindowID); if (!this.innerWindowIDSet.size) { this._detachEventListeners(); } break; // void methods case 'setVisible': case 'setActive': case 'sendMouseEvent': case 'sendTouchEvent': case 'goBack': case 'goForward': case 'reload': case 'stop': case 'zoom': case 'findAll': case 'findNext': case 'clearMatch': case 'mute': case 'unmute': case 'setVolume': this._frameElement[data.methodName] .apply(this._frameElement, data.args); break; // DOMRequest methods case 'getVisible': case 'download': case 'purgeHistory': case 'getCanGoBack': case 'getCanGoForward': case 'getContentDimensions': case 'setInputMethodActive': case 'executeScript': case 'getMuted': case 'getVolume': let req = this._frameElement[data.methodName] .apply(this._frameElement, data.args); req.onsuccess = () => { this._sendToProxy({ domRequestId: data.domRequestId, innerWindowID: data.innerWindowID, result: req.result }); }; req.onerror = () => { this._sendToProxy({ domRequestId: data.domRequestId, innerWindowID: data.innerWindowID, err: req.error }); }; break; // Not implemented case 'getActive': // Sync ??? case 'addNextPaintListener': // Takes a callback case 'removeNextPaintListener': // Takes a callback case 'getScreenshot': // Need to pass a blob back dump("BrowserElementParentProxyCallHandler Error:" + "Attempt to call unimplemented method " + data.methodName + ".\n"); break; default: dump("BrowserElementParentProxyCallHandler Error:" + "Attempt to call non-exist method " + data.methodName + ".\n"); break; } }, // Receving events from the frame element and forward it. handleEvent: function(evt) { // Ignore the events from nested mozbrowser iframes if (evt.target !== this._frameElement) { return; } let detailString; try { detailString = JSON.stringify(evt.detail); } catch (e) { dump("BrowserElementParentProxyCallHandler Error:" + "Event detail of " + evt.type + " can't be stingified.\n"); return; } this.innerWindowIDSet.forEach((innerWindowID) => { this._sendToProxy({ eventName: evt.type, innerWindowID: innerWindowID, eventDetailString: detailString }); }); }, _sendToProxy: function(data) { this._mm.sendAsyncMessage("browser-element-api:proxy", data); }, _attachEventListeners: function() { this.MOZBROWSER_EVENT_NAMES.forEach(function(eventName) { this._frameElement.addEventListener( "mozbrowser" + eventName, this, true); }, this); }, _detachEventListeners: function() { this.MOZBROWSER_EVENT_NAMES.forEach(function(eventName) { this._frameElement.removeEventListener( "mozbrowser" + eventName, this, true); }, this); } }; function BrowserElementParent() { debug("Creating new BrowserElementParent object"); this._domRequestCounter = 0; this._domRequestReady = false; this._pendingAPICalls = []; this._pendingDOMRequests = {}; this._pendingSetInputMethodActive = []; this._nextPaintListeners = []; this._pendingDOMFullscreen = false; Services.obs.addObserver(this, 'oop-frameloader-crashed', /* ownsWeak = */ true); Services.obs.addObserver(this, 'ask-children-to-execute-copypaste-command', /* ownsWeak = */ true); Services.obs.addObserver(this, 'back-docommand', /* ownsWeak = */ true); this.proxyCallHandler = new BrowserElementParentProxyCallHandler(); } BrowserElementParent.prototype = { classDescription: "BrowserElementAPI implementation", classID: Components.ID("{9f171ac4-0939-4ef8-b360-3408aedc3060}"), contractID: "@mozilla.org/dom/browser-element-api;1", QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserElementAPI, Ci.nsIObserver, Ci.nsISupportsWeakReference]), setFrameLoader: function(frameLoader) { debug("Setting frameLoader"); this._frameLoader = frameLoader; this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement; if (!this._frameElement) { debug("No frame element?"); return; } // Listen to visibilitychange on the iframe's owner window, and forward // changes down to the child. We want to do this while registering as few // visibilitychange listeners on _window as possible, because such a listener // may live longer than this BrowserElementParent object. // // To accomplish this, we register just one listener on the window, and have // it reference a WeakMap whose keys are all the BrowserElementParent objects // on the window. Then when the listener fires, we iterate over the // WeakMap's keys (which we can do, because we're chrome) to notify the // BrowserElementParents. if (!this._window._browserElementParents) { this._window._browserElementParents = new WeakMap(); let handler = handleWindowEvent.bind(this._window); let windowEvents = ['visibilitychange', 'fullscreenchange']; let els = Cc["@mozilla.org/eventlistenerservice;1"] .getService(Ci.nsIEventListenerService); for (let event of windowEvents) { els.addSystemEventListener(this._window, event, handler, /* useCapture = */ true); } } this._window._browserElementParents.set(this, null); // Insert ourself into the prompt service. BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this); this._setupMessageListener(); this.proxyCallHandler.init( this._frameElement, this._frameLoader.messageManager); }, destroyFrameScripts() { debug("Destroying frame scripts"); this._mm.sendAsyncMessage("browser-element-api:destroy"); }, _runPendingAPICall: function() { if (!this._pendingAPICalls) { return; } for (let i = 0; i < this._pendingAPICalls.length; i++) { try { this._pendingAPICalls[i](); } catch (e) { // throw the expections from pending functions. debug('Exception when running pending API call: ' + e); } } delete this._pendingAPICalls; }, _setupMessageListener: function() { this._mm = this._frameLoader.messageManager; this._mm.addMessageListener('browser-element-api:call', this); this._mm.loadFrameScript("chrome://global/content/extensions.js", true); }, receiveMessage: function(aMsg) { if (!this._isAlive()) { return; } // Messages we receive are handed to functions which take a (data) argument, // where |data| is the message manager's data object. // We use a single message and dispatch to various function based // on data.msg_name let mmCalls = { "hello": this._recvHello, "loadstart": this._fireProfiledEventFromMsg, "loadend": this._fireProfiledEventFromMsg, "close": this._fireEventFromMsg, "error": this._fireEventFromMsg, "firstpaint": this._fireProfiledEventFromMsg, "documentfirstpaint": this._fireProfiledEventFromMsg, "nextpaint": this._recvNextPaint, "got-purge-history": this._gotDOMRequestResult, "got-screenshot": this._gotDOMRequestResult, "got-contentdimensions": this._gotDOMRequestResult, "got-can-go-back": this._gotDOMRequestResult, "got-can-go-forward": this._gotDOMRequestResult, "got-muted": this._gotDOMRequestResult, "got-volume": this._gotDOMRequestResult, "requested-dom-fullscreen": this._requestedDOMFullscreen, "fullscreen-origin-change": this._fullscreenOriginChange, "exit-dom-fullscreen": this._exitDomFullscreen, "got-visible": this._gotDOMRequestResult, "visibilitychange": this._childVisibilityChange, "got-set-input-method-active": this._gotDOMRequestResult, "scrollviewchange": this._handleScrollViewChange, "caretstatechanged": this._handleCaretStateChanged, "findchange": this._handleFindChange, "execute-script-done": this._gotDOMRequestResult, "got-audio-channel-volume": this._gotDOMRequestResult, "got-set-audio-channel-volume": this._gotDOMRequestResult, "got-audio-channel-muted": this._gotDOMRequestResult, "got-set-audio-channel-muted": this._gotDOMRequestResult, "got-is-audio-channel-active": this._gotDOMRequestResult, "got-web-manifest": this._gotDOMRequestResult, }; let mmSecuritySensitiveCalls = { "audioplaybackchange": this._fireEventFromMsg, "showmodalprompt": this._handleShowModalPrompt, "contextmenu": this._fireCtxMenuEvent, "securitychange": this._fireEventFromMsg, "locationchange": this._fireEventFromMsg, "iconchange": this._fireEventFromMsg, "scrollareachanged": this._fireEventFromMsg, "titlechange": this._fireProfiledEventFromMsg, "opensearch": this._fireEventFromMsg, "manifestchange": this._fireEventFromMsg, "metachange": this._fireEventFromMsg, "resize": this._fireEventFromMsg, "activitydone": this._fireEventFromMsg, "scroll": this._fireEventFromMsg, "opentab": this._fireEventFromMsg }; if (aMsg.data.msg_name in mmCalls) { return mmCalls[aMsg.data.msg_name].apply(this, arguments); } else if (aMsg.data.msg_name in mmSecuritySensitiveCalls) { return mmSecuritySensitiveCalls[aMsg.data.msg_name].apply(this, arguments); } }, _removeMessageListener: function() { this._mm.removeMessageListener('browser-element-api:call', this); }, /** * You shouldn't touch this._frameElement or this._window if _isAlive is * false. (You'll likely get an exception if you do.) */ _isAlive: function() { return !Cu.isDeadWrapper(this._frameElement) && !Cu.isDeadWrapper(this._frameElement.ownerDocument) && !Cu.isDeadWrapper(this._frameElement.ownerDocument.defaultView); }, get _window() { return this._frameElement.ownerDocument.defaultView; }, get _windowUtils() { return this._window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); }, promptAuth: function(authDetail, callback) { let evt; let self = this; let callbackCalled = false; let cancelCallback = function() { if (!callbackCalled) { callbackCalled = true; callback(false, null, null); } }; // We don't handle password-only prompts. if (authDetail.isOnlyPassword) { cancelCallback(); return; } /* username and password */ let detail = { host: authDetail.host, path: authDetail.path, realm: authDetail.realm, isProxy: authDetail.isProxy }; evt = this._createEvent('usernameandpasswordrequired', detail, /* cancelable */ true); Cu.exportFunction(function(username, password) { if (callbackCalled) return; callbackCalled = true; callback(true, username, password); }, evt.detail, { defineAs: 'authenticate' }); Cu.exportFunction(cancelCallback, evt.detail, { defineAs: 'cancel' }); this._frameElement.dispatchEvent(evt); if (!evt.defaultPrevented) { cancelCallback(); } }, _sendAsyncMsg: function(msg, data) { try { if (!data) { data = { }; } data.msg_name = msg; this._mm.sendAsyncMessage('browser-element-api:call', data); } catch (e) { return false; } return true; }, _recvHello: function() { debug("recvHello"); // Inform our child if our owner element's document is invisible. Note // that we must do so here, rather than in the BrowserElementParent // constructor, because the BrowserElementChild may not be initialized when // we run our constructor. if (this._window.document.hidden) { this._ownerVisibilityChange(); } if (!this._domRequestReady) { // At least, one message listener such as for hello is registered. // So we can use sendAsyncMessage now. this._domRequestReady = true; this._runPendingAPICall(); } }, _fireCtxMenuEvent: function(data) { let detail = data.json; let evtName = detail.msg_name; debug('fireCtxMenuEventFromMsg: ' + evtName + ' ' + detail); let evt = this._createEvent(evtName, detail, /* cancellable */ true); if (detail.contextmenu) { var self = this; Cu.exportFunction(function(id) { self._sendAsyncMsg('fire-ctx-callback', {menuitem: id}); }, evt.detail, { defineAs: 'contextMenuItemSelected' }); } // The embedder may have default actions on context menu events, so // we fire a context menu event even if the child didn't define a // custom context menu return !this._frameElement.dispatchEvent(evt); }, /** * add profiler marker for each event fired. */ _fireProfiledEventFromMsg: function(data) { if (Services.profiler !== undefined) { Services.profiler.AddMarker(data.json.msg_name); } this._fireEventFromMsg(data); }, /** * Fire either a vanilla or a custom event, depending on the contents of * |data|. */ _fireEventFromMsg: function(data) { let detail = data.json; let name = detail.msg_name; // For events that send a "_payload_" property, we just want to transmit // this in the event. if ("_payload_" in detail) { detail = detail._payload_; } debug('fireEventFromMsg: ' + name + ', ' + JSON.stringify(detail)); let evt = this._createEvent(name, detail, /* cancelable = */ false); this._frameElement.dispatchEvent(evt); }, _handleShowModalPrompt: function(data) { // Fire a showmodalprmopt event on the iframe. When this method is called, // the child is spinning in a nested event loop waiting for an // unblock-modal-prompt message. // // If the embedder calls preventDefault() on the showmodalprompt event, // we'll block the child until event.detail.unblock() is called. // // Otherwise, if preventDefault() is not called, we'll send the // unblock-modal-prompt message to the child as soon as the event is done // dispatching. let detail = data.json; debug('handleShowPrompt ' + JSON.stringify(detail)); // Strip off the windowID property from the object we send along in the // event. let windowID = detail.windowID; delete detail.windowID; debug("Event will have detail: " + JSON.stringify(detail)); let evt = this._createEvent('showmodalprompt', detail, /* cancelable = */ true); let self = this; let unblockMsgSent = false; function sendUnblockMsg() { if (unblockMsgSent) { return; } unblockMsgSent = true; // We don't need to sanitize evt.detail.returnValue (e.g. converting the // return value of confirm() to a boolean); Gecko does that for us. let data = { windowID: windowID, returnValue: evt.detail.returnValue }; self._sendAsyncMsg('unblock-modal-prompt', data); } Cu.exportFunction(sendUnblockMsg, evt.detail, { defineAs: 'unblock' }); this._frameElement.dispatchEvent(evt); if (!evt.defaultPrevented) { // Unblock the inner frame immediately. Otherwise we'll unblock upon // evt.detail.unblock(). sendUnblockMsg(); } }, // Called when state of accessible caret in child has changed. // The fields of data is as following: // - rect: Contains bounding rectangle of selection, Include width, height, // top, bottom, left and right. // - commands: Describe what commands can be executed in child. Include canSelectAll, // canCut, canCopy and canPaste. For example: if we want to check if cut // command is available, using following code, if (data.commands.canCut) {}. // - zoomFactor: Current zoom factor in child frame. // - reason: The reason causes the state changed. Include "visibilitychange", // "updateposition", "longpressonemptycontent", "taponcaret", "presscaret", // "releasecaret". // - collapsed: Indicate current selection is collapsed or not. // - caretVisible: Indicate the caret visiibility. // - selectionVisible: Indicate current selection is visible or not. // - selectionEditable: Indicate current selection is editable or not. // - selectedTextContent: Contains current selected text content, which is // equivalent to the string returned by Selection.toString(). _handleCaretStateChanged: function(data) { let evt = this._createEvent('caretstatechanged', data.json, /* cancelable = */ false); let self = this; function sendDoCommandMsg(cmd) { let data = { command: cmd }; self._sendAsyncMsg('copypaste-do-command', data); } Cu.exportFunction(sendDoCommandMsg, evt.detail, { defineAs: 'sendDoCommandMsg' }); this._frameElement.dispatchEvent(evt); }, _handleScrollViewChange: function(data) { let evt = this._createEvent("scrollviewchange", data.json, /* cancelable = */ false); this._frameElement.dispatchEvent(evt); }, _handleFindChange: function(data) { let evt = this._createEvent("findchange", data.json, /* cancelable = */ false); this._frameElement.dispatchEvent(evt); }, _createEvent: function(evtName, detail, cancelable) { // This will have to change if we ever want to send a CustomEvent with null // detail. For now, it's OK. if (detail !== undefined && detail !== null) { detail = Cu.cloneInto(detail, this._window); return new this._window.CustomEvent('mozbrowser' + evtName, { bubbles: true, cancelable: cancelable, detail: detail }); } return new this._window.Event('mozbrowser' + evtName, { bubbles: true, cancelable: cancelable }); }, /** * Kick off a DOMRequest in the child process. * * We'll fire an event called |msgName| on the child process, passing along * an object with two fields: * * - id: the ID of this request. * - arg: arguments to pass to the child along with this request. * * We expect the child to pass the ID back to us upon completion of the * request. See _gotDOMRequestResult. */ _sendDOMRequest: function(msgName, args) { let id = 'req_' + this._domRequestCounter++; let req = Services.DOMRequest.createRequest(this._window); let self = this; let send = function() { if (!self._isAlive()) { return; } if (self._sendAsyncMsg(msgName, {id: id, args: args})) { self._pendingDOMRequests[id] = req; } else { Services.DOMRequest.fireErrorAsync(req, "fail"); } }; if (this._domRequestReady) { send(); } else { // Child haven't been loaded. this._pendingAPICalls.push(send); } return req; }, /** * Called when the child process finishes handling a DOMRequest. data.json * must have the fields [id, successRv], if the DOMRequest was successful, or * [id, errorMsg], if the request was not successful. * * The fields have the following meanings: * * - id: the ID of the DOM request (see _sendDOMRequest) * - successRv: the request's return value, if the request succeeded * - errorMsg: the message to pass to DOMRequest.fireError(), if the request * failed. * */ _gotDOMRequestResult: function(data) { let req = this._pendingDOMRequests[data.json.id]; delete this._pendingDOMRequests[data.json.id]; if ('successRv' in data.json) { debug("Successful gotDOMRequestResult."); let clientObj = Cu.cloneInto(data.json.successRv, this._window); Services.DOMRequest.fireSuccess(req, clientObj); } else { debug("Got error in gotDOMRequestResult."); Services.DOMRequest.fireErrorAsync(req, Cu.cloneInto(data.json.errorMsg, this._window)); } }, setVisible: defineNoReturnMethod(function(visible) { this._sendAsyncMsg('set-visible', {visible: visible}); this._frameLoader.visible = visible; }), getVisible: defineDOMRequestMethod('get-visible'), setActive: defineNoReturnMethod(function(active) { this._frameLoader.visible = active; }), getActive: function() { if (!this._isAlive()) { throw Components.Exception("Dead content process", Cr.NS_ERROR_DOM_INVALID_STATE_ERR); } return this._frameLoader.visible; }, getChildProcessOffset: function() { let offset = { x: 0, y: 0 }; let tabParent = this._frameLoader.tabParent; if (tabParent) { let offsetX = {}; let offsetY = {}; tabParent.getChildProcessOffset(offsetX, offsetY); offset.x = offsetX.value; offset.y = offsetY.value; } return offset; }, sendMouseEvent: defineNoReturnMethod(function(type, x, y, button, clickCount, modifiers) { let offset = this.getChildProcessOffset(); x += offset.x; y += offset.y; this._sendAsyncMsg("send-mouse-event", { "type": type, "x": x, "y": y, "button": button, "clickCount": clickCount, "modifiers": modifiers }); }), sendTouchEvent: defineNoReturnMethod(function(type, identifiers, touchesX, touchesY, radiisX, radiisY, rotationAngles, forces, count, modifiers) { let offset = this.getChildProcessOffset(); for (var i = 0; i < touchesX.length; i++) { touchesX[i] += offset.x; } for (var i = 0; i < touchesY.length; i++) { touchesY[i] += offset.y; } this._sendAsyncMsg("send-touch-event", { "type": type, "identifiers": identifiers, "touchesX": touchesX, "touchesY": touchesY, "radiisX": radiisX, "radiisY": radiisY, "rotationAngles": rotationAngles, "forces": forces, "count": count, "modifiers": modifiers }); }), getCanGoBack: defineDOMRequestMethod('get-can-go-back'), getCanGoForward: defineDOMRequestMethod('get-can-go-forward'), getContentDimensions: defineDOMRequestMethod('get-contentdimensions'), findAll: defineNoReturnMethod(function(searchString, caseSensitivity) { return this._sendAsyncMsg('find-all', { searchString, caseSensitive: caseSensitivity == Ci.nsIBrowserElementAPI.FIND_CASE_SENSITIVE }); }), findNext: defineNoReturnMethod(function(direction) { return this._sendAsyncMsg('find-next', { backward: direction == Ci.nsIBrowserElementAPI.FIND_BACKWARD }); }), clearMatch: defineNoReturnMethod(function() { return this._sendAsyncMsg('clear-match'); }), mute: defineNoReturnMethod(function() { this._sendAsyncMsg('mute'); }), unmute: defineNoReturnMethod(function() { this._sendAsyncMsg('unmute'); }), getMuted: defineDOMRequestMethod('get-muted'), getVolume: defineDOMRequestMethod('get-volume'), setVolume: defineNoReturnMethod(function(volume) { this._sendAsyncMsg('set-volume', {volume}); }), goBack: defineNoReturnMethod(function() { this._sendAsyncMsg('go-back'); }), goForward: defineNoReturnMethod(function() { this._sendAsyncMsg('go-forward'); }), reload: defineNoReturnMethod(function(hardReload) { this._sendAsyncMsg('reload', {hardReload: hardReload}); }), stop: defineNoReturnMethod(function() { this._sendAsyncMsg('stop'); }), executeScript: function(script, options) { if (!this._isAlive()) { throw Components.Exception("Dead content process", Cr.NS_ERROR_DOM_INVALID_STATE_ERR); } // Enforcing options.url or options.origin if (!options.url && !options.origin) { throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); } return this._sendDOMRequest('execute-script', {script, options}); }, /* * The valid range of zoom scale is defined in preference "zoom.maxPercent" and "zoom.minPercent". */ zoom: defineNoReturnMethod(function(zoom) { zoom *= 100; zoom = Math.min(Services.prefs.getIntPref("zoom.maxPercent", 300), zoom); zoom = Math.max(Services.prefs.getIntPref("zoom.minPercent", 50), zoom); this._sendAsyncMsg('zoom', {zoom: zoom / 100.0}); }), purgeHistory: defineDOMRequestMethod('purge-history'), download: function(_url, _options) { if (!this._isAlive()) { return null; } let uri = Services.io.newURI(_url, null, null); let url = uri.QueryInterface(Ci.nsIURL); debug('original _options = ' + uneval(_options)); // Ensure we have _options, we always use it to send the filename. _options = _options || {}; if (!_options.filename) { _options.filename = url.fileName; } debug('final _options = ' + uneval(_options)); // Ensure we have a filename. if (!_options.filename) { throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); } let interfaceRequestor = this._frameLoader.loadContext.QueryInterface(Ci.nsIInterfaceRequestor); let req = Services.DOMRequest.createRequest(this._window); function DownloadListener() { debug('DownloadListener Constructor'); } DownloadListener.prototype = { extListener: null, onStartRequest: function(aRequest, aContext) { debug('DownloadListener - onStartRequest'); let extHelperAppSvc = Cc['@mozilla.org/uriloader/external-helper-app-service;1']. getService(Ci.nsIExternalHelperAppService); let channel = aRequest.QueryInterface(Ci.nsIChannel); // First, we'll ensure the filename doesn't have any leading // periods. We have to do it here to avoid ending up with a filename // that's only an extension with no extension (e.g. Sending in // '.jpeg' without stripping the '.' would result in a filename of // 'jpeg' where we want 'jpeg.jpeg'. _options.filename = _options.filename.replace(/^\.+/, ""); let ext = null; let mimeSvc = extHelperAppSvc.QueryInterface(Ci.nsIMIMEService); try { ext = '.' + mimeSvc.getPrimaryExtension(channel.contentType, ''); } catch (e) { ext = null; } // Check if we need to add an extension to the filename. if (ext && !_options.filename.endsWith(ext)) { _options.filename += ext; } // Set the filename to use when saving to disk. channel.contentDispositionFilename = _options.filename; this.extListener = extHelperAppSvc.doContent( channel.contentType, aRequest, interfaceRequestor, true); this.extListener.onStartRequest(aRequest, aContext); }, onStopRequest: function(aRequest, aContext, aStatusCode) { debug('DownloadListener - onStopRequest (aStatusCode = ' + aStatusCode + ')'); if (aStatusCode == Cr.NS_OK) { // Everything looks great. debug('DownloadListener - Download Successful.'); Services.DOMRequest.fireSuccess(req, aStatusCode); } else { // In case of failure, we'll simply return the failure status code. debug('DownloadListener - Download Failed!'); Services.DOMRequest.fireError(req, aStatusCode); } if (this.extListener) { this.extListener.onStopRequest(aRequest, aContext, aStatusCode); } }, onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { this.extListener.onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount); }, QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIRequestObserver]) }; let referrer = Services.io.newURI(_options.referrer, null, null); let principal = Services.scriptSecurityManager.createCodebasePrincipal( referrer, this._frameLoader.loadContext.originAttributes); let channel = NetUtil.newChannel({ uri: url, loadingPrincipal: principal, securityFlags: SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS, contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER }); // XXX We would set private browsing information prior to calling this. channel.notificationCallbacks = interfaceRequestor; // Since we're downloading our own local copy we'll want to bypass the // cache and local cache if the channel let's us specify this. let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS | Ci.nsIChannel.LOAD_BYPASS_CACHE; if (channel instanceof Ci.nsICachingChannel) { debug('This is a caching channel. Forcing bypass.'); flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY; } channel.loadFlags |= flags; if (channel instanceof Ci.nsIHttpChannel) { debug('Setting HTTP referrer = ' + (referrer && referrer.spec)); channel.referrer = referrer; if (channel instanceof Ci.nsIHttpChannelInternal) { channel.forceAllowThirdPartyCookie = true; } } // Set-up complete, let's get things started. channel.asyncOpen2(new DownloadListener()); return req; }, getScreenshot: function(_width, _height, _mimeType) { if (!this._isAlive()) { throw Components.Exception("Dead content process", Cr.NS_ERROR_DOM_INVALID_STATE_ERR); } let width = parseInt(_width); let height = parseInt(_height); let mimeType = (typeof _mimeType === 'string') ? _mimeType.trim().toLowerCase() : 'image/jpeg'; if (isNaN(width) || isNaN(height) || width < 0 || height < 0) { throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); } return this._sendDOMRequest('get-screenshot', {width: width, height: height, mimeType: mimeType}); }, _recvNextPaint: function(data) { let listeners = this._nextPaintListeners; this._nextPaintListeners = []; for (let listener of listeners) { try { listener.recvNextPaint(); } catch (e) { // If a listener throws we'll continue. } } }, addNextPaintListener: function(listener) { if (!this._isAlive()) { throw Components.Exception("Dead content process", Cr.NS_ERROR_DOM_INVALID_STATE_ERR); } let self = this; let run = function() { if (self._nextPaintListeners.push(listener) == 1) self._sendAsyncMsg('activate-next-paint-listener'); }; if (!this._domRequestReady) { this._pendingAPICalls.push(run); } else { run(); } }, removeNextPaintListener: function(listener) { if (!this._isAlive()) { throw Components.Exception("Dead content process", Cr.NS_ERROR_DOM_INVALID_STATE_ERR); } let self = this; let run = function() { for (let i = self._nextPaintListeners.length - 1; i >= 0; i--) { if (self._nextPaintListeners[i] == listener) { self._nextPaintListeners.splice(i, 1); break; } } if (self._nextPaintListeners.length == 0) self._sendAsyncMsg('deactivate-next-paint-listener'); }; if (!this._domRequestReady) { this._pendingAPICalls.push(run); } else { run(); } }, setInputMethodActive: function(isActive) { if (!this._isAlive()) { throw Components.Exception("Dead content process", Cr.NS_ERROR_DOM_INVALID_STATE_ERR); } if (typeof isActive !== 'boolean') { throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); } return this._sendDOMRequest('set-input-method-active', {isActive: isActive}); }, getAudioChannelVolume: function(aAudioChannel) { return this._sendDOMRequest('get-audio-channel-volume', {audioChannel: aAudioChannel}); }, setAudioChannelVolume: function(aAudioChannel, aVolume) { return this._sendDOMRequest('set-audio-channel-volume', {audioChannel: aAudioChannel, volume: aVolume}); }, getAudioChannelMuted: function(aAudioChannel) { return this._sendDOMRequest('get-audio-channel-muted', {audioChannel: aAudioChannel}); }, setAudioChannelMuted: function(aAudioChannel, aMuted) { return this._sendDOMRequest('set-audio-channel-muted', {audioChannel: aAudioChannel, muted: aMuted}); }, isAudioChannelActive: function(aAudioChannel) { return this._sendDOMRequest('get-is-audio-channel-active', {audioChannel: aAudioChannel}); }, getWebManifest: defineDOMRequestMethod('get-web-manifest'), /** * Called when the visibility of the window which owns this iframe changes. */ _ownerVisibilityChange: function() { this._sendAsyncMsg('owner-visibility-change', {visible: !this._window.document.hidden}); }, /* * Called when the child notices that its visibility has changed. * * This is sometimes redundant; for example, the child's visibility may * change in response to a setVisible request that we made here! But it's * not always redundant; for example, the child's visibility may change in * response to its parent docshell being hidden. */ _childVisibilityChange: function(data) { debug("_childVisibilityChange(" + data.json.visible + ")"); this._frameLoader.visible = data.json.visible; this._fireEventFromMsg(data); }, _requestedDOMFullscreen: function() { this._pendingDOMFullscreen = true; this._windowUtils.remoteFrameFullscreenChanged(this._frameElement); }, _fullscreenOriginChange: function(data) { Services.obs.notifyObservers( this._frameElement, "fullscreen-origin-change", data.json.originNoSuffix); }, _exitDomFullscreen: function(data) { this._windowUtils.remoteFrameFullscreenReverted(); }, _handleOwnerEvent: function(evt) { switch (evt.type) { case 'visibilitychange': this._ownerVisibilityChange(); break; case 'fullscreenchange': if (!this._window.document.fullscreenElement) { this._sendAsyncMsg('exit-fullscreen'); } else if (this._pendingDOMFullscreen) { this._pendingDOMFullscreen = false; this._sendAsyncMsg('entered-fullscreen'); } break; } }, _fireFatalError: function() { let evt = this._createEvent('error', {type: 'fatal'}, /* cancelable = */ false); this._frameElement.dispatchEvent(evt); }, observe: function(subject, topic, data) { switch(topic) { case 'oop-frameloader-crashed': if (this._isAlive() && subject == this._frameLoader) { this._fireFatalError(); } break; case 'ask-children-to-execute-copypaste-command': if (this._isAlive() && this._frameElement == subject.wrappedJSObject) { this._sendAsyncMsg('copypaste-do-command', { command: data }); } break; case 'back-docommand': if (this._isAlive() && this._frameLoader.visible) { this.goBack(); } break; default: debug('Unknown topic: ' + topic); break; }; }, }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([BrowserElementParent]);