/* 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 LoginInfo = Components.Constructor( "@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "dialogsBundle", function () { return Services.strings.createBundle( "chrome://global/locale/commonDialogs.properties" ); }); ChromeUtils.defineLazyGetter(lazy, "brandFullName", function () { return Services.strings .createBundle("chrome://branding/locale/brand.properties") .GetStringFromName("brandFullName"); }); ChromeUtils.defineLazyGetter(lazy, "log", () => { return console.createInstance({ prefix: "mail.asyncprompter", maxLogLevel: "Warn", maxLogLevelPref: "mail.asyncprompter.loglevel", }); }); /** * @implements {nsIRunnable} */ class RunnablePrompter { #asyncPrompter = null; #hashKey = null; constructor(asyncPrompter, hashKey) { this.#asyncPrompter = asyncPrompter; this.#hashKey = hashKey; } #promiseAuthPrompt(listener) { return new Promise((resolve, reject) => { try { listener.onPromptStartAsync({ onAuthResult: resolve }); } catch (e) { reject(e); } }); } async run() { lazy.log.debug("Running prompt for " + this.#hashKey); await Services.logins.initializationPromise; const prompter = this.#asyncPrompter.pendingPrompts[this.#hashKey]; let ok = false; try { ok = await this.#promiseAuthPrompt(prompter.first); } catch (ex) { lazy.log.error("RunnablePrompter:run: ", ex); prompter.first.onPromptCanceled(); } delete this.#asyncPrompter.pendingPrompts[this.#hashKey]; for (const 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. lazy.log.error( "RunnablePrompter:run: consumer.onPrompt* reported an exception: ", ex ); } } this.#asyncPrompter.asyncPromptInProgress--; lazy.log.debug("Finished running prompter for " + this.#hashKey); this.#asyncPrompter.doAsyncAuthPrompt(); } } /** * @implements {nsIMsgAsyncPrompter} */ export class MsgAsyncPrompter { QueryInterface = ChromeUtils.generateQI(["nsIMsgAsyncPrompter"]); pendingPrompts = null; asyncPromptInProgress = 0; constructor() { this.pendingPrompts = {}; } queueAsyncAuthPrompt(aKey, aJumpQueue, aCaller) { if (aKey in this.pendingPrompts) { lazy.log.debug( "Prompt bound to an existing one in the queue, key: " + aKey ); this.pendingPrompts[aKey].consumers.push(aCaller); return; } lazy.log.debug("Adding new prompt to the queue, key: " + aKey); const asyncPrompt = { first: aCaller, consumers: [], }; this.pendingPrompts[aKey] = asyncPrompt; if (aJumpQueue) { this.asyncPromptInProgress++; lazy.log.debug("Forcing RunnablePrompter for " + aKey); const runnable = new RunnablePrompter(this, aKey); Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL); } else { this.doAsyncAuthPrompt(); } } doAsyncAuthPrompt() { if (this.asyncPromptInProgress > 0) { lazy.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++; lazy.log.debug("Dispatching RunnablePrompter for " + hashKey); const 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} */ export class MsgAuthPrompt { QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt"]); static l10n = new Localization(["messenger/msgAuthPrompt.ftl"], true); #getRealmInfo(aRealmString) { const httpRealm = /^.+ \(.+\)$/; if (httpRealm.test(aRealmString)) { return [null, null, null]; } const uri = Services.io.newURI(aRealmString); let pathname = ""; if (uri.pathQueryRef != "/") { pathname = uri.pathQueryRef; } const formattedOrigin = uri.scheme + "://" + uri.displayHostPort; return [formattedOrigin, formattedOrigin + pathname, uri.username]; } /** * 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 ); } const checkBox = { value: false }; let checkBoxLabel = null; const [origin, realm] = this.#getRealmInfo(aPasswordRealm); // If origin is null, we can't save this login. if (origin) { const 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 = MsgAuthPrompt.l10n.formatValueSync( "remember-password-checkbox-label" ); } for (const 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; } } } } const ok = nsIPrompt_promptUsernameAndPassword( aDialogTitle, aText, aUsername, aPassword, checkBoxLabel, checkBox ); if (!ok || !checkBox.value || !origin) { return ok; } const newLogin = new LoginInfo( origin, null, realm, aUsername.value, aPassword.value ); Services.logins.addLoginAsync(newLogin); Services.tm.spinEventLoopUntilEmpty(); 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 ); } const 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) { const 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 = MsgAuthPrompt.l10n.formatValueSync( "remember-password-checkbox-label" ); } if (!aPassword.value) { // Look for existing logins. for (const login of Services.logins.findLogins(origin, null, realm)) { if (login.username == username) { aPassword.value = login.password; return true; } } } } const ok = nsIPrompt_promptPassword( aDialogTitle, aText, aPassword, checkBoxLabel, checkBox ); if (ok && checkBox.value && origin && aPassword.value) { const newLogin = new LoginInfo( origin, null, realm, username, aPassword.value ); Services.logins.addLoginAsync(newLogin); Services.tm.spinEventLoopUntilEmpty(); } 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) { const title = lazy.dialogsBundle.formatStringFromName( "PromptUsernameAndPassword3", [lazy.brandFullName] ); const text = lazy.dialogsBundle.formatStringFromName( "EnterUserPasswordFor2", [`${channel.URI.scheme}://${channel.URI.host}`] ); const username = { value: authInfo.username || "" }; const password = { value: authInfo.password || "" }; const 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] ); } const args = { promptType: "promptUserAndPass", title: dialogTitle, text, user: username.value, pass: password.value, checkLabel: checkMsg, checked: checkValue.value, ok: false, }; const 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? const 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] ); } const args = { promptType: "promptPassword", title: dialogTitle, text, pass: password.value, checkLabel: checkMsg, checked: checkValue.value, ok: false, }; const 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? const ok = args.ok; if (ok) { checkValue.value = args.checked; password.value = args.pass; } return ok; }