summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/MsgAsyncPrompter.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base/src/MsgAsyncPrompter.jsm')
-rw-r--r--comm/mailnews/base/src/MsgAsyncPrompter.jsm621
1 files changed, 621 insertions, 0 deletions
diff --git a/comm/mailnews/base/src/MsgAsyncPrompter.jsm b/comm/mailnews/base/src/MsgAsyncPrompter.jsm
new file mode 100644
index 0000000000..e04e9a9418
--- /dev/null
+++ b/comm/mailnews/base/src/MsgAsyncPrompter.jsm
@@ -0,0 +1,621 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["MsgAsyncPrompter", "MsgAuthPrompt"];
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const LoginInfo = Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo",
+ "init"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Deprecated: "resource://gre/modules/Deprecated.sys.mjs",
+ PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "dialogsBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://global/locale/commonDialogs.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "passwordsBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://passwordmgr/locale/passwordmgr.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "brandFullName", function () {
+ return Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandFullName");
+});
+
+function runnablePrompter(asyncPrompter, hashKey) {
+ this._asyncPrompter = asyncPrompter;
+ this._hashKey = hashKey;
+}
+
+runnablePrompter.prototype = {
+ _asyncPrompter: null,
+ _hashKey: null,
+
+ _promiseAuthPrompt(listener) {
+ return new Promise((resolve, reject) => {
+ try {
+ listener.onPromptStartAsync({ onAuthResult: resolve });
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED) {
+ // Fall back to onPromptStart, for add-ons compat
+ lazy.Deprecated.warning(
+ "onPromptStart has been replaced by onPromptStartAsync",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1176399"
+ );
+ let ok = listener.onPromptStart();
+ resolve(ok);
+ } else {
+ reject(e);
+ }
+ }
+ });
+ },
+
+ async run() {
+ await Services.logins.initializationPromise;
+ this._asyncPrompter._log.debug("Running prompt for " + this._hashKey);
+ let prompter = this._asyncPrompter._pendingPrompts[this._hashKey];
+ let ok = false;
+ try {
+ ok = await this._promiseAuthPrompt(prompter.first);
+ } catch (ex) {
+ console.error("runnablePrompter:run: " + ex + "\n");
+ prompter.first.onPromptCanceled();
+ }
+
+ delete this._asyncPrompter._pendingPrompts[this._hashKey];
+
+ for (var consumer of prompter.consumers) {
+ try {
+ if (ok) {
+ consumer.onPromptAuthAvailable();
+ } else {
+ consumer.onPromptCanceled();
+ }
+ } catch (ex) {
+ // Log the error for extension devs and others to pick up.
+ console.error(
+ "runnablePrompter:run: consumer.onPrompt* reported an exception: " +
+ ex +
+ "\n"
+ );
+ }
+ }
+ this._asyncPrompter._asyncPromptInProgress--;
+
+ this._asyncPrompter._log.debug(
+ "Finished running prompter for " + this._hashKey
+ );
+ this._asyncPrompter._doAsyncAuthPrompt();
+ },
+};
+
+function MsgAsyncPrompter() {
+ this._pendingPrompts = {};
+ // By default, only log warnings to the error console
+ // You can use the preference:
+ // msgAsyncPrompter.loglevel
+ // To change this up. Values should be one of:
+ // Fatal/Error/Warn/Info/Config/Debug/Trace/All
+ this._log = console.createInstance({
+ prefix: "mail.asyncprompter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.asyncprompter.loglevel",
+ });
+}
+
+MsgAsyncPrompter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgAsyncPrompter"]),
+
+ _pendingPrompts: null,
+ _asyncPromptInProgress: 0,
+ _log: null,
+
+ queueAsyncAuthPrompt(aKey, aJumpQueue, aCaller) {
+ if (aKey in this._pendingPrompts) {
+ this._log.debug(
+ "Prompt bound to an existing one in the queue, key: " + aKey
+ );
+ this._pendingPrompts[aKey].consumers.push(aCaller);
+ return;
+ }
+
+ this._log.debug("Adding new prompt to the queue, key: " + aKey);
+ let asyncPrompt = {
+ first: aCaller,
+ consumers: [],
+ };
+
+ this._pendingPrompts[aKey] = asyncPrompt;
+ if (aJumpQueue) {
+ this._asyncPromptInProgress++;
+
+ this._log.debug("Forcing runnablePrompter for " + aKey);
+
+ let runnable = new runnablePrompter(this, aKey);
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ } else {
+ this._doAsyncAuthPrompt();
+ }
+ },
+
+ _doAsyncAuthPrompt() {
+ if (this._asyncPromptInProgress > 0) {
+ this._log.debug(
+ "_doAsyncAuthPrompt bypassed - prompt already in progress"
+ );
+ return;
+ }
+
+ // Find the first prompt key we have in the queue.
+ let hashKey = null;
+ for (hashKey in this._pendingPrompts) {
+ break;
+ }
+
+ if (!hashKey) {
+ return;
+ }
+
+ this._asyncPromptInProgress++;
+
+ this._log.debug("Dispatching runnablePrompter for " + hashKey);
+
+ let runnable = new runnablePrompter(this, hashKey);
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+};
+
+/**
+ * An implementation of nsIAuthPrompt which is roughly the same as
+ * LoginManagerAuthPrompter was before the check box option was removed from
+ * nsIPromptService.
+ *
+ * Calls our own version of promptUsernameAndPassword/promptPassword, which
+ * directly open the prompt.
+ *
+ * @implements {nsIAuthPrompt}
+ */
+class MsgAuthPrompt {
+ QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt"]);
+
+ _getFormattedOrigin(aURI) {
+ let uri;
+ if (aURI instanceof Ci.nsIURI) {
+ uri = aURI;
+ } else {
+ uri = Services.io.newURI(aURI);
+ }
+
+ return uri.scheme + "://" + uri.displayHostPort;
+ }
+
+ _getRealmInfo(aRealmString) {
+ let httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString)) {
+ return [null, null, null];
+ }
+
+ let uri = Services.io.newURI(aRealmString);
+ let pathname = "";
+
+ if (uri.pathQueryRef != "/") {
+ pathname = uri.pathQueryRef;
+ }
+
+ let formattedOrigin = this._getFormattedOrigin(uri);
+
+ return [formattedOrigin, formattedOrigin + pathname, uri.username];
+ }
+
+ _getLocalizedString(key, formatArgs) {
+ if (formatArgs) {
+ return lazy.passwordsBundle.formatStringFromName(key, formatArgs);
+ }
+ return lazy.passwordsBundle.GetStringFromName(key);
+ }
+
+ /**
+ * Wrapper around the prompt service prompt. Saving random fields here
+ * doesn't really make sense and therefore isn't implemented.
+ */
+ prompt(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aDefaultText,
+ aResult
+ ) {
+ if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) {
+ throw new Components.Exception(
+ "prompt only supports SAVE_PASSWORD_NEVER",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ if (aDefaultText) {
+ aResult.value = aDefaultText;
+ }
+
+ return Services.prompt.prompt(
+ this._chromeWindow,
+ aDialogTitle,
+ aText,
+ aResult,
+ null,
+ {}
+ );
+ }
+
+ /**
+ * Looks up a username and password in the database. Will prompt the user
+ * with a dialog, even if a username and password are found.
+ */
+ promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aUsername,
+ aPassword
+ ) {
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
+ throw new Components.Exception(
+ "promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ let checkBox = { value: false };
+ let checkBoxLabel = null;
+ let [origin, realm] = this._getRealmInfo(aPasswordRealm);
+
+ // If origin is null, we can't save this login.
+ if (origin) {
+ let canRememberLogin =
+ aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
+ Services.logins.getLoginSavingEnabled(origin);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin) {
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+ }
+
+ for (let login of Services.logins.findLogins(origin, null, realm)) {
+ if (login.username == aUsername.value) {
+ checkBox.value = true;
+ aUsername.value = login.username;
+ // If the caller provided a password, prefer it.
+ if (!aPassword.value) {
+ aPassword.value = login.password;
+ }
+ }
+ }
+ }
+
+ let ok = nsIPrompt_promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (!ok || !checkBox.value || !origin) {
+ return ok;
+ }
+
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ aUsername.value,
+ aPassword.value
+ );
+ Services.logins.addLogin(newLogin);
+
+ return ok;
+ }
+
+ /**
+ * If a password is found in the database for the password realm, it is
+ * returned straight away without displaying a dialog.
+ *
+ * If a password is not found in the database, the user will be prompted
+ * with a dialog with a text field and ok/cancel buttons. If the user
+ * allows it, then the password will be saved in the database.
+ */
+ promptPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aPassword
+ ) {
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
+ throw new Components.Exception(
+ "promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ let checkBox = { value: false };
+ let checkBoxLabel = null;
+ let [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
+
+ username = decodeURIComponent(username);
+
+ // If origin is null, we can't save this login.
+ if (origin) {
+ let canRememberLogin =
+ aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
+ Services.logins.getLoginSavingEnabled(origin);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin) {
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+ }
+
+ if (!aPassword.value) {
+ // Look for existing logins.
+ for (let login of Services.logins.findLogins(origin, null, realm)) {
+ if (login.username == username) {
+ aPassword.value = login.password;
+ return true;
+ }
+ }
+ }
+ }
+
+ let ok = nsIPrompt_promptPassword(
+ aDialogTitle,
+ aText,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (ok && checkBox.value && origin && aPassword.value) {
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ username,
+ aPassword.value
+ );
+
+ Services.logins.addLogin(newLogin);
+ }
+
+ return ok;
+ }
+
+ /**
+ * Implements nsIPrompt.promptPassword as it was before the check box option
+ * was removed.
+ *
+ * Puts up a dialog with a password field and an optional, labelled checkbox.
+ *
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} password - Contains the default value for the password
+ * 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} checkMsg - 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.
+ */
+ promptPassword2(dialogTitle, text, password, checkMsg, checkValue) {
+ return nsIPrompt_promptPassword(
+ dialogTitle,
+ text,
+ password,
+ checkMsg,
+ checkValue
+ );
+ }
+
+ /**
+ * Requests a username and a password. Implementations will commonly show a
+ * dialog with a username and password field, depending on flags also a
+ * domain field.
+ *
+ * @param {nsIChannel} channel - The channel that requires authentication.
+ * @param {number} level - One of the level constants from nsIAuthPrompt2.
+ * See there for descriptions of the levels.
+ * @param {nsIAuthInformation} authInfo - Authentication information object.
+ * The implementation should fill in this object with the information
+ * entered by the user before returning.
+ * @param {string} checkboxLabel
+ * 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.
+ */
+ promptAuth(channel, level, authInfo, checkboxLabel, checkValue) {
+ let title = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ let text = lazy.dialogsBundle.formatStringFromName(
+ "EnterUserPasswordFor2",
+ [`${channel.URI.scheme}://${channel.URI.host}`]
+ );
+
+ let username = { value: authInfo.username || "" };
+ let password = { value: authInfo.password || "" };
+
+ let ok = nsIPrompt_promptUsernameAndPassword(
+ title,
+ text,
+ username,
+ password,
+ checkboxLabel,
+ checkValue
+ );
+
+ if (ok) {
+ authInfo.username = username.value;
+ authInfo.password = password.value;
+ }
+
+ return ok;
+ }
+}
+
+/**
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} username
+ * 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.
+ * @param {?object} password - Contains the default value for the password
+ * 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} checkMsg - 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.
+ */
+function nsIPrompt_promptUsernameAndPassword(
+ dialogTitle,
+ text,
+ username,
+ password,
+ checkMsg,
+ checkValue
+) {
+ if (!dialogTitle) {
+ dialogTitle = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ }
+
+ let args = {
+ promptType: "promptUserAndPass",
+ title: dialogTitle,
+ text,
+ user: username.value,
+ pass: password.value,
+ checkLabel: checkMsg,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ let propBag = lazy.PromptUtils.objectToPropBag(args);
+ Services.ww.openWindow(
+ Services.ww.activeWindow,
+ "chrome://global/content/commonDialog.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ propBag
+ );
+ lazy.PromptUtils.propBagToObject(propBag, args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ username.value = args.user;
+ password.value = args.pass;
+ }
+
+ return ok;
+}
+
+/**
+ * Implements nsIPrompt.promptPassword as it was before the check box option
+ * was removed.
+ *
+ * Puts up a dialog with a password field and an optional, labelled checkbox.
+ *
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} password - Contains the default value for the password
+ * 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} checkMsg - 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.
+ */
+function nsIPrompt_promptPassword(
+ dialogTitle,
+ text,
+ password,
+ checkMsg,
+ checkValue
+) {
+ if (!dialogTitle) {
+ dialogTitle = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ }
+
+ let args = {
+ promptType: "promptPassword",
+ title: dialogTitle,
+ text,
+ pass: password.value,
+ checkLabel: checkMsg,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ let propBag = lazy.PromptUtils.objectToPropBag(args);
+ Services.ww.openWindow(
+ Services.ww.activeWindow,
+ "chrome://global/content/commonDialog.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ propBag
+ );
+ lazy.PromptUtils.propBagToObject(propBag, args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ password.value = args.pass;
+ }
+
+ return ok;
+}