diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/components/prompts/src | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/prompts/src')
-rw-r--r-- | toolkit/components/prompts/src/CommonDialog.jsm | 308 | ||||
-rw-r--r-- | toolkit/components/prompts/src/SharedPromptUtils.jsm | 157 | ||||
-rw-r--r-- | toolkit/components/prompts/src/moz.build | 16 | ||||
-rw-r--r-- | toolkit/components/prompts/src/nsPrompter.js | 958 | ||||
-rw-r--r-- | toolkit/components/prompts/src/nsPrompter.manifest | 6 |
5 files changed, 1445 insertions, 0 deletions
diff --git a/toolkit/components/prompts/src/CommonDialog.jsm b/toolkit/components/prompts/src/CommonDialog.jsm new file mode 100644 index 000000000..c4200feb3 --- /dev/null +++ b/toolkit/components/prompts/src/CommonDialog.jsm @@ -0,0 +1,308 @@ +/* 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/. */ + +this.EXPORTED_SYMBOLS = ["CommonDialog"]; + +const Ci = Components.interfaces; +const Cr = Components.results; +const Cc = Components.classes; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper", + "resource://gre/modules/SharedPromptUtils.jsm"); + + +this.CommonDialog = function CommonDialog(args, ui) { + this.args = args; + this.ui = ui; +} + +CommonDialog.prototype = { + args : null, + ui : null, + + hasInputField : true, + numButtons : undefined, + iconClass : undefined, + soundID : undefined, + focusTimer : null, + + onLoad : function(xulDialog) { + 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 "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", ""); + 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: + Cu.reportError("commonDialog opened for unknown type: " + this.args.promptType); + throw "unknown dialog type"; + } + + // set the document title + let title = this.args.title; + // OS X doesn't have a title on modal dialogs, this is hidden on other platforms. + let infoTitle = this.ui.infoTitle; + infoTitle.appendChild(infoTitle.ownerDocument.createTextNode(title)); + if (xulDialog) + xulDialog.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); + } + 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.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 (xulDialog) + xulDialog.defaultButton = ['accept', 'cancel', 'extra1', 'extra2'][b]; + else + button.setAttribute("default", "true"); + + // Set default focus / selection. + this.setDefaultFocus(true); + + if (this.args.enableDelay) { + this.delayHelper = new EnableDelayHelper({ + disableDialog: () => this.setButtonsEnabledState(false), + enableDialog: () => this.setButtonsEnabledState(true), + focusTarget: this.ui.focusTarget + }); + } + + // Play a sound (unless we're tab-modal -- don't want those to feel like OS prompts). + try { + if (xulDialog && this.soundID) { + Cc["@mozilla.org/sound;1"]. + createInstance(Ci.nsISound). + playEventSound(this.soundID); + } + } catch (e) { + Cu.reportError("Couldn't play common dialog event sound: " + e); + } + + let topic = "common-dialog-loaded"; + if (!xulDialog) + topic = "tabmodal-dialog-loaded"; + Services.obs.notifyObservers(this.ui.prompt, topic, null); + }, + + setLabelForNode: function(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 : function (aName, aValue) { + this.ui[aName + "Container"].hidden = false; + this.ui[aName + "Textbox"].setAttribute("value", + aValue !== null ? aValue : ""); + }, + + setButtonsEnabledState : function(enabled) { + this.ui.button0.disabled = !enabled; + // button1 (cancel) remains enabled. + this.ui.button2.disabled = !enabled; + this.ui.button3.disabled = !enabled; + }, + + setDefaultFocus : function(isInitialLoad) { + let b = (this.args.defaultButtonNum || 0); + let button = this.ui["button" + b]; + + if (!this.hasInputField) { + let isOSX = ("nsILocalFileMac" in Components.interfaces); + if (isOSX) + this.ui.infoBody.focus(); + else + button.focus(); + } 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(); + } + }, + + onCheckbox : function() { + this.args.checked = this.ui.checkbox.checked; + }, + + onButton0 : function() { + 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 : function() { + this.args.promptActive = false; + this.args.buttonNumClicked = 1; + }, + + onButton2 : function() { + this.args.promptActive = false; + this.args.buttonNumClicked = 2; + }, + + onButton3 : function() { + this.args.promptActive = false; + this.args.buttonNumClicked = 3; + }, + + abortPrompt : function() { + this.args.promptActive = false; + this.args.promptAborted = true; + }, + +}; diff --git a/toolkit/components/prompts/src/SharedPromptUtils.jsm b/toolkit/components/prompts/src/SharedPromptUtils.jsm new file mode 100644 index 000000000..b27096ac2 --- /dev/null +++ b/toolkit/components/prompts/src/SharedPromptUtils.jsm @@ -0,0 +1,157 @@ +this.EXPORTED_SYMBOLS = [ "PromptUtils", "EnableDelayHelper" ]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); + +this.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 : function (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.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + winUtils.dispatchEventToChromeOnly(target, event); + }, + + objectToPropBag : function (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 : function (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. + */ +this.EnableDelayHelper = function({enableDialog, disableDialog, focusTarget}) { + this.enableDialog = makeSafe(enableDialog); + this.disableDialog = makeSafe(disableDialog); + this.focusTarget = focusTarget; + + this.disableDialog(); + + this.focusTarget.addEventListener("blur", this, false); + this.focusTarget.addEventListener("focus", this, false); + this.focusTarget.document.addEventListener("unload", this, false); + + this.startOnFocusDelay(); +}; + +this.EnableDelayHelper.prototype = { + get delayTime() { + return Services.prefs.getIntPref("security.dialog_enable_delay"); + }, + + handleEvent : function(event) { + if (event.target != this.focusTarget && + event.target != this.focusTarget.document) + return; + + switch (event.type) { + case "blur": + this.onBlur(); + break; + + case "focus": + this.onFocus(); + break; + + case "unload": + this.onUnload(); + break; + } + }, + + onBlur : function () { + 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 : function () { + this.startOnFocusDelay(); + }, + + onUnload: function() { + this.focusTarget.removeEventListener("blur", this, false); + this.focusTarget.removeEventListener("focus", this, false); + this.focusTarget.document.removeEventListener("unload", this, false); + + if (this._focusTimer) { + this._focusTimer.cancel(); + this._focusTimer = null; + } + + this.focusTarget = this.enableDialog = this.disableDialog = null; + }, + + startOnFocusDelay : function() { + 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 : function() { + 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/moz.build b/toolkit/components/prompts/src/moz.build new file mode 100644 index 000000000..b13e47b42 --- /dev/null +++ b/toolkit/components/prompts/src/moz.build @@ -0,0 +1,16 @@ +# -*- 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_COMPONENTS += [ + 'nsPrompter.js', + 'nsPrompter.manifest', +] + +EXTRA_JS_MODULES += [ + 'CommonDialog.jsm', + 'SharedPromptUtils.jsm', +] + diff --git a/toolkit/components/prompts/src/nsPrompter.js b/toolkit/components/prompts/src/nsPrompter.js new file mode 100644 index 000000000..26efe28cc --- /dev/null +++ b/toolkit/components/prompts/src/nsPrompter.js @@ -0,0 +1,958 @@ +/* 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"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/SharedPromptUtils.jsm"); + +function Prompter() { + // Note that EmbedPrompter clones this implementation. +} + +Prompter.prototype = { + classID : Components.ID("{1c978d25-b37f-43a8-a2d6-0c7a239ead87}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIPromptService, Ci.nsIPromptService2]), + + + /* ---------- private members ---------- */ + + pickPrompter : function (domWin) { + return new ModalPrompter(domWin); + }, + + + /* ---------- nsIPromptFactory ---------- */ + + + getPrompt : function (domWin, iid) { + // This is still kind of dumb; the C++ code delegated to login manager + // here, which in turn calls back into us via nsIPromptService2. + 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) { + Cu.reportError("nsPrompter: Delegation to password manager failed: " + e); + } + } + + let p = new ModalPrompter(domWin); + p.QueryInterface(iid); + return p; + }, + + + /* ---------- nsIPromptService ---------- */ + + + alert : function (domWin, title, text) { + let p = this.pickPrompter(domWin); + p.alert(title, text); + }, + + alertCheck : function (domWin, title, text, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + p.alertCheck(title, text, checkLabel, checkValue); + }, + + confirm : function (domWin, title, text) { + let p = this.pickPrompter(domWin); + return p.confirm(title, text); + }, + + confirmCheck : function (domWin, title, text, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.confirmCheck(title, text, checkLabel, checkValue); + }, + + confirmEx : function (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); + }, + + prompt : function (domWin, title, text, value, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.nsIPrompt_prompt(title, text, value, checkLabel, checkValue); + }, + + promptUsernameAndPassword : function (domWin, title, text, user, pass, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.nsIPrompt_promptUsernameAndPassword(title, text, user, pass, checkLabel, checkValue); + }, + + promptPassword : function (domWin, title, text, pass, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.nsIPrompt_promptPassword(title, text, pass, checkLabel, checkValue); + }, + + select : function (domWin, title, text, count, list, selected) { + let p = this.pickPrompter(domWin); + return p.select(title, text, count, list, selected); + }, + + + /* ---------- nsIPromptService2 ---------- */ + + + promptAuth : function (domWin, channel, level, authInfo, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.promptAuth(channel, level, authInfo, checkLabel, checkValue); + }, + + asyncPromptAuth : function (domWin, channel, callback, context, level, authInfo, checkLabel, checkValue) { + let p = this.pickPrompter(domWin); + return p.asyncPromptAuth(channel, callback, context, level, authInfo, checkLabel, checkValue); + }, + +}; + + +// Common utils not specific to a particular prompter style. +var PromptUtilsTemp = { + __proto__ : PromptUtils, + + getLocalizedString : function (key, formatArgs) { + if (formatArgs) + return this.strBundle.formatStringFromName(key, formatArgs, formatArgs.length); + return this.strBundle.GetStringFromName(key); + }, + + confirmExHelper : function (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 = PromptUtils.getLocalizedString("OK"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_CANCEL: + buttonLabel = PromptUtils.getLocalizedString("Cancel"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_YES: + buttonLabel = PromptUtils.getLocalizedString("Yes"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_NO: + buttonLabel = PromptUtils.getLocalizedString("No"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_SAVE: + buttonLabel = PromptUtils.getLocalizedString("Save"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE: + buttonLabel = PromptUtils.getLocalizedString("DontSave"); + break; + case Ci.nsIPrompt.BUTTON_TITLE_REVERT: + buttonLabel = PromptUtils.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 : function (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 : function (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 : function (uri) { + return uri.scheme + "://" + uri.hostPort; + }, + + // Copied from login manager + getAuthTarget : function (aChannel, aAuthInfo) { + let hostname, 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 "proxy auth needs nsIProxiedChannel"; + + let info = aChannel.proxyInfo; + if (!info) + throw "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); + hostname = "moz-proxy://" + + idnService.convertUTF8toACE(info.host) + + ":" + info.port; + realm = aAuthInfo.realm; + if (!realm) + realm = hostname; + + return [hostname, realm]; + } + + hostname = this.getFormattedHostname(aChannel.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 = aAuthInfo.realm; + if (!realm) + realm = hostname; + + return [hostname, realm]; + }, + + + makeAuthMessage : function (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 = ""; + + // Trim obnoxiously long realms. + if (realm.length > 150) { + realm = realm.substring(0, 150); + // Append "..." (or localized equivalent). + realm += this.ellipsis; + } + + let text; + if (isProxy) { + text = PromptUtils.getLocalizedString("EnterLoginForProxy3", [realm, displayHost]); + } else if (isPassOnly) { + text = PromptUtils.getLocalizedString("EnterPasswordFor", [username, displayHost]); + } else if (isCrossOrig) { + text = PromptUtils.getLocalizedString("EnterUserPasswordForCrossOrigin2", [displayHost]); + } else if (!realm) { + text = PromptUtils.getLocalizedString("EnterUserPasswordFor2", [displayHost]); + } else { + text = PromptUtils.getLocalizedString("EnterLoginForRealm3", [realm, displayHost]); + } + + return text; + }, + + getTabModalPrompt : function (domWin) { + var promptBox = null; + + try { + // Get the topmost window, in case we're in a frame. + var promptWin = domWin.top; + + // Get the chrome window for the content window we're using. + // (Unwrap because we need a non-IDL property below.) + var chromeWin = promptWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler.ownerDocument + .defaultView.wrappedJSObject; + + if (chromeWin.getTabModalPromptBox) + promptBox = chromeWin.getTabModalPromptBox(promptWin); + } catch (e) { + // If any errors happen, just assume no tabmodal prompter. + } + + return promptBox; + }, +}; + +PromptUtils = PromptUtilsTemp; + +XPCOMUtils.defineLazyGetter(PromptUtils, "strBundle", function () { + let bunService = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + let bundle = bunService.createBundle("chrome://global/locale/commonDialogs.properties"); + if (!bundle) + throw "String bundle for Prompter not present!"; + return bundle; +}); + +XPCOMUtils.defineLazyGetter(PromptUtils, "ellipsis", function () { + let ellipsis = "\u2026"; + try { + ellipsis = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; + } catch (e) { } + return ellipsis; +}); + + + +function openModalWindow(domWin, uri, args) { + // 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. + // There's also a requirement for prompts to be blocked if a window is + // passed and that window is hidden (eg, auth prompts are supressed if the + // passed window is the hidden window). + // See bug 875157 comment 30 for more... + if (domWin) { + // a domWin was passed, so we can apply the check for it being hidden. + let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + if (winUtils && !winUtils.isParentWindowMainWidgetVisible) { + throw Components.Exception("Cannot call openModalWindow on a hidden window", + Cr.NS_ERROR_NOT_AVAILABLE); + } + } else { + // We try and find a window to use as the parent, but don't consider + // if that is visible before showing the prompt. + domWin = Services.ww.activeWindow; + // domWin may still be null here if there are _no_ windows open. + } + // Note that we don't need to fire DOMWillOpenModalDialog and + // DOMModalDialogClosed events here, wwatcher's OpenWindowInternal + // will do that. Similarly for enterModalState / leaveModalState. + + Services.ww.openWindow(domWin, uri, "_blank", "centerscreen,chrome,modal,titlebar", args); +} + +function openTabPrompt(domWin, tabPrompt, args) { + let docShell = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + let inPermitUnload = docShell.contentViewer && docShell.contentViewer.inPermitUnload; + let eventDetail = Cu.cloneInto({tabPrompt: true, inPermitUnload}, domWin); + PromptUtils.fireDialogEvent(domWin, "DOMWillOpenModalDialog", null, eventDetail); + + let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + winUtils.enterModalState(); + + let frameMM = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIContentFrameMessageManager); + frameMM.QueryInterface(Ci.nsIDOMEventTarget); + + // We provide a callback so the prompt can close itself. We don't want to + // wait for this event loop to return... Otherwise the presence of other + // prompts on the call stack would in this dialog appearing unresponsive + // until the other prompts had been closed. + let callbackInvoked = false; + let newPrompt; + function onPromptClose(forceCleanup) { + if (!newPrompt && !forceCleanup) + return; + callbackInvoked = true; + if (newPrompt) + tabPrompt.removePrompt(newPrompt); + + frameMM.removeEventListener("pagehide", pagehide, true); + + winUtils.leaveModalState(); + + PromptUtils.fireDialogEvent(domWin, "DOMModalDialogClosed"); + } + + frameMM.addEventListener("pagehide", pagehide, true); + function pagehide(e) { + // Check whether the event relates to our window or its ancestors + let window = domWin; + let eventWindow = e.target.defaultView; + while (window != eventWindow && window.parent != window) { + window = window.parent; + } + if (window != eventWindow) { + return; + } + frameMM.removeEventListener("pagehide", pagehide, true); + + if (newPrompt) { + newPrompt.abortPrompt(); + } + } + + try { + let topPrincipal = domWin.top.document.nodePrincipal; + let promptPrincipal = domWin.document.nodePrincipal; + args.showAlertOrigin = topPrincipal.equals(promptPrincipal); + args.promptActive = true; + + newPrompt = tabPrompt.appendPrompt(args, onPromptClose); + + // TODO since we don't actually open a window, need to check if + // there's other stuff in nsWindowWatcher::OpenWindowInternal + // that we might need to do here as well. + + let thread = Services.tm.currentThread; + while (args.promptActive) + thread.processNextEvent(true); + delete args.promptActive; + + if (args.promptAborted) + throw Components.Exception("prompt aborted by user", Cr.NS_ERROR_NOT_AVAILABLE); + } finally { + // If the prompt unexpectedly failed to invoke the callback, do so here. + if (!callbackInvoked) + onPromptClose(true); + } +} + +function openRemotePrompt(domWin, args, tabPrompt) { + let docShell = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell); + let messageManager = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsITabChild) + .messageManager; + + let inPermitUnload = docShell.contentViewer && docShell.contentViewer.inPermitUnload; + let eventDetail = Cu.cloneInto({tabPrompt, inPermitUnload}, domWin); + PromptUtils.fireDialogEvent(domWin, "DOMWillOpenModalDialog", null, eventDetail); + + let winUtils = domWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + winUtils.enterModalState(); + let closed = false; + + let frameMM = docShell.getInterface(Ci.nsIContentFrameMessageManager); + frameMM.QueryInterface(Ci.nsIDOMEventTarget); + + // It should be hard or impossible to cause a window to create multiple + // prompts, but just in case, give our prompt an ID. + let id = "id" + Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + + messageManager.addMessageListener("Prompt:Close", function listener(message) { + if (message.data._remoteId !== id) { + return; + } + + messageManager.removeMessageListener("Prompt:Close", listener); + frameMM.removeEventListener("pagehide", pagehide, true); + + winUtils.leaveModalState(); + PromptUtils.fireDialogEvent(domWin, "DOMModalDialogClosed"); + + // Copy the response from the closed prompt into our args, it will be + // read by our caller. + if (message.data) { + for (let key in message.data) { + args[key] = message.data[key]; + } + } + + // Exit our nested event loop when we unwind. + closed = true; + }); + + frameMM.addEventListener("pagehide", pagehide, true); + function pagehide(e) { + // Check whether the event relates to our window or its ancestors + let window = domWin; + let eventWindow = e.target.defaultView; + while (window != eventWindow && window.parent != window) { + window = window.parent; + } + if (window != eventWindow) { + return; + } + frameMM.removeEventListener("pagehide", pagehide, true); + messageManager.sendAsyncMessage("Prompt:ForceClose", { _remoteId: id }); + } + + let topPrincipal = domWin.top.document.nodePrincipal; + let promptPrincipal = domWin.document.nodePrincipal; + args.promptPrincipal = promptPrincipal; + args.showAlertOrigin = topPrincipal.equals(promptPrincipal); + args.inPermitUnload = inPermitUnload; + + args._remoteId = id; + + messageManager.sendAsyncMessage("Prompt:Open", args, {}); + + let thread = Services.tm.currentThread; + while (!closed) { + thread.processNextEvent(true); + } +} + +function ModalPrompter(domWin) { + this.domWin = domWin; +} +ModalPrompter.prototype = { + domWin : null, + /* + * Default to not using a tab-modal prompt, unless the caller opts in by + * QIing to nsIWritablePropertyBag and setting the value of this property + * to true. + */ + allowTabModal : false, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIPrompt, Ci.nsIAuthPrompt, + Ci.nsIAuthPrompt2, + Ci.nsIWritablePropertyBag2]), + + + /* ---------- internal methods ---------- */ + + + openPrompt : function (args) { + // Check pref, if false/missing do not ever allow tab-modal prompts. + const prefName = "prompts.tab_modal.enabled"; + let prefValue = false; + if (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_BOOL) + prefValue = Services.prefs.getBoolPref(prefName); + + let allowTabModal = this.allowTabModal && prefValue; + + if (allowTabModal && this.domWin) { + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + openRemotePrompt(this.domWin, args, true); + return; + } + + let tabPrompt = PromptUtils.getTabModalPrompt(this.domWin); + if (tabPrompt) { + openTabPrompt(this.domWin, tabPrompt, args); + return; + } + } + + // If we can't do a tab modal prompt, fallback to using a window-modal dialog. + const COMMON_DIALOG = "chrome://global/content/commonDialog.xul"; + const SELECT_DIALOG = "chrome://global/content/selectDialog.xul"; + + let uri = (args.promptType == "select") ? SELECT_DIALOG : COMMON_DIALOG; + + if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { + args.uri = uri; + openRemotePrompt(this.domWin, args); + return; + } + + let propBag = PromptUtils.objectToPropBag(args); + openModalWindow(this.domWin, uri, propBag); + PromptUtils.propBagToObject(propBag, args); + }, + + + + /* + * ---------- 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 : function() { + // 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 : function() { + // Both have 6 args, so use types. + if (typeof arguments[2] == "object") + return this.nsIPrompt_promptUsernameAndPassword.apply(this, arguments); + return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments); + }, + + promptPassword : function() { + // Both have 5 args, so use types. + if (typeof arguments[2] == "object") + return this.nsIPrompt_promptPassword.apply(this, arguments); + return this.nsIAuthPrompt_promptPassword.apply(this, arguments); + }, + + + /* ---------- nsIPrompt ---------- */ + + + alert : function (title, text) { + if (!title) + title = PromptUtils.getLocalizedString("Alert"); + + let args = { + promptType: "alert", + title: title, + text: text, + }; + + this.openPrompt(args); + }, + + alertCheck : function (title, text, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("Alert"); + + let args = { + promptType: "alertCheck", + title: title, + text: text, + checkLabel: checkLabel, + checked: checkValue.value, + }; + + this.openPrompt(args); + + // Checkbox state always returned, even if cancel clicked. + checkValue.value = args.checked; + }, + + confirm : function (title, text) { + if (!title) + title = PromptUtils.getLocalizedString("Confirm"); + + let args = { + promptType: "confirm", + title: title, + text: text, + ok: false, + }; + + this.openPrompt(args); + + // Did user click Ok or Cancel? + return args.ok; + }, + + confirmCheck : function (title, text, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("ConfirmCheck"); + + let args = { + promptType: "confirmCheck", + title: title, + text: text, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + }; + + this.openPrompt(args); + + // Checkbox state always returned, even if cancel clicked. + checkValue.value = args.checked; + + // Did user click Ok or Cancel? + return args.ok; + }, + + confirmEx : function (title, text, flags, button0, button1, button2, + checkLabel, checkValue) { + + if (!title) + title = PromptUtils.getLocalizedString("Confirm"); + + let args = { + promptType: "confirmEx", + title: title, + text: text, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + buttonNumClicked: 1, + }; + + let [label0, label1, label2, defaultButtonNum, isDelayEnabled] = + PromptUtils.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; + } + } + } + + this.openPrompt(args); + + // Checkbox state always returned, even if cancel clicked. + checkValue.value = args.checked; + + // Get the number of the button the user clicked. + return args.buttonNumClicked; + }, + + nsIPrompt_prompt : function (title, text, value, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("Prompt"); + + let args = { + promptType: "prompt", + title: title, + text: text, + value: value.value, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + }; + + this.openPrompt(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 : function (title, text, user, pass, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("PromptUsernameAndPassword2"); + + let args = { + promptType: "promptUserAndPass", + title: title, + text: text, + user: user.value, + pass: pass.value, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + }; + + this.openPrompt(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + checkValue.value = args.checked; + user.value = args.user; + pass.value = args.pass; + } + + return ok; + }, + + nsIPrompt_promptPassword : function (title, text, pass, checkLabel, checkValue) { + if (!title) + title = PromptUtils.getLocalizedString("PromptPassword2"); + + let args = { + promptType: "promptPassword", + title: title, + text: text, + pass: pass.value, + checkLabel: checkLabel, + checked: checkValue.value, + ok: false, + } + + this.openPrompt(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + checkValue.value = args.checked; + pass.value = args.pass; + } + + return ok; + }, + + select : function (title, text, count, list, selected) { + if (!title) + title = PromptUtils.getLocalizedString("Select"); + + let args = { + promptType: "select", + title: title, + text: text, + list: list, + selected: -1, + ok: false, + }; + + this.openPrompt(args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) + selected.value = args.selected; + + return ok; + }, + + + /* ---------- nsIAuthPrompt ---------- */ + + + nsIAuthPrompt_prompt : function (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 : function (title, text, passwordRealm, savePassword, user, pass) { + // The passwordRealm and savePassword args were ignored by nsPrompt.cpp + return this.nsIPrompt_promptUsernameAndPassword(title, text, user, pass, null, {}); + }, + + nsIAuthPrompt_promptPassword : function (title, text, passwordRealm, savePassword, pass) { + // The passwordRealm and savePassword args were ignored by nsPrompt.cpp + return this.nsIPrompt_promptPassword(title, text, pass, null, {}); + }, + + + /* ---------- nsIAuthPrompt2 ---------- */ + + + promptAuth : function (channel, level, authInfo, checkLabel, checkValue) { + let message = PromptUtils.makeAuthMessage(channel, authInfo); + + let [username, password] = PromptUtils.getAuthInfo(authInfo); + + let userParam = { value: username }; + let passParam = { value: password }; + + let ok; + if (authInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) + ok = this.nsIPrompt_promptPassword(null, message, passParam, checkLabel, checkValue); + else + ok = this.nsIPrompt_promptUsernameAndPassword(null, message, userParam, passParam, checkLabel, checkValue); + + if (ok) + PromptUtils.setAuthInfo(authInfo, userParam.value, passParam.value); + return ok; + }, + + asyncPromptAuth : function (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 Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + /* ---------- nsIWritablePropertyBag2 ---------- */ + + // Only a partial implementation, for one specific use case... + + setPropertyAsBool : function(name, value) { + if (name == "allowTabModal") + this.allowTabModal = value; + else + throw Cr.NS_ERROR_ILLEGAL_VALUE; + }, +}; + + +function AuthPromptAdapterFactory() { +} +AuthPromptAdapterFactory.prototype = { + classID : Components.ID("{6e134924-6c3a-4d86-81ac-69432dd971dc}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPromptAdapterFactory]), + + /* ---------- nsIAuthPromptAdapterFactory ---------- */ + + createAdapter : function (oldPrompter) { + return new AuthPromptAdapter(oldPrompter); + } +}; + + +// Takes an nsIAuthPrompt implementation, wraps it with a nsIAuthPrompt2 shell. +function AuthPromptAdapter(oldPrompter) { + this.oldPrompter = oldPrompter; +} +AuthPromptAdapter.prototype = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]), + oldPrompter : null, + + /* ---------- nsIAuthPrompt2 ---------- */ + + promptAuth : function (channel, level, authInfo, checkLabel, checkValue) { + let message = PromptUtils.makeAuthMessage(channel, authInfo); + + let [username, password] = PromptUtils.getAuthInfo(authInfo); + let userParam = { value: username }; + let passParam = { value: password }; + + let [host, realm] = PromptUtils.getAuthTarget(channel, authInfo); + let authTarget = host + " (" + 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) + PromptUtils.setAuthInfo(authInfo, userParam.value, passParam.value); + return ok; + }, + + asyncPromptAuth : function (channel, callback, context, level, authInfo, checkLabel, checkValue) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } +}; + + +// Wrapper using the old embedding contractID, since it's already common in +// the addon ecosystem. +function EmbedPrompter() { +} +EmbedPrompter.prototype = new Prompter(); +EmbedPrompter.prototype.classID = Components.ID("{7ad1b327-6dfa-46ec-9234-f2a620ea7e00}"); + +var component = [Prompter, EmbedPrompter, AuthPromptAdapterFactory]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); diff --git a/toolkit/components/prompts/src/nsPrompter.manifest b/toolkit/components/prompts/src/nsPrompter.manifest new file mode 100644 index 000000000..582f6bf59 --- /dev/null +++ b/toolkit/components/prompts/src/nsPrompter.manifest @@ -0,0 +1,6 @@ +component {1c978d25-b37f-43a8-a2d6-0c7a239ead87} nsPrompter.js +contract @mozilla.org/prompter;1 {1c978d25-b37f-43a8-a2d6-0c7a239ead87} +component {6e134924-6c3a-4d86-81ac-69432dd971dc} nsPrompter.js +contract @mozilla.org/network/authprompt-adapter-factory;1 {6e134924-6c3a-4d86-81ac-69432dd971dc} +component {7ad1b327-6dfa-46ec-9234-f2a620ea7e00} nsPrompter.js +contract @mozilla.org/embedcomp/prompt-service;1 {7ad1b327-6dfa-46ec-9234-f2a620ea7e00} |