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://browser/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: __brandBundle : null, // String bundle for L10N michael@0: get _brandBundle() { michael@0: if (!this.__brandBundle) { michael@0: var bunService = Cc["@mozilla.org/intl/stringbundle;1"]. michael@0: getService(Ci.nsIStringBundleService); michael@0: this.__brandBundle = bunService.createBundle( michael@0: "chrome://branding/locale/brand.properties"); michael@0: if (!this.__brandBundle) michael@0: throw "Branding string bundle not present!"; michael@0: } michael@0: michael@0: return this.__brandBundle; 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: var notifyBox = this._getNotifyBox(); michael@0: if (notifyBox) michael@0: this._showSaveLoginNotification(notifyBox, 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://browser/skin/images/infobar-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 (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 (aNotifyBox, aLogin) { 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("notifyBarNotForThisSiteButtonText"); michael@0: var neverButtonAccessKey = michael@0: this._getLocalizedString("notifyBarNotForThisSiteButtonAccessKey"); michael@0: var rememberButtonText = michael@0: this._getLocalizedString("notifyBarRememberPasswordButtonText"); michael@0: var rememberButtonAccessKey = michael@0: this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey"); michael@0: michael@0: var brandShortName = michael@0: this._brandBundle.GetStringFromName("brandShortName"); 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: "saveLoginText", michael@0: [brandShortName, displayUser, displayHost]); michael@0: } else { michael@0: notificationText = this._getLocalizedString( michael@0: "saveLoginTextNoUsername", michael@0: [brandShortName, 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: 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(aNotificationBar, 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(aNotificationBar, aButton) { michael@0: pwmgr.setLoginSavingEnabled(aLogin.hostname, false); michael@0: } michael@0: } michael@0: ]; michael@0: michael@0: this._showLoginNotification(aNotifyBox, "password-save", michael@0: notificationText, buttons); 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 notifyBox = this._getNotifyBox(); michael@0: if (notifyBox) michael@0: this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin.password); michael@0: }, michael@0: michael@0: /* michael@0: * _showChangeLoginNotification michael@0: * michael@0: * Shows the Change Password notification bar. michael@0: * michael@0: */ michael@0: _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewPassword) { michael@0: var notificationText; michael@0: if (aOldLogin.username) michael@0: notificationText = this._getLocalizedString( michael@0: "passwordChangeText", michael@0: [aOldLogin.username]); michael@0: else michael@0: notificationText = this._getLocalizedString( michael@0: "passwordChangeTextNoUser"); michael@0: michael@0: var changeButtonText = michael@0: this._getLocalizedString("notifyBarChangeButtonText"); michael@0: var changeButtonAccessKey = michael@0: this._getLocalizedString("notifyBarChangeButtonAccessKey"); michael@0: var dontChangeButtonText = michael@0: this._getLocalizedString("notifyBarDontChangeButtonText2"); michael@0: var dontChangeButtonAccessKey = michael@0: this._getLocalizedString("notifyBarDontChangeButtonAccessKey"); 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: // "Yes" button michael@0: { michael@0: label: changeButtonText, michael@0: accessKey: changeButtonAccessKey, michael@0: popup: null, michael@0: callback: function(aNotificationBar, 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(aNotificationBar, aButton) { michael@0: // do nothing michael@0: } michael@0: } michael@0: ]; michael@0: michael@0: this._showLoginNotification(aNotifyBox, "password-change", michael@0: notificationText, buttons); 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: /* ---------- Internal Methods ---------- */ 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: * _getNotifyWindow michael@0: */ michael@0: _getNotifyWindow: function () { 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: * _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: * _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: let windowID = notifyWin.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; 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: let browser = chromeWin.Browser.getBrowserForWindowId(windowID); michael@0: michael@0: notifyBox = chromeWin.getNotificationBox(browser); michael@0: } catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: michael@0: return notifyBox; 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: * _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: