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: michael@0: /* ==================== LoginManagerPrompter ==================== */ 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: * 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: michael@0: LoginManagerPrompter.prototype = { michael@0: michael@0: classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"), michael@0: QueryInterface : XPCOMUtils.generateQI([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: __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: /* 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: /* ---------- 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: this._showSaveLoginNotification(aLogin); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _showLoginNotification michael@0: * michael@0: * Displays a notification doorhanger. michael@0: * michael@0: */ michael@0: _showLoginNotification : function (aName, aText, aButtons) { michael@0: this.log("Adding new " + aName + " notification bar"); michael@0: let notifyWin = this._window.top; michael@0: let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; michael@0: let browser = chromeWin.BrowserApp.getBrowserForWindow(notifyWin); michael@0: let tabID = chromeWin.BrowserApp.getTabForBrowser(browser).id; 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: 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: michael@0: let options = { michael@0: persistWhileVisible: true, michael@0: timeout: Date.now() + 10000 michael@0: } michael@0: michael@0: var nativeWindow = this._getNativeWindow(); michael@0: if (nativeWindow) michael@0: nativeWindow.doorhanger.show(aText, aName, aButtons, tabID, options); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _showSaveLoginNotification michael@0: * michael@0: * Displays a notification doorhanger (rather than a popup), to allow the user to michael@0: * 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: */ michael@0: _showSaveLoginNotification : function (aLogin) { 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("savePassword", [displayUser, displayHost]); michael@0: } else { michael@0: notificationText = this._getLocalizedString("savePasswordNoUser", [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: var buttons = [ michael@0: { michael@0: label: this._getLocalizedString("saveButton"), michael@0: callback: function() { michael@0: pwmgr.addLogin(aLogin); michael@0: } michael@0: }, michael@0: { michael@0: label: this._getLocalizedString("dontSaveButton"), michael@0: callback: function() { michael@0: // Don't set a permanent exception michael@0: } michael@0: } michael@0: ]; michael@0: michael@0: this._showLoginNotification("password-save", notificationText, buttons); 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: this._showChangeLoginNotification(aOldLogin, aNewLogin.password); michael@0: }, michael@0: michael@0: /* michael@0: * _showChangeLoginNotification michael@0: * michael@0: * Shows the Change Password notification doorhanger. michael@0: * michael@0: */ michael@0: _showChangeLoginNotification : function (aOldLogin, aNewPassword) { michael@0: var notificationText; michael@0: if (aOldLogin.username) { michael@0: let displayUser = this._sanitizeUsername(aOldLogin.username); michael@0: notificationText = this._getLocalizedString("updatePassword", [displayUser]); michael@0: } else { michael@0: notificationText = this._getLocalizedString("updatePasswordNoUser"); 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 self = this; michael@0: michael@0: var buttons = [ michael@0: { michael@0: label: this._getLocalizedString("updateButton"), michael@0: callback: function() { michael@0: self._updateLogin(aOldLogin, aNewPassword); michael@0: } michael@0: }, michael@0: { michael@0: label: this._getLocalizedString("dontUpdateButton"), michael@0: callback: function() { michael@0: // do nothing michael@0: } michael@0: } michael@0: ]; michael@0: michael@0: this._showLoginNotification("password-change", notificationText, buttons); 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(null, 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: * _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: * _getNativeWindow michael@0: * michael@0: * Returns the NativeWindow to this prompter, or null if there isn't michael@0: * a NativeWindow available (w/ error sent to logcat). michael@0: */ michael@0: _getNativeWindow : function () { michael@0: let nativeWindow = null; michael@0: try { michael@0: let notifyWin = this._window.top; michael@0: let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject; michael@0: if (chromeWin.NativeWindow) { michael@0: nativeWindow = chromeWin.NativeWindow; michael@0: } else { michael@0: Cu.reportError("NativeWindow not available on window"); michael@0: } michael@0: michael@0: } catch (e) { michael@0: // If any errors happen, just assume no native window helper. michael@0: Cu.reportError("No NativeWindow available: " + e); michael@0: } michael@0: return nativeWindow; 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: }; // end of LoginManagerPrompter implementation michael@0: michael@0: michael@0: var component = [LoginManagerPrompter]; michael@0: this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component); michael@0: