From f37ce3195bdd74cc7ceab20624a741a4469ecc7f Mon Sep 17 00:00:00 2001 From: Myk Melez Date: Sun, 3 May 2009 02:23:08 -0700 Subject: [PATCH] basic implementation of support for saved logins via login manager --- services/sync/modules/LoginManager.js | 1296 +++++++++++++++++++++++++ 1 file changed, 1296 insertions(+) create mode 100644 services/sync/modules/LoginManager.js diff --git a/services/sync/modules/LoginManager.js b/services/sync/modules/LoginManager.js new file mode 100644 index 000000000000..21be263dc045 --- /dev/null +++ b/services/sync/modules/LoginManager.js @@ -0,0 +1,1296 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is mozilla.org code. + * + * The Initial Developer of the Original Code is Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2007 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Justin Dolske (original author) + * Ehsan Akhgari + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +const EXPORTED_SYMBOLS = ["WeaveLoginManager"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +let WeaveLoginManager = { + + classDescription: "LoginManager", + contractID: "@mozilla.org/login-manager;1", + classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"), + QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager, + Ci.nsISupportsWeakReference]), + + + /* ---------- private memebers ---------- */ + + + __logService : null, // Console logging service, used for debugging. + get _logService() { + if (!this.__logService) + this.__logService = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + return this.__logService; + }, + + + __ioService: null, // IO service for string -> nsIURI conversion + get _ioService() { + if (!this.__ioService) + this.__ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + return this.__ioService; + }, + + + __formFillService : null, // FormFillController, for username autocompleting + get _formFillService() { + if (!this.__formFillService) + this.__formFillService = + Cc["@mozilla.org/satchel/form-fill-controller;1"]. + getService(Ci.nsIFormFillController); + return this.__formFillService; + }, + + + __observerService : null, // Observer Service, for notifications + get _observerService() { + if (!this.__observerService) + this.__observerService = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + return this.__observerService; + }, + + + __storage : null, // Storage component which contains the saved logins + get _storage() { + if (!this.__storage) { + + var contractID = "@mozilla.org/login-manager/storage/mozStorage;1"; + try { + var catMan = Cc["@mozilla.org/categorymanager;1"]. + getService(Ci.nsICategoryManager); + contractID = catMan.getCategoryEntry("login-manager-storage", + "nsILoginManagerStorage"); + this.log("Found alternate nsILoginManagerStorage with " + + "contract ID: " + contractID); + } catch (e) { + this.log("No alternate nsILoginManagerStorage registered"); + } + + this.__storage = Cc[contractID]. + createInstance(Ci.nsILoginManagerStorage); + try { + this.__storage.init(); + } catch (e) { + this.log("Initialization of storage component failed: " + e); + this.__storage = null; + } + } + + return this.__storage; + }, + + + // Private Browsing Service + // If the service is not available, null will be returned. + __privateBrowsingService : undefined, + get _privateBrowsingService() { + if (this.__privateBrowsingService == undefined) { + if ("@mozilla.org/privatebrowsing;1" in Cc) + this.__privateBrowsingService = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); + else + this.__privateBrowsingService = null; + } + return this.__privateBrowsingService; + }, + + + // Whether we are in private browsing mode + get _inPrivateBrowsing() { + var pbSvc = this._privateBrowsingService; + if (pbSvc) + return pbSvc.privateBrowsingEnabled; + else + return false; + }, + + _prefBranch : null, // Preferences service + _nsLoginInfo : null, // Constructor for nsILoginInfo implementation + + _remember : true, // mirrors signon.rememberSignons preference + _debug : false, // mirrors signon.debug + + + /* + * init + * + * Initialize the Login Manager. Automatically called when service + * is created. + * + * Note: Service created in /browser/base/content/browser.js, + * delayedStartup() + */ + init : function () { + + // Cache references to current |this| in utility objects + this._webProgressListener._domEventListener = this._domEventListener; + this._webProgressListener._pwmgr = this; + this._domEventListener._pwmgr = this; + this._observer._pwmgr = this; + + // Preferences. Add observer so we get notified of changes. + this._prefBranch = Cc["@mozilla.org/preferences-service;1"]. + getService(Ci.nsIPrefService).getBranch("signon."); + this._prefBranch.QueryInterface(Ci.nsIPrefBranch2); + this._prefBranch.addObserver("", this._observer, false); + + // Get current preference values. + this._debug = this._prefBranch.getBoolPref("debug"); + + this._remember = this._prefBranch.getBoolPref("rememberSignons"); + + + // Get constructor for nsILoginInfo + this._nsLoginInfo = new Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo); + + + // Form submit observer checks forms for new logins and pw changes. + this._observerService.addObserver(this._observer, "earlyformsubmit", false); + this._observerService.addObserver(this._observer, "xpcom-shutdown", false); + + // WebProgressListener for getting notification of new doc loads. + var progress = Cc["@mozilla.org/docloaderservice;1"]. + getService(Ci.nsIWebProgress); + progress.addProgressListener(this._webProgressListener, + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); + + + }, + + + /* + * log + * + * Internal function for logging debug messages to the Error Console window + */ + log : function (message) { + if (!this._debug) + return; + dump("Login Manager: " + message + "\n"); + this._logService.logStringMessage("Login Manager: " + message); + }, + + + /* ---------- Utility objects ---------- */ + + + /* + * _observer object + * + * Internal utility object, implements the nsIObserver interface. + * Used to receive notification for: form submission, preference changes. + */ + _observer : { + _pwmgr : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsIFormSubmitObserver, + Ci.nsISupportsWeakReference]), + + + // nsFormSubmitObserver + notify : function (formElement, aWindow, actionURI) { + this._pwmgr.log("observer notified for form submission."); + + // We're invoked before the content's |onsubmit| handlers, so we + // can grab form data before it might be modified (see bug 257781). + + try { + this._pwmgr._onFormSubmit(formElement); + } catch (e) { + this._pwmgr.log("Caught error in onFormSubmit: " + e); + } + + return true; // Always return true, or form submit will be canceled. + }, + + // nsObserver + observe : function (subject, topic, data) { + + if (topic == "nsPref:changed") { + var prefName = data; + this._pwmgr.log("got change to " + prefName + " preference"); + + if (prefName == "debug") { + this._pwmgr._debug = + this._pwmgr._prefBranch.getBoolPref("debug"); + } else if (prefName == "rememberSignons") { + this._pwmgr._remember = + this._pwmgr._prefBranch.getBoolPref("rememberSignons"); + } else { + this._pwmgr.log("Oops! Pref not handled, change ignored."); + } + } else if (topic == "xpcom-shutdown") { + for (let i in this._pwmgr) { + try { + this._pwmgr[i] = null; + } catch(ex) {} + } + this._pwmgr = null; + } else { + this._pwmgr.log("Oops! Unexpected notification: " + topic); + } + } + }, + + + /* + * _webProgressListener object + * + * Internal utility object, implements nsIWebProgressListener interface. + * This is attached to the document loader service, so we get + * notifications about all page loads. + */ + _webProgressListener : { + _pwmgr : null, + _domEventListener : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + + onStateChange : function (aWebProgress, aRequest, + aStateFlags, aStatus) { + + // STATE_START is too early, doc is still the old page. + if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING)) + return; + + if (!this._pwmgr._remember) + return; + + var domWin = aWebProgress.DOMWindow; + var domDoc = domWin.document; + + // Only process things which might have HTML forms. + if (!(domDoc instanceof Ci.nsIDOMHTMLDocument)) + return; + + this._pwmgr.log("onStateChange accepted: req = " + + (aRequest ? aRequest.name : "(null)") + + ", flags = 0x" + aStateFlags.toString(16)); + + // Fastback doesn't fire DOMContentLoaded, so process forms now. + if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) { + this._pwmgr.log("onStateChange: restoring document"); + return this._pwmgr._fillDocument(domDoc); + } + + // Add event listener to process page when DOM is complete. + domDoc.addEventListener("DOMContentLoaded", + this._domEventListener, false); + return; + }, + + // stubs for the nsIWebProgressListener interfaces which we don't use. + onProgressChange : function() { throw "Unexpected onProgressChange"; }, + onLocationChange : function() { throw "Unexpected onLocationChange"; }, + onStatusChange : function() { throw "Unexpected onStatusChange"; }, + onSecurityChange : function() { throw "Unexpected onSecurityChange"; } + }, + + + /* + * _domEventListener object + * + * Internal utility object, implements nsIDOMEventListener + * Used to catch certain DOM events needed to properly implement form fill. + */ + _domEventListener : { + _pwmgr : null, + + QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener, + Ci.nsISupportsWeakReference]), + + + handleEvent : function (event) { + this._pwmgr.log("domEventListener: got event " + event.type); + + switch (event.type) { + case "DOMContentLoaded": + this._pwmgr._fillDocument(event.target); + return; + + case "DOMAutoComplete": + case "blur": + var acInputField = event.target; + var acForm = acInputField.form; + // Make sure the username field fillForm will use is the + // same field as the autocomplete was activated on. If + // not, the DOM has been altered and we'll just give up. + var [usernameField, passwordField, ignored] = + this._pwmgr._getFormFields(acForm, false); + if (usernameField == acInputField && passwordField) { + // Clobber any existing password. + passwordField.value = ""; + this._pwmgr._fillForm(acForm, true, true, null); + } else { + this._pwmgr.log("Oops, form changed before AC invoked"); + } + return; + + default: + this._pwmgr.log("Oops! This event unexpected."); + return; + } + } + }, + + + + + /* ---------- Primary Public interfaces ---------- */ + + + + + /* + * addLogin + * + * Add a new login to login storage. + */ + addLogin : function (login) { + // Sanity check the login + if (login.hostname == null || login.hostname.length == 0) + throw "Can't add a login with a null or empty hostname."; + + // For logins w/o a username, set to "", not null. + if (login.username == null) + throw "Can't add a login with a null username."; + + if (login.password == null || login.password.length == 0) + throw "Can't add a login with a null or empty password."; + + if (login.formSubmitURL || login.formSubmitURL == "") { + // We have a form submit URL. Can't have a HTTP realm. + if (login.httpRealm != null) + throw "Can't add a login with both a httpRealm and formSubmitURL."; + } else if (login.httpRealm) { + // We have a HTTP realm. Can't have a form submit URL. + if (login.formSubmitURL != null) + throw "Can't add a login with both a httpRealm and formSubmitURL."; + } else { + // Need one or the other! + throw "Can't add a login without a httpRealm or formSubmitURL."; + } + + + // Look for an existing entry. + var logins = this.findLogins({}, login.hostname, login.formSubmitURL, + login.httpRealm); + + if (logins.some(function(l) login.matches(l, true))) + throw "This login already exists."; + + this.log("Adding login: " + login); + return this._storage.addLogin(login); + }, + + + /* + * removeLogin + * + * Remove the specified login from the stored logins. + */ + removeLogin : function (login) { + this.log("Removing login: " + login); + return this._storage.removeLogin(login); + }, + + + /* + * modifyLogin + * + * Change the specified login to match the new login. + */ + modifyLogin : function (oldLogin, newLogin) { + this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin); + return this._storage.modifyLogin(oldLogin, newLogin); + }, + + + /* + * getAllLogins + * + * Get a dump of all stored logins. Used by the login manager UI. + * + * |count| is only needed for XPCOM. + * + * Returns an array of logins. If there are no logins, the array is empty. + */ + getAllLogins : function (count) { + this.log("Getting a list of all logins"); + return this._storage.getAllLogins(count); + }, + + + /* + * removeAllLogins + * + * Remove all stored logins. + */ + removeAllLogins : function () { + this.log("Removing all logins"); + this._storage.removeAllLogins(); + }, + + /* + * getAllDisabledHosts + * + * Get a list of all hosts for which logins are disabled. + * + * |count| is only needed for XPCOM. + * + * Returns an array of disabled logins. If there are no disabled logins, + * the array is empty. + */ + getAllDisabledHosts : function (count) { + this.log("Getting a list of all disabled hosts"); + return this._storage.getAllDisabledHosts(count); + }, + + + /* + * findLogins + * + * Search for the known logins for entries matching the specified criteria. + */ + findLogins : function (count, hostname, formSubmitURL, httpRealm) { + this.log("Searching for logins matching host: " + hostname + + ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); + + return this._storage.findLogins(count, hostname, formSubmitURL, + httpRealm); + }, + + + /* + * searchLogins + * + * Public wrapper around _searchLogins to convert the nsIPropertyBag to a + * JavaScript object and decrypt the results. + * + * Returns an array of decrypted nsILoginInfo. + */ + searchLogins : function(count, matchData) { + this.log("Searching for logins"); + + return this._storage.searchLogins(count, matchData); + }, + + + /* + * countLogins + * + * Search for the known logins for entries matching the specified criteria, + * returns only the count. + */ + countLogins : function (hostname, formSubmitURL, httpRealm) { + this.log("Counting logins matching host: " + hostname + + ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); + + return this._storage.countLogins(hostname, formSubmitURL, httpRealm); + }, + + + /* + * getLoginSavingEnabled + * + * Check to see if user has disabled saving logins for the host. + */ + getLoginSavingEnabled : function (host) { + this.log("Checking if logins to " + host + " can be saved."); + if (!this._remember) + return false; + + return this._storage.getLoginSavingEnabled(host); + }, + + + /* + * setLoginSavingEnabled + * + * Enable or disable storing logins for the specified host. + */ + setLoginSavingEnabled : function (hostname, enabled) { + // Nulls won't round-trip with getAllDisabledHosts(). + if (hostname.indexOf("\0") != -1) + throw "Invalid hostname"; + + this.log("Saving logins for " + hostname + " enabled? " + enabled); + return this._storage.setLoginSavingEnabled(hostname, enabled); + }, + + + /* + * autoCompleteSearch + * + * Yuck. This is called directly by satchel: + * nsFormFillController::StartSearch() + * [toolkit/components/satchel/src/nsFormFillController.cpp] + * + * We really ought to have a simple way for code to register an + * auto-complete provider, and not have satchel calling pwmgr directly. + */ + autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) { + // aPreviousResult & aResult are nsIAutoCompleteResult, + // aElement is nsIDOMHTMLInputElement + + if (!this._remember) + return false; + + this.log("AutoCompleteSearch invoked. Search is: " + aSearchString); + + var result = null; + + if (aPreviousResult) { + this.log("Using previous autocomplete result"); + result = aPreviousResult; + + // We have a list of results for a shorter search string, so just + // filter them further based on the new search string. + // Count backwards, because result.matchCount is decremented + // when we remove an entry. + for (var i = result.matchCount - 1; i >= 0; i--) { + var match = result.getValueAt(i); + + // Remove results that are too short, or have different prefix. + if (aSearchString.length > match.length || + aSearchString.toLowerCase() != + match.substr(0, aSearchString.length).toLowerCase()) + { + this.log("Removing autocomplete entry '" + match + "'"); + result.removeValueAt(i, false); + } + } + } else { + this.log("Creating new autocomplete search result."); + + var doc = aElement.ownerDocument; + var origin = this._getPasswordOrigin(doc.documentURI); + var actionOrigin = this._getActionOrigin(aElement.form); + + var logins = this.findLogins({}, origin, actionOrigin, null); + var matchingLogins = []; + + for (i = 0; i < logins.length; i++) { + var username = logins[i].username.toLowerCase(); + if (aSearchString.length <= username.length && + aSearchString.toLowerCase() == + username.substr(0, aSearchString.length)) + { + matchingLogins.push(logins[i]); + } + } + this.log(matchingLogins.length + " autocomplete logins avail."); + result = new UserAutoCompleteResult(aSearchString, matchingLogins); + } + + return result; + }, + + + + + /* ------- Internal methods / callbacks for document integration ------- */ + + + + + /* + * _getPasswordFields + * + * Returns an array of password field elements for the specified form. + * If no pw fields are found, or if more than 3 are found, then null + * is returned. + * + * skipEmptyFields can be set to ignore password fields with no value. + */ + _getPasswordFields : function (form, skipEmptyFields) { + // Locate the password fields in the form. + var pwFields = []; + for (var i = 0; i < form.elements.length; i++) { + if (form.elements[i].type != "password") + continue; + + if (skipEmptyFields && !form.elements[i].value) + continue; + + pwFields[pwFields.length] = { + index : i, + element : form.elements[i] + }; + } + + // If too few or too many fields, bail out. + if (pwFields.length == 0) { + this.log("(form ignored -- no password fields.)"); + return null; + } else if (pwFields.length > 3) { + this.log("(form ignored -- too many password fields. [got " + + pwFields.length + "])"); + return null; + } + + return pwFields; + }, + + + /* + * _getFormFields + * + * Returns the username and password fields found in the form. + * Can handle complex forms by trying to figure out what the + * relevant fields are. + * + * Returns: [usernameField, newPasswordField, oldPasswordField] + * + * usernameField may be null. + * newPasswordField will always be non-null. + * oldPasswordField may be null. If null, newPasswordField is just + * "theLoginField". If not null, the form is apparently a + * change-password field, with oldPasswordField containing the password + * that is being changed. + */ + _getFormFields : function (form, isSubmission) { + var usernameField = null; + + // Locate the password field(s) in the form. Up to 3 supported. + // If there's no password field, there's nothing for us to do. + var pwFields = this._getPasswordFields(form, isSubmission); + if (!pwFields) + return [null, null, null]; + + + // Locate the username field in the form by searching backwards + // from the first passwordfield, assume the first text field is the + // username. We might not find a username field if the user is + // already logged in to the site. + for (var i = pwFields[0].index - 1; i >= 0; i--) { + if (form.elements[i].type == "text") { + usernameField = form.elements[i]; + break; + } + } + + if (!usernameField) + this.log("(form -- no username field found)"); + + + // If we're not submitting a form (it's a page load), there are no + // password field values for us to use for identifying fields. So, + // just assume the first password field is the one to be filled in. + if (!isSubmission || pwFields.length == 1) + return [usernameField, pwFields[0].element, null]; + + + // Try to figure out WTF is in the form based on the password values. + var oldPasswordField, newPasswordField; + var pw1 = pwFields[0].element.value; + var pw2 = pwFields[1].element.value; + var pw3 = (pwFields[2] ? pwFields[2].element.value : null); + + if (pwFields.length == 3) { + // Look for two identical passwords, that's the new password + + if (pw1 == pw2 && pw2 == pw3) { + // All 3 passwords the same? Weird! Treat as if 1 pw field. + newPasswordField = pwFields[0].element; + oldPasswordField = null; + } else if (pw1 == pw2) { + newPasswordField = pwFields[0].element; + oldPasswordField = pwFields[2].element; + } else if (pw2 == pw3) { + oldPasswordField = pwFields[0].element; + newPasswordField = pwFields[2].element; + } else if (pw1 == pw3) { + // A bit odd, but could make sense with the right page layout. + newPasswordField = pwFields[0].element; + oldPasswordField = pwFields[1].element; + } else { + // We can't tell which of the 3 passwords should be saved. + this.log("(form ignored -- all 3 pw fields differ)"); + return [null, null, null]; + } + } else { // pwFields.length == 2 + if (pw1 == pw2) { + // Treat as if 1 pw field + newPasswordField = pwFields[0].element; + oldPasswordField = null; + } else { + // Just assume that the 2nd password is the new password + oldPasswordField = pwFields[0].element; + newPasswordField = pwFields[1].element; + } + } + + return [usernameField, newPasswordField, oldPasswordField]; + }, + + + /* + * _isAutoCompleteDisabled + * + * Returns true if the page requests autocomplete be disabled for the + * specified form input. + */ + _isAutocompleteDisabled : function (element) { + if (element && element.hasAttribute("autocomplete") && + element.getAttribute("autocomplete").toLowerCase() == "off") + return true; + + return false; + }, + + /* + * _onFormSubmit + * + * Called by the our observer when notified of a form submission. + * [Note that this happens before any DOM onsubmit handlers are invoked.] + * Looks for a password change in the submitted form, so we can update + * our stored password. + */ + _onFormSubmit : function (form) { + + // local helper function + function getPrompter(aWindow) { + var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"]. + createInstance(Ci.nsILoginManagerPrompter); + prompterSvc.init(aWindow); + return prompterSvc; + } + + if (this._inPrivateBrowsing) { + // We won't do anything in private browsing mode anyway, + // so there's no need to perform further checks. + this.log("(form submission ignored in private browsing mode)"); + return; + } + + var doc = form.ownerDocument; + var win = doc.defaultView; + + // If password saving is disabled (globally or for host), bail out now. + if (!this._remember) + return; + + var hostname = this._getPasswordOrigin(doc.documentURI); + var formSubmitURL = this._getActionOrigin(form) + if (!this.getLoginSavingEnabled(hostname)) { + this.log("(form submission ignored -- saving is " + + "disabled for: " + hostname + ")"); + return; + } + + + // Get the appropriate fields from the form. + var [usernameField, newPasswordField, oldPasswordField] = + this._getFormFields(form, true); + + // Need at least 1 valid password field to do anything. + if (newPasswordField == null) + return; + + // Check for autocomplete=off attribute. We don't use it to prevent + // autofilling (for existing logins), but won't save logins when it's + // present. + if (this._isAutocompleteDisabled(form) || + this._isAutocompleteDisabled(usernameField) || + this._isAutocompleteDisabled(newPasswordField) || + this._isAutocompleteDisabled(oldPasswordField)) { + this.log("(form submission ignored -- autocomplete=off found)"); + return; + } + + + var formLogin = new this._nsLoginInfo(); + formLogin.init(hostname, formSubmitURL, null, + (usernameField ? usernameField.value : ""), + newPasswordField.value, + (usernameField ? usernameField.name : ""), + newPasswordField.name); + + // If we didn't find a username field, but seem to be changing a + // password, allow the user to select from a list of applicable + // logins to update the password for. + if (!usernameField && oldPasswordField) { + + var logins = this.findLogins({}, hostname, formSubmitURL, null); + + if (logins.length == 0) { + // Could prompt to save this as a new password-only login. + // This seems uncommon, and might be wrong, so ignore. + this.log("(no logins for this host -- pwchange ignored)"); + return; + } + + var prompter = getPrompter(win); + + if (logins.length == 1) { + var oldLogin = logins[0]; + formLogin.username = oldLogin.username; + formLogin.usernameField = oldLogin.usernameField; + + prompter.promptToChangePassword(oldLogin, formLogin); + } else { + prompter.promptToChangePasswordWithUsernames( + logins, logins.length, formLogin); + } + + return; + } + + + // Look for an existing login that matches the form login. + var existingLogin = null; + var logins = this.findLogins({}, hostname, formSubmitURL, null); + + for (var i = 0; i < logins.length; i++) { + var same, login = logins[i]; + + // If one login has a username but the other doesn't, ignore + // the username when comparing and only match if they have the + // same password. Otherwise, compare the logins and match even + // if the passwords differ. + if (!login.username && formLogin.username) { + var restoreMe = formLogin.username; + formLogin.username = ""; + same = formLogin.matches(login, false); + formLogin.username = restoreMe; + } else if (!formLogin.username && login.username) { + formLogin.username = login.username; + same = formLogin.matches(login, false); + formLogin.username = ""; // we know it's always blank. + } else { + same = formLogin.matches(login, true); + } + + if (same) { + existingLogin = login; + break; + } + } + + if (existingLogin) { + this.log("Found an existing login matching this form submission"); + + // Change password if needed. + if (existingLogin.password != formLogin.password) { + this.log("...passwords differ, prompting to change."); + prompter = getPrompter(win); + prompter.promptToChangePassword(existingLogin, formLogin); + } + + return; + } + + + // Prompt user to save login (via dialog or notification bar) + prompter = getPrompter(win); + prompter.promptToSavePassword(formLogin); + }, + + + /* + * _getPasswordOrigin + * + * Get the parts of the URL we want for identification. + */ + _getPasswordOrigin : function (uriString, allowJS) { + var realm = ""; + try { + var uri = this._ioService.newURI(uriString, null, null); + + if (allowJS && uri.scheme == "javascript") + return "javascript:" + + realm = uri.scheme + "://" + uri.host; + + // If the URI explicitly specified a port, only include it when + // it's not the default. (We never want "http://foo.com:80") + var port = uri.port; + if (port != -1) { + var handler = this._ioService.getProtocolHandler(uri.scheme); + if (port != handler.defaultPort) + realm += ":" + port; + } + + } catch (e) { + // bug 159484 - disallow url types that don't support a hostPort. + // (although we handle "javascript:..." as a special case above.) + this.log("Couldn't parse origin for " + uriString); + realm = null; + } + + return realm; + }, + + _getActionOrigin : function (form) { + var uriString = form.action; + + // A blank or mission action submits to where it came from. + if (uriString == "") + uriString = form.baseURI; // ala bug 297761 + + return this._getPasswordOrigin(uriString, true); + }, + + + /* + * _fillDocument + * + * Called when a page has loaded. For each form in the document, + * we check to see if it can be filled with a stored login. + */ + _fillDocument : function (doc) { + var forms = doc.forms; + if (!forms || forms.length == 0) + return []; + + var formOrigin = this._getPasswordOrigin(doc.documentURI); + + // If there are no logins for this site, bail out now. + if (!this.countLogins(formOrigin, "", null)) + return []; + + this.log("fillDocument processing " + forms.length + + " forms on " + doc.documentURI); + + var autofillForm = !this._inPrivateBrowsing && + this._prefBranch.getBoolPref("autofillForms"); + var previousActionOrigin = null; + var foundLogins = null; + + let auths = []; + for (var i = 0; i < forms.length; i++) { + var form = forms[i]; + + // Only the actionOrigin might be changing, so if it's the same + // as the last form on the page we can reuse the same logins. + var actionOrigin = this._getActionOrigin(form); + if (actionOrigin != previousActionOrigin) { + foundLogins = null; + previousActionOrigin = actionOrigin; + } + this.log("_fillDocument processing form[" + i + "]"); + let auth = this._fillForm(form, autofillForm, false, foundLogins); + foundLogins = auth.foundLogins; + if (auth.canFillForm) + auths.push(auth); + } // foreach form + return auths; + }, + + + /* + * _fillform + * + * Fill the form with login information if we can find it. This will find + * an array of logins if not given any, otherwise it will use the logins + * passed in. The logins are returned so they can be reused for + * optimization. Success of action is also returned in format + * [success, foundLogins]. autofillForm denotes if we should fill the form + * in automatically, ignoreAutocomplete denotes if we should ignore + * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo + * for optimization + */ + _fillForm : function (form, autofillForm, ignoreAutocomplete, foundLogins) { + // Heuristically determine what the user/pass fields are + // We do this before checking to see if logins are stored, + // so that the user isn't prompted for a master password + // without need. + var [usernameField, passwordField, ignored] = + this._getFormFields(form, false); + + // Need a valid password field to do anything. + if (passwordField == null) + return { foundLogins: foundLogins }; + + // If the fields are disabled or read-only, there's nothing to do. + if (passwordField.disabled || passwordField.readOnly || + usernameField && (usernameField.disabled || + usernameField.readOnly)) { + this.log("not filling form, login fields disabled"); + return { foundLogins: foundLogins }; + } + + // Need to get a list of logins if we weren't given them + if (foundLogins == null) { + var formOrigin = + this._getPasswordOrigin(form.ownerDocument.documentURI); + var actionOrigin = this._getActionOrigin(form); + foundLogins = this.findLogins({}, formOrigin, actionOrigin, null); + this.log("found " + foundLogins.length + " matching logins."); + } else { + this.log("reusing logins from last form."); + } + + // Discard logins which have username/password values that don't + // fit into the fields (as specified by the maxlength attribute). + // The user couldn't enter these values anyway, and it helps + // with sites that have an extra PIN to be entered (bug 391514) + var maxUsernameLen = Number.MAX_VALUE; + var maxPasswordLen = Number.MAX_VALUE; + + // If attribute wasn't set, default is -1. + if (usernameField && usernameField.maxLength >= 0) + maxUsernameLen = usernameField.maxLength; + if (passwordField.maxLength >= 0) + maxPasswordLen = passwordField.maxLength; + + logins = foundLogins.filter(function (l) { + var fit = (l.username.length <= maxUsernameLen && + l.password.length <= maxPasswordLen); + if (!fit) + this.log("Ignored " + l.username + " login: won't fit"); + + return fit; + }, this); + + + // Nothing to do if we have no matching logins available. + if (logins.length == 0) + return { foundLogins: foundLogins }; + + + + // Attach autocomplete stuff to the username field, if we have + // one. This is normally used to select from multiple accounts, + // but even with one account we should refill if the user edits. + //if (usernameField) + // this._attachToInput(usernameField); + + // Don't clobber an existing password. + if (passwordField.value) + return { foundLogins: foundLogins }; + + // If the form has an autocomplete=off attribute in play, don't + // fill in the login automatically. We check this after attaching + // the autocomplete stuff to the username field, so the user can + // still manually select a login to be filled in. + var isFormDisabled = false; + if (!ignoreAutocomplete && + (this._isAutocompleteDisabled(form) || + this._isAutocompleteDisabled(usernameField) || + this._isAutocompleteDisabled(passwordField))) { + + isFormDisabled = true; + this.log("form not filled, has autocomplete=off"); + } + + // Variable such that we reduce code duplication and can be sure we + // should be firing notifications if and only if we can fill the form. + var selectedLogin = null; + + if (usernameField && usernameField.value) { + // If username was specified in the form, only fill in the + // password if we find a matching login. + var username = usernameField.value.toLowerCase(); + + let matchingLogins = logins.filter(function(l) + l.username.toLowerCase() == username); + if (matchingLogins.length) + selectedLogin = matchingLogins[0]; + else + this.log("Password not filled. None of the stored " + + "logins match the username already present."); + } else if (logins.length == 1) { + selectedLogin = logins[0]; + } else { + // We have multiple logins. Handle a special case here, for sites + // which have a normal user+pass login *and* a password-only login + // (eg, a PIN). Prefer the login that matches the type of the form + // (user+pass or pass-only) when there's exactly one that matches. + let matchingLogins; + if (usernameField) + matchingLogins = logins.filter(function(l) l.username); + else + matchingLogins = logins.filter(function(l) !l.username); + if (matchingLogins.length == 1) + selectedLogin = matchingLogins[0]; + else + this.log("Multiple logins for form, so not filling any."); + } + + //var didFillForm = false; + //if (selectedLogin && autofillForm && !isFormDisabled) { + // // Fill the form + // if (usernameField) + // usernameField.value = selectedLogin.username; + // passwordField.value = selectedLogin.password; + // didFillForm = true; + //} else if (selectedLogin && !autofillForm) { + // // For when autofillForm is false, but we still have the information + // // to fill a form, we notify observers. + // this._observerService.notifyObservers(form, "passwordmgr-found-form", "noAutofillForms"); + // this.log("autofillForms=false but form can be filled; notified observers"); + //} else if (selectedLogin && isFormDisabled) { + // // For when autocomplete is off, but we still have the information + // // to fill a form, we notify observers. + // this._observerService.notifyObservers(form, "passwordmgr-found-form", "autocompleteOff"); + // this.log("autocomplete=off but form can be filled; notified observers"); + //} + + return { canFillForm: true, + form: form, + usernameField: usernameField, + passwordField: passwordField, + foundLogins: foundLogins, + selectedLogin: selectedLogin }; + }, + + + /* + * fillForm + * + * Fill the form with login information if we can find it. + */ + fillForm : function (form) { + this.log("fillForm processing form[id=" + form.id + "]"); + return this._fillForm(form, true, true, null)[0]; + }, + + + /* + * _attachToInput + * + * Hooks up autocomplete support to a username field, to allow + * a user editing the field to select an existing login and have + * the password field filled in. + */ + _attachToInput : function (element) { + this.log("attaching autocomplete stuff"); + element.addEventListener("blur", + this._domEventListener, false); + element.addEventListener("DOMAutoComplete", + this._domEventListener, false); + this._formFillService.markAsLoginManagerField(element); + } +}; // end of LoginManager implementation + + + + +// nsIAutoCompleteResult implementation +function UserAutoCompleteResult (aSearchString, matchingLogins) { + function loginSort(a,b) { + var userA = a.username.toLowerCase(); + var userB = b.username.toLowerCase(); + + if (userA < userB) + return -1; + + if (userB > userA) + return 1; + + return 0; + }; + + this.searchString = aSearchString; + this.logins = matchingLogins.sort(loginSort); + this.matchCount = matchingLogins.length; + + if (this.matchCount > 0) { + this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS; + this.defaultIndex = 0; + } +} + +UserAutoCompleteResult.prototype = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult, + Ci.nsISupportsWeakReference]), + + // private + logins : null, + + // Interfaces from idl... + searchString : null, + searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH, + defaultIndex : -1, + errorDescription : "", + matchCount : 0, + + getValueAt : function (index) { + if (index < 0 || index >= this.logins.length) + throw "Index out of range."; + + return this.logins[index].username; + }, + + getCommentAt : function (index) { + return ""; + }, + + getStyleAt : function (index) { + return ""; + }, + + getImageAt : function (index) { + return ""; + }, + + removeValueAt : function (index, removeFromDB) { + if (index < 0 || index >= this.logins.length) + throw "Index out of range."; + + var [removedLogin] = this.logins.splice(index, 1); + + this.matchCount--; + if (this.defaultIndex > this.logins.length) + this.defaultIndex--; + + if (removeFromDB) { + var pwmgr = Cc["@mozilla.org/login-manager;1"]. + getService(Ci.nsILoginManager); + pwmgr.removeLogin(removedLogin); + } + } +}; + +//var component = [LoginManager]; +//function NSGetModule (compMgr, fileSpec) { +// return XPCOMUtils.generateModule(component); +//} +WeaveLoginManager.init();