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: this.EXPORTED_SYMBOLS = ["LoginManagerContent"]; michael@0: michael@0: const Ci = Components.interfaces; michael@0: const Cr = Components.results; michael@0: const Cc = Components.classes; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: michael@0: // These mirror signon.* prefs. michael@0: var gEnabled, gDebug, gAutofillForms, gStoreWhenAutocompleteOff; michael@0: michael@0: function log(...pieces) { michael@0: function generateLogMessage(args) { michael@0: let strings = ['Login Manager (content):']; michael@0: michael@0: args.forEach(function(arg) { michael@0: if (typeof arg === 'string') { michael@0: strings.push(arg); michael@0: } else if (typeof arg === 'undefined') { michael@0: strings.push('undefined'); michael@0: } else if (arg === null) { michael@0: strings.push('null'); michael@0: } else { michael@0: try { michael@0: strings.push(JSON.stringify(arg, null, 2)); michael@0: } catch(err) { michael@0: strings.push("<>"); michael@0: } michael@0: } michael@0: }); michael@0: return strings.join(' '); michael@0: } michael@0: michael@0: if (!gDebug) michael@0: return; michael@0: michael@0: let message = generateLogMessage(pieces); michael@0: dump(message + "\n"); michael@0: Services.console.logStringMessage(message); michael@0: } michael@0: michael@0: michael@0: var observer = { michael@0: QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, michael@0: Ci.nsIFormSubmitObserver, michael@0: Ci.nsISupportsWeakReference]), michael@0: michael@0: // nsIFormSubmitObserver michael@0: notify : function (formElement, aWindow, actionURI) { michael@0: log("observer notified for form submission."); michael@0: michael@0: // We're invoked before the content's |onsubmit| handlers, so we michael@0: // can grab form data before it might be modified (see bug 257781). michael@0: michael@0: try { michael@0: LoginManagerContent._onFormSubmit(formElement); michael@0: } catch (e) { michael@0: log("Caught error in onFormSubmit:", e); michael@0: } michael@0: michael@0: return true; // Always return true, or form submit will be canceled. michael@0: }, michael@0: michael@0: onPrefChange : function() { michael@0: gDebug = Services.prefs.getBoolPref("signon.debug"); michael@0: gEnabled = Services.prefs.getBoolPref("signon.rememberSignons"); michael@0: gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms"); michael@0: gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff"); michael@0: }, michael@0: }; michael@0: michael@0: Services.obs.addObserver(observer, "earlyformsubmit", false); michael@0: var prefBranch = Services.prefs.getBranch("signon."); michael@0: prefBranch.addObserver("", observer.onPrefChange, false); michael@0: michael@0: observer.onPrefChange(); // read initial values michael@0: michael@0: michael@0: var LoginManagerContent = { michael@0: michael@0: __formFillService : null, // FormFillController, for username autocompleting michael@0: get _formFillService() { michael@0: if (!this.__formFillService) michael@0: this.__formFillService = michael@0: Cc["@mozilla.org/satchel/form-fill-controller;1"]. michael@0: getService(Ci.nsIFormFillController); michael@0: return this.__formFillService; michael@0: }, michael@0: michael@0: michael@0: onFormPassword: function (event) { michael@0: if (!event.isTrusted) michael@0: return; michael@0: michael@0: if (!gEnabled) michael@0: return; michael@0: michael@0: let form = event.target; michael@0: let doc = form.ownerDocument; michael@0: michael@0: log("onFormPassword for", doc.documentURI); michael@0: michael@0: // If there are no logins for this site, bail out now. michael@0: let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI); michael@0: if (!Services.logins.countLogins(formOrigin, "", null)) michael@0: return; michael@0: michael@0: // If we're currently displaying a master password prompt, defer michael@0: // processing this form until the user handles the prompt. michael@0: if (Services.logins.uiBusy) { michael@0: log("deferring onFormPassword for", doc.documentURI); michael@0: let self = this; michael@0: let observer = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), michael@0: michael@0: observe: function (subject, topic, data) { michael@0: log("Got deferred onFormPassword notification:", topic); michael@0: // Only run observer once. michael@0: Services.obs.removeObserver(this, "passwordmgr-crypto-login"); michael@0: Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled"); michael@0: if (topic == "passwordmgr-crypto-loginCanceled") michael@0: return; michael@0: self.onFormPassword(event); michael@0: }, michael@0: handleEvent : function (event) { michael@0: // Not expected to be called michael@0: } michael@0: }; michael@0: // Trickyness follows: We want an observer, but don't want it to michael@0: // cause leaks. So add the observer with a weak reference, and use michael@0: // a dummy event listener (a strong reference) to keep it alive michael@0: // until the form is destroyed. michael@0: Services.obs.addObserver(observer, "passwordmgr-crypto-login", true); michael@0: Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true); michael@0: form.addEventListener("mozCleverClosureHack", observer); michael@0: return; michael@0: } michael@0: michael@0: let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isWindowPrivate(doc.defaultView); michael@0: michael@0: this._fillForm(form, autofillForm, false, false, null); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * onUsernameInput michael@0: * michael@0: * Listens for DOMAutoComplete and blur events on an input field. michael@0: */ michael@0: onUsernameInput : function(event) { michael@0: if (!event.isTrusted) michael@0: return; michael@0: michael@0: if (!gEnabled) michael@0: return; michael@0: michael@0: var acInputField = event.target; michael@0: michael@0: // This is probably a bit over-conservatative. michael@0: if (!(acInputField.ownerDocument instanceof Ci.nsIDOMHTMLDocument)) michael@0: return; michael@0: michael@0: if (!this._isUsernameFieldType(acInputField)) michael@0: return; michael@0: michael@0: var acForm = acInputField.form; michael@0: if (!acForm) michael@0: return; michael@0: michael@0: // If the username is blank, bail out now -- we don't want michael@0: // fillForm() to try filling in a login without a username michael@0: // to filter on (bug 471906). michael@0: if (!acInputField.value) michael@0: return; michael@0: michael@0: log("onUsernameInput from", event.type); michael@0: michael@0: // Make sure the username field fillForm will use is the michael@0: // same field as the autocomplete was activated on. michael@0: var [usernameField, passwordField, ignored] = michael@0: this._getFormFields(acForm, false); michael@0: if (usernameField == acInputField && passwordField) { michael@0: // If the user has a master password but itsn't logged in, bail michael@0: // out now to prevent annoying prompts. michael@0: if (!Services.logins.isLoggedIn) michael@0: return; michael@0: michael@0: this._fillForm(acForm, true, true, true, null); michael@0: } else { michael@0: // Ignore the event, it's for some input we don't care about. michael@0: } michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getPasswordFields michael@0: * michael@0: * Returns an array of password field elements for the specified form. michael@0: * If no pw fields are found, or if more than 3 are found, then null michael@0: * is returned. michael@0: * michael@0: * skipEmptyFields can be set to ignore password fields with no value. michael@0: */ michael@0: _getPasswordFields : function (form, skipEmptyFields) { michael@0: // Locate the password fields in the form. michael@0: var pwFields = []; michael@0: for (var i = 0; i < form.elements.length; i++) { michael@0: var element = form.elements[i]; michael@0: if (!(element instanceof Ci.nsIDOMHTMLInputElement) || michael@0: element.type != "password") michael@0: continue; michael@0: michael@0: if (skipEmptyFields && !element.value) michael@0: continue; michael@0: michael@0: pwFields[pwFields.length] = { michael@0: index : i, michael@0: element : element michael@0: }; michael@0: } michael@0: michael@0: // If too few or too many fields, bail out. michael@0: if (pwFields.length == 0) { michael@0: log("(form ignored -- no password fields.)"); michael@0: return null; michael@0: } else if (pwFields.length > 3) { michael@0: log("(form ignored -- too many password fields. [ got ", michael@0: pwFields.length, "])"); michael@0: return null; michael@0: } michael@0: michael@0: return pwFields; michael@0: }, michael@0: michael@0: michael@0: _isUsernameFieldType: function(element) { michael@0: if (!(element instanceof Ci.nsIDOMHTMLInputElement)) michael@0: return false; michael@0: michael@0: let fieldType = (element.hasAttribute("type") ? michael@0: element.getAttribute("type").toLowerCase() : michael@0: element.type); michael@0: if (fieldType == "text" || michael@0: fieldType == "email" || michael@0: fieldType == "url" || michael@0: fieldType == "tel" || michael@0: fieldType == "number") { michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _getFormFields michael@0: * michael@0: * Returns the username and password fields found in the form. michael@0: * Can handle complex forms by trying to figure out what the michael@0: * relevant fields are. michael@0: * michael@0: * Returns: [usernameField, newPasswordField, oldPasswordField] michael@0: * michael@0: * usernameField may be null. michael@0: * newPasswordField will always be non-null. michael@0: * oldPasswordField may be null. If null, newPasswordField is just michael@0: * "theLoginField". If not null, the form is apparently a michael@0: * change-password field, with oldPasswordField containing the password michael@0: * that is being changed. michael@0: */ michael@0: _getFormFields : function (form, isSubmission) { michael@0: var usernameField = null; michael@0: michael@0: // Locate the password field(s) in the form. Up to 3 supported. michael@0: // If there's no password field, there's nothing for us to do. michael@0: var pwFields = this._getPasswordFields(form, isSubmission); michael@0: if (!pwFields) michael@0: return [null, null, null]; michael@0: michael@0: michael@0: // Locate the username field in the form by searching backwards michael@0: // from the first passwordfield, assume the first text field is the michael@0: // username. We might not find a username field if the user is michael@0: // already logged in to the site. michael@0: for (var i = pwFields[0].index - 1; i >= 0; i--) { michael@0: var element = form.elements[i]; michael@0: if (this._isUsernameFieldType(element)) { michael@0: usernameField = element; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!usernameField) michael@0: log("(form -- no username field found)"); michael@0: michael@0: michael@0: // If we're not submitting a form (it's a page load), there are no michael@0: // password field values for us to use for identifying fields. So, michael@0: // just assume the first password field is the one to be filled in. michael@0: if (!isSubmission || pwFields.length == 1) michael@0: return [usernameField, pwFields[0].element, null]; michael@0: michael@0: michael@0: // Try to figure out WTF is in the form based on the password values. michael@0: var oldPasswordField, newPasswordField; michael@0: var pw1 = pwFields[0].element.value; michael@0: var pw2 = pwFields[1].element.value; michael@0: var pw3 = (pwFields[2] ? pwFields[2].element.value : null); michael@0: michael@0: if (pwFields.length == 3) { michael@0: // Look for two identical passwords, that's the new password michael@0: michael@0: if (pw1 == pw2 && pw2 == pw3) { michael@0: // All 3 passwords the same? Weird! Treat as if 1 pw field. michael@0: newPasswordField = pwFields[0].element; michael@0: oldPasswordField = null; michael@0: } else if (pw1 == pw2) { michael@0: newPasswordField = pwFields[0].element; michael@0: oldPasswordField = pwFields[2].element; michael@0: } else if (pw2 == pw3) { michael@0: oldPasswordField = pwFields[0].element; michael@0: newPasswordField = pwFields[2].element; michael@0: } else if (pw1 == pw3) { michael@0: // A bit odd, but could make sense with the right page layout. michael@0: newPasswordField = pwFields[0].element; michael@0: oldPasswordField = pwFields[1].element; michael@0: } else { michael@0: // We can't tell which of the 3 passwords should be saved. michael@0: log("(form ignored -- all 3 pw fields differ)"); michael@0: return [null, null, null]; michael@0: } michael@0: } else { // pwFields.length == 2 michael@0: if (pw1 == pw2) { michael@0: // Treat as if 1 pw field michael@0: newPasswordField = pwFields[0].element; michael@0: oldPasswordField = null; michael@0: } else { michael@0: // Just assume that the 2nd password is the new password michael@0: oldPasswordField = pwFields[0].element; michael@0: newPasswordField = pwFields[1].element; michael@0: } michael@0: } michael@0: michael@0: return [usernameField, newPasswordField, oldPasswordField]; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _isAutoCompleteDisabled michael@0: * michael@0: * Returns true if the page requests autocomplete be disabled for the michael@0: * specified form input. michael@0: */ michael@0: _isAutocompleteDisabled : function (element) { michael@0: if (element && element.hasAttribute("autocomplete") && michael@0: element.getAttribute("autocomplete").toLowerCase() == "off") michael@0: return true; michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _onFormSubmit michael@0: * michael@0: * Called by the our observer when notified of a form submission. michael@0: * [Note that this happens before any DOM onsubmit handlers are invoked.] michael@0: * Looks for a password change in the submitted form, so we can update michael@0: * our stored password. michael@0: */ michael@0: _onFormSubmit : function (form) { michael@0: michael@0: // For E10S this will need to move. michael@0: function getPrompter(aWindow) { michael@0: var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"]. michael@0: createInstance(Ci.nsILoginManagerPrompter); michael@0: prompterSvc.init(aWindow); michael@0: return prompterSvc; michael@0: } michael@0: michael@0: var doc = form.ownerDocument; michael@0: var win = doc.defaultView; michael@0: michael@0: if (PrivateBrowsingUtils.isWindowPrivate(win)) { michael@0: // We won't do anything in private browsing mode anyway, michael@0: // so there's no need to perform further checks. michael@0: log("(form submission ignored in private browsing mode)"); michael@0: return; michael@0: } michael@0: michael@0: // If password saving is disabled (globally or for host), bail out now. michael@0: if (!gEnabled) michael@0: return; michael@0: michael@0: var hostname = LoginUtils._getPasswordOrigin(doc.documentURI); michael@0: if (!hostname) { michael@0: log("(form submission ignored -- invalid hostname)"); michael@0: return; michael@0: } michael@0: michael@0: // Somewhat gross hack - we don't want to show the "remember password" michael@0: // notification on about:accounts for Firefox. michael@0: let topWin = win.top; michael@0: if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) { michael@0: log("(form submission ignored -- about:accounts)"); michael@0: return; michael@0: } michael@0: michael@0: var formSubmitURL = LoginUtils._getActionOrigin(form) michael@0: if (!Services.logins.getLoginSavingEnabled(hostname)) { michael@0: log("(form submission ignored -- saving is disabled for:", hostname, ")"); michael@0: return; michael@0: } michael@0: michael@0: michael@0: // Get the appropriate fields from the form. michael@0: var [usernameField, newPasswordField, oldPasswordField] = michael@0: this._getFormFields(form, true); michael@0: michael@0: // Need at least 1 valid password field to do anything. michael@0: if (newPasswordField == null) michael@0: return; michael@0: michael@0: // Check for autocomplete=off attribute. We don't use it to prevent michael@0: // autofilling (for existing logins), but won't save logins when it's michael@0: // present and the storeWhenAutocompleteOff pref is false. michael@0: // XXX spin out a bug that we don't update timeLastUsed in this case? michael@0: if ((this._isAutocompleteDisabled(form) || michael@0: this._isAutocompleteDisabled(usernameField) || michael@0: this._isAutocompleteDisabled(newPasswordField) || michael@0: this._isAutocompleteDisabled(oldPasswordField)) && michael@0: !gStoreWhenAutocompleteOff) { michael@0: log("(form submission ignored -- autocomplete=off found)"); michael@0: return; michael@0: } michael@0: michael@0: michael@0: var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. michael@0: createInstance(Ci.nsILoginInfo); michael@0: formLogin.init(hostname, formSubmitURL, null, michael@0: (usernameField ? usernameField.value : ""), michael@0: newPasswordField.value, michael@0: (usernameField ? usernameField.name : ""), michael@0: newPasswordField.name); michael@0: michael@0: // If we didn't find a username field, but seem to be changing a michael@0: // password, allow the user to select from a list of applicable michael@0: // logins to update the password for. michael@0: if (!usernameField && oldPasswordField) { michael@0: michael@0: var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null); michael@0: michael@0: if (logins.length == 0) { michael@0: // Could prompt to save this as a new password-only login. michael@0: // This seems uncommon, and might be wrong, so ignore. michael@0: log("(no logins for this host -- pwchange ignored)"); michael@0: return; michael@0: } michael@0: michael@0: var prompter = getPrompter(win); michael@0: michael@0: if (logins.length == 1) { michael@0: var oldLogin = logins[0]; michael@0: formLogin.username = oldLogin.username; michael@0: formLogin.usernameField = oldLogin.usernameField; michael@0: michael@0: prompter.promptToChangePassword(oldLogin, formLogin); michael@0: } else { michael@0: prompter.promptToChangePasswordWithUsernames( michael@0: logins, logins.length, formLogin); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: michael@0: // Look for an existing login that matches the form login. michael@0: var existingLogin = null; michael@0: var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null); michael@0: michael@0: for (var i = 0; i < logins.length; i++) { michael@0: var same, login = logins[i]; michael@0: michael@0: // If one login has a username but the other doesn't, ignore michael@0: // the username when comparing and only match if they have the michael@0: // same password. Otherwise, compare the logins and match even michael@0: // if the passwords differ. michael@0: if (!login.username && formLogin.username) { michael@0: var restoreMe = formLogin.username; michael@0: formLogin.username = ""; michael@0: same = formLogin.matches(login, false); michael@0: formLogin.username = restoreMe; michael@0: } else if (!formLogin.username && login.username) { michael@0: formLogin.username = login.username; michael@0: same = formLogin.matches(login, false); michael@0: formLogin.username = ""; // we know it's always blank. michael@0: } else { michael@0: same = formLogin.matches(login, true); michael@0: } michael@0: michael@0: if (same) { michael@0: existingLogin = login; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (existingLogin) { michael@0: log("Found an existing login matching this form submission"); michael@0: michael@0: // Change password if needed. michael@0: if (existingLogin.password != formLogin.password) { michael@0: log("...passwords differ, prompting to change."); michael@0: prompter = getPrompter(win); michael@0: prompter.promptToChangePassword(existingLogin, formLogin); michael@0: } else { michael@0: // Update the lastUsed timestamp. michael@0: var propBag = Cc["@mozilla.org/hash-property-bag;1"]. michael@0: createInstance(Ci.nsIWritablePropertyBag); michael@0: propBag.setProperty("timeLastUsed", Date.now()); michael@0: propBag.setProperty("timesUsedIncrement", 1); michael@0: Services.logins.modifyLogin(existingLogin, propBag); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: michael@0: // Prompt user to save login (via dialog or notification bar) michael@0: prompter = getPrompter(win); michael@0: prompter.promptToSavePassword(formLogin); michael@0: }, michael@0: michael@0: michael@0: /* michael@0: * _fillform michael@0: * michael@0: * Fill the form with login information if we can find it. This will find michael@0: * an array of logins if not given any, otherwise it will use the logins michael@0: * passed in. The logins are returned so they can be reused for michael@0: * optimization. Success of action is also returned in format michael@0: * [success, foundLogins]. autofillForm denotes if we should fill the form michael@0: * in automatically, ignoreAutocomplete denotes if we should ignore michael@0: * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo michael@0: * for optimization michael@0: */ michael@0: _fillForm : function (form, autofillForm, ignoreAutocomplete, michael@0: clobberPassword, foundLogins) { michael@0: // Heuristically determine what the user/pass fields are michael@0: // We do this before checking to see if logins are stored, michael@0: // so that the user isn't prompted for a master password michael@0: // without need. michael@0: var [usernameField, passwordField, ignored] = michael@0: this._getFormFields(form, false); michael@0: michael@0: // Need a valid password field to do anything. michael@0: if (passwordField == null) michael@0: return [false, foundLogins]; michael@0: michael@0: // If the password field is disabled or read-only, there's nothing to do. michael@0: if (passwordField.disabled || passwordField.readOnly) { michael@0: log("not filling form, password field disabled or read-only"); michael@0: return [false, foundLogins]; michael@0: } michael@0: michael@0: // Need to get a list of logins if we weren't given them michael@0: if (foundLogins == null) { michael@0: var formOrigin = michael@0: LoginUtils._getPasswordOrigin(form.ownerDocument.documentURI); michael@0: var actionOrigin = LoginUtils._getActionOrigin(form); michael@0: foundLogins = Services.logins.findLogins({}, formOrigin, actionOrigin, null); michael@0: log("found", foundLogins.length, "matching logins."); michael@0: } else { michael@0: log("reusing logins from last form."); michael@0: } michael@0: michael@0: // Discard logins which have username/password values that don't michael@0: // fit into the fields (as specified by the maxlength attribute). michael@0: // The user couldn't enter these values anyway, and it helps michael@0: // with sites that have an extra PIN to be entered (bug 391514) michael@0: var maxUsernameLen = Number.MAX_VALUE; michael@0: var maxPasswordLen = Number.MAX_VALUE; michael@0: michael@0: // If attribute wasn't set, default is -1. michael@0: if (usernameField && usernameField.maxLength >= 0) michael@0: maxUsernameLen = usernameField.maxLength; michael@0: if (passwordField.maxLength >= 0) michael@0: maxPasswordLen = passwordField.maxLength; michael@0: michael@0: var logins = foundLogins.filter(function (l) { michael@0: var fit = (l.username.length <= maxUsernameLen && michael@0: l.password.length <= maxPasswordLen); michael@0: if (!fit) michael@0: log("Ignored", l.username, "login: won't fit"); michael@0: michael@0: return fit; michael@0: }, this); michael@0: michael@0: michael@0: // Nothing to do if we have no matching logins available. michael@0: if (logins.length == 0) michael@0: return [false, foundLogins]; michael@0: michael@0: michael@0: // The reason we didn't end up filling the form, if any. We include michael@0: // this in the formInfo object we send with the passwordmgr-found-logins michael@0: // notification. See the _notifyFoundLogins docs for possible values. michael@0: var didntFillReason = null; michael@0: michael@0: // Attach autocomplete stuff to the username field, if we have michael@0: // one. This is normally used to select from multiple accounts, michael@0: // but even with one account we should refill if the user edits. michael@0: if (usernameField) michael@0: this._formFillService.markAsLoginManagerField(usernameField); michael@0: michael@0: // Don't clobber an existing password. michael@0: if (passwordField.value && !clobberPassword) { michael@0: didntFillReason = "existingPassword"; michael@0: this._notifyFoundLogins(didntFillReason, usernameField, michael@0: passwordField, foundLogins, null); michael@0: return [false, foundLogins]; michael@0: } michael@0: michael@0: // If the form has an autocomplete=off attribute in play, don't michael@0: // fill in the login automatically. We check this after attaching michael@0: // the autocomplete stuff to the username field, so the user can michael@0: // still manually select a login to be filled in. michael@0: var isFormDisabled = false; michael@0: if (!ignoreAutocomplete && michael@0: (this._isAutocompleteDisabled(form) || michael@0: this._isAutocompleteDisabled(usernameField) || michael@0: this._isAutocompleteDisabled(passwordField))) { michael@0: michael@0: isFormDisabled = true; michael@0: log("form not filled, has autocomplete=off"); michael@0: } michael@0: michael@0: // Variable such that we reduce code duplication and can be sure we michael@0: // should be firing notifications if and only if we can fill the form. michael@0: var selectedLogin = null; michael@0: michael@0: if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) { michael@0: // If username was specified in the field, it's disabled or it's readOnly, only fill in the michael@0: // password if we find a matching login. michael@0: var username = usernameField.value.toLowerCase(); michael@0: michael@0: let matchingLogins = logins.filter(function(l) michael@0: l.username.toLowerCase() == username); michael@0: if (matchingLogins.length) { michael@0: selectedLogin = matchingLogins[0]; michael@0: } else { michael@0: didntFillReason = "existingUsername"; michael@0: log("Password not filled. None of the stored logins match the username already present."); michael@0: } michael@0: } else if (logins.length == 1) { michael@0: selectedLogin = logins[0]; michael@0: } else { michael@0: // We have multiple logins. Handle a special case here, for sites michael@0: // which have a normal user+pass login *and* a password-only login michael@0: // (eg, a PIN). Prefer the login that matches the type of the form michael@0: // (user+pass or pass-only) when there's exactly one that matches. michael@0: let matchingLogins; michael@0: if (usernameField) michael@0: matchingLogins = logins.filter(function(l) l.username); michael@0: else michael@0: matchingLogins = logins.filter(function(l) !l.username); michael@0: if (matchingLogins.length == 1) { michael@0: selectedLogin = matchingLogins[0]; michael@0: } else { michael@0: didntFillReason = "multipleLogins"; michael@0: log("Multiple logins for form, so not filling any."); michael@0: } michael@0: } michael@0: michael@0: var didFillForm = false; michael@0: if (selectedLogin && autofillForm && !isFormDisabled) { michael@0: // Fill the form michael@0: // Don't modify the username field if it's disabled or readOnly so we preserve its case. michael@0: if (usernameField && !(usernameField.disabled || usernameField.readOnly)) michael@0: usernameField.value = selectedLogin.username; michael@0: passwordField.value = selectedLogin.password; michael@0: didFillForm = true; michael@0: } else if (selectedLogin && !autofillForm) { michael@0: // For when autofillForm is false, but we still have the information michael@0: // to fill a form, we notify observers. michael@0: didntFillReason = "noAutofillForms"; michael@0: Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); michael@0: log("autofillForms=false but form can be filled; notified observers"); michael@0: } else if (selectedLogin && isFormDisabled) { michael@0: // For when autocomplete is off, but we still have the information michael@0: // to fill a form, we notify observers. michael@0: didntFillReason = "autocompleteOff"; michael@0: Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); michael@0: log("autocomplete=off but form can be filled; notified observers"); michael@0: } michael@0: michael@0: this._notifyFoundLogins(didntFillReason, usernameField, passwordField, michael@0: foundLogins, selectedLogin); michael@0: michael@0: return [didFillForm, foundLogins]; michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Notify observers about an attempt to fill a form that resulted in some michael@0: * saved logins being found for the form. michael@0: * michael@0: * This does not get called if the login manager attempts to fill a form michael@0: * but does not find any saved logins. It does, however, get called when michael@0: * the login manager does find saved logins whether or not it actually michael@0: * fills the form with one of them. michael@0: * michael@0: * @param didntFillReason {String} michael@0: * the reason the login manager didn't fill the form, if any; michael@0: * if the value of this parameter is null, then the form was filled; michael@0: * otherwise, this parameter will be one of these values: michael@0: * existingUsername: the username field already contains a username michael@0: * that doesn't match any stored usernames michael@0: * existingPassword: the password field already contains a password michael@0: * autocompleteOff: autocomplete has been disabled for the form michael@0: * or its username or password fields michael@0: * multipleLogins: we have multiple logins for the form michael@0: * noAutofillForms: the autofillForms pref is set to false michael@0: * michael@0: * @param usernameField {HTMLInputElement} michael@0: * the username field detected by the login manager, if any; michael@0: * otherwise null michael@0: * michael@0: * @param passwordField {HTMLInputElement} michael@0: * the password field detected by the login manager michael@0: * michael@0: * @param foundLogins {Array} michael@0: * an array of nsILoginInfos that can be used to fill the form michael@0: * michael@0: * @param selectedLogin {nsILoginInfo} michael@0: * the nsILoginInfo that was/would be used to fill the form, if any; michael@0: * otherwise null; whether or not it was actually used depends on michael@0: * the value of the didntFillReason parameter michael@0: */ michael@0: _notifyFoundLogins : function (didntFillReason, usernameField, michael@0: passwordField, foundLogins, selectedLogin) { michael@0: // We need .setProperty(), which is a method on the original michael@0: // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2 michael@0: // doesn't inherit from that, so the additional QI is needed. michael@0: let formInfo = Cc["@mozilla.org/hash-property-bag;1"]. michael@0: createInstance(Ci.nsIWritablePropertyBag2). michael@0: QueryInterface(Ci.nsIWritablePropertyBag); michael@0: michael@0: formInfo.setPropertyAsACString("didntFillReason", didntFillReason); michael@0: formInfo.setPropertyAsInterface("usernameField", usernameField); michael@0: formInfo.setPropertyAsInterface("passwordField", passwordField); michael@0: formInfo.setProperty("foundLogins", foundLogins.concat()); michael@0: formInfo.setPropertyAsInterface("selectedLogin", selectedLogin); michael@0: michael@0: Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null); michael@0: }, michael@0: michael@0: }; michael@0: michael@0: michael@0: michael@0: michael@0: LoginUtils = { michael@0: /* michael@0: * _getPasswordOrigin michael@0: * michael@0: * Get the parts of the URL we want for identification. michael@0: */ michael@0: _getPasswordOrigin : function (uriString, allowJS) { michael@0: var realm = ""; michael@0: try { michael@0: var uri = Services.io.newURI(uriString, null, null); michael@0: michael@0: if (allowJS && uri.scheme == "javascript") michael@0: return "javascript:" michael@0: michael@0: realm = uri.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: var port = uri.port; michael@0: if (port != -1) { michael@0: var handler = Services.io.getProtocolHandler(uri.scheme); michael@0: if (port != handler.defaultPort) michael@0: realm += ":" + port; michael@0: } michael@0: michael@0: } catch (e) { michael@0: // bug 159484 - disallow url types that don't support a hostPort. michael@0: // (although we handle "javascript:..." as a special case above.) michael@0: log("Couldn't parse origin for", uriString); michael@0: realm = null; michael@0: } michael@0: michael@0: return realm; michael@0: }, michael@0: michael@0: _getActionOrigin : function (form) { michael@0: var uriString = form.action; michael@0: michael@0: // A blank or missing action submits to where it came from. michael@0: if (uriString == "") michael@0: uriString = form.baseURI; // ala bug 297761 michael@0: michael@0: return this._getPasswordOrigin(uriString, true); michael@0: }, michael@0: michael@0: };