michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: michael@0: /* michael@0: * LoginManagerPromptFactory michael@0: * michael@0: * Implements nsIPromptFactory michael@0: * michael@0: * Invoked by [toolkit/components/prompts/src/nsPrompter.js] michael@0: */ michael@0: function LoginManagerPromptFactory() { michael@0: Services.obs.addObserver(this, "quit-application-granted", true); michael@0: Services.obs.addObserver(this, "passwordmgr-crypto-login", true); michael@0: Services.obs.addObserver(this, "passwordmgr-crypto-loginCanceled", true); michael@0: } michael@0: michael@0: LoginManagerPromptFactory.prototype = { michael@0: michael@0: classID : Components.ID("{749e62f4-60ae-4569-a8a2-de78b649660e}"), michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIObserver, Ci.nsISupportsWeakReference]), michael@0: michael@0: _debug : false, michael@0: _asyncPrompts : {}, michael@0: _asyncPromptInProgress : false, michael@0: michael@0: observe : function (subject, topic, data) { michael@0: this.log("Observed: " + topic); michael@0: if (topic == "quit-application-granted") { michael@0: this._cancelPendingPrompts(); michael@0: } else if (topic == "passwordmgr-crypto-login") { michael@0: // Start processing the deferred prompters. michael@0: this._doAsyncPrompt(); michael@0: } else if (topic == "passwordmgr-crypto-loginCanceled") { michael@0: // User canceled a Master Password prompt, so go ahead and cancel michael@0: // all pending auth prompts to avoid nagging over and over. michael@0: this._cancelPendingPrompts(); michael@0: } michael@0: }, michael@0: michael@0: getPrompt : function (aWindow, aIID) { michael@0: var prefBranch = Services.prefs.getBranch("signon."); michael@0: this._debug = prefBranch.getBoolPref("debug"); michael@0: michael@0: var prompt = new LoginManagerPrompter().QueryInterface(aIID); michael@0: prompt.init(aWindow, this); michael@0: return prompt; michael@0: }, michael@0: michael@0: _doAsyncPrompt : function() { michael@0: if (this._asyncPromptInProgress) { michael@0: this.log("_doAsyncPrompt bypassed, already in progress"); michael@0: return; michael@0: } michael@0: michael@0: // Find the first prompt key we have in the queue michael@0: var hashKey = null; michael@0: for (hashKey in this._asyncPrompts) michael@0: break; michael@0: michael@0: if (!hashKey) { michael@0: this.log("_doAsyncPrompt:run bypassed, no prompts in the queue"); michael@0: return; michael@0: } michael@0: michael@0: // If login manger has logins for this host, defer prompting if we're michael@0: // already waiting on a master password entry. michael@0: var prompt = this._asyncPrompts[hashKey]; michael@0: var prompter = prompt.prompter; michael@0: var [hostname, httpRealm] = prompter._getAuthTarget(prompt.channel, prompt.authInfo); michael@0: var hasLogins = (prompter._pwmgr.countLogins(hostname, null, httpRealm) > 0); michael@0: if (hasLogins && prompter._pwmgr.uiBusy) { michael@0: this.log("_doAsyncPrompt:run bypassed, master password UI busy"); michael@0: return; michael@0: } michael@0: michael@0: this._asyncPromptInProgress = true; michael@0: prompt.inProgress = true; michael@0: michael@0: var self = this; michael@0: michael@0: var runnable = { michael@0: run : function() { michael@0: var ok = false; michael@0: try { michael@0: self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'"); michael@0: ok = prompter.promptAuth(prompt.channel, michael@0: prompt.level, michael@0: prompt.authInfo); michael@0: } catch (e if (e instanceof Components.Exception) && michael@0: e.result == Cr.NS_ERROR_NOT_AVAILABLE) { michael@0: self.log("_doAsyncPrompt:run bypassed, UI is not available in this context"); michael@0: } catch (e) { michael@0: Components.utils.reportError("LoginManagerPrompter: " + michael@0: "_doAsyncPrompt:run: " + e + "\n"); michael@0: } michael@0: michael@0: delete self._asyncPrompts[hashKey]; michael@0: prompt.inProgress = false; michael@0: self._asyncPromptInProgress = false; michael@0: michael@0: for each (var consumer in prompt.consumers) { michael@0: if (!consumer.callback) michael@0: // Not having a callback means that consumer didn't provide it michael@0: // or canceled the notification michael@0: continue; michael@0: michael@0: self.log("Calling back to " + consumer.callback + " ok=" + ok); michael@0: try { michael@0: if (ok) michael@0: consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo); michael@0: else michael@0: consumer.callback.onAuthCancelled(consumer.context, true); michael@0: } catch (e) { /* Throw away exceptions caused by callback */ } michael@0: } michael@0: self._doAsyncPrompt(); michael@0: } michael@0: } michael@0: michael@0: Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL); michael@0: this.log("_doAsyncPrompt:run dispatched"); michael@0: }, michael@0: michael@0: michael@0: _cancelPendingPrompts : function() { michael@0: this.log("Canceling all pending prompts..."); michael@0: var asyncPrompts = this._asyncPrompts; michael@0: this.__proto__._asyncPrompts = {}; michael@0: michael@0: for each (var prompt in asyncPrompts) { michael@0: // Watch out! If this prompt is currently prompting, let it handle michael@0: // notifying the callbacks of success/failure, since it's already michael@0: // asking the user for input. Reusing a callback can be crashy. michael@0: if (prompt.inProgress) { michael@0: this.log("skipping a prompt in progress"); michael@0: continue; michael@0: } michael@0: michael@0: for each (var consumer in prompt.consumers) { michael@0: if (!consumer.callback) michael@0: continue; michael@0: michael@0: this.log("Canceling async auth prompt callback " + consumer.callback); michael@0: try { michael@0: consumer.callback.onAuthCancelled(consumer.context, true); michael@0: } catch (e) { /* Just ignore exceptions from the callback */ } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: log : function (message) { michael@0: if (!this._debug) michael@0: return; michael@0: michael@0: dump("Pwmgr PromptFactory: " + message + "\n"); michael@0: Services.console.logStringMessage("Pwmgr PrompFactory: " + message); michael@0: } michael@0: }; // end of LoginManagerPromptFactory implementation michael@0: michael@0: michael@0: michael@0: michael@0: /* ==================== LoginManagerPrompter ==================== */ michael@0: michael@0: michael@0: michael@0: michael@0: /* michael@0: * LoginManagerPrompter michael@0: * michael@0: * Implements interfaces for prompting the user to enter/save/change auth info. michael@0: * michael@0: * nsIAuthPrompt: Used by SeaMonkey, Thunderbird, but not Firefox. michael@0: * michael@0: * nsIAuthPrompt2: Is invoked by a channel for protocol-based authentication michael@0: * (eg HTTP Authenticate, FTP login). michael@0: * michael@0: * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins michael@0: * found in HTML forms. michael@0: */ michael@0: function LoginManagerPrompter() {} michael@0: michael@0: LoginManagerPrompter.prototype = { michael@0: michael@0: classID : Components.ID("{8aa66d77-1bbb-45a6-991e-b8f47751c291}"), michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIAuthPrompt, michael@0: Ci.nsIAuthPrompt2, michael@0: Ci.nsILoginManagerPrompter]), michael@0: michael@0: _factory : null, michael@0: _window : null, michael@0: _debug : false, // mirrors signon.debug michael@0: michael@0: __pwmgr : null, // Password Manager service michael@0: get _pwmgr() { michael@0: if (!this.__pwmgr) michael@0: this.__pwmgr = Cc["@mozilla.org/login-manager;1"]. michael@0: getService(Ci.nsILoginManager); michael@0: return this.__pwmgr; michael@0: }, michael@0: michael@0: __promptService : null, // Prompt service for user interaction michael@0: get _promptService() { michael@0: if (!this.__promptService) michael@0: this.__promptService = michael@0: Cc["@mozilla.org/embedcomp/prompt-service;1"]. michael@0: getService(Ci.nsIPromptService2); michael@0: return this.__promptService; michael@0: }, michael@0: michael@0: michael@0: __strBundle : null, // String bundle for L10N michael@0: get _strBundle() { michael@0: if (!this.__strBundle) { michael@0: var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. michael@0: getService(Ci.nsIStringBundleService); michael@0: this.__strBundle = bunService.createBundle( michael@0: "chrome://passwordmgr/locale/passwordmgr.properties"); michael@0: if (!this.__strBundle) michael@0: throw "String bundle for Login Manager not present!"; michael@0: } michael@0: michael@0: return this.__strBundle; michael@0: }, michael@0: michael@0: michael@0: __ellipsis : null, michael@0: get _ellipsis() { michael@0: if (!this.__ellipsis) { michael@0: this.__ellipsis = "\u2026"; michael@0: try { michael@0: this.__ellipsis = Services.prefs.getComplexValue( michael@0: "intl.ellipsis", Ci.nsIPrefLocalizedString).data; michael@0: } catch (e) { } michael@0: } michael@0: return this.__ellipsis; michael@0: }, michael@0: michael@0: michael@0: // Whether we are in private browsing mode michael@0: get _inPrivateBrowsing() { michael@0: if (this._window) { michael@0: return PrivateBrowsingUtils.isWindowPrivate(this._window); michael@0: } else { michael@0: // If we don't that we're in private browsing mode if the caller did michael@0: // not provide a window. The callers which really care about this michael@0: // will indeed pass down a window to us, and for those who don't, michael@0: // we can just assume that we don't want to save the entered login michael@0: // information. michael@0: return true; michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * log michael@0: * michael@0: * Internal function for logging debug messages to the Error Console window. michael@0: */ michael@0: log : function (message) { michael@0: if (!this._debug) michael@0: return; michael@0: michael@0: dump("Pwmgr Prompter: " + message + "\n"); michael@0: Services.console.logStringMessage("Pwmgr Prompter: " + message); michael@0: }, michael@0: michael@0: michael@0: michael@0: michael@0: /* ---------- nsIAuthPrompt prompts ---------- */ michael@0: michael@0: michael@0: /* michael@0: * prompt michael@0: * michael@0: * Wrapper around the prompt service prompt. Saving random fields here michael@0: * doesn't really make sense and therefore isn't implemented. michael@0: */ michael@0: prompt : function (aDialogTitle, aText, aPasswordRealm, michael@0: aSavePassword, aDefaultText, aResult) { michael@0: if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: michael@0: this.log("===== prompt() called ====="); michael@0: michael@0: if (aDefaultText) { michael@0: aResult.value = aDefaultText; michael@0: } michael@0: michael@0: return this._promptService.prompt(this._window, michael@0: aDialogTitle, aText, aResult, null, {}); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * promptUsernameAndPassword michael@0: * michael@0: * Looks up a username and password in the database. Will prompt the user michael@0: * with a dialog, even if a username and password are found. michael@0: */ michael@0: promptUsernameAndPassword : function (aDialogTitle, aText, aPasswordRealm, michael@0: aSavePassword, aUsername, aPassword) { michael@0: this.log("===== promptUsernameAndPassword() called ====="); michael@0: michael@0: if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: michael@0: var selectedLogin = null; michael@0: var checkBox = { value : false }; michael@0: var checkBoxLabel = null; michael@0: var [hostname, realm, unused] = this._getRealmInfo(aPasswordRealm); michael@0: michael@0: // If hostname is null, we can't save this login. michael@0: if (hostname) { michael@0: var canRememberLogin; michael@0: if (this._inPrivateBrowsing) michael@0: canRememberLogin = false; michael@0: else michael@0: canRememberLogin = (aSavePassword == michael@0: Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) && michael@0: this._pwmgr.getLoginSavingEnabled(hostname); michael@0: michael@0: // if checkBoxLabel is null, the checkbox won't be shown at all. michael@0: if (canRememberLogin) michael@0: checkBoxLabel = this._getLocalizedString("rememberPassword"); michael@0: michael@0: // Look for existing logins. michael@0: var foundLogins = this._pwmgr.findLogins({}, hostname, null, michael@0: realm); michael@0: michael@0: // XXX Like the original code, we can't deal with multiple michael@0: // account selection. (bug 227632) michael@0: if (foundLogins.length > 0) { michael@0: selectedLogin = foundLogins[0]; michael@0: michael@0: // If the caller provided a username, try to use it. If they michael@0: // provided only a password, this will try to find a password-only michael@0: // login (or return null if none exists). michael@0: if (aUsername.value) michael@0: selectedLogin = this._repickSelectedLogin(foundLogins, michael@0: aUsername.value); michael@0: michael@0: if (selectedLogin) { michael@0: checkBox.value = true; michael@0: aUsername.value = selectedLogin.username; michael@0: // If the caller provided a password, prefer it. michael@0: if (!aPassword.value) michael@0: aPassword.value = selectedLogin.password; michael@0: } michael@0: } michael@0: } michael@0: michael@0: var ok = this._promptService.promptUsernameAndPassword(this._window, michael@0: aDialogTitle, aText, aUsername, aPassword, michael@0: checkBoxLabel, checkBox); michael@0: michael@0: if (!ok || !checkBox.value || !hostname) michael@0: return ok; michael@0: michael@0: if (!aPassword.value) { michael@0: this.log("No password entered, so won't offer to save."); michael@0: return ok; michael@0: } michael@0: michael@0: // XXX We can't prompt with multiple logins yet (bug 227632), so michael@0: // the entered login might correspond to an existing login michael@0: // other than the one we originally selected. michael@0: selectedLogin = this._repickSelectedLogin(foundLogins, aUsername.value); michael@0: michael@0: // If we didn't find an existing login, or if the username michael@0: // changed, save as a new login. michael@0: if (!selectedLogin) { michael@0: // add as new michael@0: this.log("New login seen for " + realm); michael@0: var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. michael@0: createInstance(Ci.nsILoginInfo); michael@0: newLogin.init(hostname, null, realm, michael@0: aUsername.value, aPassword.value, "", ""); michael@0: this._pwmgr.addLogin(newLogin); michael@0: } else if (aPassword.value != selectedLogin.password) { michael@0: // update password michael@0: this.log("Updating password for " + realm); michael@0: this._updateLogin(selectedLogin, aPassword.value); michael@0: } else { michael@0: this.log("Login unchanged, no further action needed."); michael@0: this._updateLogin(selectedLogin); michael@0: } michael@0: michael@0: return ok; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * promptPassword michael@0: * michael@0: * If a password is found in the database for the password realm, it is michael@0: * returned straight away without displaying a dialog. michael@0: * michael@0: * If a password is not found in the database, the user will be prompted michael@0: * with a dialog with a text field and ok/cancel buttons. If the user michael@0: * allows it, then the password will be saved in the database. michael@0: */ michael@0: promptPassword : function (aDialogTitle, aText, aPasswordRealm, michael@0: aSavePassword, aPassword) { michael@0: this.log("===== promptPassword called() ====="); michael@0: michael@0: if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) michael@0: throw Components.results.NS_ERROR_NOT_IMPLEMENTED; michael@0: michael@0: var checkBox = { value : false }; michael@0: var checkBoxLabel = null; michael@0: var [hostname, realm, username] = this._getRealmInfo(aPasswordRealm); michael@0: michael@0: username = decodeURIComponent(username); michael@0: michael@0: // If hostname is null, we can't save this login. michael@0: if (hostname && !this._inPrivateBrowsing) { michael@0: var canRememberLogin = (aSavePassword == michael@0: Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY) && michael@0: this._pwmgr.getLoginSavingEnabled(hostname); michael@0: michael@0: // if checkBoxLabel is null, the checkbox won't be shown at all. michael@0: if (canRememberLogin) michael@0: checkBoxLabel = this._getLocalizedString("rememberPassword"); michael@0: michael@0: if (!aPassword.value) { michael@0: // Look for existing logins. michael@0: var foundLogins = this._pwmgr.findLogins({}, hostname, null, michael@0: realm); michael@0: michael@0: // XXX Like the original code, we can't deal with multiple michael@0: // account selection (bug 227632). We can deal with finding the michael@0: // account based on the supplied username - but in this case we'll michael@0: // just return the first match. michael@0: for (var i = 0; i < foundLogins.length; ++i) { michael@0: if (foundLogins[i].username == username) { michael@0: aPassword.value = foundLogins[i].password; michael@0: // wallet returned straight away, so this mimics that code michael@0: return true; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: var ok = this._promptService.promptPassword(this._window, aDialogTitle, michael@0: aText, aPassword, michael@0: checkBoxLabel, checkBox); michael@0: michael@0: if (ok && checkBox.value && hostname && aPassword.value) { michael@0: var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. michael@0: createInstance(Ci.nsILoginInfo); michael@0: newLogin.init(hostname, null, realm, username, michael@0: aPassword.value, "", ""); michael@0: michael@0: this.log("New login seen for " + realm); michael@0: michael@0: this._pwmgr.addLogin(newLogin); michael@0: } michael@0: michael@0: return ok; michael@0: }, michael@0: michael@0: /* ---------- nsIAuthPrompt helpers ---------- */ michael@0: michael@0: michael@0: /** michael@0: * Given aRealmString, such as "http://user@example.com/foo", returns an michael@0: * array of: michael@0: * - the formatted hostname michael@0: * - the realm (hostname + path) michael@0: * - the username, if present michael@0: * michael@0: * If aRealmString is in the format produced by NS_GetAuthKey for HTTP[S] michael@0: * channels, e.g. "example.com:80 (httprealm)", null is returned for all michael@0: * arguments to let callers know the login can't be saved because we don't michael@0: * know whether it's http or https. michael@0: */ michael@0: _getRealmInfo : function (aRealmString) { michael@0: var httpRealm = /^.+ \(.+\)$/; michael@0: if (httpRealm.test(aRealmString)) michael@0: return [null, null, null]; michael@0: michael@0: var uri = Services.io.newURI(aRealmString, null, null); michael@0: var pathname = ""; michael@0: michael@0: if (uri.path != "/") michael@0: pathname = uri.path; michael@0: michael@0: var formattedHostname = this._getFormattedHostname(uri); michael@0: michael@0: return [formattedHostname, formattedHostname + pathname, uri.username]; michael@0: }, michael@0: michael@0: /* ---------- nsIAuthPrompt2 prompts ---------- */ michael@0: michael@0: michael@0: michael@0: michael@0: /* michael@0: * promptAuth michael@0: * michael@0: * Implementation of nsIAuthPrompt2. michael@0: * michael@0: * nsIChannel aChannel michael@0: * int aLevel michael@0: * nsIAuthInformation aAuthInfo michael@0: */ michael@0: promptAuth : function (aChannel, aLevel, aAuthInfo) { michael@0: var selectedLogin = null; michael@0: var checkbox = { value : false }; michael@0: var checkboxLabel = null; michael@0: var epicfail = false; michael@0: var canAutologin = false; michael@0: michael@0: try { michael@0: michael@0: this.log("===== promptAuth called ====="); michael@0: michael@0: // If the user submits a login but it fails, we need to remove the michael@0: // notification bar that was displayed. Conveniently, the user will michael@0: // be prompted for authentication again, which brings us here. michael@0: this._removeLoginNotifications(); michael@0: michael@0: var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo); michael@0: michael@0: michael@0: // Looks for existing logins to prefill the prompt with. michael@0: var foundLogins = this._pwmgr.findLogins({}, michael@0: hostname, null, httpRealm); michael@0: this.log("found " + foundLogins.length + " matching logins."); michael@0: michael@0: // XXX Can't select from multiple accounts yet. (bug 227632) michael@0: if (foundLogins.length > 0) { michael@0: selectedLogin = foundLogins[0]; michael@0: this._SetAuthInfo(aAuthInfo, selectedLogin.username, michael@0: selectedLogin.password); michael@0: michael@0: // Allow automatic proxy login michael@0: if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY && michael@0: !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) && michael@0: Services.prefs.getBoolPref("signon.autologin.proxy") && michael@0: !this._inPrivateBrowsing) { michael@0: michael@0: this.log("Autologin enabled, skipping auth prompt."); michael@0: canAutologin = true; michael@0: } michael@0: michael@0: checkbox.value = true; michael@0: } michael@0: michael@0: var canRememberLogin = this._pwmgr.getLoginSavingEnabled(hostname); michael@0: if (this._inPrivateBrowsing) michael@0: canRememberLogin = false; michael@0: michael@0: // if checkboxLabel is null, the checkbox won't be shown at all. michael@0: var notifyBox = this._getNotifyBox(); michael@0: if (canRememberLogin && !notifyBox) michael@0: checkboxLabel = this._getLocalizedString("rememberPassword"); michael@0: } catch (e) { michael@0: // Ignore any errors and display the prompt anyway. michael@0: epicfail = true; michael@0: Components.utils.reportError("LoginManagerPrompter: " + michael@0: "Epic fail in promptAuth: " + e + "\n"); michael@0: } michael@0: michael@0: var ok = canAutologin || michael@0: this._promptService.promptAuth(this._window, michael@0: aChannel, aLevel, aAuthInfo, michael@0: checkboxLabel, checkbox); michael@0: michael@0: // If there's a notification box, use it to allow the user to michael@0: // determine if the login should be saved. If there isn't a michael@0: // notification box, only save the login if the user set the michael@0: // checkbox to do so. michael@0: var rememberLogin = notifyBox ? canRememberLogin : checkbox.value; michael@0: if (!ok || !rememberLogin || epicfail) michael@0: return ok; michael@0: michael@0: try { michael@0: var [username, password] = this._GetAuthInfo(aAuthInfo); michael@0: michael@0: if (!password) { michael@0: this.log("No password entered, so won't offer to save."); michael@0: return ok; michael@0: } michael@0: michael@0: // XXX We can't prompt with multiple logins yet (bug 227632), so michael@0: // the entered login might correspond to an existing login michael@0: // other than the one we originally selected. michael@0: selectedLogin = this._repickSelectedLogin(foundLogins, username); michael@0: michael@0: // If we didn't find an existing login, or if the username michael@0: // changed, save as a new login. michael@0: if (!selectedLogin) { michael@0: this.log("New login seen for " + username + michael@0: " @ " + hostname + " (" + httpRealm + ")"); michael@0: michael@0: var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. michael@0: createInstance(Ci.nsILoginInfo); michael@0: newLogin.init(hostname, null, httpRealm, michael@0: username, password, "", ""); michael@0: var notifyObj = this._getPopupNote() || notifyBox; michael@0: if (notifyObj) michael@0: this._showSaveLoginNotification(notifyObj, newLogin); michael@0: else michael@0: this._pwmgr.addLogin(newLogin); michael@0: michael@0: } else if (password != selectedLogin.password) { michael@0: michael@0: this.log("Updating password for " + username + michael@0: " @ " + hostname + " (" + httpRealm + ")"); michael@0: var notifyObj = this._getPopupNote() || notifyBox; michael@0: if (notifyObj) michael@0: this._showChangeLoginNotification(notifyObj, michael@0: selectedLogin, password); michael@0: else michael@0: this._updateLogin(selectedLogin, password); michael@0: michael@0: } else { michael@0: this.log("Login unchanged, no further action needed."); michael@0: this._updateLogin(selectedLogin); michael@0: } michael@0: } catch (e) { michael@0: Components.utils.reportError("LoginManagerPrompter: " + michael@0: "Fail2 in promptAuth: " + e + "\n"); michael@0: } michael@0: michael@0: return ok; michael@0: }, michael@0: michael@0: asyncPromptAuth : function (aChannel, aCallback, aContext, aLevel, aAuthInfo) { michael@0: var cancelable = null; michael@0: michael@0: try { michael@0: this.log("===== asyncPromptAuth called ====="); michael@0: michael@0: // If the user submits a login but it fails, we need to remove the michael@0: // notification bar that was displayed. Conveniently, the user will michael@0: // be prompted for authentication again, which brings us here. michael@0: this._removeLoginNotifications(); michael@0: michael@0: cancelable = this._newAsyncPromptConsumer(aCallback, aContext); michael@0: michael@0: var [hostname, httpRealm] = this._getAuthTarget(aChannel, aAuthInfo); michael@0: michael@0: var hashKey = aLevel + "|" + hostname + "|" + httpRealm; michael@0: this.log("Async prompt key = " + hashKey); michael@0: var asyncPrompt = this._factory._asyncPrompts[hashKey]; michael@0: if (asyncPrompt) { michael@0: this.log("Prompt bound to an existing one in the queue, callback = " + aCallback); michael@0: asyncPrompt.consumers.push(cancelable); michael@0: return cancelable; michael@0: } michael@0: michael@0: this.log("Adding new prompt to the queue, callback = " + aCallback); michael@0: asyncPrompt = { michael@0: consumers: [cancelable], michael@0: channel: aChannel, michael@0: authInfo: aAuthInfo, michael@0: level: aLevel, michael@0: inProgress : false, michael@0: prompter: this michael@0: } michael@0: michael@0: this._factory._asyncPrompts[hashKey] = asyncPrompt; michael@0: this._factory._doAsyncPrompt(); michael@0: } michael@0: catch (e) { michael@0: Components.utils.reportError("LoginManagerPrompter: " + michael@0: "asyncPromptAuth: " + e + "\nFalling back to promptAuth\n"); michael@0: // Fail the prompt operation to let the consumer fall back michael@0: // to synchronous promptAuth method michael@0: throw e; michael@0: } michael@0: michael@0: return cancelable; michael@0: }, michael@0: michael@0: michael@0: michael@0: michael@0: /* ---------- nsILoginManagerPrompter prompts ---------- */ michael@0: michael@0: michael@0: michael@0: michael@0: /* michael@0: * init michael@0: * michael@0: */ michael@0: init : function (aWindow, aFactory) { michael@0: this._window = aWindow; michael@0: this._factory = aFactory || null; michael@0: michael@0: var prefBranch = Services.prefs.getBranch("signon."); michael@0: this._debug = prefBranch.getBoolPref("debug"); michael@0: this.log("===== initialized ====="); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * promptToSavePassword michael@0: * michael@0: */ michael@0: promptToSavePassword : function (aLogin) { michael@0: var notifyObj = this._getPopupNote() || this._getNotifyBox(); michael@0: michael@0: if (notifyObj) michael@0: this._showSaveLoginNotification(notifyObj, aLogin); michael@0: else michael@0: this._showSaveLoginDialog(aLogin); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _showLoginNotification michael@0: * michael@0: * Displays a notification bar. michael@0: * michael@0: */ michael@0: _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) { michael@0: var oldBar = aNotifyBox.getNotificationWithValue(aName); michael@0: const priority = aNotifyBox.PRIORITY_INFO_MEDIUM; michael@0: michael@0: this.log("Adding new " + aName + " notification bar"); michael@0: var newBar = aNotifyBox.appendNotification( michael@0: aText, aName, michael@0: "chrome://mozapps/skin/passwordmgr/key.png", michael@0: priority, aButtons); michael@0: michael@0: // The page we're going to hasn't loaded yet, so we want to persist michael@0: // across the first location change. michael@0: newBar.persistence++; michael@0: michael@0: // Sites like Gmail perform a funky redirect dance before you end up michael@0: // at the post-authentication page. I don't see a good way to michael@0: // heuristically determine when to ignore such location changes, so michael@0: // we'll try ignoring location changes based on a time interval. michael@0: newBar.timeout = Date.now() + 20000; // 20 seconds michael@0: michael@0: if (oldBar) { michael@0: this.log("(...and removing old " + aName + " notification bar)"); michael@0: aNotifyBox.removeNotification(oldBar); michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _showSaveLoginNotification michael@0: * michael@0: * Displays a notification bar or a popup notification, to allow the user michael@0: * to save the specified login. This allows the user to see the results of michael@0: * their login, and only save a login which they know worked. michael@0: * michael@0: * @param aNotifyObj michael@0: * A notification box or a popup notification. michael@0: */ michael@0: _showSaveLoginNotification : function (aNotifyObj, aLogin) { michael@0: michael@0: // Ugh. We can't use the strings from the popup window, because they michael@0: // have the access key marked in the string (eg "Mo&zilla"), along michael@0: // with some weird rules for handling access keys that do not occur michael@0: // in the string, for L10N. See commonDialog.js's setLabelForNode(). michael@0: var neverButtonText = michael@0: this._getLocalizedString("notifyBarNeverRememberButtonText"); michael@0: var neverButtonAccessKey = michael@0: this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"); michael@0: var rememberButtonText = michael@0: this._getLocalizedString("notifyBarRememberPasswordButtonText"); michael@0: var rememberButtonAccessKey = michael@0: this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey"); michael@0: michael@0: var displayHost = this._getShortDisplayHost(aLogin.hostname); michael@0: var notificationText; michael@0: if (aLogin.username) { michael@0: var displayUser = this._sanitizeUsername(aLogin.username); michael@0: notificationText = this._getLocalizedString( michael@0: "rememberPasswordMsg", michael@0: [displayUser, displayHost]); michael@0: } else { michael@0: notificationText = this._getLocalizedString( michael@0: "rememberPasswordMsgNoUsername", michael@0: [displayHost]); michael@0: } michael@0: michael@0: // The callbacks in |buttons| have a closure to access the variables michael@0: // in scope here; set one to |this._pwmgr| so we can get back to pwmgr michael@0: // without a getService() call. michael@0: var pwmgr = this._pwmgr; michael@0: michael@0: // Notification is a PopupNotification michael@0: if (aNotifyObj == this._getPopupNote()) { michael@0: // "Remember" button michael@0: var mainAction = { michael@0: label: rememberButtonText, michael@0: accessKey: rememberButtonAccessKey, michael@0: callback: function(aNotifyObj, aButton) { michael@0: pwmgr.addLogin(aLogin); michael@0: browser.focus(); michael@0: } michael@0: }; michael@0: michael@0: var secondaryActions = [ michael@0: // "Never for this site" button michael@0: { michael@0: label: neverButtonText, michael@0: accessKey: neverButtonAccessKey, michael@0: callback: function(aNotifyObj, aButton) { michael@0: pwmgr.setLoginSavingEnabled(aLogin.hostname, false); michael@0: browser.focus(); michael@0: } michael@0: } michael@0: ]; michael@0: michael@0: var notifyWin = this._getNotifyWindow(); michael@0: var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; michael@0: var browser = chromeWin.gBrowser. michael@0: getBrowserForDocument(notifyWin.top.document); michael@0: michael@0: aNotifyObj.show(browser, "password-save", notificationText, michael@0: "password-notification-icon", mainAction, michael@0: secondaryActions, { timeout: Date.now() + 10000, michael@0: persistWhileVisible: true }); michael@0: } else { michael@0: var notNowButtonText = michael@0: this._getLocalizedString("notifyBarNotNowButtonText"); michael@0: var notNowButtonAccessKey = michael@0: this._getLocalizedString("notifyBarNotNowButtonAccessKey"); michael@0: var buttons = [ michael@0: // "Remember" button michael@0: { michael@0: label: rememberButtonText, michael@0: accessKey: rememberButtonAccessKey, michael@0: popup: null, michael@0: callback: function(aNotifyObj, aButton) { michael@0: pwmgr.addLogin(aLogin); michael@0: } michael@0: }, michael@0: michael@0: // "Never for this site" button michael@0: { michael@0: label: neverButtonText, michael@0: accessKey: neverButtonAccessKey, michael@0: popup: null, michael@0: callback: function(aNotifyObj, aButton) { michael@0: pwmgr.setLoginSavingEnabled(aLogin.hostname, false); michael@0: } michael@0: }, michael@0: michael@0: // "Not now" button michael@0: { michael@0: label: notNowButtonText, michael@0: accessKey: notNowButtonAccessKey, michael@0: popup: null, michael@0: callback: function() { /* NOP */ } michael@0: } michael@0: ]; michael@0: michael@0: this._showLoginNotification(aNotifyObj, "password-save", michael@0: notificationText, buttons); michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _removeLoginNotifications michael@0: * michael@0: */ michael@0: _removeLoginNotifications : function () { michael@0: var popupNote = this._getPopupNote(); michael@0: if (popupNote) michael@0: popupNote = popupNote.getNotification("password-save"); michael@0: if (popupNote) michael@0: popupNote.remove(); michael@0: michael@0: var notifyBox = this._getNotifyBox(); michael@0: if (notifyBox) { michael@0: var oldBar = notifyBox.getNotificationWithValue("password-save"); michael@0: if (oldBar) { michael@0: this.log("Removing save-password notification bar."); michael@0: notifyBox.removeNotification(oldBar); michael@0: } michael@0: michael@0: oldBar = notifyBox.getNotificationWithValue("password-change"); michael@0: if (oldBar) { michael@0: this.log("Removing change-password notification bar."); michael@0: notifyBox.removeNotification(oldBar); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _showSaveLoginDialog michael@0: * michael@0: * Called when we detect a new login in a form submission, michael@0: * asks the user what to do. michael@0: * michael@0: */ michael@0: _showSaveLoginDialog : function (aLogin) { michael@0: const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT + michael@0: (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + michael@0: (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) + michael@0: (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2); michael@0: michael@0: var displayHost = this._getShortDisplayHost(aLogin.hostname); michael@0: michael@0: var dialogText; michael@0: if (aLogin.username) { michael@0: var displayUser = this._sanitizeUsername(aLogin.username); michael@0: dialogText = this._getLocalizedString( michael@0: "rememberPasswordMsg", michael@0: [displayUser, displayHost]); michael@0: } else { michael@0: dialogText = this._getLocalizedString( michael@0: "rememberPasswordMsgNoUsername", michael@0: [displayHost]); michael@0: michael@0: } michael@0: var dialogTitle = this._getLocalizedString( michael@0: "savePasswordTitle"); michael@0: var neverButtonText = this._getLocalizedString( michael@0: "neverForSiteButtonText"); michael@0: var rememberButtonText = this._getLocalizedString( michael@0: "rememberButtonText"); michael@0: var notNowButtonText = this._getLocalizedString( michael@0: "notNowButtonText"); michael@0: michael@0: this.log("Prompting user to save/ignore login"); michael@0: var userChoice = this._promptService.confirmEx(this._window, michael@0: dialogTitle, dialogText, michael@0: buttonFlags, rememberButtonText, michael@0: notNowButtonText, neverButtonText, michael@0: null, {}); michael@0: // Returns: michael@0: // 0 - Save the login michael@0: // 1 - Ignore the login this time michael@0: // 2 - Never save logins for this site michael@0: if (userChoice == 2) { michael@0: this.log("Disabling " + aLogin.hostname + " logins by request."); michael@0: this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false); michael@0: } else if (userChoice == 0) { michael@0: this.log("Saving login for " + aLogin.hostname); michael@0: this._pwmgr.addLogin(aLogin); michael@0: } else { michael@0: // userChoice == 1 --> just ignore the login. michael@0: this.log("Ignoring login."); michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * promptToChangePassword michael@0: * michael@0: * Called when we think we detect a password change for an existing michael@0: * login, when the form being submitted contains multiple password michael@0: * fields. michael@0: * michael@0: */ michael@0: promptToChangePassword : function (aOldLogin, aNewLogin) { michael@0: var notifyObj = this._getPopupNote() || this._getNotifyBox(); michael@0: michael@0: if (notifyObj) michael@0: this._showChangeLoginNotification(notifyObj, aOldLogin, michael@0: aNewLogin.password); michael@0: else michael@0: this._showChangeLoginDialog(aOldLogin, aNewLogin.password); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _showChangeLoginNotification michael@0: * michael@0: * Shows the Change Password notification bar or popup notification. michael@0: * michael@0: * @param aNotifyObj michael@0: * A notification box or a popup notification. michael@0: */ michael@0: _showChangeLoginNotification : function (aNotifyObj, aOldLogin, aNewPassword) { michael@0: var notificationText; michael@0: if (aOldLogin.username) { michael@0: var displayUser = this._sanitizeUsername(aOldLogin.username); michael@0: notificationText = this._getLocalizedString( michael@0: "updatePasswordMsg", michael@0: [displayUser]); michael@0: } else { michael@0: notificationText = this._getLocalizedString( michael@0: "updatePasswordMsgNoUser"); michael@0: } michael@0: michael@0: var changeButtonText = michael@0: this._getLocalizedString("notifyBarUpdateButtonText"); michael@0: var changeButtonAccessKey = michael@0: this._getLocalizedString("notifyBarUpdateButtonAccessKey"); michael@0: michael@0: // The callbacks in |buttons| have a closure to access the variables michael@0: // in scope here; set one to |this._pwmgr| so we can get back to pwmgr michael@0: // without a getService() call. michael@0: var self = this; michael@0: michael@0: // Notification is a PopupNotification michael@0: if (aNotifyObj == this._getPopupNote()) { michael@0: // "Yes" button michael@0: var mainAction = { michael@0: label: changeButtonText, michael@0: accessKey: changeButtonAccessKey, michael@0: popup: null, michael@0: callback: function(aNotifyObj, aButton) { michael@0: self._updateLogin(aOldLogin, aNewPassword); michael@0: } michael@0: }; michael@0: michael@0: var notifyWin = this._getNotifyWindow(); michael@0: var chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; michael@0: var browser = chromeWin.gBrowser. michael@0: getBrowserForDocument(notifyWin.top.document); michael@0: michael@0: aNotifyObj.show(browser, "password-change", notificationText, michael@0: "password-notification-icon", mainAction, michael@0: null, { timeout: Date.now() + 10000, michael@0: persistWhileVisible: true }); michael@0: } else { michael@0: var dontChangeButtonText = michael@0: this._getLocalizedString("notifyBarDontChangeButtonText"); michael@0: var dontChangeButtonAccessKey = michael@0: this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); michael@0: var buttons = [ michael@0: // "Yes" button michael@0: { michael@0: label: changeButtonText, michael@0: accessKey: changeButtonAccessKey, michael@0: popup: null, michael@0: callback: function(aNotifyObj, aButton) { michael@0: self._updateLogin(aOldLogin, aNewPassword); michael@0: } michael@0: }, michael@0: michael@0: // "No" button michael@0: { michael@0: label: dontChangeButtonText, michael@0: accessKey: dontChangeButtonAccessKey, michael@0: popup: null, michael@0: callback: function(aNotifyObj, aButton) { michael@0: // do nothing michael@0: } michael@0: } michael@0: ]; michael@0: michael@0: this._showLoginNotification(aNotifyObj, "password-change", michael@0: notificationText, buttons); michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _showChangeLoginDialog michael@0: * michael@0: * Shows the Change Password dialog. michael@0: * michael@0: */ michael@0: _showChangeLoginDialog : function (aOldLogin, aNewPassword) { michael@0: const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; michael@0: michael@0: var dialogText; michael@0: if (aOldLogin.username) michael@0: dialogText = this._getLocalizedString( michael@0: "updatePasswordMsg", michael@0: [aOldLogin.username]); michael@0: else michael@0: dialogText = this._getLocalizedString( michael@0: "updatePasswordMsgNoUser"); michael@0: michael@0: var dialogTitle = this._getLocalizedString( michael@0: "passwordChangeTitle"); michael@0: michael@0: // returns 0 for yes, 1 for no. michael@0: var ok = !this._promptService.confirmEx(this._window, michael@0: dialogTitle, dialogText, buttonFlags, michael@0: null, null, null, michael@0: null, {}); michael@0: if (ok) { michael@0: this.log("Updating password for user " + aOldLogin.username); michael@0: this._updateLogin(aOldLogin, aNewPassword); michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * promptToChangePasswordWithUsernames michael@0: * michael@0: * Called when we detect a password change in a form submission, but we michael@0: * don't know which existing login (username) it's for. Asks the user michael@0: * to select a username and confirm the password change. michael@0: * michael@0: * Note: The caller doesn't know the username for aNewLogin, so this michael@0: * function fills in .username and .usernameField with the values michael@0: * from the login selected by the user. michael@0: * michael@0: * Note; XPCOM stupidity: |count| is just |logins.length|. michael@0: */ michael@0: promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) { michael@0: const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS; michael@0: michael@0: var usernames = logins.map(function (l) l.username); michael@0: var dialogText = this._getLocalizedString("userSelectText"); michael@0: var dialogTitle = this._getLocalizedString("passwordChangeTitle"); michael@0: var selectedIndex = { value: null }; michael@0: michael@0: // If user selects ok, outparam.value is set to the index michael@0: // of the selected username. michael@0: var ok = this._promptService.select(this._window, michael@0: dialogTitle, dialogText, michael@0: usernames.length, usernames, michael@0: selectedIndex); michael@0: if (ok) { michael@0: // Now that we know which login to use, modify its password. michael@0: var selectedLogin = logins[selectedIndex.value]; michael@0: this.log("Updating password for user " + selectedLogin.username); michael@0: this._updateLogin(selectedLogin, aNewLogin.password); michael@0: } michael@0: }, michael@0: michael@0: michael@0: michael@0: michael@0: /* ---------- Internal Methods ---------- */ michael@0: michael@0: michael@0: michael@0: michael@0: /* michael@0: * _updateLogin michael@0: */ michael@0: _updateLogin : function (login, newPassword) { michael@0: var now = Date.now(); michael@0: var propBag = Cc["@mozilla.org/hash-property-bag;1"]. michael@0: createInstance(Ci.nsIWritablePropertyBag); michael@0: if (newPassword) { michael@0: propBag.setProperty("password", newPassword); michael@0: // Explicitly set the password change time here (even though it would michael@0: // be changed automatically), to ensure that it's exactly the same michael@0: // value as timeLastUsed. michael@0: propBag.setProperty("timePasswordChanged", now); michael@0: } michael@0: propBag.setProperty("timeLastUsed", now); michael@0: propBag.setProperty("timesUsedIncrement", 1); michael@0: this._pwmgr.modifyLogin(login, propBag); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getChromeWindow michael@0: * michael@0: * Given a content DOM window, returns the chrome window it's in. michael@0: */ michael@0: _getChromeWindow: function (aWindow) { michael@0: var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell) michael@0: .chromeEventHandler.ownerDocument.defaultView; michael@0: return chromeWin; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getNotifyWindow michael@0: */ michael@0: _getNotifyWindow: function () { michael@0: michael@0: try { michael@0: // Get topmost window, in case we're in a frame. michael@0: var notifyWin = this._window.top; michael@0: michael@0: // Some sites pop up a temporary login window, when disappears michael@0: // upon submission of credentials. We want to put the notification michael@0: // bar in the opener window if this seems to be happening. michael@0: if (notifyWin.opener) { michael@0: var chromeDoc = this._getChromeWindow(notifyWin). michael@0: document.documentElement; michael@0: var webnav = notifyWin. michael@0: QueryInterface(Ci.nsIInterfaceRequestor). michael@0: getInterface(Ci.nsIWebNavigation); michael@0: michael@0: // Check to see if the current window was opened with chrome michael@0: // disabled, and if so use the opener window. But if the window michael@0: // has been used to visit other pages (ie, has a history), michael@0: // assume it'll stick around and *don't* use the opener. michael@0: if (chromeDoc.getAttribute("chromehidden") && michael@0: webnav.sessionHistory.count == 1) { michael@0: this.log("Using opener window for notification bar."); michael@0: notifyWin = notifyWin.opener; michael@0: } michael@0: } michael@0: michael@0: return notifyWin; michael@0: michael@0: } catch (e) { michael@0: // If any errors happen, just assume no notification box. michael@0: this.log("Unable to get notify window"); michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getPopupNote michael@0: * michael@0: * Returns the popup notification to this prompter, michael@0: * or null if there isn't one available. michael@0: */ michael@0: _getPopupNote : function () { michael@0: let popupNote = null; michael@0: michael@0: try { michael@0: let notifyWin = this._getNotifyWindow(); michael@0: michael@0: // Get the chrome window for the content window we're using. michael@0: // .wrappedJSObject needed here -- see bug 422974 comment 5. michael@0: let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; michael@0: michael@0: popupNote = chromeWin.PopupNotifications; michael@0: } catch (e) { michael@0: this.log("Popup notifications not available on window"); michael@0: } michael@0: michael@0: return popupNote; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getNotifyBox michael@0: * michael@0: * Returns the notification box to this prompter, or null if there isn't michael@0: * a notification box available. michael@0: */ michael@0: _getNotifyBox : function () { michael@0: let notifyBox = null; michael@0: michael@0: try { michael@0: let notifyWin = this._getNotifyWindow(); michael@0: michael@0: // Get the chrome window for the content window we're using. michael@0: // .wrappedJSObject needed here -- see bug 422974 comment 5. michael@0: let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; michael@0: michael@0: notifyBox = chromeWin.getNotificationBox(notifyWin); michael@0: } catch (e) { michael@0: this.log("Notification bars not available on window"); michael@0: } michael@0: michael@0: return notifyBox; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _repickSelectedLogin michael@0: * michael@0: * The user might enter a login that isn't the one we prefilled, but michael@0: * is the same as some other existing login. So, pick a login with a michael@0: * matching username, or return null. michael@0: */ michael@0: _repickSelectedLogin : function (foundLogins, username) { michael@0: for (var i = 0; i < foundLogins.length; i++) michael@0: if (foundLogins[i].username == username) michael@0: return foundLogins[i]; michael@0: return null; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getLocalizedString michael@0: * michael@0: * Can be called as: michael@0: * _getLocalizedString("key1"); michael@0: * _getLocalizedString("key2", ["arg1"]); michael@0: * _getLocalizedString("key3", ["arg1", "arg2"]); michael@0: * (etc) michael@0: * michael@0: * Returns the localized string for the specified key, michael@0: * formatted if required. michael@0: * michael@0: */ michael@0: _getLocalizedString : function (key, formatArgs) { michael@0: if (formatArgs) michael@0: return this._strBundle.formatStringFromName( michael@0: key, formatArgs, formatArgs.length); michael@0: else michael@0: return this._strBundle.GetStringFromName(key); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _sanitizeUsername michael@0: * michael@0: * Sanitizes the specified username, by stripping quotes and truncating if michael@0: * it's too long. This helps prevent an evil site from messing with the michael@0: * "save password?" prompt too much. michael@0: */ michael@0: _sanitizeUsername : function (username) { michael@0: if (username.length > 30) { michael@0: username = username.substring(0, 30); michael@0: username += this._ellipsis; michael@0: } michael@0: return username.replace(/['"]/g, ""); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getFormattedHostname michael@0: * michael@0: * The aURI parameter may either be a string uri, or an nsIURI instance. michael@0: * michael@0: * Returns the hostname to use in a nsILoginInfo object (for example, michael@0: * "http://example.com"). michael@0: */ michael@0: _getFormattedHostname : function (aURI) { michael@0: var uri; michael@0: if (aURI instanceof Ci.nsIURI) { michael@0: uri = aURI; michael@0: } else { michael@0: uri = Services.io.newURI(aURI, null, null); michael@0: } michael@0: var scheme = uri.scheme; michael@0: michael@0: var hostname = scheme + "://" + uri.host; michael@0: michael@0: // If the URI explicitly specified a port, only include it when michael@0: // it's not the default. (We never want "http://foo.com:80") michael@0: port = uri.port; michael@0: if (port != -1) { michael@0: var handler = Services.io.getProtocolHandler(scheme); michael@0: if (port != handler.defaultPort) michael@0: hostname += ":" + port; michael@0: } michael@0: michael@0: return hostname; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getShortDisplayHost michael@0: * michael@0: * Converts a login's hostname field (a URL) to a short string for michael@0: * prompting purposes. Eg, "http://foo.com" --> "foo.com", or michael@0: * "ftp://www.site.co.uk" --> "site.co.uk". michael@0: */ michael@0: _getShortDisplayHost: function (aURIString) { michael@0: var displayHost; michael@0: michael@0: var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. michael@0: getService(Ci.nsIEffectiveTLDService); michael@0: var idnService = Cc["@mozilla.org/network/idn-service;1"]. michael@0: getService(Ci.nsIIDNService); michael@0: try { michael@0: var uri = Services.io.newURI(aURIString, null, null); michael@0: var baseDomain = eTLDService.getBaseDomain(uri); michael@0: displayHost = idnService.convertToDisplayIDN(baseDomain, {}); michael@0: } catch (e) { michael@0: this.log("_getShortDisplayHost couldn't process " + aURIString); michael@0: } michael@0: michael@0: if (!displayHost) michael@0: displayHost = aURIString; michael@0: michael@0: return displayHost; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getAuthTarget michael@0: * michael@0: * Returns the hostname and realm for which authentication is being michael@0: * requested, in the format expected to be used with nsILoginInfo. michael@0: */ michael@0: _getAuthTarget : function (aChannel, aAuthInfo) { michael@0: var hostname, realm; michael@0: michael@0: // If our proxy is demanding authentication, don't use the michael@0: // channel's actual destination. michael@0: if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { michael@0: this.log("getAuthTarget is for proxy auth"); michael@0: if (!(aChannel instanceof Ci.nsIProxiedChannel)) michael@0: throw "proxy auth needs nsIProxiedChannel"; michael@0: michael@0: var info = aChannel.proxyInfo; michael@0: if (!info) michael@0: throw "proxy auth needs nsIProxyInfo"; michael@0: michael@0: // Proxies don't have a scheme, but we'll use "moz-proxy://" michael@0: // so that it's more obvious what the login is for. michael@0: var idnService = Cc["@mozilla.org/network/idn-service;1"]. michael@0: getService(Ci.nsIIDNService); michael@0: hostname = "moz-proxy://" + michael@0: idnService.convertUTF8toACE(info.host) + michael@0: ":" + info.port; michael@0: realm = aAuthInfo.realm; michael@0: if (!realm) michael@0: realm = hostname; michael@0: michael@0: return [hostname, realm]; michael@0: } michael@0: michael@0: hostname = this._getFormattedHostname(aChannel.URI); michael@0: michael@0: // If a HTTP WWW-Authenticate header specified a realm, that value michael@0: // will be available here. If it wasn't set or wasn't HTTP, we'll use michael@0: // the formatted hostname instead. michael@0: realm = aAuthInfo.realm; michael@0: if (!realm) michael@0: realm = hostname; michael@0: michael@0: return [hostname, realm]; michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Returns [username, password] as extracted from aAuthInfo (which michael@0: * holds this info after having prompted the user). michael@0: * michael@0: * If the authentication was for a Windows domain, we'll prepend the michael@0: * return username with the domain. (eg, "domain\user") michael@0: */ michael@0: _GetAuthInfo : function (aAuthInfo) { michael@0: var username, password; michael@0: michael@0: var flags = aAuthInfo.flags; michael@0: if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain) michael@0: username = aAuthInfo.domain + "\\" + aAuthInfo.username; michael@0: else michael@0: username = aAuthInfo.username; michael@0: michael@0: password = aAuthInfo.password; michael@0: michael@0: return [username, password]; michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Given a username (possibly in DOMAIN\user form) and password, parses the michael@0: * domain out of the username if necessary and sets domain, username and michael@0: * password on the auth information object. michael@0: */ michael@0: _SetAuthInfo : function (aAuthInfo, username, password) { michael@0: var flags = aAuthInfo.flags; michael@0: if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) { michael@0: // Domain is separated from username by a backslash michael@0: var idx = username.indexOf("\\"); michael@0: if (idx == -1) { michael@0: aAuthInfo.username = username; michael@0: } else { michael@0: aAuthInfo.domain = username.substring(0, idx); michael@0: aAuthInfo.username = username.substring(idx+1); michael@0: } michael@0: } else { michael@0: aAuthInfo.username = username; michael@0: } michael@0: aAuthInfo.password = password; michael@0: }, michael@0: michael@0: _newAsyncPromptConsumer : function(aCallback, aContext) { michael@0: return { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]), michael@0: callback: aCallback, michael@0: context: aContext, michael@0: cancel: function() { michael@0: this.callback.onAuthCancelled(this.context, false); michael@0: this.callback = null; michael@0: this.context = null; michael@0: } michael@0: } michael@0: } michael@0: michael@0: }; // end of LoginManagerPrompter implementation michael@0: michael@0: michael@0: var component = [LoginManagerPromptFactory, LoginManagerPrompter]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);