toolkit/components/passwordmgr/LoginManagerContent.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 this.EXPORTED_SYMBOLS = ["LoginManagerContent"];
michael@0 6
michael@0 7 const Ci = Components.interfaces;
michael@0 8 const Cr = Components.results;
michael@0 9 const Cc = Components.classes;
michael@0 10 const Cu = Components.utils;
michael@0 11
michael@0 12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 13 Cu.import("resource://gre/modules/Services.jsm");
michael@0 14 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 15
michael@0 16 // These mirror signon.* prefs.
michael@0 17 var gEnabled, gDebug, gAutofillForms, gStoreWhenAutocompleteOff;
michael@0 18
michael@0 19 function log(...pieces) {
michael@0 20 function generateLogMessage(args) {
michael@0 21 let strings = ['Login Manager (content):'];
michael@0 22
michael@0 23 args.forEach(function(arg) {
michael@0 24 if (typeof arg === 'string') {
michael@0 25 strings.push(arg);
michael@0 26 } else if (typeof arg === 'undefined') {
michael@0 27 strings.push('undefined');
michael@0 28 } else if (arg === null) {
michael@0 29 strings.push('null');
michael@0 30 } else {
michael@0 31 try {
michael@0 32 strings.push(JSON.stringify(arg, null, 2));
michael@0 33 } catch(err) {
michael@0 34 strings.push("<<something>>");
michael@0 35 }
michael@0 36 }
michael@0 37 });
michael@0 38 return strings.join(' ');
michael@0 39 }
michael@0 40
michael@0 41 if (!gDebug)
michael@0 42 return;
michael@0 43
michael@0 44 let message = generateLogMessage(pieces);
michael@0 45 dump(message + "\n");
michael@0 46 Services.console.logStringMessage(message);
michael@0 47 }
michael@0 48
michael@0 49
michael@0 50 var observer = {
michael@0 51 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
michael@0 52 Ci.nsIFormSubmitObserver,
michael@0 53 Ci.nsISupportsWeakReference]),
michael@0 54
michael@0 55 // nsIFormSubmitObserver
michael@0 56 notify : function (formElement, aWindow, actionURI) {
michael@0 57 log("observer notified for form submission.");
michael@0 58
michael@0 59 // We're invoked before the content's |onsubmit| handlers, so we
michael@0 60 // can grab form data before it might be modified (see bug 257781).
michael@0 61
michael@0 62 try {
michael@0 63 LoginManagerContent._onFormSubmit(formElement);
michael@0 64 } catch (e) {
michael@0 65 log("Caught error in onFormSubmit:", e);
michael@0 66 }
michael@0 67
michael@0 68 return true; // Always return true, or form submit will be canceled.
michael@0 69 },
michael@0 70
michael@0 71 onPrefChange : function() {
michael@0 72 gDebug = Services.prefs.getBoolPref("signon.debug");
michael@0 73 gEnabled = Services.prefs.getBoolPref("signon.rememberSignons");
michael@0 74 gAutofillForms = Services.prefs.getBoolPref("signon.autofillForms");
michael@0 75 gStoreWhenAutocompleteOff = Services.prefs.getBoolPref("signon.storeWhenAutocompleteOff");
michael@0 76 },
michael@0 77 };
michael@0 78
michael@0 79 Services.obs.addObserver(observer, "earlyformsubmit", false);
michael@0 80 var prefBranch = Services.prefs.getBranch("signon.");
michael@0 81 prefBranch.addObserver("", observer.onPrefChange, false);
michael@0 82
michael@0 83 observer.onPrefChange(); // read initial values
michael@0 84
michael@0 85
michael@0 86 var LoginManagerContent = {
michael@0 87
michael@0 88 __formFillService : null, // FormFillController, for username autocompleting
michael@0 89 get _formFillService() {
michael@0 90 if (!this.__formFillService)
michael@0 91 this.__formFillService =
michael@0 92 Cc["@mozilla.org/satchel/form-fill-controller;1"].
michael@0 93 getService(Ci.nsIFormFillController);
michael@0 94 return this.__formFillService;
michael@0 95 },
michael@0 96
michael@0 97
michael@0 98 onFormPassword: function (event) {
michael@0 99 if (!event.isTrusted)
michael@0 100 return;
michael@0 101
michael@0 102 if (!gEnabled)
michael@0 103 return;
michael@0 104
michael@0 105 let form = event.target;
michael@0 106 let doc = form.ownerDocument;
michael@0 107
michael@0 108 log("onFormPassword for", doc.documentURI);
michael@0 109
michael@0 110 // If there are no logins for this site, bail out now.
michael@0 111 let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
michael@0 112 if (!Services.logins.countLogins(formOrigin, "", null))
michael@0 113 return;
michael@0 114
michael@0 115 // If we're currently displaying a master password prompt, defer
michael@0 116 // processing this form until the user handles the prompt.
michael@0 117 if (Services.logins.uiBusy) {
michael@0 118 log("deferring onFormPassword for", doc.documentURI);
michael@0 119 let self = this;
michael@0 120 let observer = {
michael@0 121 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
michael@0 122
michael@0 123 observe: function (subject, topic, data) {
michael@0 124 log("Got deferred onFormPassword notification:", topic);
michael@0 125 // Only run observer once.
michael@0 126 Services.obs.removeObserver(this, "passwordmgr-crypto-login");
michael@0 127 Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled");
michael@0 128 if (topic == "passwordmgr-crypto-loginCanceled")
michael@0 129 return;
michael@0 130 self.onFormPassword(event);
michael@0 131 },
michael@0 132 handleEvent : function (event) {
michael@0 133 // Not expected to be called
michael@0 134 }
michael@0 135 };
michael@0 136 // Trickyness follows: We want an observer, but don't want it to
michael@0 137 // cause leaks. So add the observer with a weak reference, and use
michael@0 138 // a dummy event listener (a strong reference) to keep it alive
michael@0 139 // until the form is destroyed.
michael@0 140 Services.obs.addObserver(observer, "passwordmgr-crypto-login", true);
michael@0 141 Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true);
michael@0 142 form.addEventListener("mozCleverClosureHack", observer);
michael@0 143 return;
michael@0 144 }
michael@0 145
michael@0 146 let autofillForm = gAutofillForms && !PrivateBrowsingUtils.isWindowPrivate(doc.defaultView);
michael@0 147
michael@0 148 this._fillForm(form, autofillForm, false, false, null);
michael@0 149 },
michael@0 150
michael@0 151
michael@0 152 /*
michael@0 153 * onUsernameInput
michael@0 154 *
michael@0 155 * Listens for DOMAutoComplete and blur events on an input field.
michael@0 156 */
michael@0 157 onUsernameInput : function(event) {
michael@0 158 if (!event.isTrusted)
michael@0 159 return;
michael@0 160
michael@0 161 if (!gEnabled)
michael@0 162 return;
michael@0 163
michael@0 164 var acInputField = event.target;
michael@0 165
michael@0 166 // This is probably a bit over-conservatative.
michael@0 167 if (!(acInputField.ownerDocument instanceof Ci.nsIDOMHTMLDocument))
michael@0 168 return;
michael@0 169
michael@0 170 if (!this._isUsernameFieldType(acInputField))
michael@0 171 return;
michael@0 172
michael@0 173 var acForm = acInputField.form;
michael@0 174 if (!acForm)
michael@0 175 return;
michael@0 176
michael@0 177 // If the username is blank, bail out now -- we don't want
michael@0 178 // fillForm() to try filling in a login without a username
michael@0 179 // to filter on (bug 471906).
michael@0 180 if (!acInputField.value)
michael@0 181 return;
michael@0 182
michael@0 183 log("onUsernameInput from", event.type);
michael@0 184
michael@0 185 // Make sure the username field fillForm will use is the
michael@0 186 // same field as the autocomplete was activated on.
michael@0 187 var [usernameField, passwordField, ignored] =
michael@0 188 this._getFormFields(acForm, false);
michael@0 189 if (usernameField == acInputField && passwordField) {
michael@0 190 // If the user has a master password but itsn't logged in, bail
michael@0 191 // out now to prevent annoying prompts.
michael@0 192 if (!Services.logins.isLoggedIn)
michael@0 193 return;
michael@0 194
michael@0 195 this._fillForm(acForm, true, true, true, null);
michael@0 196 } else {
michael@0 197 // Ignore the event, it's for some input we don't care about.
michael@0 198 }
michael@0 199 },
michael@0 200
michael@0 201
michael@0 202 /*
michael@0 203 * _getPasswordFields
michael@0 204 *
michael@0 205 * Returns an array of password field elements for the specified form.
michael@0 206 * If no pw fields are found, or if more than 3 are found, then null
michael@0 207 * is returned.
michael@0 208 *
michael@0 209 * skipEmptyFields can be set to ignore password fields with no value.
michael@0 210 */
michael@0 211 _getPasswordFields : function (form, skipEmptyFields) {
michael@0 212 // Locate the password fields in the form.
michael@0 213 var pwFields = [];
michael@0 214 for (var i = 0; i < form.elements.length; i++) {
michael@0 215 var element = form.elements[i];
michael@0 216 if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
michael@0 217 element.type != "password")
michael@0 218 continue;
michael@0 219
michael@0 220 if (skipEmptyFields && !element.value)
michael@0 221 continue;
michael@0 222
michael@0 223 pwFields[pwFields.length] = {
michael@0 224 index : i,
michael@0 225 element : element
michael@0 226 };
michael@0 227 }
michael@0 228
michael@0 229 // If too few or too many fields, bail out.
michael@0 230 if (pwFields.length == 0) {
michael@0 231 log("(form ignored -- no password fields.)");
michael@0 232 return null;
michael@0 233 } else if (pwFields.length > 3) {
michael@0 234 log("(form ignored -- too many password fields. [ got ",
michael@0 235 pwFields.length, "])");
michael@0 236 return null;
michael@0 237 }
michael@0 238
michael@0 239 return pwFields;
michael@0 240 },
michael@0 241
michael@0 242
michael@0 243 _isUsernameFieldType: function(element) {
michael@0 244 if (!(element instanceof Ci.nsIDOMHTMLInputElement))
michael@0 245 return false;
michael@0 246
michael@0 247 let fieldType = (element.hasAttribute("type") ?
michael@0 248 element.getAttribute("type").toLowerCase() :
michael@0 249 element.type);
michael@0 250 if (fieldType == "text" ||
michael@0 251 fieldType == "email" ||
michael@0 252 fieldType == "url" ||
michael@0 253 fieldType == "tel" ||
michael@0 254 fieldType == "number") {
michael@0 255 return true;
michael@0 256 }
michael@0 257 return false;
michael@0 258 },
michael@0 259
michael@0 260
michael@0 261 /*
michael@0 262 * _getFormFields
michael@0 263 *
michael@0 264 * Returns the username and password fields found in the form.
michael@0 265 * Can handle complex forms by trying to figure out what the
michael@0 266 * relevant fields are.
michael@0 267 *
michael@0 268 * Returns: [usernameField, newPasswordField, oldPasswordField]
michael@0 269 *
michael@0 270 * usernameField may be null.
michael@0 271 * newPasswordField will always be non-null.
michael@0 272 * oldPasswordField may be null. If null, newPasswordField is just
michael@0 273 * "theLoginField". If not null, the form is apparently a
michael@0 274 * change-password field, with oldPasswordField containing the password
michael@0 275 * that is being changed.
michael@0 276 */
michael@0 277 _getFormFields : function (form, isSubmission) {
michael@0 278 var usernameField = null;
michael@0 279
michael@0 280 // Locate the password field(s) in the form. Up to 3 supported.
michael@0 281 // If there's no password field, there's nothing for us to do.
michael@0 282 var pwFields = this._getPasswordFields(form, isSubmission);
michael@0 283 if (!pwFields)
michael@0 284 return [null, null, null];
michael@0 285
michael@0 286
michael@0 287 // Locate the username field in the form by searching backwards
michael@0 288 // from the first passwordfield, assume the first text field is the
michael@0 289 // username. We might not find a username field if the user is
michael@0 290 // already logged in to the site.
michael@0 291 for (var i = pwFields[0].index - 1; i >= 0; i--) {
michael@0 292 var element = form.elements[i];
michael@0 293 if (this._isUsernameFieldType(element)) {
michael@0 294 usernameField = element;
michael@0 295 break;
michael@0 296 }
michael@0 297 }
michael@0 298
michael@0 299 if (!usernameField)
michael@0 300 log("(form -- no username field found)");
michael@0 301
michael@0 302
michael@0 303 // If we're not submitting a form (it's a page load), there are no
michael@0 304 // password field values for us to use for identifying fields. So,
michael@0 305 // just assume the first password field is the one to be filled in.
michael@0 306 if (!isSubmission || pwFields.length == 1)
michael@0 307 return [usernameField, pwFields[0].element, null];
michael@0 308
michael@0 309
michael@0 310 // Try to figure out WTF is in the form based on the password values.
michael@0 311 var oldPasswordField, newPasswordField;
michael@0 312 var pw1 = pwFields[0].element.value;
michael@0 313 var pw2 = pwFields[1].element.value;
michael@0 314 var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
michael@0 315
michael@0 316 if (pwFields.length == 3) {
michael@0 317 // Look for two identical passwords, that's the new password
michael@0 318
michael@0 319 if (pw1 == pw2 && pw2 == pw3) {
michael@0 320 // All 3 passwords the same? Weird! Treat as if 1 pw field.
michael@0 321 newPasswordField = pwFields[0].element;
michael@0 322 oldPasswordField = null;
michael@0 323 } else if (pw1 == pw2) {
michael@0 324 newPasswordField = pwFields[0].element;
michael@0 325 oldPasswordField = pwFields[2].element;
michael@0 326 } else if (pw2 == pw3) {
michael@0 327 oldPasswordField = pwFields[0].element;
michael@0 328 newPasswordField = pwFields[2].element;
michael@0 329 } else if (pw1 == pw3) {
michael@0 330 // A bit odd, but could make sense with the right page layout.
michael@0 331 newPasswordField = pwFields[0].element;
michael@0 332 oldPasswordField = pwFields[1].element;
michael@0 333 } else {
michael@0 334 // We can't tell which of the 3 passwords should be saved.
michael@0 335 log("(form ignored -- all 3 pw fields differ)");
michael@0 336 return [null, null, null];
michael@0 337 }
michael@0 338 } else { // pwFields.length == 2
michael@0 339 if (pw1 == pw2) {
michael@0 340 // Treat as if 1 pw field
michael@0 341 newPasswordField = pwFields[0].element;
michael@0 342 oldPasswordField = null;
michael@0 343 } else {
michael@0 344 // Just assume that the 2nd password is the new password
michael@0 345 oldPasswordField = pwFields[0].element;
michael@0 346 newPasswordField = pwFields[1].element;
michael@0 347 }
michael@0 348 }
michael@0 349
michael@0 350 return [usernameField, newPasswordField, oldPasswordField];
michael@0 351 },
michael@0 352
michael@0 353
michael@0 354 /*
michael@0 355 * _isAutoCompleteDisabled
michael@0 356 *
michael@0 357 * Returns true if the page requests autocomplete be disabled for the
michael@0 358 * specified form input.
michael@0 359 */
michael@0 360 _isAutocompleteDisabled : function (element) {
michael@0 361 if (element && element.hasAttribute("autocomplete") &&
michael@0 362 element.getAttribute("autocomplete").toLowerCase() == "off")
michael@0 363 return true;
michael@0 364
michael@0 365 return false;
michael@0 366 },
michael@0 367
michael@0 368
michael@0 369 /*
michael@0 370 * _onFormSubmit
michael@0 371 *
michael@0 372 * Called by the our observer when notified of a form submission.
michael@0 373 * [Note that this happens before any DOM onsubmit handlers are invoked.]
michael@0 374 * Looks for a password change in the submitted form, so we can update
michael@0 375 * our stored password.
michael@0 376 */
michael@0 377 _onFormSubmit : function (form) {
michael@0 378
michael@0 379 // For E10S this will need to move.
michael@0 380 function getPrompter(aWindow) {
michael@0 381 var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
michael@0 382 createInstance(Ci.nsILoginManagerPrompter);
michael@0 383 prompterSvc.init(aWindow);
michael@0 384 return prompterSvc;
michael@0 385 }
michael@0 386
michael@0 387 var doc = form.ownerDocument;
michael@0 388 var win = doc.defaultView;
michael@0 389
michael@0 390 if (PrivateBrowsingUtils.isWindowPrivate(win)) {
michael@0 391 // We won't do anything in private browsing mode anyway,
michael@0 392 // so there's no need to perform further checks.
michael@0 393 log("(form submission ignored in private browsing mode)");
michael@0 394 return;
michael@0 395 }
michael@0 396
michael@0 397 // If password saving is disabled (globally or for host), bail out now.
michael@0 398 if (!gEnabled)
michael@0 399 return;
michael@0 400
michael@0 401 var hostname = LoginUtils._getPasswordOrigin(doc.documentURI);
michael@0 402 if (!hostname) {
michael@0 403 log("(form submission ignored -- invalid hostname)");
michael@0 404 return;
michael@0 405 }
michael@0 406
michael@0 407 // Somewhat gross hack - we don't want to show the "remember password"
michael@0 408 // notification on about:accounts for Firefox.
michael@0 409 let topWin = win.top;
michael@0 410 if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) {
michael@0 411 log("(form submission ignored -- about:accounts)");
michael@0 412 return;
michael@0 413 }
michael@0 414
michael@0 415 var formSubmitURL = LoginUtils._getActionOrigin(form)
michael@0 416 if (!Services.logins.getLoginSavingEnabled(hostname)) {
michael@0 417 log("(form submission ignored -- saving is disabled for:", hostname, ")");
michael@0 418 return;
michael@0 419 }
michael@0 420
michael@0 421
michael@0 422 // Get the appropriate fields from the form.
michael@0 423 var [usernameField, newPasswordField, oldPasswordField] =
michael@0 424 this._getFormFields(form, true);
michael@0 425
michael@0 426 // Need at least 1 valid password field to do anything.
michael@0 427 if (newPasswordField == null)
michael@0 428 return;
michael@0 429
michael@0 430 // Check for autocomplete=off attribute. We don't use it to prevent
michael@0 431 // autofilling (for existing logins), but won't save logins when it's
michael@0 432 // present and the storeWhenAutocompleteOff pref is false.
michael@0 433 // XXX spin out a bug that we don't update timeLastUsed in this case?
michael@0 434 if ((this._isAutocompleteDisabled(form) ||
michael@0 435 this._isAutocompleteDisabled(usernameField) ||
michael@0 436 this._isAutocompleteDisabled(newPasswordField) ||
michael@0 437 this._isAutocompleteDisabled(oldPasswordField)) &&
michael@0 438 !gStoreWhenAutocompleteOff) {
michael@0 439 log("(form submission ignored -- autocomplete=off found)");
michael@0 440 return;
michael@0 441 }
michael@0 442
michael@0 443
michael@0 444 var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
michael@0 445 createInstance(Ci.nsILoginInfo);
michael@0 446 formLogin.init(hostname, formSubmitURL, null,
michael@0 447 (usernameField ? usernameField.value : ""),
michael@0 448 newPasswordField.value,
michael@0 449 (usernameField ? usernameField.name : ""),
michael@0 450 newPasswordField.name);
michael@0 451
michael@0 452 // If we didn't find a username field, but seem to be changing a
michael@0 453 // password, allow the user to select from a list of applicable
michael@0 454 // logins to update the password for.
michael@0 455 if (!usernameField && oldPasswordField) {
michael@0 456
michael@0 457 var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null);
michael@0 458
michael@0 459 if (logins.length == 0) {
michael@0 460 // Could prompt to save this as a new password-only login.
michael@0 461 // This seems uncommon, and might be wrong, so ignore.
michael@0 462 log("(no logins for this host -- pwchange ignored)");
michael@0 463 return;
michael@0 464 }
michael@0 465
michael@0 466 var prompter = getPrompter(win);
michael@0 467
michael@0 468 if (logins.length == 1) {
michael@0 469 var oldLogin = logins[0];
michael@0 470 formLogin.username = oldLogin.username;
michael@0 471 formLogin.usernameField = oldLogin.usernameField;
michael@0 472
michael@0 473 prompter.promptToChangePassword(oldLogin, formLogin);
michael@0 474 } else {
michael@0 475 prompter.promptToChangePasswordWithUsernames(
michael@0 476 logins, logins.length, formLogin);
michael@0 477 }
michael@0 478
michael@0 479 return;
michael@0 480 }
michael@0 481
michael@0 482
michael@0 483 // Look for an existing login that matches the form login.
michael@0 484 var existingLogin = null;
michael@0 485 var logins = Services.logins.findLogins({}, hostname, formSubmitURL, null);
michael@0 486
michael@0 487 for (var i = 0; i < logins.length; i++) {
michael@0 488 var same, login = logins[i];
michael@0 489
michael@0 490 // If one login has a username but the other doesn't, ignore
michael@0 491 // the username when comparing and only match if they have the
michael@0 492 // same password. Otherwise, compare the logins and match even
michael@0 493 // if the passwords differ.
michael@0 494 if (!login.username && formLogin.username) {
michael@0 495 var restoreMe = formLogin.username;
michael@0 496 formLogin.username = "";
michael@0 497 same = formLogin.matches(login, false);
michael@0 498 formLogin.username = restoreMe;
michael@0 499 } else if (!formLogin.username && login.username) {
michael@0 500 formLogin.username = login.username;
michael@0 501 same = formLogin.matches(login, false);
michael@0 502 formLogin.username = ""; // we know it's always blank.
michael@0 503 } else {
michael@0 504 same = formLogin.matches(login, true);
michael@0 505 }
michael@0 506
michael@0 507 if (same) {
michael@0 508 existingLogin = login;
michael@0 509 break;
michael@0 510 }
michael@0 511 }
michael@0 512
michael@0 513 if (existingLogin) {
michael@0 514 log("Found an existing login matching this form submission");
michael@0 515
michael@0 516 // Change password if needed.
michael@0 517 if (existingLogin.password != formLogin.password) {
michael@0 518 log("...passwords differ, prompting to change.");
michael@0 519 prompter = getPrompter(win);
michael@0 520 prompter.promptToChangePassword(existingLogin, formLogin);
michael@0 521 } else {
michael@0 522 // Update the lastUsed timestamp.
michael@0 523 var propBag = Cc["@mozilla.org/hash-property-bag;1"].
michael@0 524 createInstance(Ci.nsIWritablePropertyBag);
michael@0 525 propBag.setProperty("timeLastUsed", Date.now());
michael@0 526 propBag.setProperty("timesUsedIncrement", 1);
michael@0 527 Services.logins.modifyLogin(existingLogin, propBag);
michael@0 528 }
michael@0 529
michael@0 530 return;
michael@0 531 }
michael@0 532
michael@0 533
michael@0 534 // Prompt user to save login (via dialog or notification bar)
michael@0 535 prompter = getPrompter(win);
michael@0 536 prompter.promptToSavePassword(formLogin);
michael@0 537 },
michael@0 538
michael@0 539
michael@0 540 /*
michael@0 541 * _fillform
michael@0 542 *
michael@0 543 * Fill the form with login information if we can find it. This will find
michael@0 544 * an array of logins if not given any, otherwise it will use the logins
michael@0 545 * passed in. The logins are returned so they can be reused for
michael@0 546 * optimization. Success of action is also returned in format
michael@0 547 * [success, foundLogins]. autofillForm denotes if we should fill the form
michael@0 548 * in automatically, ignoreAutocomplete denotes if we should ignore
michael@0 549 * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo
michael@0 550 * for optimization
michael@0 551 */
michael@0 552 _fillForm : function (form, autofillForm, ignoreAutocomplete,
michael@0 553 clobberPassword, foundLogins) {
michael@0 554 // Heuristically determine what the user/pass fields are
michael@0 555 // We do this before checking to see if logins are stored,
michael@0 556 // so that the user isn't prompted for a master password
michael@0 557 // without need.
michael@0 558 var [usernameField, passwordField, ignored] =
michael@0 559 this._getFormFields(form, false);
michael@0 560
michael@0 561 // Need a valid password field to do anything.
michael@0 562 if (passwordField == null)
michael@0 563 return [false, foundLogins];
michael@0 564
michael@0 565 // If the password field is disabled or read-only, there's nothing to do.
michael@0 566 if (passwordField.disabled || passwordField.readOnly) {
michael@0 567 log("not filling form, password field disabled or read-only");
michael@0 568 return [false, foundLogins];
michael@0 569 }
michael@0 570
michael@0 571 // Need to get a list of logins if we weren't given them
michael@0 572 if (foundLogins == null) {
michael@0 573 var formOrigin =
michael@0 574 LoginUtils._getPasswordOrigin(form.ownerDocument.documentURI);
michael@0 575 var actionOrigin = LoginUtils._getActionOrigin(form);
michael@0 576 foundLogins = Services.logins.findLogins({}, formOrigin, actionOrigin, null);
michael@0 577 log("found", foundLogins.length, "matching logins.");
michael@0 578 } else {
michael@0 579 log("reusing logins from last form.");
michael@0 580 }
michael@0 581
michael@0 582 // Discard logins which have username/password values that don't
michael@0 583 // fit into the fields (as specified by the maxlength attribute).
michael@0 584 // The user couldn't enter these values anyway, and it helps
michael@0 585 // with sites that have an extra PIN to be entered (bug 391514)
michael@0 586 var maxUsernameLen = Number.MAX_VALUE;
michael@0 587 var maxPasswordLen = Number.MAX_VALUE;
michael@0 588
michael@0 589 // If attribute wasn't set, default is -1.
michael@0 590 if (usernameField && usernameField.maxLength >= 0)
michael@0 591 maxUsernameLen = usernameField.maxLength;
michael@0 592 if (passwordField.maxLength >= 0)
michael@0 593 maxPasswordLen = passwordField.maxLength;
michael@0 594
michael@0 595 var logins = foundLogins.filter(function (l) {
michael@0 596 var fit = (l.username.length <= maxUsernameLen &&
michael@0 597 l.password.length <= maxPasswordLen);
michael@0 598 if (!fit)
michael@0 599 log("Ignored", l.username, "login: won't fit");
michael@0 600
michael@0 601 return fit;
michael@0 602 }, this);
michael@0 603
michael@0 604
michael@0 605 // Nothing to do if we have no matching logins available.
michael@0 606 if (logins.length == 0)
michael@0 607 return [false, foundLogins];
michael@0 608
michael@0 609
michael@0 610 // The reason we didn't end up filling the form, if any. We include
michael@0 611 // this in the formInfo object we send with the passwordmgr-found-logins
michael@0 612 // notification. See the _notifyFoundLogins docs for possible values.
michael@0 613 var didntFillReason = null;
michael@0 614
michael@0 615 // Attach autocomplete stuff to the username field, if we have
michael@0 616 // one. This is normally used to select from multiple accounts,
michael@0 617 // but even with one account we should refill if the user edits.
michael@0 618 if (usernameField)
michael@0 619 this._formFillService.markAsLoginManagerField(usernameField);
michael@0 620
michael@0 621 // Don't clobber an existing password.
michael@0 622 if (passwordField.value && !clobberPassword) {
michael@0 623 didntFillReason = "existingPassword";
michael@0 624 this._notifyFoundLogins(didntFillReason, usernameField,
michael@0 625 passwordField, foundLogins, null);
michael@0 626 return [false, foundLogins];
michael@0 627 }
michael@0 628
michael@0 629 // If the form has an autocomplete=off attribute in play, don't
michael@0 630 // fill in the login automatically. We check this after attaching
michael@0 631 // the autocomplete stuff to the username field, so the user can
michael@0 632 // still manually select a login to be filled in.
michael@0 633 var isFormDisabled = false;
michael@0 634 if (!ignoreAutocomplete &&
michael@0 635 (this._isAutocompleteDisabled(form) ||
michael@0 636 this._isAutocompleteDisabled(usernameField) ||
michael@0 637 this._isAutocompleteDisabled(passwordField))) {
michael@0 638
michael@0 639 isFormDisabled = true;
michael@0 640 log("form not filled, has autocomplete=off");
michael@0 641 }
michael@0 642
michael@0 643 // Variable such that we reduce code duplication and can be sure we
michael@0 644 // should be firing notifications if and only if we can fill the form.
michael@0 645 var selectedLogin = null;
michael@0 646
michael@0 647 if (usernameField && (usernameField.value || usernameField.disabled || usernameField.readOnly)) {
michael@0 648 // If username was specified in the field, it's disabled or it's readOnly, only fill in the
michael@0 649 // password if we find a matching login.
michael@0 650 var username = usernameField.value.toLowerCase();
michael@0 651
michael@0 652 let matchingLogins = logins.filter(function(l)
michael@0 653 l.username.toLowerCase() == username);
michael@0 654 if (matchingLogins.length) {
michael@0 655 selectedLogin = matchingLogins[0];
michael@0 656 } else {
michael@0 657 didntFillReason = "existingUsername";
michael@0 658 log("Password not filled. None of the stored logins match the username already present.");
michael@0 659 }
michael@0 660 } else if (logins.length == 1) {
michael@0 661 selectedLogin = logins[0];
michael@0 662 } else {
michael@0 663 // We have multiple logins. Handle a special case here, for sites
michael@0 664 // which have a normal user+pass login *and* a password-only login
michael@0 665 // (eg, a PIN). Prefer the login that matches the type of the form
michael@0 666 // (user+pass or pass-only) when there's exactly one that matches.
michael@0 667 let matchingLogins;
michael@0 668 if (usernameField)
michael@0 669 matchingLogins = logins.filter(function(l) l.username);
michael@0 670 else
michael@0 671 matchingLogins = logins.filter(function(l) !l.username);
michael@0 672 if (matchingLogins.length == 1) {
michael@0 673 selectedLogin = matchingLogins[0];
michael@0 674 } else {
michael@0 675 didntFillReason = "multipleLogins";
michael@0 676 log("Multiple logins for form, so not filling any.");
michael@0 677 }
michael@0 678 }
michael@0 679
michael@0 680 var didFillForm = false;
michael@0 681 if (selectedLogin && autofillForm && !isFormDisabled) {
michael@0 682 // Fill the form
michael@0 683 // Don't modify the username field if it's disabled or readOnly so we preserve its case.
michael@0 684 if (usernameField && !(usernameField.disabled || usernameField.readOnly))
michael@0 685 usernameField.value = selectedLogin.username;
michael@0 686 passwordField.value = selectedLogin.password;
michael@0 687 didFillForm = true;
michael@0 688 } else if (selectedLogin && !autofillForm) {
michael@0 689 // For when autofillForm is false, but we still have the information
michael@0 690 // to fill a form, we notify observers.
michael@0 691 didntFillReason = "noAutofillForms";
michael@0 692 Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
michael@0 693 log("autofillForms=false but form can be filled; notified observers");
michael@0 694 } else if (selectedLogin && isFormDisabled) {
michael@0 695 // For when autocomplete is off, but we still have the information
michael@0 696 // to fill a form, we notify observers.
michael@0 697 didntFillReason = "autocompleteOff";
michael@0 698 Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
michael@0 699 log("autocomplete=off but form can be filled; notified observers");
michael@0 700 }
michael@0 701
michael@0 702 this._notifyFoundLogins(didntFillReason, usernameField, passwordField,
michael@0 703 foundLogins, selectedLogin);
michael@0 704
michael@0 705 return [didFillForm, foundLogins];
michael@0 706 },
michael@0 707
michael@0 708
michael@0 709 /**
michael@0 710 * Notify observers about an attempt to fill a form that resulted in some
michael@0 711 * saved logins being found for the form.
michael@0 712 *
michael@0 713 * This does not get called if the login manager attempts to fill a form
michael@0 714 * but does not find any saved logins. It does, however, get called when
michael@0 715 * the login manager does find saved logins whether or not it actually
michael@0 716 * fills the form with one of them.
michael@0 717 *
michael@0 718 * @param didntFillReason {String}
michael@0 719 * the reason the login manager didn't fill the form, if any;
michael@0 720 * if the value of this parameter is null, then the form was filled;
michael@0 721 * otherwise, this parameter will be one of these values:
michael@0 722 * existingUsername: the username field already contains a username
michael@0 723 * that doesn't match any stored usernames
michael@0 724 * existingPassword: the password field already contains a password
michael@0 725 * autocompleteOff: autocomplete has been disabled for the form
michael@0 726 * or its username or password fields
michael@0 727 * multipleLogins: we have multiple logins for the form
michael@0 728 * noAutofillForms: the autofillForms pref is set to false
michael@0 729 *
michael@0 730 * @param usernameField {HTMLInputElement}
michael@0 731 * the username field detected by the login manager, if any;
michael@0 732 * otherwise null
michael@0 733 *
michael@0 734 * @param passwordField {HTMLInputElement}
michael@0 735 * the password field detected by the login manager
michael@0 736 *
michael@0 737 * @param foundLogins {Array}
michael@0 738 * an array of nsILoginInfos that can be used to fill the form
michael@0 739 *
michael@0 740 * @param selectedLogin {nsILoginInfo}
michael@0 741 * the nsILoginInfo that was/would be used to fill the form, if any;
michael@0 742 * otherwise null; whether or not it was actually used depends on
michael@0 743 * the value of the didntFillReason parameter
michael@0 744 */
michael@0 745 _notifyFoundLogins : function (didntFillReason, usernameField,
michael@0 746 passwordField, foundLogins, selectedLogin) {
michael@0 747 // We need .setProperty(), which is a method on the original
michael@0 748 // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2
michael@0 749 // doesn't inherit from that, so the additional QI is needed.
michael@0 750 let formInfo = Cc["@mozilla.org/hash-property-bag;1"].
michael@0 751 createInstance(Ci.nsIWritablePropertyBag2).
michael@0 752 QueryInterface(Ci.nsIWritablePropertyBag);
michael@0 753
michael@0 754 formInfo.setPropertyAsACString("didntFillReason", didntFillReason);
michael@0 755 formInfo.setPropertyAsInterface("usernameField", usernameField);
michael@0 756 formInfo.setPropertyAsInterface("passwordField", passwordField);
michael@0 757 formInfo.setProperty("foundLogins", foundLogins.concat());
michael@0 758 formInfo.setPropertyAsInterface("selectedLogin", selectedLogin);
michael@0 759
michael@0 760 Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null);
michael@0 761 },
michael@0 762
michael@0 763 };
michael@0 764
michael@0 765
michael@0 766
michael@0 767
michael@0 768 LoginUtils = {
michael@0 769 /*
michael@0 770 * _getPasswordOrigin
michael@0 771 *
michael@0 772 * Get the parts of the URL we want for identification.
michael@0 773 */
michael@0 774 _getPasswordOrigin : function (uriString, allowJS) {
michael@0 775 var realm = "";
michael@0 776 try {
michael@0 777 var uri = Services.io.newURI(uriString, null, null);
michael@0 778
michael@0 779 if (allowJS && uri.scheme == "javascript")
michael@0 780 return "javascript:"
michael@0 781
michael@0 782 realm = uri.scheme + "://" + uri.host;
michael@0 783
michael@0 784 // If the URI explicitly specified a port, only include it when
michael@0 785 // it's not the default. (We never want "http://foo.com:80")
michael@0 786 var port = uri.port;
michael@0 787 if (port != -1) {
michael@0 788 var handler = Services.io.getProtocolHandler(uri.scheme);
michael@0 789 if (port != handler.defaultPort)
michael@0 790 realm += ":" + port;
michael@0 791 }
michael@0 792
michael@0 793 } catch (e) {
michael@0 794 // bug 159484 - disallow url types that don't support a hostPort.
michael@0 795 // (although we handle "javascript:..." as a special case above.)
michael@0 796 log("Couldn't parse origin for", uriString);
michael@0 797 realm = null;
michael@0 798 }
michael@0 799
michael@0 800 return realm;
michael@0 801 },
michael@0 802
michael@0 803 _getActionOrigin : function (form) {
michael@0 804 var uriString = form.action;
michael@0 805
michael@0 806 // A blank or missing action submits to where it came from.
michael@0 807 if (uriString == "")
michael@0 808 uriString = form.baseURI; // ala bug 297761
michael@0 809
michael@0 810 return this._getPasswordOrigin(uriString, true);
michael@0 811 },
michael@0 812
michael@0 813 };

mercurial