1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,813 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +this.EXPORTED_SYMBOLS = ["LoginManagerContent"]; 1.9 + 1.10 +const Ci = Components.interfaces; 1.11 +const Cr = Components.results; 1.12 +const Cc = Components.classes; 1.13 +const Cu = Components.utils; 1.14 + 1.15 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.16 +Cu.import("resource://gre/modules/Services.jsm"); 1.17 +Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.18 + 1.19 +// These mirror signon.* prefs. 1.20 +var gEnabled, gDebug, gAutofillForms, gStoreWhenAutocompleteOff; 1.21 + 1.22 +function log(...pieces) { 1.23 + function generateLogMessage(args) { 1.24 + let strings = ['Login Manager (content):']; 1.25 + 1.26 + args.forEach(function(arg) { 1.27 + if (typeof arg === 'string') { 1.28 + strings.push(arg); 1.29 + } else if (typeof arg === 'undefined') { 1.30 + strings.push('undefined'); 1.31 + } else if (arg === null) { 1.32 + strings.push('null'); 1.33 + } else { 1.34 + try { 1.35 + strings.push(JSON.stringify(arg, null, 2)); 1.36 + } catch(err) { 1.37 + strings.push("<<something>>"); 1.38 + } 1.39 + } 1.40 + }); 1.41 + return strings.join(' '); 1.42 + } 1.43 + 1.44 + if (!gDebug) 1.45 + return; 1.46 + 1.47 + let message = generateLogMessage(pieces); 1.48 + dump(message + "\n"); 1.49 + Services.console.logStringMessage(message); 1.50 +} 1.51 + 1.52 + 1.53 +var observer = { 1.54 + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 1.55 + Ci.nsIFormSubmitObserver, 1.56 + Ci.nsISupportsWeakReference]), 1.57 + 1.58 + // nsIFormSubmitObserver 1.59 + notify : function (formElement, aWindow, actionURI) { 1.60 + log("observer notified for form submission."); 1.61 + 1.62 + // We're invoked before the content's |onsubmit| handlers, so we 1.63 + // can grab form data before it might be modified (see bug 257781). 1.64 + 1.65 + try { 1.66 + LoginManagerContent._onFormSubmit(formElement); 1.67 + } catch (e) { 1.68 + log("Caught error in onFormSubmit:", e); 1.69 + } 1.70 + 1.71 + return true; // Always return true, or form submit will be canceled. 1.72 + }, 1.73 + 1.74 + onPrefChange : function() { 1.75 + gDebug = Services.prefs.getBoolPref("signon.debug"); 1.76 + gEnabled = Services.prefs.getBoolPref("signon.rememberSignons"); 1.77 + gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms"); 1.78 + gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff"); 1.79 + }, 1.80 +}; 1.81 + 1.82 +Services.obs.addObserver(observer, "earlyformsubmit", false); 1.83 +var prefBranch = Services.prefs.getBranch("signon."); 1.84 +prefBranch.addObserver("", observer.onPrefChange, false); 1.85 + 1.86 +observer.onPrefChange(); // read initial values 1.87 + 1.88 + 1.89 +var LoginManagerContent = { 1.90 + 1.91 + __formFillService : null, // FormFillController, for username autocompleting 1.92 + get _formFillService() { 1.93 + if (!this.__formFillService) 1.94 + this.__formFillService = 1.95 + Cc["@mozilla.org/satchel/form-fill-controller;1"]. 1.96 + getService(Ci.nsIFormFillController); 1.97 + return this.__formFillService; 1.98 + }, 1.99 + 1.100 + 1.101 + onFormPassword: function (event) { 1.102 + if (!event.isTrusted) 1.103 + return; 1.104 + 1.105 + if (!gEnabled) 1.106 + return; 1.107 + 1.108 + let form = event.target; 1.109 + let doc = form.ownerDocument; 1.110 + 1.111 + log("onFormPassword for", doc.documentURI); 1.112 + 1.113 + // If there are no logins for this site, bail out now. 1.114 + let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI); 1.115 + if (!Services.logins.countLogins(formOrigin, "", null)) 1.116 + return; 1.117 + 1.118 + // If we're currently displaying a master password prompt, defer 1.119 + // processing this form until the user handles the prompt. 1.120 + if (Services.logins.uiBusy) { 1.121 + log("deferring onFormPassword for", doc.documentURI); 1.122 + let self = this; 1.123 + let observer = { 1.124 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), 1.125 + 1.126 + observe: function (subject, topic, data) { 1.127 + log("Got deferred onFormPassword notification:", topic); 1.128 + // Only run observer once. 1.129 + Services.obs.removeObserver(this, "passwordmgr-crypto-login"); 1.130 + Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled"); 1.131 + if (topic == "passwordmgr-crypto-loginCanceled") 1.132 + return; 1.133 + self.onFormPassword(event); 1.134 + }, 1.135 + handleEvent : function (event) { 1.136 + // Not expected to be called 1.137 + } 1.138 + }; 1.139 + // Trickyness follows: We want an observer, but don't want it to 1.140 + // cause leaks. So add the observer with a weak reference, and use 1.141 + // a dummy event listener (a strong reference) to keep it alive 1.142 + // until the form is destroyed. 1.143 + Services.obs.addObserver(observer, "passwordmgr-crypto-login", true); 1.144 + Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true); 1.145 + form.addEventListener("mozCleverClosureHack", observer); 1.146 + return; 1.147 + } 1.148 + 1.149 + let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isWindowPrivate(doc.defaultView); 1.150 + 1.151 + this._fillForm(form, autofillForm, false, false, null); 1.152 + }, 1.153 + 1.154 + 1.155 + /* 1.156 + * onUsernameInput 1.157 + * 1.158 + * Listens for DOMAutoComplete and blur events on an input field. 1.159 + */ 1.160 + onUsernameInput : function(event) { 1.161 + if (!event.isTrusted) 1.162 + return; 1.163 + 1.164 + if (!gEnabled) 1.165 + return; 1.166 + 1.167 + var acInputField = event.target; 1.168 + 1.169 + // This is probably a bit over-conservatative. 1.170 + if (!(acInputField.ownerDocument instanceof Ci.nsIDOMHTMLDocument)) 1.171 + return; 1.172 + 1.173 + if (!this._isUsernameFieldType(acInputField)) 1.174 + return; 1.175 + 1.176 + var acForm = acInputField.form; 1.177 + if (!acForm) 1.178 + return; 1.179 + 1.180 + // If the username is blank, bail out now -- we don't want 1.181 + // fillForm() to try filling in a login without a username 1.182 + // to filter on (bug 471906). 1.183 + if (!acInputField.value) 1.184 + return; 1.185 + 1.186 + log("onUsernameInput from", event.type); 1.187 + 1.188 + // Make sure the username field fillForm will use is the 1.189 + // same field as the autocomplete was activated on. 1.190 + var [usernameField, passwordField, ignored] = 1.191 + this._getFormFields(acForm, false); 1.192 + if (usernameField == acInputField && passwordField) { 1.193 + // If the user has a master password but itsn't logged in, bail 1.194 + // out now to prevent annoying prompts. 1.195 + if (!Services.logins.isLoggedIn) 1.196 + return; 1.197 + 1.198 + this._fillForm(acForm, true, true, true, null); 1.199 + } else { 1.200 + // Ignore the event, it's for some input we don't care about. 1.201 + } 1.202 + }, 1.203 + 1.204 + 1.205 + /* 1.206 + * _getPasswordFields 1.207 + * 1.208 + * Returns an array of password field elements for the specified form. 1.209 + * If no pw fields are found, or if more than 3 are found, then null 1.210 + * is returned. 1.211 + * 1.212 + * skipEmptyFields can be set to ignore password fields with no value. 1.213 + */ 1.214 + _getPasswordFields : function (form, skipEmptyFields) { 1.215 + // Locate the password fields in the form. 1.216 + var pwFields = []; 1.217 + for (var i = 0; i < form.elements.length; i++) { 1.218 + var element = form.elements[i]; 1.219 + if (!(element instanceof Ci.nsIDOMHTMLInputElement) || 1.220 + element.type != "password") 1.221 + continue; 1.222 + 1.223 + if (skipEmptyFields && !element.value) 1.224 + continue; 1.225 + 1.226 + pwFields[pwFields.length] = { 1.227 + index : i, 1.228 + element : element 1.229 + }; 1.230 + } 1.231 + 1.232 + // If too few or too many fields, bail out. 1.233 + if (pwFields.length == 0) { 1.234 + log("(form ignored -- no password fields.)"); 1.235 + return null; 1.236 + } else if (pwFields.length > 3) { 1.237 + log("(form ignored -- too many password fields. [ got ", 1.238 + pwFields.length, "])"); 1.239 + return null; 1.240 + } 1.241 + 1.242 + return pwFields; 1.243 + }, 1.244 + 1.245 + 1.246 + _isUsernameFieldType: function(element) { 1.247 + if (!(element instanceof Ci.nsIDOMHTMLInputElement)) 1.248 + return false; 1.249 + 1.250 + let fieldType = (element.hasAttribute("type") ? 1.251 + element.getAttribute("type").toLowerCase() : 1.252 + element.type); 1.253 + if (fieldType == "text" || 1.254 + fieldType == "email" || 1.255 + fieldType == "url" || 1.256 + fieldType == "tel" || 1.257 + fieldType == "number") { 1.258 + return true; 1.259 + } 1.260 + return false; 1.261 + }, 1.262 + 1.263 + 1.264 + /* 1.265 + * _getFormFields 1.266 + * 1.267 + * Returns the username and password fields found in the form. 1.268 + * Can handle complex forms by trying to figure out what the 1.269 + * relevant fields are. 1.270 + * 1.271 + * Returns: [usernameField, newPasswordField, oldPasswordField] 1.272 + * 1.273 + * usernameField may be null. 1.274 + * newPasswordField will always be non-null. 1.275 + * oldPasswordField may be null. If null, newPasswordField is just 1.276 + * "theLoginField". If not null, the form is apparently a 1.277 + * change-password field, with oldPasswordField containing the password 1.278 + * that is being changed. 1.279 + */ 1.280 + _getFormFields : function (form, isSubmission) { 1.281 + var usernameField = null; 1.282 + 1.283 + // Locate the password field(s) in the form. Up to 3 supported. 1.284 + // If there's no password field, there's nothing for us to do. 1.285 + var pwFields = this._getPasswordFields(form, isSubmission); 1.286 + if (!pwFields) 1.287 + return [null, null, null]; 1.288 + 1.289 + 1.290 + // Locate the username field in the form by searching backwards 1.291 + // from the first passwordfield, assume the first text field is the 1.292 + // username. We might not find a username field if the user is 1.293 + // already logged in to the site. 1.294 + for (var i = pwFields[0].index - 1; i >= 0; i--) { 1.295 + var element = form.elements[i]; 1.296 + if (this._isUsernameFieldType(element)) { 1.297 + usernameField = element; 1.298 + break; 1.299 + } 1.300 + } 1.301 + 1.302 + if (!usernameField) 1.303 + log("(form -- no username field found)"); 1.304 + 1.305 + 1.306 + // If we're not submitting a form (it's a page load), there are no 1.307 + // password field values for us to use for identifying fields. So, 1.308 + // just assume the first password field is the one to be filled in. 1.309 + if (!isSubmission || pwFields.length == 1) 1.310 + return [usernameField, pwFields[0].element, null]; 1.311 + 1.312 + 1.313 + // Try to figure out WTF is in the form based on the password values. 1.314 + var oldPasswordField, newPasswordField; 1.315 + var pw1 = pwFields[0].element.value; 1.316 + var pw2 = pwFields[1].element.value; 1.317 + var pw3 = (pwFields[2] ? pwFields[2].element.value : null); 1.318 + 1.319 + if (pwFields.length == 3) { 1.320 + // Look for two identical passwords, that's the new password 1.321 + 1.322 + if (pw1 == pw2 && pw2 == pw3) { 1.323 + // All 3 passwords the same? Weird! Treat as if 1 pw field. 1.324 + newPasswordField = pwFields[0].element; 1.325 + oldPasswordField = null; 1.326 + } else if (pw1 == pw2) { 1.327 + newPasswordField = pwFields[0].element; 1.328 + oldPasswordField = pwFields[2].element; 1.329 + } else if (pw2 == pw3) { 1.330 + oldPasswordField = pwFields[0].element; 1.331 + newPasswordField = pwFields[2].element; 1.332 + } else if (pw1 == pw3) { 1.333 + // A bit odd, but could make sense with the right page layout. 1.334 + newPasswordField = pwFields[0].element; 1.335 + oldPasswordField = pwFields[1].element; 1.336 + } else { 1.337 + // We can't tell which of the 3 passwords should be saved. 1.338 + log("(form ignored -- all 3 pw fields differ)"); 1.339 + return [null, null, null]; 1.340 + } 1.341 + } else { // pwFields.length == 2 1.342 + if (pw1 == pw2) { 1.343 + // Treat as if 1 pw field 1.344 + newPasswordField = pwFields[0].element; 1.345 + oldPasswordField = null; 1.346 + } else { 1.347 + // Just assume that the 2nd password is the new password 1.348 + oldPasswordField = pwFields[0].element; 1.349 + newPasswordField = pwFields[1].element; 1.350 + } 1.351 + } 1.352 + 1.353 + return [usernameField, newPasswordField, oldPasswordField]; 1.354 + }, 1.355 + 1.356 + 1.357 + /* 1.358 + * _isAutoCompleteDisabled 1.359 + * 1.360 + * Returns true if the page requests autocomplete be disabled for the 1.361 + * specified form input. 1.362 + */ 1.363 + _isAutocompleteDisabled : function (element) { 1.364 + if (element && element.hasAttribute("autocomplete") && 1.365 + element.getAttribute("autocomplete").toLowerCase() == "off") 1.366 + return true; 1.367 + 1.368 + return false; 1.369 + }, 1.370 + 1.371 + 1.372 + /* 1.373 + * _onFormSubmit 1.374 + * 1.375 + * Called by the our observer when notified of a form submission. 1.376 + * [Note that this happens before any DOM onsubmit handlers are invoked.] 1.377 + * Looks for a password change in the submitted form, so we can update 1.378 + * our stored password. 1.379 + */ 1.380 + _onFormSubmit : function (form) { 1.381 + 1.382 + // For E10S this will need to move. 1.383 + function getPrompter(aWindow) { 1.384 + var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"]. 1.385 + createInstance(Ci.nsILoginManagerPrompter); 1.386 + prompterSvc.init(aWindow); 1.387 + return prompterSvc; 1.388 + } 1.389 + 1.390 + var doc = form.ownerDocument; 1.391 + var win = doc.defaultView; 1.392 + 1.393 + if (PrivateBrowsingUtils.isWindowPrivate(win)) { 1.394 + // We won't do anything in private browsing mode anyway, 1.395 + // so there's no need to perform further checks. 1.396 + log("(form submission ignored in private browsing mode)"); 1.397 + return; 1.398 + } 1.399 + 1.400 + // If password saving is disabled (globally or for host), bail out now. 1.401 + if (!gEnabled) 1.402 + return; 1.403 + 1.404 + var hostname = LoginUtils._getPasswordOrigin(doc.documentURI); 1.405 + if (!hostname) { 1.406 + log("(form submission ignored -- invalid hostname)"); 1.407 + return; 1.408 + } 1.409 + 1.410 + // Somewhat gross hack - we don't want to show the "remember password" 1.411 + // notification on about:accounts for Firefox. 1.412 + let topWin = win.top; 1.413 + if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) { 1.414 + log("(form submission ignored -- about:accounts)"); 1.415 + return; 1.416 + } 1.417 + 1.418 + var formSubmitURL = LoginUtils._getActionOrigin(form) 1.419 + if (!Services.logins.getLoginSavingEnabled(hostname)) { 1.420 + log("(form submission ignored -- saving is disabled for:", hostname, ")"); 1.421 + return; 1.422 + } 1.423 + 1.424 + 1.425 + // Get the appropriate fields from the form. 1.426 + var [usernameField, newPasswordField, oldPasswordField] = 1.427 + this._getFormFields(form, true); 1.428 + 1.429 + // Need at least 1 valid password field to do anything. 1.430 + if (newPasswordField == null) 1.431 + return; 1.432 + 1.433 + // Check for autocomplete=off attribute. We don't use it to prevent 1.434 + // autofilling (for existing logins), but won't save logins when it's 1.435 + // present and the storeWhenAutocompleteOff pref is false. 1.436 + // XXX spin out a bug that we don't update timeLastUsed in this case? 1.437 + if ((this._isAutocompleteDisabled(form) || 1.438 + this._isAutocompleteDisabled(usernameField) || 1.439 + this._isAutocompleteDisabled(newPasswordField) || 1.440 + this._isAutocompleteDisabled(oldPasswordField)) && 1.441 + !gStoreWhenAutocompleteOff) { 1.442 + log("(form submission ignored -- autocomplete=off found)"); 1.443 + return; 1.444 + } 1.445 + 1.446 + 1.447 + var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"]. 1.448 + createInstance(Ci.nsILoginInfo); 1.449 + formLogin.init(hostname, formSubmitURL, null, 1.450 + (usernameField ? usernameField.value : ""), 1.451 + newPasswordField.value, 1.452 + (usernameField ? usernameField.name : ""), 1.453 + newPasswordField.name); 1.454 + 1.455 + // If we didn't find a username field, but seem to be changing a 1.456 + // password, allow the user to select from a list of applicable 1.457 + // logins to update the password for. 1.458 + if (!usernameField && oldPasswordField) { 1.459 + 1.460 + var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null); 1.461 + 1.462 + if (logins.length == 0) { 1.463 + // Could prompt to save this as a new password-only login. 1.464 + // This seems uncommon, and might be wrong, so ignore. 1.465 + log("(no logins for this host -- pwchange ignored)"); 1.466 + return; 1.467 + } 1.468 + 1.469 + var prompter = getPrompter(win); 1.470 + 1.471 + if (logins.length == 1) { 1.472 + var oldLogin = logins[0]; 1.473 + formLogin.username = oldLogin.username; 1.474 + formLogin.usernameField = oldLogin.usernameField; 1.475 + 1.476 + prompter.promptToChangePassword(oldLogin, formLogin); 1.477 + } else { 1.478 + prompter.promptToChangePasswordWithUsernames( 1.479 + logins, logins.length, formLogin); 1.480 + } 1.481 + 1.482 + return; 1.483 + } 1.484 + 1.485 + 1.486 + // Look for an existing login that matches the form login. 1.487 + var existingLogin = null; 1.488 + var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null); 1.489 + 1.490 + for (var i = 0; i < logins.length; i++) { 1.491 + var same, login = logins[i]; 1.492 + 1.493 + // If one login has a username but the other doesn't, ignore 1.494 + // the username when comparing and only match if they have the 1.495 + // same password. Otherwise, compare the logins and match even 1.496 + // if the passwords differ. 1.497 + if (!login.username && formLogin.username) { 1.498 + var restoreMe = formLogin.username; 1.499 + formLogin.username = ""; 1.500 + same = formLogin.matches(login, false); 1.501 + formLogin.username = restoreMe; 1.502 + } else if (!formLogin.username && login.username) { 1.503 + formLogin.username = login.username; 1.504 + same = formLogin.matches(login, false); 1.505 + formLogin.username = ""; // we know it's always blank. 1.506 + } else { 1.507 + same = formLogin.matches(login, true); 1.508 + } 1.509 + 1.510 + if (same) { 1.511 + existingLogin = login; 1.512 + break; 1.513 + } 1.514 + } 1.515 + 1.516 + if (existingLogin) { 1.517 + log("Found an existing login matching this form submission"); 1.518 + 1.519 + // Change password if needed. 1.520 + if (existingLogin.password != formLogin.password) { 1.521 + log("...passwords differ, prompting to change."); 1.522 + prompter = getPrompter(win); 1.523 + prompter.promptToChangePassword(existingLogin, formLogin); 1.524 + } else { 1.525 + // Update the lastUsed timestamp. 1.526 + var propBag = Cc["@mozilla.org/hash-property-bag;1"]. 1.527 + createInstance(Ci.nsIWritablePropertyBag); 1.528 + propBag.setProperty("timeLastUsed", Date.now()); 1.529 + propBag.setProperty("timesUsedIncrement", 1); 1.530 + Services.logins.modifyLogin(existingLogin, propBag); 1.531 + } 1.532 + 1.533 + return; 1.534 + } 1.535 + 1.536 + 1.537 + // Prompt user to save login (via dialog or notification bar) 1.538 + prompter = getPrompter(win); 1.539 + prompter.promptToSavePassword(formLogin); 1.540 + }, 1.541 + 1.542 + 1.543 + /* 1.544 + * _fillform 1.545 + * 1.546 + * Fill the form with login information if we can find it. This will find 1.547 + * an array of logins if not given any, otherwise it will use the logins 1.548 + * passed in. The logins are returned so they can be reused for 1.549 + * optimization. Success of action is also returned in format 1.550 + * [success, foundLogins]. autofillForm denotes if we should fill the form 1.551 + * in automatically, ignoreAutocomplete denotes if we should ignore 1.552 + * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo 1.553 + * for optimization 1.554 + */ 1.555 + _fillForm : function (form, autofillForm, ignoreAutocomplete, 1.556 + clobberPassword, foundLogins) { 1.557 + // Heuristically determine what the user/pass fields are 1.558 + // We do this before checking to see if logins are stored, 1.559 + // so that the user isn't prompted for a master password 1.560 + // without need. 1.561 + var [usernameField, passwordField, ignored] = 1.562 + this._getFormFields(form, false); 1.563 + 1.564 + // Need a valid password field to do anything. 1.565 + if (passwordField == null) 1.566 + return [false, foundLogins]; 1.567 + 1.568 + // If the password field is disabled or read-only, there's nothing to do. 1.569 + if (passwordField.disabled || passwordField.readOnly) { 1.570 + log("not filling form, password field disabled or read-only"); 1.571 + return [false, foundLogins]; 1.572 + } 1.573 + 1.574 + // Need to get a list of logins if we weren't given them 1.575 + if (foundLogins == null) { 1.576 + var formOrigin = 1.577 + LoginUtils._getPasswordOrigin(form.ownerDocument.documentURI); 1.578 + var actionOrigin = LoginUtils._getActionOrigin(form); 1.579 + foundLogins = Services.logins.findLogins({}, formOrigin, actionOrigin, null); 1.580 + log("found", foundLogins.length, "matching logins."); 1.581 + } else { 1.582 + log("reusing logins from last form."); 1.583 + } 1.584 + 1.585 + // Discard logins which have username/password values that don't 1.586 + // fit into the fields (as specified by the maxlength attribute). 1.587 + // The user couldn't enter these values anyway, and it helps 1.588 + // with sites that have an extra PIN to be entered (bug 391514) 1.589 + var maxUsernameLen = Number.MAX_VALUE; 1.590 + var maxPasswordLen = Number.MAX_VALUE; 1.591 + 1.592 + // If attribute wasn't set, default is -1. 1.593 + if (usernameField && usernameField.maxLength >= 0) 1.594 + maxUsernameLen = usernameField.maxLength; 1.595 + if (passwordField.maxLength >= 0) 1.596 + maxPasswordLen = passwordField.maxLength; 1.597 + 1.598 + var logins = foundLogins.filter(function (l) { 1.599 + var fit = (l.username.length <= maxUsernameLen && 1.600 + l.password.length <= maxPasswordLen); 1.601 + if (!fit) 1.602 + log("Ignored", l.username, "login: won't fit"); 1.603 + 1.604 + return fit; 1.605 + }, this); 1.606 + 1.607 + 1.608 + // Nothing to do if we have no matching logins available. 1.609 + if (logins.length == 0) 1.610 + return [false, foundLogins]; 1.611 + 1.612 + 1.613 + // The reason we didn't end up filling the form, if any. We include 1.614 + // this in the formInfo object we send with the passwordmgr-found-logins 1.615 + // notification. See the _notifyFoundLogins docs for possible values. 1.616 + var didntFillReason = null; 1.617 + 1.618 + // Attach autocomplete stuff to the username field, if we have 1.619 + // one. This is normally used to select from multiple accounts, 1.620 + // but even with one account we should refill if the user edits. 1.621 + if (usernameField) 1.622 + this._formFillService.markAsLoginManagerField(usernameField); 1.623 + 1.624 + // Don't clobber an existing password. 1.625 + if (passwordField.value && !clobberPassword) { 1.626 + didntFillReason = "existingPassword"; 1.627 + this._notifyFoundLogins(didntFillReason, usernameField, 1.628 + passwordField, foundLogins, null); 1.629 + return [false, foundLogins]; 1.630 + } 1.631 + 1.632 + // If the form has an autocomplete=off attribute in play, don't 1.633 + // fill in the login automatically. We check this after attaching 1.634 + // the autocomplete stuff to the username field, so the user can 1.635 + // still manually select a login to be filled in. 1.636 + var isFormDisabled = false; 1.637 + if (!ignoreAutocomplete && 1.638 + (this._isAutocompleteDisabled(form) || 1.639 + this._isAutocompleteDisabled(usernameField) || 1.640 + this._isAutocompleteDisabled(passwordField))) { 1.641 + 1.642 + isFormDisabled = true; 1.643 + log("form not filled, has autocomplete=off"); 1.644 + } 1.645 + 1.646 + // Variable such that we reduce code duplication and can be sure we 1.647 + // should be firing notifications if and only if we can fill the form. 1.648 + var selectedLogin = null; 1.649 + 1.650 + if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) { 1.651 + // If username was specified in the field, it's disabled or it's readOnly, only fill in the 1.652 + // password if we find a matching login. 1.653 + var username = usernameField.value.toLowerCase(); 1.654 + 1.655 + let matchingLogins = logins.filter(function(l) 1.656 + l.username.toLowerCase() == username); 1.657 + if (matchingLogins.length) { 1.658 + selectedLogin = matchingLogins[0]; 1.659 + } else { 1.660 + didntFillReason = "existingUsername"; 1.661 + log("Password not filled. None of the stored logins match the username already present."); 1.662 + } 1.663 + } else if (logins.length == 1) { 1.664 + selectedLogin = logins[0]; 1.665 + } else { 1.666 + // We have multiple logins. Handle a special case here, for sites 1.667 + // which have a normal user+pass login *and* a password-only login 1.668 + // (eg, a PIN). Prefer the login that matches the type of the form 1.669 + // (user+pass or pass-only) when there's exactly one that matches. 1.670 + let matchingLogins; 1.671 + if (usernameField) 1.672 + matchingLogins = logins.filter(function(l) l.username); 1.673 + else 1.674 + matchingLogins = logins.filter(function(l) !l.username); 1.675 + if (matchingLogins.length == 1) { 1.676 + selectedLogin = matchingLogins[0]; 1.677 + } else { 1.678 + didntFillReason = "multipleLogins"; 1.679 + log("Multiple logins for form, so not filling any."); 1.680 + } 1.681 + } 1.682 + 1.683 + var didFillForm = false; 1.684 + if (selectedLogin && autofillForm && !isFormDisabled) { 1.685 + // Fill the form 1.686 + // Don't modify the username field if it's disabled or readOnly so we preserve its case. 1.687 + if (usernameField && !(usernameField.disabled || usernameField.readOnly)) 1.688 + usernameField.value = selectedLogin.username; 1.689 + passwordField.value = selectedLogin.password; 1.690 + didFillForm = true; 1.691 + } else if (selectedLogin && !autofillForm) { 1.692 + // For when autofillForm is false, but we still have the information 1.693 + // to fill a form, we notify observers. 1.694 + didntFillReason = "noAutofillForms"; 1.695 + Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); 1.696 + log("autofillForms=false but form can be filled; notified observers"); 1.697 + } else if (selectedLogin && isFormDisabled) { 1.698 + // For when autocomplete is off, but we still have the information 1.699 + // to fill a form, we notify observers. 1.700 + didntFillReason = "autocompleteOff"; 1.701 + Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); 1.702 + log("autocomplete=off but form can be filled; notified observers"); 1.703 + } 1.704 + 1.705 + this._notifyFoundLogins(didntFillReason, usernameField, passwordField, 1.706 + foundLogins, selectedLogin); 1.707 + 1.708 + return [didFillForm, foundLogins]; 1.709 + }, 1.710 + 1.711 + 1.712 + /** 1.713 + * Notify observers about an attempt to fill a form that resulted in some 1.714 + * saved logins being found for the form. 1.715 + * 1.716 + * This does not get called if the login manager attempts to fill a form 1.717 + * but does not find any saved logins. It does, however, get called when 1.718 + * the login manager does find saved logins whether or not it actually 1.719 + * fills the form with one of them. 1.720 + * 1.721 + * @param didntFillReason {String} 1.722 + * the reason the login manager didn't fill the form, if any; 1.723 + * if the value of this parameter is null, then the form was filled; 1.724 + * otherwise, this parameter will be one of these values: 1.725 + * existingUsername: the username field already contains a username 1.726 + * that doesn't match any stored usernames 1.727 + * existingPassword: the password field already contains a password 1.728 + * autocompleteOff: autocomplete has been disabled for the form 1.729 + * or its username or password fields 1.730 + * multipleLogins: we have multiple logins for the form 1.731 + * noAutofillForms: the autofillForms pref is set to false 1.732 + * 1.733 + * @param usernameField {HTMLInputElement} 1.734 + * the username field detected by the login manager, if any; 1.735 + * otherwise null 1.736 + * 1.737 + * @param passwordField {HTMLInputElement} 1.738 + * the password field detected by the login manager 1.739 + * 1.740 + * @param foundLogins {Array} 1.741 + * an array of nsILoginInfos that can be used to fill the form 1.742 + * 1.743 + * @param selectedLogin {nsILoginInfo} 1.744 + * the nsILoginInfo that was/would be used to fill the form, if any; 1.745 + * otherwise null; whether or not it was actually used depends on 1.746 + * the value of the didntFillReason parameter 1.747 + */ 1.748 + _notifyFoundLogins : function (didntFillReason, usernameField, 1.749 + passwordField, foundLogins, selectedLogin) { 1.750 + // We need .setProperty(), which is a method on the original 1.751 + // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2 1.752 + // doesn't inherit from that, so the additional QI is needed. 1.753 + let formInfo = Cc["@mozilla.org/hash-property-bag;1"]. 1.754 + createInstance(Ci.nsIWritablePropertyBag2). 1.755 + QueryInterface(Ci.nsIWritablePropertyBag); 1.756 + 1.757 + formInfo.setPropertyAsACString("didntFillReason", didntFillReason); 1.758 + formInfo.setPropertyAsInterface("usernameField", usernameField); 1.759 + formInfo.setPropertyAsInterface("passwordField", passwordField); 1.760 + formInfo.setProperty("foundLogins", foundLogins.concat()); 1.761 + formInfo.setPropertyAsInterface("selectedLogin", selectedLogin); 1.762 + 1.763 + Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null); 1.764 + }, 1.765 + 1.766 +}; 1.767 + 1.768 + 1.769 + 1.770 + 1.771 +LoginUtils = { 1.772 + /* 1.773 + * _getPasswordOrigin 1.774 + * 1.775 + * Get the parts of the URL we want for identification. 1.776 + */ 1.777 + _getPasswordOrigin : function (uriString, allowJS) { 1.778 + var realm = ""; 1.779 + try { 1.780 + var uri = Services.io.newURI(uriString, null, null); 1.781 + 1.782 + if (allowJS && uri.scheme == "javascript") 1.783 + return "javascript:" 1.784 + 1.785 + realm = uri.scheme + "://" + uri.host; 1.786 + 1.787 + // If the URI explicitly specified a port, only include it when 1.788 + // it's not the default. (We never want "http://foo.com:80") 1.789 + var port = uri.port; 1.790 + if (port != -1) { 1.791 + var handler = Services.io.getProtocolHandler(uri.scheme); 1.792 + if (port != handler.defaultPort) 1.793 + realm += ":" + port; 1.794 + } 1.795 + 1.796 + } catch (e) { 1.797 + // bug 159484 - disallow url types that don't support a hostPort. 1.798 + // (although we handle "javascript:..." as a special case above.) 1.799 + log("Couldn't parse origin for", uriString); 1.800 + realm = null; 1.801 + } 1.802 + 1.803 + return realm; 1.804 + }, 1.805 + 1.806 + _getActionOrigin : function (form) { 1.807 + var uriString = form.action; 1.808 + 1.809 + // A blank or missing action submits to where it came from. 1.810 + if (uriString == "") 1.811 + uriString = form.baseURI; // ala bug 297761 1.812 + 1.813 + return this._getPasswordOrigin(uriString, true); 1.814 + }, 1.815 + 1.816 +};