diff options
Diffstat (limited to 'dom/browser-element/BrowserElementParent.js')
-rw-r--r-- | dom/browser-element/BrowserElementParent.js | 1202 |
1 files changed, 1202 insertions, 0 deletions
diff --git a/dom/browser-element/BrowserElementParent.js b/dom/browser-element/BrowserElementParent.js new file mode 100644 index 000000000..67d05f0ab --- /dev/null +++ b/dom/browser-element/BrowserElementParent.js @@ -0,0 +1,1202 @@ +/* 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 getIntPref(prefName, def) { + try { + return Services.prefs.getIntPref(prefName); + } + catch(err) { + return def; + } +} + +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(getIntPref("zoom.maxPercent", 300), zoom); + zoom = Math.max(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]); |