diff options
Diffstat (limited to 'toolkit/components/prompts/src')
-rw-r--r-- | toolkit/components/prompts/src/CommonDialog.sys.mjs | 381 | ||||
-rw-r--r-- | toolkit/components/prompts/src/PromptUtils.sys.mjs | 186 | ||||
-rw-r--r-- | toolkit/components/prompts/src/Prompter.sys.mjs | 1824 | ||||
-rw-r--r-- | toolkit/components/prompts/src/components.conf | 26 | ||||
-rw-r--r-- | toolkit/components/prompts/src/moz.build | 15 |
5 files changed, 2432 insertions, 0 deletions
diff --git a/toolkit/components/prompts/src/CommonDialog.sys.mjs b/toolkit/components/prompts/src/CommonDialog.sys.mjs new file mode 100644 index 0000000000..a0812aa8ec --- /dev/null +++ b/toolkit/components/prompts/src/CommonDialog.sys.mjs @@ -0,0 +1,381 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + EnableDelayHelper: "resource://gre/modules/PromptUtils.sys.mjs", +}); + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +export function CommonDialog(args, ui) { + this.args = args; + this.ui = ui; + this.initialFocusPromise = new Promise(resolve => { + this.initialFocusResolver = resolve; + }); +} + +CommonDialog.prototype = { + args: null, + ui: null, + + hasInputField: true, + numButtons: undefined, + iconClass: undefined, + soundID: undefined, + focusTimer: null, + initialFocusPromise: null, + initialFocusResolver: null, + + /** + * @param [commonDialogEl] - Dialog element from commonDialog.xhtml, + * null for TabModalPrompts. + */ + async onLoad(commonDialogEl = null) { + let isEmbedded = !!commonDialogEl?.ownerGlobal.docShell.chromeEventHandler; + + switch (this.args.promptType) { + case "alert": + case "alertCheck": + this.hasInputField = false; + this.numButtons = 1; + this.iconClass = ["alert-icon"]; + this.soundID = Ci.nsISound.EVENT_ALERT_DIALOG_OPEN; + break; + case "confirmCheck": + case "confirm": + this.hasInputField = false; + this.numButtons = 2; + this.iconClass = ["question-icon"]; + this.soundID = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN; + break; + case "confirmEx": + var numButtons = 0; + if (this.args.button0Label) { + numButtons++; + } + if (this.args.button1Label) { + numButtons++; + } + if (this.args.button2Label) { + numButtons++; + } + if (this.args.button3Label) { + numButtons++; + } + if (numButtons == 0) { + throw new Error("A dialog with no buttons? Can not haz."); + } + this.numButtons = numButtons; + this.hasInputField = false; + this.iconClass = ["question-icon"]; + this.soundID = Ci.nsISound.EVENT_CONFIRM_DIALOG_OPEN; + break; + case "prompt": + this.numButtons = 2; + this.iconClass = ["question-icon"]; + this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN; + this.initTextbox("login", this.args.value); + // Clear the label, since this isn't really a username prompt. + this.ui.loginLabel.setAttribute("value", ""); + // Ensure the labeling for the prompt is correct. + this.ui.loginTextbox.setAttribute("aria-labelledby", "infoBody"); + break; + case "promptUserAndPass": + this.numButtons = 2; + this.iconClass = ["authentication-icon", "question-icon"]; + this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN; + this.initTextbox("login", this.args.user); + this.initTextbox("password1", this.args.pass); + break; + case "promptPassword": + this.numButtons = 2; + this.iconClass = ["authentication-icon", "question-icon"]; + this.soundID = Ci.nsISound.EVENT_PROMPT_DIALOG_OPEN; + this.initTextbox("password1", this.args.pass); + // Clear the label, since the message presumably indicates its purpose. + this.ui.password1Label.setAttribute("value", ""); + break; + default: + console.error( + "commonDialog opened for unknown type: ", + this.args.promptType + ); + throw new Error("unknown dialog type"); + } + + if (commonDialogEl) { + commonDialogEl.setAttribute( + "windowtype", + "prompt:" + this.args.promptType + ); + } + + // set the document title + let title = this.args.title; + let infoTitle = this.ui.infoTitle; + infoTitle.appendChild(infoTitle.ownerDocument.createTextNode(title)); + + // Specific check to prevent showing the title on the old content prompts for macOS. + // This should be removed when the old content prompts are removed. + let contentSubDialogPromptEnabled = Services.prefs.getBoolPref( + "prompts.contentPromptSubDialog" + ); + let isOldContentPrompt = + !contentSubDialogPromptEnabled && + this.args.modalType == Ci.nsIPrompt.MODAL_TYPE_CONTENT; + + // After making these preventative checks, we can determine to show it if we're on + // macOS (where there is no titlebar) or if the prompt is a common dialog document + // and has been embedded (has a chromeEventHandler). + infoTitle.hidden = + isOldContentPrompt || !(AppConstants.platform === "macosx" || isEmbedded); + + if (commonDialogEl) { + commonDialogEl.ownerDocument.title = title; + } + + // Set button labels and visibility + // + // This assumes that button0 defaults to a visible "ok" button, and + // button1 defaults to a visible "cancel" button. The other 2 buttons + // have no default labels (and are hidden). + switch (this.numButtons) { + case 4: + this.setLabelForNode(this.ui.button3, this.args.button3Label); + this.ui.button3.hidden = false; + // fall through + case 3: + this.setLabelForNode(this.ui.button2, this.args.button2Label); + this.ui.button2.hidden = false; + // fall through + case 2: + // Defaults to a visible "cancel" button + if (this.args.button1Label) { + this.setLabelForNode(this.ui.button1, this.args.button1Label); + } + break; + + case 1: + this.ui.button1.hidden = true; + break; + } + // Defaults to a visible "ok" button + if (this.args.button0Label) { + this.setLabelForNode(this.ui.button0, this.args.button0Label); + } + + // display the main text + let croppedMessage = ""; + if (this.args.text) { + // Bug 317334 - crop string length as a workaround. + croppedMessage = this.args.text.substr(0, 10000); + // TabModalPrompts don't have an infoRow to hide / not hide here, so + // guard on that here so long as they are in use. + if (this.ui.infoRow) { + this.ui.infoRow.hidden = false; + } + } + let infoBody = this.ui.infoBody; + infoBody.appendChild(infoBody.ownerDocument.createTextNode(croppedMessage)); + + let label = this.args.checkLabel; + if (label) { + // Only show the checkbox if label has a value. + this.ui.checkboxContainer.hidden = false; + this.ui.checkboxContainer.clientTop; // style flush to assure binding is attached + this.setLabelForNode(this.ui.checkbox, label); + this.ui.checkbox.checked = this.args.checked; + } + + // set the icon + let icon = this.ui.infoIcon; + if (icon) { + this.iconClass.forEach((el, idx, arr) => icon.classList.add(el)); + } + + // set default result to cancelled + this.args.ok = false; + this.args.buttonNumClicked = 1; + + // Set the default button + let b = this.args.defaultButtonNum || 0; + let button = this.ui["button" + b]; + + if (commonDialogEl) { + commonDialogEl.defaultButton = ["accept", "cancel", "extra1", "extra2"][ + b + ]; + } else { + button.setAttribute("default", "true"); + } + + if (!isEmbedded && !this.ui.promptContainer?.hidden) { + // Set default focus and select textbox contents if applicable. If we're + // embedded SubDialogManager will call setDefaultFocus for us. + this.setDefaultFocus(true); + } + + if (this.args.enableDelay) { + this.delayHelper = new lazy.EnableDelayHelper({ + disableDialog: () => this.setButtonsEnabledState(false), + enableDialog: () => this.setButtonsEnabledState(true), + focusTarget: this.ui.focusTarget, + }); + } + + // Play a sound (unless we're showing a content prompt -- don't want those + // to feel like OS prompts). + try { + if (commonDialogEl && this.soundID && !this.args.openedWithTabDialog) { + Cc["@mozilla.org/sound;1"] + .getService(Ci.nsISound) + .playEventSound(this.soundID); + } + } catch (e) { + console.error("Couldn't play common dialog event sound: ", e); + } + + if (commonDialogEl) { + if (isEmbedded) { + // If we delayed default focus above, wait for it to be ready before + // sending the notification. + await this.initialFocusPromise; + } + Services.obs.notifyObservers(this.ui.prompt, "common-dialog-loaded"); + } else { + // ui.promptContainer is the <tabmodalprompt> element. + Services.obs.notifyObservers( + this.ui.promptContainer, + "tabmodal-dialog-loaded" + ); + } + }, + + setLabelForNode(aNode, aLabel) { + // This is for labels which may contain embedded access keys. + // If we end in (&X) where X represents the access key, optionally preceded + // by spaces and/or followed by the ':' character, store the access key and + // remove the access key placeholder + leading spaces from the label. + // Otherwise a character preceded by one but not two &s is the access key. + // Store it and remove the &. + + // Note that if you change the following code, see the comment of + // nsTextBoxFrame::UpdateAccessTitle. + var accessKey = null; + if (/ *\(\&([^&])\)(:?)$/.test(aLabel)) { + aLabel = RegExp.leftContext + RegExp.$2; + accessKey = RegExp.$1; + } else if (/^([^&]*)\&(([^&]).*$)/.test(aLabel)) { + aLabel = RegExp.$1 + RegExp.$2; + accessKey = RegExp.$3; + } + + // && is the magic sequence to embed an & in your label. + aLabel = aLabel.replace(/\&\&/g, "&"); + aNode.label = aLabel; + + // XXXjag bug 325251 + // Need to set this after aNode.setAttribute("value", aLabel); + if (accessKey) { + aNode.accessKey = accessKey; + } + }, + + initTextbox(aName, aValue) { + this.ui[aName + "Container"].hidden = false; + this.ui[aName + "Textbox"].setAttribute( + "value", + aValue !== null ? aValue : "" + ); + }, + + setButtonsEnabledState(enabled) { + this.ui.button0.disabled = !enabled; + // button1 (cancel) remains enabled. + this.ui.button2.disabled = !enabled; + this.ui.button3.disabled = !enabled; + }, + + setDefaultFocus(isInitialLoad) { + let b = this.args.defaultButtonNum || 0; + let button = this.ui["button" + b]; + + if (!this.hasInputField) { + let isOSX = "nsILocalFileMac" in Ci; + // If the infoRow exists and is is hidden, then the infoBody is also hidden, + // which means it can't be focused. At that point, we fall back to focusing + // the default button, regardless of platform. + if (isOSX && !(this.ui.infoRow && this.ui.infoRow.hidden)) { + this.ui.infoBody.focus(); + } else { + button.focus({ focusVisible: false }); + } + } else if (this.args.promptType == "promptPassword") { + // When the prompt is initialized, focus and select the textbox + // contents. Afterwards, only focus the textbox. + if (isInitialLoad) { + this.ui.password1Textbox.select(); + } else { + this.ui.password1Textbox.focus(); + } + } else if (isInitialLoad) { + this.ui.loginTextbox.select(); + } else { + this.ui.loginTextbox.focus(); + } + + if (isInitialLoad) { + this.initialFocusResolver(); + } + }, + + onCheckbox() { + this.args.checked = this.ui.checkbox.checked; + }, + + onButton0() { + this.args.promptActive = false; + this.args.ok = true; + this.args.buttonNumClicked = 0; + + let username = this.ui.loginTextbox.value; + let password = this.ui.password1Textbox.value; + + // Return textfield values + switch (this.args.promptType) { + case "prompt": + this.args.value = username; + break; + case "promptUserAndPass": + this.args.user = username; + this.args.pass = password; + break; + case "promptPassword": + this.args.pass = password; + break; + } + }, + + onButton1() { + this.args.promptActive = false; + this.args.buttonNumClicked = 1; + }, + + onButton2() { + this.args.promptActive = false; + this.args.buttonNumClicked = 2; + }, + + onButton3() { + this.args.promptActive = false; + this.args.buttonNumClicked = 3; + }, + + abortPrompt() { + this.args.promptActive = false; + this.args.promptAborted = true; + }, +}; diff --git a/toolkit/components/prompts/src/PromptUtils.sys.mjs b/toolkit/components/prompts/src/PromptUtils.sys.mjs new file mode 100644 index 0000000000..7a15802b84 --- /dev/null +++ b/toolkit/components/prompts/src/PromptUtils.sys.mjs @@ -0,0 +1,186 @@ +/* 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/. */ + +export var PromptUtils = { + // Fire a dialog open/close event. Used by tabbrowser to focus the + // tab which is triggering a prompt. + // For remote dialogs, we pass in a different DOM window and a separate + // target. If the caller doesn't pass in the target, then we'll simply use + // the passed-in DOM window. + // The detail may contain information about the principal on which the + // prompt is triggered, as well as whether or not this is a tabprompt + // (ie tabmodal alert/prompt/confirm and friends) + fireDialogEvent(domWin, eventName, maybeTarget, detail) { + let target = maybeTarget || domWin; + let eventOptions = { cancelable: true, bubbles: true }; + if (detail) { + eventOptions.detail = detail; + } + let event = new domWin.CustomEvent(eventName, eventOptions); + let winUtils = domWin.windowUtils; + winUtils.dispatchEventToChromeOnly(target, event); + }, + + objectToPropBag(obj) { + let bag = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag2 + ); + bag.QueryInterface(Ci.nsIWritablePropertyBag); + + for (let propName in obj) { + bag.setProperty(propName, obj[propName]); + } + + return bag; + }, + + propBagToObject(propBag, obj) { + // Here we iterate over the object's original properties, not the bag + // (ie, the prompt can't return more/different properties than were + // passed in). This just helps ensure that the caller provides default + // values, lest the prompt forget to set them. + for (let propName in obj) { + obj[propName] = propBag.getProperty(propName); + } + }, +}; + +/** + * This helper handles the enabling/disabling of dialogs that might + * be subject to fast-clicking attacks. It handles the initial delayed + * enabling of the dialog, as well as disabling it on blur and reapplying + * the delay when the dialog regains focus. + * + * @param enableDialog A custom function to be called when the dialog + * is to be enabled. + * @param diableDialog A custom function to be called when the dialog + * is to be disabled. + * @param focusTarget The window used to watch focus/blur events. + */ +export var EnableDelayHelper = function ({ + enableDialog, + disableDialog, + focusTarget, +}) { + this.enableDialog = makeSafe(enableDialog); + this.disableDialog = makeSafe(disableDialog); + this.focusTarget = focusTarget; + + this.disableDialog(); + + this.focusTarget.addEventListener("blur", this); + this.focusTarget.addEventListener("focus", this); + // While the user key-repeats, we want to renew the timer until keyup: + this.focusTarget.addEventListener("keyup", this, true); + this.focusTarget.addEventListener("keydown", this, true); + this.focusTarget.document.addEventListener("unload", this); + + this.startOnFocusDelay(); +}; + +EnableDelayHelper.prototype = { + get delayTime() { + return Services.prefs.getIntPref("security.dialog_enable_delay"); + }, + + handleEvent(event) { + if ( + !event.type.startsWith("key") && + event.target != this.focusTarget && + event.target != this.focusTarget.document + ) { + return; + } + + switch (event.type) { + case "keyup": + // As soon as any key goes up, we can stop treating keypresses + // as indicative of key-repeating that should prolong the timer. + this.focusTarget.removeEventListener("keyup", this, true); + this.focusTarget.removeEventListener("keydown", this, true); + break; + + case "keydown": + // Renew timer for repeating keydowns: + if (this._focusTimer) { + this._focusTimer.cancel(); + this._focusTimer = null; + this.startOnFocusDelay(); + event.preventDefault(); + } + break; + + case "blur": + this.onBlur(); + break; + + case "focus": + this.onFocus(); + break; + + case "unload": + this.onUnload(); + break; + } + }, + + onBlur() { + this.disableDialog(); + // If we blur while waiting to enable the buttons, just cancel the + // timer to ensure the delay doesn't fire while not focused. + if (this._focusTimer) { + this._focusTimer.cancel(); + this._focusTimer = null; + } + }, + + onFocus() { + this.startOnFocusDelay(); + }, + + onUnload() { + this.focusTarget.removeEventListener("blur", this); + this.focusTarget.removeEventListener("focus", this); + this.focusTarget.removeEventListener("keyup", this, true); + this.focusTarget.removeEventListener("keydown", this, true); + this.focusTarget.document.removeEventListener("unload", this); + + if (this._focusTimer) { + this._focusTimer.cancel(); + this._focusTimer = null; + } + + this.focusTarget = this.enableDialog = this.disableDialog = null; + }, + + startOnFocusDelay() { + if (this._focusTimer) { + return; + } + + this._focusTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._focusTimer.initWithCallback( + () => { + this.onFocusTimeout(); + }, + this.delayTime, + Ci.nsITimer.TYPE_ONE_SHOT + ); + }, + + onFocusTimeout() { + this._focusTimer = null; + this.enableDialog(); + }, +}; + +function makeSafe(fn) { + return function () { + // The dialog could be gone by now (if the user closed it), + // which makes it likely that the given fn might throw. + try { + fn(); + } catch (e) {} + }; +} diff --git a/toolkit/components/prompts/src/Prompter.sys.mjs b/toolkit/components/prompts/src/Prompter.sys.mjs new file mode 100644 index 0000000000..fe44992378 --- /dev/null +++ b/toolkit/components/prompts/src/Prompter.sys.mjs @@ -0,0 +1,1824 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +// This is redefined below, for strange and unfortunate reasons. +import { PromptUtils } from "resource://gre/modules/PromptUtils.sys.mjs"; + +const { + MODAL_TYPE_TAB, + MODAL_TYPE_CONTENT, + MODAL_TYPE_WINDOW, + MODAL_TYPE_INTERNAL_WINDOW, +} = Ci.nsIPrompt; + +const COMMON_DIALOG = "chrome://global/content/commonDialog.xhtml"; +const SELECT_DIALOG = "chrome://global/content/selectDialog.xhtml"; + +export function Prompter() { + // Note that EmbedPrompter clones this implementation. +} + +/** + * Implements nsIPromptService and nsIPromptFactory + * @class Prompter + */ +Prompter.prototype = { + classID: Components.ID("{1c978d25-b37f-43a8-a2d6-0c7a239ead87}"), + QueryInterface: ChromeUtils.generateQI([ + "nsIPromptFactory", + "nsIPromptService", + ]), + + /* ---------- private members ---------- */ + + pickPrompter(options) { + return new ModalPrompter(options); + }, + + /* ---------- nsIPromptFactory ---------- */ + + getPrompt(domWin, iid) { + // This is still kind of dumb; the C++ code delegated to login manager + // here, which in turn calls back into us via nsIPromptService. + if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPrompt)) { + try { + let pwmgr = Cc[ + "@mozilla.org/passwordmanager/authpromptfactory;1" + ].getService(Ci.nsIPromptFactory); + return pwmgr.getPrompt(domWin, iid); + } catch (e) { + console.error("nsPrompter: Delegation to password manager failed: ", e); + } + } + + let p = new ModalPrompter({ domWin }); + p.QueryInterface(iid); + return p; + }, + + /* ---------- nsIPromptService ---------- */ + + /** + * Puts up an alert dialog with an OK button. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + */ + alert(domWin, title, text) { + let p = this.pickPrompter({ domWin }); + p.alert(title, text); + }, + + /** + * Puts up an alert dialog with an OK button. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + */ + alertBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + p.alert(...promptArgs); + }, + + /** + * Puts up an alert dialog with an OK button. + * + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @returns {Promise} A promise which resolves when the prompt is dismissed. + */ + asyncAlert(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.alert(...promptArgs); + }, + + /** + * Puts up an alert dialog with an OK button and a labeled checkbox. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} checkLabel - Text to appear with the checkbox. + * @param {Object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state + * after this method returns. + */ + alertCheck(domWin, title, text, checkLabel, checkValue) { + let p = this.pickPrompter({ domWin }); + p.alertCheck(title, text, checkLabel, checkValue); + }, + + /** + * Puts up an alert dialog with an OK button and a labeled checkbox. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} checkLabel - Text to appear with the checkbox. + * @param {Object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state + * after this method returns. + */ + alertCheckBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + p.alertCheck(...promptArgs); + }, + + /** + * Puts up an alert dialog with an OK button and a labeled checkbox. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} checkLabel - Text to appear with the checkbox. + * @param {Boolean} checkValue - The initial checked state of the checkbox. + * @returns {Promise<nsIPropertyBag<{ checked: Boolean }>>} + * A promise which resolves when the prompt is dismissed. + */ + asyncAlertCheck(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.alertCheck(...promptArgs); + }, + + /** + * Puts up a dialog with OK and Cancel buttons. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @returns {Boolean} true for OK, false for Cancel. + */ + confirm(domWin, title, text) { + let p = this.pickPrompter({ domWin }); + return p.confirm(title, text); + }, + + /** + * Puts up a dialog with OK and Cancel buttons. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @returns {Boolean} true for OK, false for Cancel. + */ + confirmBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + return p.confirm(...promptArgs); + }, + + /** + * Puts up a dialog with OK and Cancel buttons. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @returns {Promise<nsIPropertyBag<{ ok: Boolean }>>} + * A promise which resolves when the prompt is dismissed. + */ + asyncConfirm(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.confirm(...promptArgs); + }, + + /** + * Puts up a dialog with OK and Cancel buttons and a labeled checkbox. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} checkLabel - Text to appear with the checkbox. + * @param {Object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state + * after this method returns. + */ + confirmCheck(domWin, title, text, checkLabel, checkValue) { + let p = this.pickPrompter({ domWin }); + return p.confirmCheck(title, text, checkLabel, checkValue); + }, + + /** + * Puts up a dialog with OK and Cancel buttons and a labeled checkbox. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} checkLabel - Text to appear with the checkbox. + * @param {Object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state + * after this method returns. + * @returns {Boolean} true for OK, false for Cancel + */ + confirmCheckBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + return p.confirmCheck(...promptArgs); + }, + + /** + * Puts up a dialog with OK and Cancel buttons and a labeled checkbox. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} checkLabel - Text to appear with the checkbox. + * @param {Boolean} checkValue - The initial checked state of the checkbox. + * @returns {Promise<nsIPropertyBag<{ ok: Boolean, checked: Boolean }>>} + * A promise which resolves when the prompt is dismissed. + */ + asyncConfirmCheck(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.confirmCheck(...promptArgs); + }, + + /** + * Puts up a dialog with up to 3 buttons and an optional, labeled checkbox. + * + * Buttons are numbered 0 - 2. Button 0 is the default button unless one of + * the Button Default Flags is specified. + * + * A button may use a predefined title, specified by one of the Button Title + * Flags values. Each title value can be multiplied by a position value to + * assign the title to a particular button. If BUTTON_TITLE_IS_STRING is + * used for a button, the string parameter for that button will be used. If + * the value for a button position is zero, the button will not be shown. + * + * In general, flags is constructed per the following example: + * + * flags = (BUTTON_POS_0) * (BUTTON_TITLE_AAA) + + * (BUTTON_POS_1) * (BUTTON_TITLE_BBB) + + * BUTTON_POS_1_DEFAULT; + * + * where "AAA" and "BBB" correspond to one of the button titles. + * + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Number} flags - A combination of Button Flags. + * @param {String} button0 - Used when button 0 uses TITLE_IS_STRING. + * @param {String} button1 - Used when button 1 uses TITLE_IS_STRING. + * @param {String} button2 - Used when button 2 uses TITLE_IS_STRING. + * @param {String} checkLabel - Text to appear with the checkbox. + * Null if no checkbox. + * @param {Object} checkValue - Contains the initial checked state of the + * checkbox when this method + * is called and the final checked state after this method returns. + * @returns {Number} The index of the button pressed. + */ + confirmEx( + domWin, + title, + text, + flags, + button0, + button1, + button2, + checkLabel, + checkValue + ) { + let p = this.pickPrompter({ domWin }); + return p.confirmEx( + title, + text, + flags, + button0, + button1, + button2, + checkLabel, + checkValue + ); + }, + + /** + * Puts up a dialog with up to 3 buttons and an optional, labeled checkbox. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Number} flags - A combination of Button Flags. + * @param {String} button0 - Used when button 0 uses TITLE_IS_STRING. + * @param {String} button1 - Used when button 1 uses TITLE_IS_STRING. + * @param {String} button2 - Used when button 2 uses TITLE_IS_STRING. + * @param {String} checkLabel - Text to appear with the checkbox. + * Null if no checkbox. + * @param {Object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state + * after this method returns. + * @returns {Number} The index of the button pressed. + */ + confirmExBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + return p.confirmEx(...promptArgs); + }, + + /** + * Puts up a dialog with up to 3 buttons and an optional, labeled checkbox. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Number} flags - A combination of Button Flags. + * @param {String} button0 - Used when button 0 uses TITLE_IS_STRING. + * @param {String} button1 - Used when button 1 uses TITLE_IS_STRING. + * @param {String} button2 - Used when button 2 uses TITLE_IS_STRING. + * @param {String} checkLabel - Text to appear with the checkbox. + * Null if no checkbox. + * @param {Boolean} checkValue - The initial checked state of the checkbox. + * @param {Object} [extraArgs] - Extra arguments for the prompt metadata. + * @returns {Promise<nsIPropertyBag<{ buttonNumClicked: Number, checked: Boolean }>>} + */ + asyncConfirmEx(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.confirmEx(...promptArgs); + }, + + /** + * Puts up a dialog with an edit field and an optional, labeled checkbox. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Object} value - Contains the default value for the dialog field + * when this method is called (null value is ok). Upon return, if + * the user pressed OK, then this parameter contains a newly + * allocated string value. + * Otherwise, the parameter's value is unmodified. + * @param {String} checkLabel - Text to appear with the checkbox. + * If null, check box will not be shown. + * @param {Object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state + * after this method returns. + * @returns {Boolean} true for OK, false for Cancel. + */ + prompt(domWin, title, text, value, checkLabel, checkValue) { + let p = this.pickPrompter({ domWin }); + return p.nsIPrompt_prompt(title, text, value, checkLabel, checkValue); + }, + + /** + * Puts up a dialog with an edit field and an optional, labeled checkbox. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Object} value - Contains the default value for the dialog field + * when this method is called (null value is ok). Upon return, if + * the user pressed OK, then this parameter contains a newly + * allocated string value. + * Otherwise, the parameter's value is unmodified. + * @param {String} checkLabel - Text to appear with the checkbox. + * If null, check box will not be shown. + * @param {Object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state + * after this method returns. + * @returns {Boolean} true for OK, false for Cancel. + */ + promptBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + return p.nsIPrompt_prompt(...promptArgs); + }, + + /** + * Puts up a dialog with an edit field and an optional, labeled checkbox. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} value - The default value for the dialog text field. + * @param {String} checkLabel - Text to appear with the checkbox. + * If null, check box will not be shown. + * @param {Boolean} checkValue - The initial checked state of the checkbox. + * @returns {Promise<nsIPropertyBag<{ ok: Boolean, checked: Boolean, value: String }>>} + * A promise which resolves when the prompt is dismissed. + */ + asyncPrompt(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.nsIPrompt_prompt(...promptArgs); + }, + + /** + * Puts up a dialog with an edit field and a password field. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Object} user - Contains the default value for the username + * field when this method is called (null value is ok). + * Upon return, if the user pressed OK, then this parameter contains + * a newly allocated string value. Otherwise, the parameter's value + * is unmodified. + * @param {Object} pass - Contains the default value for the password field + * when this method is called (null value is ok). Upon return, if the + * user pressed OK, this parameter contains a newly allocated string + * value. Otherwise, the parameter's value is unmodified. + * @returns {Boolean} true for OK, false for Cancel. + */ + promptUsernameAndPassword(domWin, title, text, user, pass) { + let p = this.pickPrompter({ domWin }); + return p.nsIPrompt_promptUsernameAndPassword(null, title, text, user, pass); + }, + + /** + * Puts up a dialog with an edit field and a password field. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Object} user - Contains the default value for the username + * field when this method is called (null value is ok). + * Upon return, if the user pressed OK, then this parameter contains + * a newly allocated string value. Otherwise, the parameter's value + * is unmodified. + * @param {Object} pass - Contains the default value for the password field + * when this method is called (null value is ok). Upon return, if the + * user pressed OK, this parameter contains a newly allocated string + * value. Otherwise, the parameter's value is unmodified. + * @returns {Boolean} true for OK, false for Cancel. + */ + promptUsernameAndPasswordBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + return p.nsIPrompt_promptUsernameAndPassword(null, ...promptArgs); + }, + + /** + * Puts up a dialog with an edit field and a password field. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} user - Default value for the username field. + * @param {String} pass - Contains the default value for the password field. + * @returns {Promise<nsIPropertyBag<{ ok: Boolean, user: String, pass: String }>>} + * A promise which resolves when the prompt is dismissed. + */ + asyncPromptUsernameAndPassword(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.nsIPrompt_promptUsernameAndPassword(null, ...promptArgs); + }, + + /** + * Puts up a dialog with a password field. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Object} pass - Contains the default value for the password field + * when this method is called (null value is ok). Upon return, if the + * user pressed OK, this parameter contains a newly allocated string + * value. Otherwise, the parameter's value is unmodified. + * @returns {Boolean} true for OK, false for Cancel. + */ + promptPassword(domWin, title, text, pass) { + let p = this.pickPrompter({ domWin }); + return p.nsIPrompt_promptPassword( + null, // no channel. + title, + text, + pass + ); + }, + + /** + * Puts up a dialog with a password field. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {Object} pass - Contains the default value for the password field + * when this method is called (null value is ok). Upon return, if the + * user pressed OK, this parameter contains a newly allocated string + * value. Otherwise, the parameter's value is unmodified. + * @returns {Boolean} true for OK, false for Cancel. + */ + promptPasswordBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + return p.nsIPrompt_promptPassword(null, ...promptArgs); + }, + + /** + * Puts up a dialog with a password field. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String} pass - Contains the default value for the password field. + * @returns {Promise<nsIPropertyBag<{ ok: Boolean, pass: String }>>} + * A promise which resolves when the prompt is dismissed. + */ + asyncPromptPassword(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.nsIPrompt_promptPassword(null, ...promptArgs); + }, + + /** + * Puts up a dialog box which has a list box of strings from which the user + * may make a single selection. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String[]} list - The list of strings to display. + * @param {Object} selected - Contains the index of the selected item in the + * list when this method returns true. + * @returns {Boolean} true for OK, false for Cancel. + */ + select(domWin, title, text, list, selected) { + let p = this.pickPrompter({ domWin }); + return p.select(title, text, list, selected); + }, + + /** + * Puts up a dialog box which has a list box of strings from which the user + * may make a single selection. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String[]} list - The list of strings to display. + * @param {Object} selected - Contains the index of the selected item in the + * list when this method returns true. + * @returns {Boolean} true for OK, false for Cancel. + */ + selectBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + return p.select(...promptArgs); + }, + + /** + * Puts up a dialog box which has a list box of strings from which the user + * may make a single selection. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {String} title - Text to appear in the title of the dialog. + * @param {String} text - Text to appear in the body of the dialog. + * @param {String[]} list - The list of strings to display. + * @returns {Promise<nsIPropertyBag<{ selected: Number, ok: Boolean }>>} + * A promise which resolves when the prompt is dismissed. + */ + asyncSelect(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.select(...promptArgs); + }, + + /** + * Requests a username and a password. Shows a dialog with username and + * password field, depending on flags also a domain field. + * @param {mozIDOMWindowProxy} domWin - The parent window or null. + * @param {nsIChannel} channel - The channel that requires authentication. + * @param {Number} level - Security level of the credential transmission. + * Any of nsIAuthPrompt2.<LEVEL_NONE|LEVEL_PW_ENCRYPTED|LEVEL_SECURE> + * @param {nsIAuthInformation} authInfo - Authentication information object. + * @returns {Boolean} + * true: Authentication can proceed using the values + * in the authInfo object. + * false: Authentication should be cancelled, usually because the + * user did not provide username/password. + */ + promptAuth(domWin, channel, level, authInfo) { + let p = this.pickPrompter({ domWin }); + return p.promptAuth(channel, level, authInfo); + }, + + /** + * Requests a username and a password. Shows a dialog with username and + * password field, depending on flags also a domain field. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {nsIChannel} channel - The channel that requires authentication. + * @param {Number} level - Security level of the credential transmission. + * Any of nsIAuthPrompt2.<LEVEL_NONE|LEVEL_PW_ENCRYPTED|LEVEL_SECURE> + * @param {nsIAuthInformation} authInfo - Authentication information object. + * @returns {Boolean} + * true: Authentication can proceed using the values + * in the authInfo object. + * false: Authentication should be cancelled, usually because the + * user did not provide username/password. + */ + promptAuthBC(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType }); + return p.promptAuth(...promptArgs); + }, + + /** + * Requests a username and a password. Shows a dialog with username and + * password field, depending on flags also a domain field. + * @param {BrowsingContext} browsingContext - The browsing context the + * prompt should be opened for. + * @param {Number} modalType - The modal type of the prompt. + * nsIPromptService.<MODAL_TYPE_WINDOW|MODAL_TYPE_TAB|MODAL_TYPE_CONTENT> + * @param {nsIChannel} channel - The channel that requires authentication. + * @param {Number} level - Security level of the credential transmission. + * Any of nsIAuthPrompt2.<LEVEL_NONE|LEVEL_PW_ENCRYPTED|LEVEL_SECURE> + * @param {nsIAuthInformation} authInfo - Authentication information object. + * @returns {Promise<nsIPropertyBag<{ ok: Boolean }>>} + * A promise which resolves when the prompt is dismissed. + */ + asyncPromptAuth(browsingContext, modalType, ...promptArgs) { + let p = this.pickPrompter({ browsingContext, modalType, async: true }); + return p.promptAuth(...promptArgs); + }, +}; + +// Common utils not specific to a particular prompter style. +var InternalPromptUtils = { + getLocalizedString(key, formatArgs) { + if (formatArgs) { + return this.strBundle.formatStringFromName(key, formatArgs); + } + return this.strBundle.GetStringFromName(key); + }, + + confirmExHelper(flags, button0, button1, button2) { + const BUTTON_DEFAULT_MASK = 0x03000000; + let defaultButtonNum = (flags & BUTTON_DEFAULT_MASK) >> 24; + let isDelayEnabled = flags & Ci.nsIPrompt.BUTTON_DELAY_ENABLE; + + // Flags can be used to select a specific pre-defined button label or + // a caller-supplied string (button0/button1/button2). If no flags are + // set for a button, then the button won't be shown. + let argText = [button0, button1, button2]; + let buttonLabels = [null, null, null]; + for (let i = 0; i < 3; i++) { + let buttonLabel; + switch (flags & 0xff) { + case Ci.nsIPrompt.BUTTON_TITLE_OK: + buttonLabel = this.getLocalizedString("OK"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_CANCEL: + buttonLabel = this.getLocalizedString("Cancel"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_YES: + buttonLabel = this.getLocalizedString("Yes"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_NO: + buttonLabel = this.getLocalizedString("No"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_SAVE: + buttonLabel = this.getLocalizedString("Save"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE: + buttonLabel = this.getLocalizedString("DontSave"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_REVERT: + buttonLabel = this.getLocalizedString("Revert"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING: + buttonLabel = argText[i]; + break; + } + if (buttonLabel) { + buttonLabels[i] = buttonLabel; + } + flags >>= 8; + } + + return [ + buttonLabels[0], + buttonLabels[1], + buttonLabels[2], + defaultButtonNum, + isDelayEnabled, + ]; + }, + + getAuthInfo(authInfo) { + let username, password; + + let flags = authInfo.flags; + if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && authInfo.domain) { + username = authInfo.domain + "\\" + authInfo.username; + } else { + username = authInfo.username; + } + + password = authInfo.password; + + return [username, password]; + }, + + setAuthInfo(authInfo, username, password) { + let flags = authInfo.flags; + if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) { + // Domain is separated from username by a backslash + let idx = username.indexOf("\\"); + if (idx == -1) { + authInfo.username = username; + } else { + authInfo.domain = username.substring(0, idx); + authInfo.username = username.substring(idx + 1); + } + } else { + authInfo.username = username; + } + authInfo.password = password; + }, + + /** + * Strip out things like userPass and path for display. + */ + getFormattedHostname(uri) { + return uri.scheme + "://" + uri.hostPort; + }, + + // Note: there's a similar implementation in the login manager. + getAuthTarget(aChannel, aAuthInfo) { + let displayHost, realm; + + // If our proxy is demanding authentication, don't use the + // channel's actual destination. + if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { + if (!(aChannel instanceof Ci.nsIProxiedChannel)) { + throw new Error("proxy auth needs nsIProxiedChannel"); + } + + let info = aChannel.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. + let idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + displayHost = + "moz-proxy://" + + idnService.convertUTF8toACE(info.host) + + ":" + + info.port; + realm = aAuthInfo.realm; + if (!realm) { + realm = displayHost; + } + + return { realm, displayHost }; + } + + displayHost = this.getFormattedHostname(aChannel.URI); + let displayHostOnly = aChannel.URI.hostPort; + + // 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 = aAuthInfo.realm; + if (!realm) { + realm = displayHost; + } + + return { realm, displayHostOnly, displayHost }; + }, + + makeAuthMessage(prompt, channel, authInfo) { + if (prompt.modalType != MODAL_TYPE_TAB) { + return this._legacyMakeAuthMessage(channel, authInfo); + } + + let isProxy = authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY; + let isPassOnly = authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD; + let isCrossOrig = + authInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE; + let username = authInfo.username; + + // We use the realm and displayHost only for proxy auth, + // and the displayHostOnly (hostPort) only for x-origin auth prompts. + // Otherwise we rely on the title of the dialog displaying the correct + // title. + let { displayHost, realm, displayHostOnly } = this.getAuthTarget( + channel, + authInfo + ); + + if (isProxy) { + // The realm is server-controlled. Trim it if it's very long, to + // avoid the dialog becoming unusable. + // For background, see https://bugzilla.mozilla.org/show_bug.cgi?id=244273 + if (realm.length > 150) { + realm = realm.substring(0, 150); + // Append "..." (or localized equivalent). + realm += this.ellipsis; + } + + return this.getLocalizedString("EnterLoginForProxy3", [ + realm, + displayHost, + ]); + } + if (isPassOnly) { + return this.getLocalizedString("EnterPasswordOnlyFor", [username]); + } + if (isCrossOrig) { + return this.getLocalizedString("EnterCredentialsCrossOrigin", [ + displayHostOnly, + ]); + } + return this.getLocalizedString("EnterCredentials"); + }, + + _legacyMakeAuthMessage(channel, authInfo) { + let isProxy = authInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY; + let isPassOnly = authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD; + let isCrossOrig = + authInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE; + + let username = authInfo.username; + let { displayHost, realm } = this.getAuthTarget(channel, authInfo); + + // Suppress "the site says: $realm" when we synthesized a missing realm. + if (!authInfo.realm && !isProxy) { + realm = ""; + } + + // The realm is server-controlled. Trim it if it's very long, to + // avoid the dialog becoming unusable. + // For background, see https://bugzilla.mozilla.org/show_bug.cgi?id=244273 + if (realm.length > 150) { + realm = realm.substring(0, 150); + // Append "..." (or localized equivalent). + realm += this.ellipsis; + } + + let text; + if (isProxy) { + text = this.getLocalizedString("EnterLoginForProxy3", [ + realm, + displayHost, + ]); + } else if (isPassOnly) { + text = this.getLocalizedString("EnterPasswordFor", [ + username, + displayHost, + ]); + } else if (isCrossOrig) { + text = this.getLocalizedString("EnterUserPasswordForCrossOrigin2", [ + displayHost, + ]); + } else if (!realm) { + text = this.getLocalizedString("EnterUserPasswordFor2", [displayHost]); + } else { + text = this.getLocalizedString("EnterLoginForRealm3", [ + realm, + displayHost, + ]); + } + + return text; + }, + + getBrandFullName() { + return this.brandBundle.GetStringFromName("brandFullName"); + }, +}; + +XPCOMUtils.defineLazyGetter(InternalPromptUtils, "strBundle", function () { + let bundle = Services.strings.createBundle( + "chrome://global/locale/commonDialogs.properties" + ); + if (!bundle) { + throw new Error("String bundle for Prompter not present!"); + } + return bundle; +}); + +XPCOMUtils.defineLazyGetter(InternalPromptUtils, "brandBundle", function () { + let bundle = Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); + if (!bundle) { + throw new Error("String bundle for branding not present!"); + } + return bundle; +}); + +XPCOMUtils.defineLazyGetter(InternalPromptUtils, "ellipsis", function () { + let ellipsis = "\u2026"; + try { + ellipsis = Services.prefs.getComplexValue( + "intl.ellipsis", + Ci.nsIPrefLocalizedString + ).data; + } catch (e) {} + return ellipsis; +}); + +class ModalPrompter { + constructor({ + browsingContext = null, + domWin = null, + modalType = null, + async = false, + }) { + if (browsingContext && domWin) { + throw new Error("Pass either browsingContext or domWin"); + } + + if (domWin) { + // We have a domWin, get the associated browsing context + this.browsingContext = BrowsingContext.getFromWindow(domWin); + } else { + this.browsingContext = browsingContext; + } + + if ( + domWin && + (!modalType || modalType == MODAL_TYPE_WINDOW) && + !this.browsingContext?.isContent && + this.browsingContext?.associatedWindow?.gDialogBox + ) { + modalType = MODAL_TYPE_INTERNAL_WINDOW; + } + + // Use given modal type or fallback to default + this.modalType = modalType || ModalPrompter.defaultModalType; + + this.async = async; + + this.QueryInterface = ChromeUtils.generateQI([ + "nsIPrompt", + "nsIAuthPrompt", + "nsIAuthPrompt2", + "nsIWritablePropertyBag2", + ]); + } + + set modalType(modalType) { + // Setting modal type window is always allowed + if (modalType == MODAL_TYPE_WINDOW) { + this._modalType = modalType; + return; + } + + // For content prompts for non-content windows, use window prompts: + if (modalType == MODAL_TYPE_CONTENT && !this.browsingContext?.isContent) { + this._modalType = MODAL_TYPE_WINDOW; + return; + } + + // We can't use content / tab prompts if we don't have a suitable parent. + if ( + !this.browsingContext?.isContent && + modalType != MODAL_TYPE_INTERNAL_WINDOW + ) { + // Only show this error if we're not about to fall back again and show a different one. + if (this.browsingContext?.associatedWindow?.gDialogBox) { + console.error( + "Prompter: Browser not available. Falling back to internal window prompt." + ); + } + modalType = MODAL_TYPE_INTERNAL_WINDOW; + } + + if ( + modalType == MODAL_TYPE_INTERNAL_WINDOW && + (this.browsingContext?.isContent || + !this.browsingContext?.associatedWindow?.gDialogBox) + ) { + console.error( + "Prompter: internal dialogs not available in this context. Falling back to window prompt." + ); + modalType = MODAL_TYPE_WINDOW; + } + + this._modalType = modalType; + } + + get modalType() { + return this._modalType; + } + + /* ---------- internal methods ---------- */ + + /** + * Synchronous wrapper around {@link ModalPrompter#openPrompt} + * @param {Object} args Prompt arguments. When prompt has been closed, they are updated to reflect the result state. + */ + openPromptSync(args) { + let closed = false; + this.openPrompt(args) + .then(returnedArgs => { + if (returnedArgs) { + for (let key in returnedArgs) { + args[key] = returnedArgs[key]; + } + } + }) + .finally(() => { + closed = true; + }); + Services.tm.spinEventLoopUntilOrQuit( + "prompts/Prompter.jsm:openPromptSync", + () => closed + ); + } + + async openPrompt(args) { + if (!this.browsingContext) { + // We don't have a browsing context, fallback to a window prompt. + args.modalType = MODAL_TYPE_WINDOW; + this.openWindowPrompt(null, args); + return args; + } + + if (this._modalType == MODAL_TYPE_INTERNAL_WINDOW) { + await this.openInternalWindowPrompt( + this.browsingContext.associatedWindow, + args + ); + return args; + } + + // Select prompts are not part of CommonDialog + // and thus not supported as tab or content prompts yet. See Bug 1622817. + // Once they are integrated this override should be removed. + if (args.promptType == "select" && this.modalType !== MODAL_TYPE_WINDOW) { + console.error( + "Prompter: 'select' prompts do not support tab/content prompting. Falling back to window prompt." + ); + args.modalType = MODAL_TYPE_WINDOW; + } else { + args.modalType = this.modalType; + } + + const IS_CONTENT = + Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; + + let actor; + try { + if (IS_CONTENT) { + // When in the content, get the PromptChild actor. + actor = + this.browsingContext.window.windowGlobalChild.getActor("Prompt"); + } else { + // When in the parent, get the PromptParent actor. + actor = this.browsingContext.currentWindowGlobal.getActor("Prompt"); + } + } catch (_) { + // We can't get the prompt actor, fallback to window prompt. + let parentWin; + // If given a chrome BC we can try to get its window + if (!this.browsingContext.isContent && this.browsingContext.window) { + parentWin = this.browsingContext.window; + } else { + // Try to get the window which is the browsers parent + parentWin = this.browsingContext.top?.embedderElement?.ownerGlobal; + } + this.openWindowPrompt(parentWin, args); + return args; + } + + /* For prompts with a channel, we want to show the origin requesting + * authentication. This is different from the prompt principal, + * which is based on the document loaded in the browsing context over + * which the prompt appears. So if page foo.com loads bar.com, and the + * latter asks for auth, we want that bar.com's origin, not foo.com. + * To avoid confusion, we use different properties + * (authOrigin / promptPrincipal) to track this information. + */ + if (args.channel) { + try { + args.authOrigin = args.channel.URI.hostPort; + } catch (ex) { + args.authOrigin = args.channel.URI.prePath; + } + args.isInsecureAuth = + args.channel.URI.schemeIs("http") && + !args.channel.loadInfo.isTopLevelLoad; + // whether we are going to prompt the user for their credentials for a different base domain. + // When true, auth prompt spoofing protection mechanisms will be triggered (see bug 791594). + args.isTopLevelCrossDomainAuth = false; + // We don't support auth prompt spoofing protections for sub resources and window prompts + if ( + args.modalType == MODAL_TYPE_TAB && + args.channel.loadInfo.isTopLevelLoad + ) { + // check if this is a request from a third party + try { + args.isTopLevelCrossDomainAuth = + this.browsingContext.currentWindowGlobal?.documentPrincipal?.isThirdPartyURI( + args.channel.URI + ); + } catch (e) { + // isThirdPartyURI failes for about:/blob/data URIs + console.warn("nsPrompter: isThirdPartyURI failed: " + e); + } + } + } else { + args.promptPrincipal = + this.browsingContext.window?.document.nodePrincipal; + } + if (IS_CONTENT) { + let docShell = this.browsingContext.docShell; + let inPermitUnload = docShell?.contentViewer?.inPermitUnload; + args.inPermitUnload = inPermitUnload; + let eventDetail = Cu.cloneInto( + { + tabPrompt: this.modalType != MODAL_TYPE_WINDOW, + inPermitUnload, + }, + this.browsingContext.window + ); + PromptUtils.fireDialogEvent( + this.browsingContext.window, + "DOMWillOpenModalDialog", + null, + eventDetail + ); + + // Put content window in the modal state while the prompt is open. + let windowUtils = this.browsingContext.window?.windowUtils; + if (windowUtils) { + windowUtils.enterModalState(); + } + } else if (args.inPermitUnload) { + args.promptPrincipal = + this.browsingContext.currentWindowGlobal.documentPrincipal; + } + + // It is technically possible for multiple prompts to be sent from a single + // BrowsingContext. See bug 1266353. We use a randomly generated UUID to + // differentiate between the different prompts. + let id = "id" + Services.uuid.generateUUID().toString(); + + args._remoteId = id; + + let returnedArgs; + try { + if (IS_CONTENT) { + // If we're in the content process, send a message to the PromptParent + // window actor. + returnedArgs = await actor.sendQuery("Prompt:Open", args); + } else { + // If we're in the parent process we already have the parent actor. + // We can call its message handler directly. + returnedArgs = await actor.receiveMessage({ + name: "Prompt:Open", + data: args, + }); + } + + if (returnedArgs?.promptAborted) { + throw Components.Exception( + "prompt aborted by user", + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + } finally { + if (IS_CONTENT) { + let windowUtils = this.browsingContext.window?.windowUtils; + if (windowUtils) { + windowUtils.leaveModalState(); + } + PromptUtils.fireDialogEvent( + this.browsingContext.window, + "DOMModalDialogClosed" + ); + } + } + return returnedArgs; + } + + /** + * Open a window modal prompt + * + * There's an implied contract that says modal prompts should still work when + * no "parent" window is passed for the dialog (eg, the "Master Password" + * dialog does this). These prompts must be shown even if there are *no* + * visible windows at all. + * We try and find a window to use as the parent, but don't consider if that + * is visible before showing the prompt. parentWindow may still be null if + * there are _no_ windows open. + * @param {Window} [parentWindow] - The parent window for the prompt, may be + * null. + * @param {Object} args - Prompt options and return values. + */ + openWindowPrompt(parentWindow = null, args) { + let uri = args.promptType == "select" ? SELECT_DIALOG : COMMON_DIALOG; + let propBag = PromptUtils.objectToPropBag(args); + Services.ww.openWindow( + parentWindow || Services.ww.activeWindow, + uri, + "_blank", + "centerscreen,chrome,modal,titlebar", + propBag + ); + PromptUtils.propBagToObject(propBag, args); + } + + async openInternalWindowPrompt(parentWindow, args) { + if (!parentWindow?.gDialogBox || !ModalPrompter.windowPromptSubDialog) { + this.openWindowPrompt(parentWindow, args); + return; + } + let propBag = PromptUtils.objectToPropBag(args); + propBag.setProperty("async", this.async); + let uri = args.promptType == "select" ? SELECT_DIALOG : COMMON_DIALOG; + await parentWindow.gDialogBox.open(uri, propBag); + propBag.deleteProperty("async"); + PromptUtils.propBagToObject(propBag, args); + } + + /** + * Calls async prompt method and optionally runs promise chained task on + * result data. Converts result data to nsIPropertyBag. + * @param {Object} args - Prompt arguments. + * @param {Function} [task] - Function which is called with the modified + * prompt args object once the prompt has been closed. Must return a + * result object for the prompt caller. + * @returns {Promise<nsIPropertyBag>} - Resolves with a property bag holding the + * prompt result properties. Resolves once prompt has been closed. + */ + async openPromptAsync(args, task) { + let result = await this.openPrompt(args); + // If task is not defined, the prompt method does not return + // anything. In this case we can resolve without value. + if (!task) { + return undefined; + } + // Convert task result to nsIPropertyBag and resolve + let taskResult = task(result); + if (!(taskResult instanceof Object)) { + throw new Error("task must return object"); + } + return PromptUtils.objectToPropBag(taskResult); + } + + /* + * ---------- interface disambiguation ---------- + * + * nsIPrompt and nsIAuthPrompt share 3 method names with slightly + * different arguments. All but prompt() have the same number of + * arguments, so look at the arg types to figure out how we're being + * called. :-( + */ + prompt() { + // also, the nsIPrompt flavor has 5 args instead of 6. + if (typeof arguments[2] == "object") { + return this.nsIPrompt_prompt.apply(this, arguments); + } + return this.nsIAuthPrompt_prompt.apply(this, arguments); + } + + promptUsernameAndPassword() { + // Both have 6 args, so use types. + if (typeof arguments[2] == "object") { + // Add the null channel: + let args = Array.from(arguments); + args.unshift(null); + return this.nsIPrompt_promptUsernameAndPassword.apply(this, args); + } + return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments); + } + + promptPassword() { + // Both have 5 args, so use types. + if (typeof arguments[2] == "object") { + // Add the null channel: + let args = Array.from(arguments); + args.unshift(null); + return this.nsIPrompt_promptPassword.apply(this, args); + } + return this.nsIAuthPrompt_promptPassword.apply(this, arguments); + } + + /* ---------- nsIPrompt ---------- */ + + alert(title, text) { + if (!title) { + title = InternalPromptUtils.getLocalizedString("Alert"); + } + + let args = { + promptType: "alert", + title, + text, + }; + + if (this.async) { + return this.openPromptAsync(args); + } + + return this.openPromptSync(args); + } + + alertCheck(title, text, checkLabel, checkValue) { + if (!title) { + title = InternalPromptUtils.getLocalizedString("Alert"); + } + + // For sync calls checkValue is an XPCOM inout. XPCOM wraps primitves in + // objects for call by reference. + // The async version of this method uses call by value. + let checked = this.async ? checkValue : checkValue.value; + + let args = { + promptType: "alertCheck", + title, + text, + checkLabel, + checked, + }; + + if (this.async) { + return this.openPromptAsync(args, result => ({ + checked: result.checked, + })); + } + + this.openPromptSync(args); + checkValue.value = args.checked; + return undefined; + } + + confirm(title, text) { + if (!title) { + title = InternalPromptUtils.getLocalizedString("Confirm"); + } + + let args = { + promptType: "confirm", + title, + text, + ok: false, + }; + + if (this.async) { + return this.openPromptAsync(args, result => ({ ok: result.ok })); + } + + this.openPromptSync(args); + return args.ok; + } + + confirmCheck(title, text, checkLabel, checkValue) { + if (!title) { + title = InternalPromptUtils.getLocalizedString("ConfirmCheck"); + } + + let checked = this.async ? checkValue : checkValue.value; + + let args = { + promptType: "confirmCheck", + title, + text, + checkLabel, + checked, + ok: false, + }; + + if (this.async) { + return this.openPromptAsync(args, result => ({ + // Checkbox state always returned, even if cancel clicked. + checked: result.checked, + // Did user click Ok or Cancel? + ok: result.ok, + })); + } + + this.openPromptSync(args); + checkValue.value = args.checked; + return args.ok; + } + + confirmEx( + title, + text, + flags, + button0, + button1, + button2, + checkLabel, + checkValue, + extraArgs = {} + ) { + if (!title) { + title = InternalPromptUtils.getLocalizedString("Confirm"); + } + + let args = { + promptType: "confirmEx", + title, + text, + checkLabel, + checked: this.async ? checkValue : checkValue.value, + ok: false, + buttonNumClicked: 1, + ...extraArgs, + }; + + let [label0, label1, label2, defaultButtonNum, isDelayEnabled] = + InternalPromptUtils.confirmExHelper(flags, button0, button1, button2); + + args.defaultButtonNum = defaultButtonNum; + args.enableDelay = isDelayEnabled; + + if (label0) { + args.button0Label = label0; + if (label1) { + args.button1Label = label1; + if (label2) { + args.button2Label = label2; + } + } + } + + if (this.async) { + return this.openPromptAsync(args, result => ({ + checked: !!result.checked, + buttonNumClicked: result.buttonNumClicked, + })); + } + + this.openPromptSync(args); + checkValue.value = args.checked; + return args.buttonNumClicked; + } + + nsIPrompt_prompt(title, text, value, checkLabel, checkValue) { + if (!title) { + title = InternalPromptUtils.getLocalizedString("Prompt"); + } + + let args = { + promptType: "prompt", + title, + text, + value: this.async ? value : value.value, + checkLabel, + checked: this.async ? checkValue : checkValue.value, + ok: false, + }; + + if (this.async) { + return this.openPromptAsync(args, result => ({ + checked: !!result.checked, + value: result.value, + ok: result.ok, + })); + } + + this.openPromptSync(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + checkValue.value = args.checked; + value.value = args.value; + } + + return ok; + } + + nsIPrompt_promptUsernameAndPassword(channel, title, text, user, pass) { + if (!title) { + title = InternalPromptUtils.getLocalizedString( + "PromptUsernameAndPassword3", + [InternalPromptUtils.getBrandFullName()] + ); + } + + let args = { + channel, + promptType: "promptUserAndPass", + title, + text, + user: this.async ? user : user.value, + pass: this.async ? pass : pass.value, + button0Label: InternalPromptUtils.getLocalizedString("SignIn"), + ok: false, + }; + + if (this.async) { + return this.openPromptAsync(args, result => ({ + user: result.user, + pass: result.pass, + ok: result.ok, + })); + } + + this.openPromptSync(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + user.value = args.user; + pass.value = args.pass; + } + + return ok; + } + + nsIPrompt_promptPassword(channel, title, text, pass) { + if (!title) { + title = InternalPromptUtils.getLocalizedString("PromptPassword3", [ + InternalPromptUtils.getBrandFullName(), + ]); + } + + let args = { + channel, + promptType: "promptPassword", + title, + text, + pass: this.async ? pass : pass.value, + button0Label: InternalPromptUtils.getLocalizedString("SignIn"), + ok: false, + }; + + if (this.async) { + return this.openPromptAsync(args, result => ({ + pass: result.pass, + ok: result.ok, + })); + } + + this.openPromptSync(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + pass.value = args.pass; + } + + return ok; + } + + select(title, text, list, selected) { + if (!title) { + title = InternalPromptUtils.getLocalizedString("Select"); + } + + let args = { + promptType: "select", + title, + text, + list, + selected: -1, + ok: false, + }; + + if (this.async) { + return this.openPromptAsync(args, result => ({ + selected: result.selected, + ok: result.ok, + })); + } + + this.openPromptSync(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + selected.value = args.selected; + } + + return ok; + } + + /* ---------- nsIAuthPrompt ---------- */ + + nsIAuthPrompt_prompt( + title, + text, + passwordRealm, + savePassword, + defaultText, + result + ) { + // The passwordRealm and savePassword args were ignored by nsPrompt.cpp + if (defaultText) { + result.value = defaultText; + } + return this.nsIPrompt_prompt(title, text, result, null, {}); + } + + nsIAuthPrompt_promptUsernameAndPassword( + title, + text, + passwordRealm, + savePassword, + user, + pass + ) { + // The passwordRealm and savePassword args were ignored by nsPrompt.cpp + return this.nsIPrompt_promptUsernameAndPassword( + null, + title, + text, + user, + pass + ); + } + + nsIAuthPrompt_promptPassword(title, text, passwordRealm, savePassword, pass) { + // The passwordRealm and savePassword args were ignored by nsPrompt.cpp, + // and we don't have a channel here. + return this.nsIPrompt_promptPassword(null, title, text, pass); + } + + /* ---------- nsIAuthPrompt2 ---------- */ + + promptAuth(channel, level, authInfo) { + let message = InternalPromptUtils.makeAuthMessage(this, channel, authInfo); + + let [username, password] = InternalPromptUtils.getAuthInfo(authInfo); + + let userParam = this.async ? username : { value: username }; + let passParam = this.async ? password : { value: password }; + + let result; + if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) { + result = this.nsIPrompt_promptPassword(channel, null, message, passParam); + } else { + result = this.nsIPrompt_promptUsernameAndPassword( + channel, + null, + message, + userParam, + passParam + ); + } + + // For the async case result is an nsIPropertyBag with prompt results. + if (this.async) { + return result.then(bag => { + let ok = bag.getProperty("ok"); + if (ok) { + let username = bag.getProperty("user"); + let password = bag.getProperty("pass"); + InternalPromptUtils.setAuthInfo(authInfo, username, password); + } + return ok; + }); + } + + // For the sync case result is the "ok" boolean which indicates whether + // the user has confirmed the dialog. + if (result) { + InternalPromptUtils.setAuthInfo( + authInfo, + userParam.value, + passParam.value + ); + } + return result; + } + + asyncPromptAuth( + channel, + callback, + context, + level, + authInfo, + checkLabel, + checkValue + ) { + // Nothing calls this directly; netwerk ends up going through + // nsIPromptService::GetPrompt, which delegates to login manager. + // Login manger handles the async bits itself, and only calls out + // promptAuth, never asyncPromptAuth. + // + // Bug 565582 will change this. + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + /* ---------- nsIWritablePropertyBag2 ---------- */ + // Legacy way to set modal type when prompting via nsIPrompt. + // Please prompt via nsIPromptService. This will be removed in the future. + setPropertyAsUint32(name, value) { + if (name == "modalType") { + this.modalType = value; + } else { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + } +} + +XPCOMUtils.defineLazyPreferenceGetter( + ModalPrompter, + "defaultModalType", + "prompts.defaultModalType", + MODAL_TYPE_WINDOW +); + +XPCOMUtils.defineLazyPreferenceGetter( + ModalPrompter, + "windowPromptSubDialog", + "prompts.windowPromptSubDialog", + false +); + +export function AuthPromptAdapterFactory() {} +AuthPromptAdapterFactory.prototype = { + classID: Components.ID("{6e134924-6c3a-4d86-81ac-69432dd971dc}"), + QueryInterface: ChromeUtils.generateQI(["nsIAuthPromptAdapterFactory"]), + + /* ---------- nsIAuthPromptAdapterFactory ---------- */ + + createAdapter(oldPrompter) { + return new AuthPromptAdapter(oldPrompter); + }, +}; + +// Takes an nsIAuthPrompt implementation, wraps it with a nsIAuthPrompt2 shell. +function AuthPromptAdapter(oldPrompter) { + this.oldPrompter = oldPrompter; +} +AuthPromptAdapter.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt2"]), + oldPrompter: null, + + /* ---------- nsIAuthPrompt2 ---------- */ + + promptAuth(channel, level, authInfo, checkLabel, checkValue) { + let message = InternalPromptUtils.makeAuthMessage( + this.oldPrompter, + channel, + authInfo + ); + + let [username, password] = InternalPromptUtils.getAuthInfo(authInfo); + let userParam = { value: username }; + let passParam = { value: password }; + + let { displayHost, realm } = InternalPromptUtils.getAuthTarget( + channel, + authInfo + ); + let authTarget = displayHost + " (" + realm + ")"; + + let ok; + if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) { + ok = this.oldPrompter.promptPassword( + null, + message, + authTarget, + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, + passParam + ); + } else { + ok = this.oldPrompter.promptUsernameAndPassword( + null, + message, + authTarget, + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, + userParam, + passParam + ); + } + + if (ok) { + InternalPromptUtils.setAuthInfo( + authInfo, + userParam.value, + passParam.value + ); + } + return ok; + }, + + asyncPromptAuth( + channel, + callback, + context, + level, + authInfo, + checkLabel, + checkValue + ) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, +}; diff --git a/toolkit/components/prompts/src/components.conf b/toolkit/components/prompts/src/components.conf new file mode 100644 index 0000000000..7be17963ed --- /dev/null +++ b/toolkit/components/prompts/src/components.conf @@ -0,0 +1,26 @@ +# -*- 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': '{6e134924-6c3a-4d86-81ac-69432dd971dc}', + 'contract_ids': ['@mozilla.org/network/authprompt-adapter-factory;1'], + 'esModule': 'resource://gre/modules/Prompter.sys.mjs', + 'constructor': 'AuthPromptAdapterFactory', + }, +] + +if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android': + Classes += [ + { + 'js_name': 'prompt', + 'cid': '{1c978d25-b37f-43a8-a2d6-0c7a239ead87}', + 'contract_ids': ['@mozilla.org/prompter;1'], + 'interfaces': ['nsIPromptService'], + 'esModule': 'resource://gre/modules/Prompter.sys.mjs', + 'constructor': 'Prompter', + }, + ] diff --git a/toolkit/components/prompts/src/moz.build b/toolkit/components/prompts/src/moz.build new file mode 100644 index 0000000000..00eea500c5 --- /dev/null +++ b/toolkit/components/prompts/src/moz.build @@ -0,0 +1,15 @@ +# -*- 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/. + +EXTRA_JS_MODULES += [ + "CommonDialog.sys.mjs", + "Prompter.sys.mjs", + "PromptUtils.sys.mjs", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] |