diff options
Diffstat (limited to 'dom/browser-element')
-rw-r--r-- | dom/browser-element/BrowserElementChild.js | 42 | ||||
-rw-r--r-- | dom/browser-element/BrowserElementChildPreload.js | 290 | ||||
-rw-r--r-- | dom/browser-element/BrowserElementParent.jsm | 276 | ||||
-rw-r--r-- | dom/browser-element/BrowserElementPromptService.jsm | 720 | ||||
-rw-r--r-- | dom/browser-element/components.conf | 14 | ||||
-rw-r--r-- | dom/browser-element/moz.build | 37 | ||||
-rw-r--r-- | dom/browser-element/nsIBrowserElementAPI.idl | 44 |
7 files changed, 1423 insertions, 0 deletions
diff --git a/dom/browser-element/BrowserElementChild.js b/dom/browser-element/BrowserElementChild.js new file mode 100644 index 0000000000..762957bcd4 --- /dev/null +++ b/dom/browser-element/BrowserElementChild.js @@ -0,0 +1,42 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ +/* global api, CopyPasteAssistent */ + +"use strict"; + +function debug(msg) { + // dump("BrowserElementChild - " + msg + "\n"); +} + +var BrowserElementIsReady; + +debug(`Might load BE scripts: BEIR: ${BrowserElementIsReady}`); +if (!BrowserElementIsReady) { + debug("Loading BE scripts"); + if (!("BrowserElementIsPreloaded" in this)) { + Services.scriptloader.loadSubScript( + "chrome://global/content/BrowserElementChildPreload.js", + this + ); + } + + function onDestroy() { + removeMessageListener("browser-element-api:destroy", onDestroy); + + if (api) { + api.destroy(); + } + + BrowserElementIsReady = false; + } + addMessageListener("browser-element-api:destroy", onDestroy); + + BrowserElementIsReady = true; +} else { + debug("BE already loaded, abort"); +} + +sendAsyncMessage("browser-element-api:call", { msg_name: "hello" }); diff --git a/dom/browser-element/BrowserElementChildPreload.js b/dom/browser-element/BrowserElementChildPreload.js new file mode 100644 index 0000000000..1bbcf9ff05 --- /dev/null +++ b/dom/browser-element/BrowserElementChildPreload.js @@ -0,0 +1,290 @@ +/* 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"; + +/* eslint-env mozilla/frame-script */ + +function debug(msg) { + // dump("BrowserElementChildPreload - " + msg + "\n"); +} + +debug("loaded"); + +var BrowserElementIsReady; + +var { BrowserElementPromptService } = ChromeUtils.import( + "resource://gre/modules/BrowserElementPromptService.jsm" +); + +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); +} + +var LISTENED_EVENTS = [ + // 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 }, +]; + +/** + * The BrowserElementChild implements one half of <iframe mozbrowser>. + * (The other half is, unsurprisingly, BrowserElementParent.) + * + * This script is injected into an <iframe mozbrowser> via + * nsIMessageManager::LoadFrameScript(). + * + * Our job here is to listen for events within this frame and bubble them up to + * the parent process. + */ + +var global = this; + +function BrowserElementChild() { + // Maps outer window id --> weak ref to window. Used by modal dialog code. + this._windowIDDict = {}; + + this._init(); +} + +BrowserElementChild.prototype = { + _init() { + debug("Starting up."); + + BrowserElementPromptService.mapWindowToBrowserElementChild(content, this); + + this._shuttingDown = false; + + LISTENED_EVENTS.forEach(event => { + addEventListener( + event.type, + this, + event.useCapture, + event.wantsUntrusted + ); + }); + + addMessageListener("browser-element-api:call", this); + }, + + /** + * Shut down the frame's side of the browser API. This is called when: + * - our BrowserChildGlobal starts to die + * - the content is moved to frame without the browser API + * This is not called when the page inside |content| unloads. + */ + destroy() { + debug("Destroying"); + this._shuttingDown = true; + + BrowserElementPromptService.unmapWindowToBrowserElementChild(content); + + LISTENED_EVENTS.forEach(event => { + removeEventListener( + event.type, + this, + event.useCapture, + event.wantsUntrusted + ); + }); + + removeMessageListener("browser-element-api:call", this); + }, + + handleEvent(event) { + switch (event.type) { + case "unload": + this.destroy(event); + break; + } + }, + + receiveMessage(message) { + let self = this; + + let mmCalls = { + "unblock-modal-prompt": this._recvStopWaiting, + }; + + if (message.data.msg_name in mmCalls) { + return mmCalls[message.data.msg_name].apply(self, arguments); + } + return undefined; + }, + + get _windowUtils() { + return content.document.defaultView.windowUtils; + }, + + _tryGetInnerWindowID(win) { + try { + return win.windowGlobalChild.innerWindowId; + } catch (e) { + return null; + } + }, + + /** + * Show a modal prompt. Called by BrowserElementPromptService. + */ + showModalPrompt(win, args) { + args.windowID = { + outer: win.docShell.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; + } + return undefined; + }, + + /** + * Spin in a nested event loop until we receive a unblock-modal-prompt message for + * this window. + */ + _waitForResult(win) { + debug("_waitForResult(" + win + ")"); + let utils = win.windowUtils; + + let outerWindowID = win.docShell.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 undefined; + } + + this._windowIDDict[outerWindowID] = Cu.getWeakReference(win); + + debug( + "Entering modal state (outerWindowID=" + + outerWindowID + + ", " + + "innerWindowID=" + + innerWindowID + + ")" + ); + + utils.enterModalState(); + + // We'll decrement win.modalDepth when we receive a unblock-modal-prompt message + // for the window. + if (!win.modalDepth) { + win.modalDepth = 0; + } + win.modalDepth++; + let origModalDepth = win.modalDepth; + + debug("Nested event loop - begin"); + Services.tm.spinEventLoopUntil( + "BrowserElementChildPreload.js:_waitForResult", + () => { + // Bail out of the loop if the inner window changed; that means the + // window navigated. Bail out when we're shutting down because otherwise + // we'll leak our window. + if (this._tryGetInnerWindowID(win) !== innerWindowID) { + debug( + "_waitForResult: Inner window ID changed " + + "while in nested event loop." + ); + return true; + } + + return win.modalDepth !== origModalDepth || this._shuttingDown; + } + ); + debug("Nested event loop - finish"); + + if (win.modalDepth == 0) { + delete this._windowIDDict[outerWindowID]; + } + + // If we exited the loop because the inner window changed, then bail on the + // modal prompt. + if (innerWindowID !== this._tryGetInnerWindowID(win)) { + throw Components.Exception( + "Modal state aborted by navigation", + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + + let returnValue = win.modalReturnValue; + delete win.modalReturnValue; + + if (!this._shuttingDown) { + utils.leaveModalState(); + } + + debug( + "Leaving modal state (outerID=" + + outerWindowID + + ", " + + "innerID=" + + innerWindowID + + ")" + ); + return returnValue; + }, + + _recvStopWaiting(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--; + }, +}; + +var api = new BrowserElementChild(); diff --git a/dom/browser-element/BrowserElementParent.jsm b/dom/browser-element/BrowserElementParent.jsm new file mode 100644 index 0000000000..ec342dace2 --- /dev/null +++ b/dom/browser-element/BrowserElementParent.jsm @@ -0,0 +1,276 @@ +/* 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"; + +/* 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. + */ + +const { BrowserElementPromptService } = ChromeUtils.import( + "resource://gre/modules/BrowserElementPromptService.jsm" +); + +function debug(msg) { + // dump("BrowserElementParent - " + msg + "\n"); +} + +function handleWindowEvent(e) { + if (this._browserElementParents) { + let beps = ChromeUtils.nondeterministicGetWeakMapKeys( + this._browserElementParents + ); + beps.forEach(bep => bep._handleOwnerEvent(e)); + } +} + +function BrowserElementParent() { + debug("Creating new BrowserElementParent object"); +} + +BrowserElementParent.prototype = { + classDescription: "BrowserElementAPI implementation", + classID: Components.ID("{9f171ac4-0939-4ef8-b360-3408aedc3060}"), + contractID: "@mozilla.org/dom/browser-element-api;1", + QueryInterface: ChromeUtils.generateQI([ + "nsIBrowserElementAPI", + "nsISupportsWeakReference", + ]), + + setFrameLoader(frameLoader) { + debug("Setting frameLoader"); + this._frameLoader = frameLoader; + this._frameElement = frameLoader.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"]; + for (let event of windowEvents) { + Services.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(); + }, + + destroyFrameScripts() { + debug("Destroying frame scripts"); + this._mm.sendAsyncMessage("browser-element-api:destroy"); + }, + + _setupMessageListener() { + this._mm = this._frameLoader.messageManager; + this._mm.addMessageListener("browser-element-api:call", this); + }, + + receiveMessage(aMsg) { + if (!this._isAlive()) { + return undefined; + } + + // 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, + }; + + let mmSecuritySensitiveCalls = { + showmodalprompt: this._handleShowModalPrompt, + }; + + 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 + ); + } + return undefined; + }, + + _removeMessageListener() { + 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() { + return ( + !Cu.isDeadWrapper(this._frameElement) && + !Cu.isDeadWrapper(this._frameElement.ownerDocument) && + !Cu.isDeadWrapper(this._frameElement.ownerGlobal) + ); + }, + + get _window() { + return this._frameElement.ownerGlobal; + }, + + _sendAsyncMsg(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() { + 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(); + } + }, + + /** + * Fire either a vanilla or a custom event, depending on the contents of + * |data|. + */ + _fireEventFromMsg(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(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, 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(); + } + }, + + _createEvent(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, + detail, + }); + } + + return new this._window.Event("mozbrowser" + evtName, { + bubbles: true, + cancelable, + }); + }, + + _handleOwnerEvent(evt) { + switch (evt.type) { + case "visibilitychange": + this._ownerVisibilityChange(); + break; + } + }, + + /** + * Called when the visibility of the window which owns this iframe changes. + */ + _ownerVisibilityChange() { + let bc = this._frameLoader?.browsingContext; + if (bc) { + bc.isActive = !this._window.document.hidden; + } + }, +}; + +var EXPORTED_SYMBOLS = ["BrowserElementParent"]; diff --git a/dom/browser-element/BrowserElementPromptService.jsm b/dom/browser-element/BrowserElementPromptService.jsm new file mode 100644 index 0000000000..dd73004959 --- /dev/null +++ b/dom/browser-element/BrowserElementPromptService.jsm @@ -0,0 +1,720 @@ +/* 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/. */ +/* vim: set ft=javascript : */ + +"use strict"; + +var Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + +var EXPORTED_SYMBOLS = ["BrowserElementPromptService"]; + +function debug(msg) { + // dump("BrowserElementPromptService - " + msg + "\n"); +} + +function BrowserElementPrompt(win, browserElementChild) { + this._win = win; + this._browserElementChild = browserElementChild; +} + +BrowserElementPrompt.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]), + + alert(title, text) { + this._browserElementChild.showModalPrompt(this._win, { + promptType: "alert", + title, + message: text, + returnValue: undefined, + }); + }, + + alertCheck(title, text, checkMsg, checkState) { + // Treat this like a normal alert() call, ignoring the checkState. The + // front-end can do its own suppression of the alert() if it wants. + this.alert(title, text); + }, + + confirm(title, text) { + return this._browserElementChild.showModalPrompt(this._win, { + promptType: "confirm", + title, + message: text, + returnValue: undefined, + }); + }, + + confirmCheck(title, text, checkMsg, checkState) { + return this.confirm(title, text); + }, + + // Each button is described by an object with the following schema + // { + // string messageType, // 'builtin' or 'custom' + // string message, // 'ok', 'cancel', 'yes', 'no', 'save', 'dontsave', + // // 'revert' or a string from caller if messageType was 'custom'. + // } + // + // Expected result from embedder: + // { + // int button, // Index of the button that user pressed. + // boolean checked, // True if the check box is checked. + // } + confirmEx( + title, + text, + buttonFlags, + button0Title, + button1Title, + button2Title, + checkMsg, + checkState + ) { + let buttonProperties = this._buildConfirmExButtonProperties( + buttonFlags, + button0Title, + button1Title, + button2Title + ); + let defaultReturnValue = { selectedButton: buttonProperties.defaultButton }; + if (checkMsg) { + defaultReturnValue.checked = checkState.value; + } + let ret = this._browserElementChild.showModalPrompt(this._win, { + promptType: "custom-prompt", + title, + message: text, + defaultButton: buttonProperties.defaultButton, + buttons: buttonProperties.buttons, + showCheckbox: !!checkMsg, + checkboxMessage: checkMsg, + checkboxCheckedByDefault: !!checkState.value, + returnValue: defaultReturnValue, + }); + if (checkMsg) { + checkState.value = ret.checked; + } + return buttonProperties.indexToButtonNumberMap[ret.selectedButton]; + }, + + prompt(title, text, value, checkMsg, checkState) { + let rv = this._browserElementChild.showModalPrompt(this._win, { + promptType: "prompt", + title, + message: text, + initialValue: value.value, + returnValue: null, + }); + + value.value = rv; + + // nsIPrompt::Prompt returns true if the user pressed "OK" at the prompt, + // and false if the user pressed "Cancel". + // + // BrowserElementChild returns null for "Cancel" and returns the string the + // user entered otherwise. + return rv !== null; + }, + + promptUsernameAndPassword(title, text, username, password) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + promptPassword(title, text, password) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + select(title, text, aSelectList, aOutSelection) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + _buildConfirmExButtonProperties( + buttonFlags, + button0Title, + button1Title, + button2Title + ) { + let r = { + defaultButton: -1, + buttons: [], + // This map is for translating array index to the button number that + // is recognized by Gecko. This shouldn't be exposed to embedder. + indexToButtonNumberMap: [], + }; + + let defaultButton = 0; // Default to Button 0. + if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_1_DEFAULT) { + defaultButton = 1; + } else if (buttonFlags & Ci.nsIPrompt.BUTTON_POS_2_DEFAULT) { + defaultButton = 2; + } + + // Properties of each button. + let buttonPositions = [ + Ci.nsIPrompt.BUTTON_POS_0, + Ci.nsIPrompt.BUTTON_POS_1, + Ci.nsIPrompt.BUTTON_POS_2, + ]; + + function buildButton(buttonTitle, buttonNumber) { + let ret = {}; + let buttonPosition = buttonPositions[buttonNumber]; + let mask = 0xff * buttonPosition; // 8 bit mask + let titleType = (buttonFlags & mask) / buttonPosition; + + ret.messageType = "builtin"; + switch (titleType) { + case Ci.nsIPrompt.BUTTON_TITLE_OK: + ret.message = "ok"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_CANCEL: + ret.message = "cancel"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_YES: + ret.message = "yes"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_NO: + ret.message = "no"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_SAVE: + ret.message = "save"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE: + ret.message = "dontsave"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_REVERT: + ret.message = "revert"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING: + ret.message = buttonTitle; + ret.messageType = "custom"; + break; + default: + // This button is not shown. + return; + } + + // If this is the default button, set r.defaultButton to + // the index of this button in the array. This value is going to be + // exposed to the embedder. + if (defaultButton === buttonNumber) { + r.defaultButton = r.buttons.length; + } + r.buttons.push(ret); + r.indexToButtonNumberMap.push(buttonNumber); + } + + buildButton(button0Title, 0); + buildButton(button1Title, 1); + buildButton(button2Title, 2); + + // If defaultButton is still -1 here, it means the default button won't + // be shown. + if (r.defaultButton === -1) { + throw new Components.Exception( + "Default button won't be shown", + Cr.NS_ERROR_FAILURE + ); + } + + return r; + }, +}; + +function BrowserElementAuthPrompt() {} + +BrowserElementAuthPrompt.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + + promptAuth: function promptAuth(channel, level, authInfo) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + + asyncPromptAuth: function asyncPromptAuth( + channel, + callback, + context, + level, + authInfo + ) { + debug("asyncPromptAuth"); + + // The cases that we don't support now. + if ( + authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY && + authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD + ) { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + let frame = this._getFrameFromChannel(channel); + if (!frame) { + debug("Cannot get frame, asyncPromptAuth fail"); + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + let browserElementParent = + BrowserElementPromptService.getBrowserElementParentForFrame(frame); + + if (!browserElementParent) { + debug("Failed to load browser element parent."); + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + let consumer = { + QueryInterface: ChromeUtils.generateQI(["nsICancelable"]), + callback, + context, + cancel() { + this.callback.onAuthCancelled(this.context, false); + this.callback = null; + this.context = null; + }, + }; + + let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo); + let hashKey = level + "|" + hostname + "|" + httpRealm; + let asyncPrompt = this._asyncPrompts[hashKey]; + if (asyncPrompt) { + asyncPrompt.consumers.push(consumer); + return consumer; + } + + asyncPrompt = { + consumers: [consumer], + channel, + authInfo, + level, + inProgress: false, + browserElementParent, + }; + + this._asyncPrompts[hashKey] = asyncPrompt; + this._doAsyncPrompt(); + return consumer; + }, + + // Utilities for nsIAuthPrompt2 ---------------- + + _asyncPrompts: {}, + _asyncPromptInProgress: new WeakMap(), + _doAsyncPrompt() { + // Find the key of a prompt whose browser element parent does not have + // async prompt in progress. + let hashKey = null; + for (let key in this._asyncPrompts) { + let prompt = this._asyncPrompts[key]; + if (!this._asyncPromptInProgress.get(prompt.browserElementParent)) { + hashKey = key; + break; + } + } + + // Didn't find an available prompt, so just return. + if (!hashKey) { + return; + } + + let prompt = this._asyncPrompts[hashKey]; + + this._asyncPromptInProgress.set(prompt.browserElementParent, true); + prompt.inProgress = true; + + let self = this; + let callback = function (ok, username, password) { + debug( + "Async auth callback is called, ok = " + ok + ", username = " + username + ); + + // Here we got the username and password provided by embedder, or + // ok = false if the prompt was cancelled by embedder. + delete self._asyncPrompts[hashKey]; + prompt.inProgress = false; + self._asyncPromptInProgress.delete(prompt.browserElementParent); + + // Fill authentication information with username and password provided + // by user. + let flags = prompt.authInfo.flags; + if (username) { + if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) { + // Domain is separated from username by a backslash + let idx = username.indexOf("\\"); + if (idx == -1) { + prompt.authInfo.username = username; + } else { + prompt.authInfo.domain = username.substring(0, idx); + prompt.authInfo.username = username.substring(idx + 1); + } + } else { + prompt.authInfo.username = username; + } + } + + if (password) { + prompt.authInfo.password = password; + } + + for (let consumer of prompt.consumers) { + if (!consumer.callback) { + // Not having a callback means that consumer didn't provide it + // or canceled the notification. + continue; + } + + try { + if (ok) { + debug("Ok, calling onAuthAvailable to finish auth"); + consumer.callback.onAuthAvailable( + consumer.context, + prompt.authInfo + ); + } else { + debug("Cancelled, calling onAuthCancelled to finish auth."); + consumer.callback.onAuthCancelled(consumer.context, true); + } + } catch (e) { + /* Throw away exceptions caused by callback */ + } + } + + // Process the next prompt, if one is pending. + self._doAsyncPrompt(); + }; + + let runnable = { + run() { + // Call promptAuth of browserElementParent, to show the prompt. + prompt.browserElementParent.promptAuth( + self._createAuthDetail(prompt.channel, prompt.authInfo), + callback + ); + }, + }; + + Services.tm.dispatchToMainThread(runnable); + }, + + _getFrameFromChannel(channel) { + let loadContext = channel.notificationCallbacks.getInterface( + Ci.nsILoadContext + ); + return loadContext.topFrameElement; + }, + + _createAuthDetail(channel, authInfo) { + let [hostname, httpRealm] = this._getAuthTarget(channel, authInfo); + return { + host: hostname, + path: channel.URI.pathQueryRef, + realm: httpRealm, + username: authInfo.username, + isProxy: !!(authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY), + isOnlyPassword: !!(authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD), + }; + }, + + // The code is taken from nsLoginManagerPrompter.js, with slight + // modification for parameter name consistency here. + _getAuthTarget(channel, authInfo) { + let hostname, realm; + + // If our proxy is demanding authentication, don't use the + // channel's actual destination. + if (authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { + if (!(channel instanceof Ci.nsIProxiedChannel)) { + throw new Error("proxy auth needs nsIProxiedChannel"); + } + + let info = channel.proxyInfo; + if (!info) { + throw new Error("proxy auth needs nsIProxyInfo"); + } + + // Proxies don't have a scheme, but we'll use "moz-proxy://" + // so that it's more obvious what the login is for. + var idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + hostname = + "moz-proxy://" + + idnService.convertUTF8toACE(info.host) + + ":" + + info.port; + realm = authInfo.realm; + if (!realm) { + realm = hostname; + } + + return [hostname, realm]; + } + + hostname = this._getFormattedHostname(channel.URI); + + // If a HTTP WWW-Authenticate header specified a realm, that value + // will be available here. If it wasn't set or wasn't HTTP, we'll use + // the formatted hostname instead. + realm = authInfo.realm; + if (!realm) { + realm = hostname; + } + + return [hostname, realm]; + }, + + /** + * Strip out things like userPass and path for display. + */ + _getFormattedHostname(uri) { + return uri.scheme + "://" + uri.hostPort; + }, +}; + +function AuthPromptWrapper(oldImpl, browserElementImpl) { + this._oldImpl = oldImpl; + this._browserElementImpl = browserElementImpl; +} + +AuthPromptWrapper.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + promptAuth(channel, level, authInfo) { + if (this._canGetParentElement(channel)) { + return this._browserElementImpl.promptAuth(channel, level, authInfo); + } + return this._oldImpl.promptAuth(channel, level, authInfo); + }, + + asyncPromptAuth(channel, callback, context, level, authInfo) { + if (this._canGetParentElement(channel)) { + return this._browserElementImpl.asyncPromptAuth( + channel, + callback, + context, + level, + authInfo + ); + } + return this._oldImpl.asyncPromptAuth( + channel, + callback, + context, + level, + authInfo + ); + }, + + _canGetParentElement(channel) { + try { + let context = channel.notificationCallbacks.getInterface( + Ci.nsILoadContext + ); + let frame = context.topFrameElement; + if (!frame) { + return false; + } + + if (!BrowserElementPromptService.getBrowserElementParentForFrame(frame)) { + return false; + } + + return true; + } catch (e) { + return false; + } + }, +}; + +function BrowserElementPromptFactory(toWrap) { + this._wrapped = toWrap; +} + +BrowserElementPromptFactory.prototype = { + classID: Components.ID("{24f3d0cf-e417-4b85-9017-c9ecf8bb1299}"), + QueryInterface: ChromeUtils.generateQI(["nsIPromptFactory"]), + + _mayUseNativePrompt() { + try { + return Services.prefs.getBoolPref("browser.prompt.allowNative"); + } catch (e) { + // This properity is default to true. + return true; + } + }, + + _getNativePromptIfAllowed(win, iid, err) { + if (this._mayUseNativePrompt()) { + return this._wrapped.getPrompt(win, iid); + } + + // Not allowed, throw an exception. + throw err; + }, + + getPrompt(win, iid) { + // It is possible for some object to get a prompt without passing + // valid reference of window, like nsNSSComponent. In such case, we + // should just fall back to the native prompt service + if (!win) { + return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG); + } + + if ( + iid.number != Ci.nsIPrompt.number && + iid.number != Ci.nsIAuthPrompt2.number + ) { + debug( + "We don't recognize the requested IID (" + + iid + + ", " + + "allowed IID: " + + "nsIPrompt=" + + Ci.nsIPrompt + + ", " + + "nsIAuthPrompt2=" + + Ci.nsIAuthPrompt2 + + ")" + ); + return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_INVALID_ARG); + } + + // Try to find a BrowserElementChild for the window. + let browserElementChild = + BrowserElementPromptService.getBrowserElementChildForWindow(win); + + if (iid.number === Ci.nsIAuthPrompt2.number) { + debug("Caller requests an instance of nsIAuthPrompt2."); + + if (browserElementChild) { + // If we are able to get a BrowserElementChild, it means that + // the auth prompt is for a mozbrowser. Therefore we don't need to + // fall back. + return new BrowserElementAuthPrompt().QueryInterface(iid); + } + + // Because nsIAuthPrompt2 is called in parent process. If caller + // wants nsIAuthPrompt2 and we cannot get BrowserElementchild, + // it doesn't mean that we should fallback. It is possible that we can + // get the BrowserElementParent from nsIChannel that passed to + // functions of nsIAuthPrompt2. + if (this._mayUseNativePrompt()) { + return new AuthPromptWrapper( + this._wrapped.getPrompt(win, iid), + new BrowserElementAuthPrompt().QueryInterface(iid) + ).QueryInterface(iid); + } + // Falling back is not allowed, so we don't need wrap the + // BrowserElementPrompt. + return new BrowserElementAuthPrompt().QueryInterface(iid); + } + + if (!browserElementChild) { + debug( + "We can't find a browserElementChild for " + win + ", " + win.location + ); + return this._getNativePromptIfAllowed(win, iid, Cr.NS_ERROR_FAILURE); + } + + debug("Returning wrapped getPrompt for " + win); + return new BrowserElementPrompt(win, browserElementChild).QueryInterface( + iid + ); + }, +}; + +var BrowserElementPromptService = { + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + _initialized: false, + + _init() { + if (this._initialized) { + return; + } + + this._initialized = true; + this._browserElementParentMap = new WeakMap(); + + Services.obs.addObserver( + this, + "outer-window-destroyed", + /* ownsWeak = */ true + ); + + // Wrap the existing @mozilla.org/prompter;1 implementation. + var contractID = "@mozilla.org/prompter;1"; + var oldCID = Cm.contractIDToCID(contractID); + var newCID = BrowserElementPromptFactory.prototype.classID; + var oldFactory = Cm.getClassObject(Cc[contractID], Ci.nsIFactory); + + if (oldCID == newCID) { + debug("WARNING: Wrapped prompt factory is already installed!"); + return; + } + + var oldInstance = oldFactory.createInstance(null, Ci.nsIPromptFactory); + var newInstance = new BrowserElementPromptFactory(oldInstance); + + var newFactory = { + createInstance(iid) { + return newInstance.QueryInterface(iid); + }, + }; + Cm.registerFactory( + newCID, + "BrowserElementPromptService's prompter;1 wrapper", + contractID, + newFactory + ); + + debug("Done installing new prompt factory."); + }, + + _getOuterWindowID(win) { + return win.docShell.outerWindowID; + }, + + _browserElementChildMap: {}, + mapWindowToBrowserElementChild(win, browserElementChild) { + this._browserElementChildMap[this._getOuterWindowID(win)] = + browserElementChild; + }, + unmapWindowToBrowserElementChild(win) { + delete this._browserElementChildMap[this._getOuterWindowID(win)]; + }, + + getBrowserElementChildForWindow(win) { + // We only have a mapping for <iframe mozbrowser>s, not their inner + // <iframes>, so we look up win.top below. window.top (when called from + // script) respects <iframe mozbrowser> boundaries. + return this._browserElementChildMap[this._getOuterWindowID(win.top)]; + }, + + mapFrameToBrowserElementParent(frame, browserElementParent) { + this._browserElementParentMap.set(frame, browserElementParent); + }, + + getBrowserElementParentForFrame(frame) { + return this._browserElementParentMap.get(frame); + }, + + _observeOuterWindowDestroyed(outerWindowID) { + let id = outerWindowID.QueryInterface(Ci.nsISupportsPRUint64).data; + debug("observeOuterWindowDestroyed " + id); + delete this._browserElementChildMap[outerWindowID.data]; + }, + + observe(subject, topic, data) { + switch (topic) { + case "outer-window-destroyed": + this._observeOuterWindowDestroyed(subject); + break; + default: + debug("Observed unexpected topic " + topic); + } + }, +}; + +BrowserElementPromptService._init(); diff --git a/dom/browser-element/components.conf b/dom/browser-element/components.conf new file mode 100644 index 0000000000..a29f7dc66a --- /dev/null +++ b/dom/browser-element/components.conf @@ -0,0 +1,14 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{9f171ac4-0939-4ef8-b360-3408aedc3060}', + 'contract_ids': ['@mozilla.org/dom/browser-element-api;1'], + 'jsm': 'resource://gre/modules/BrowserElementParent.jsm', + 'constructor': 'BrowserElementParent', + }, +] diff --git a/dom/browser-element/moz.build b/dom/browser-element/moz.build new file mode 100644 index 0000000000..1f83fbd436 --- /dev/null +++ b/dom/browser-element/moz.build @@ -0,0 +1,37 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +XPIDL_SOURCES += [ + "nsIBrowserElementAPI.idl", +] + +XPIDL_MODULE = "browser-element" + +EXTRA_JS_MODULES += [ + "BrowserElementParent.jsm", + "BrowserElementPromptService.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +LOCAL_INCLUDES += [ + "/dom/html", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +LOCAL_INCLUDES += [ + "/dom/", + "/dom/base", + "/dom/ipc", +] diff --git a/dom/browser-element/nsIBrowserElementAPI.idl b/dom/browser-element/nsIBrowserElementAPI.idl new file mode 100644 index 0000000000..4aeba96be8 --- /dev/null +++ b/dom/browser-element/nsIBrowserElementAPI.idl @@ -0,0 +1,44 @@ +/* -*- Mode: IDL; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsISupports.idl" + +webidl FrameLoader; + +%{C++ +#define BROWSER_ELEMENT_API_CONTRACTID "@mozilla.org/dom/browser-element-api;1" +#define BROWSER_ELEMENT_API_CID \ + { 0x651db7e3, 0x1734, 0x4536, \ + { 0xb1, 0x5a, 0x5b, 0x3a, 0xe6, 0x44, 0x13, 0x4c } } +%} + +/** + * Interface to the BrowserElementParent implementation. All methods + * but setFrameLoader throw when the remote process is dead. + */ +[scriptable, uuid(57758c10-6036-11e5-a837-0800200c9a66)] +interface nsIBrowserElementAPI : nsISupports +{ + /** + * Notify frame scripts that support the API to destroy. + */ + void destroyFrameScripts(); + + void setFrameLoader(in FrameLoader frameLoader); + + void sendMouseEvent(in AString type, + in uint32_t x, + in uint32_t y, + in uint32_t button, + in uint32_t clickCount, + in uint32_t mifiers); + void goBack(); + void goForward(); + void reload(in boolean hardReload); + void stop(); + Promise getCanGoBack(); + Promise getCanGoForward(); +}; |