/toolkit/components/passwordmgr/nsLoginManager.js
JavaScript | 1433 lines | 764 code | 255 blank | 414 comment | 208 complexity | d6051b2d4886eb0f85bca66486fdf778 MD5 | raw file
Large files files are truncated, but you can click here to view the full file
1/* ***** BEGIN LICENSE BLOCK ***** 2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 3 * 4 * The contents of this file are subject to the Mozilla Public License Version 5 * 1.1 (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * http://www.mozilla.org/MPL/ 8 * 9 * Software distributed under the License is distributed on an "AS IS" basis, 10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 11 * for the specific language governing rights and limitations under the 12 * License. 13 * 14 * The Original Code is mozilla.org code. 15 * 16 * The Initial Developer of the Original Code is Mozilla Foundation. 17 * Portions created by the Initial Developer are Copyright (C) 2007 18 * the Initial Developer. All Rights Reserved. 19 * 20 * Contributor(s): 21 * Justin Dolske <dolske@mozilla.com> (original author) 22 * Ehsan Akhgari <ehsan.akhgari@gmail.com> 23 * 24 * Alternatively, the contents of this file may be used under the terms of 25 * either the GNU General Public License Version 2 or later (the "GPL"), or 26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 27 * in which case the provisions of the GPL or the LGPL are applicable instead 28 * of those above. If you wish to allow use of your version of this file only 29 * under the terms of either the GPL or the LGPL, and not to allow others to 30 * use your version of this file under the terms of the MPL, indicate your 31 * decision by deleting the provisions above and replace them with the notice 32 * and other provisions required by the GPL or the LGPL. If you do not delete 33 * the provisions above, a recipient may use your version of this file under 34 * the terms of any one of the MPL, the GPL or the LGPL. 35 * 36 * ***** END LICENSE BLOCK ***** */ 37 38 39const Cc = Components.classes; 40const Ci = Components.interfaces; 41 42Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 43Components.utils.import("resource://gre/modules/Services.jsm"); 44 45function LoginManager() { 46 this.init(); 47} 48 49LoginManager.prototype = { 50 51 classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"), 52 QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager, 53 Ci.nsISupportsWeakReference, 54 Ci.nsIInterfaceRequestor]), 55 getInterface : function(aIID) { 56 if (aIID.equals(Ci.mozIStorageConnection) && this._storage) { 57 let ir = this._storage.QueryInterface(Ci.nsIInterfaceRequestor); 58 return ir.getInterface(aIID); 59 } 60 61 throw Cr.NS_ERROR_NO_INTERFACE; 62 }, 63 64 65 /* ---------- private memebers ---------- */ 66 67 68 __formFillService : null, // FormFillController, for username autocompleting 69 get _formFillService() { 70 if (!this.__formFillService) 71 this.__formFillService = 72 Cc["@mozilla.org/satchel/form-fill-controller;1"]. 73 getService(Ci.nsIFormFillController); 74 return this.__formFillService; 75 }, 76 77 78 __storage : null, // Storage component which contains the saved logins 79 get _storage() { 80 if (!this.__storage) { 81 82 var contractID = "@mozilla.org/login-manager/storage/mozStorage;1"; 83 try { 84 var catMan = Cc["@mozilla.org/categorymanager;1"]. 85 getService(Ci.nsICategoryManager); 86 contractID = catMan.getCategoryEntry("login-manager-storage", 87 "nsILoginManagerStorage"); 88 this.log("Found alternate nsILoginManagerStorage with " + 89 "contract ID: " + contractID); 90 } catch (e) { 91 this.log("No alternate nsILoginManagerStorage registered"); 92 } 93 94 this.__storage = Cc[contractID]. 95 createInstance(Ci.nsILoginManagerStorage); 96 try { 97 this.__storage.init(); 98 } catch (e) { 99 this.log("Initialization of storage component failed: " + e); 100 this.__storage = null; 101 } 102 } 103 104 return this.__storage; 105 }, 106 107 108 // Private Browsing Service 109 // If the service is not available, null will be returned. 110 __privateBrowsingService : undefined, 111 get _privateBrowsingService() { 112 if (this.__privateBrowsingService == undefined) { 113 if ("@mozilla.org/privatebrowsing;1" in Cc) 114 this.__privateBrowsingService = Cc["@mozilla.org/privatebrowsing;1"]. 115 getService(Ci.nsIPrivateBrowsingService); 116 else 117 this.__privateBrowsingService = null; 118 } 119 return this.__privateBrowsingService; 120 }, 121 122 123 // Whether we are in private browsing mode 124 get _inPrivateBrowsing() { 125 var pbSvc = this._privateBrowsingService; 126 if (pbSvc) 127 return pbSvc.privateBrowsingEnabled; 128 else 129 return false; 130 }, 131 132 _prefBranch : null, // Preferences service 133 _nsLoginInfo : null, // Constructor for nsILoginInfo implementation 134 135 _remember : true, // mirrors signon.rememberSignons preference 136 _debug : false, // mirrors signon.debug 137 138 139 /* 140 * init 141 * 142 * Initialize the Login Manager. Automatically called when service 143 * is created. 144 * 145 * Note: Service created in /browser/base/content/browser.js, 146 * delayedStartup() 147 */ 148 init : function () { 149 150 // Cache references to current |this| in utility objects 151 this._webProgressListener._domEventListener = this._domEventListener; 152 this._webProgressListener._pwmgr = this; 153 this._domEventListener._pwmgr = this; 154 this._observer._pwmgr = this; 155 156 // Preferences. Add observer so we get notified of changes. 157 this._prefBranch = Services.prefs.getBranch("signon."); 158 this._prefBranch.addObserver("", this._observer, false); 159 160 // Get current preference values. 161 this._debug = this._prefBranch.getBoolPref("debug"); 162 163 this._remember = this._prefBranch.getBoolPref("rememberSignons"); 164 165 166 // Get constructor for nsILoginInfo 167 this._nsLoginInfo = new Components.Constructor( 168 "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo); 169 170 171 // Form submit observer checks forms for new logins and pw changes. 172 Services.obs.addObserver(this._observer, "earlyformsubmit", false); 173 Services.obs.addObserver(this._observer, "xpcom-shutdown", false); 174 175 // WebProgressListener for getting notification of new doc loads. 176 var progress = Cc["@mozilla.org/docloaderservice;1"]. 177 getService(Ci.nsIWebProgress); 178 progress.addProgressListener(this._webProgressListener, 179 Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); 180 }, 181 182 183 /* 184 * log 185 * 186 * Internal function for logging debug messages to the Error Console window 187 */ 188 log : function (message) { 189 if (!this._debug) 190 return; 191 dump("Login Manager: " + message + "\n"); 192 Services.console.logStringMessage("Login Manager: " + message); 193 }, 194 195 196 /* ---------- Utility objects ---------- */ 197 198 199 /* 200 * _observer object 201 * 202 * Internal utility object, implements the nsIObserver interface. 203 * Used to receive notification for: form submission, preference changes. 204 */ 205 _observer : { 206 _pwmgr : null, 207 208 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 209 Ci.nsIFormSubmitObserver, 210 Ci.nsISupportsWeakReference]), 211 212 213 // nsFormSubmitObserver 214 notify : function (formElement, aWindow, actionURI) { 215 this._pwmgr.log("observer notified for form submission."); 216 217 // We're invoked before the content's |onsubmit| handlers, so we 218 // can grab form data before it might be modified (see bug 257781). 219 220 try { 221 this._pwmgr._onFormSubmit(formElement); 222 } catch (e) { 223 this._pwmgr.log("Caught error in onFormSubmit: " + e); 224 } 225 226 return true; // Always return true, or form submit will be canceled. 227 }, 228 229 // nsObserver 230 observe : function (subject, topic, data) { 231 232 if (topic == "nsPref:changed") { 233 var prefName = data; 234 this._pwmgr.log("got change to " + prefName + " preference"); 235 236 if (prefName == "debug") { 237 this._pwmgr._debug = 238 this._pwmgr._prefBranch.getBoolPref("debug"); 239 } else if (prefName == "rememberSignons") { 240 this._pwmgr._remember = 241 this._pwmgr._prefBranch.getBoolPref("rememberSignons"); 242 } else { 243 this._pwmgr.log("Oops! Pref not handled, change ignored."); 244 } 245 } else if (topic == "xpcom-shutdown") { 246 for (let i in this._pwmgr) { 247 try { 248 this._pwmgr[i] = null; 249 } catch(ex) {} 250 } 251 this._pwmgr = null; 252 } else { 253 this._pwmgr.log("Oops! Unexpected notification: " + topic); 254 } 255 } 256 }, 257 258 259 /* 260 * _webProgressListener object 261 * 262 * Internal utility object, implements nsIWebProgressListener interface. 263 * This is attached to the document loader service, so we get 264 * notifications about all page loads. 265 */ 266 _webProgressListener : { 267 _pwmgr : null, 268 _domEventListener : null, 269 270 QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener, 271 Ci.nsISupportsWeakReference]), 272 273 274 onStateChange : function (aWebProgress, aRequest, 275 aStateFlags, aStatus) { 276 277 // STATE_START is too early, doc is still the old page. 278 if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING)) 279 return; 280 281 if (!this._pwmgr._remember) 282 return; 283 284 var domWin = aWebProgress.DOMWindow; 285 var domDoc = domWin.document; 286 287 // Only process things which might have HTML forms. 288 if (!(domDoc instanceof Ci.nsIDOMHTMLDocument)) 289 return; 290 if (this._pwmgr._debug) { 291 let requestName = "(null)"; 292 if (aRequest) { 293 try { 294 requestName = aRequest.name; 295 } catch (ex if ex.result == Components.results.NS_ERROR_NOT_IMPLEMENTED) { 296 // do nothing - leave requestName = "(null)" 297 } 298 } 299 this._pwmgr.log("onStateChange accepted: req = " + requestName + 300 ", flags = 0x" + aStateFlags.toString(16)); 301 } 302 303 // Fastback doesn't fire DOMContentLoaded, so process forms now. 304 if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) { 305 this._pwmgr.log("onStateChange: restoring document"); 306 return this._pwmgr._fillDocument(domDoc); 307 } 308 309 // Add event listener to process page when DOM is complete. 310 domDoc.addEventListener("DOMContentLoaded", 311 this._domEventListener, false); 312 return; 313 }, 314 315 // stubs for the nsIWebProgressListener interfaces which we don't use. 316 onProgressChange : function() { throw "Unexpected onProgressChange"; }, 317 onLocationChange : function() { throw "Unexpected onLocationChange"; }, 318 onStatusChange : function() { throw "Unexpected onStatusChange"; }, 319 onSecurityChange : function() { throw "Unexpected onSecurityChange"; } 320 }, 321 322 323 /* 324 * _domEventListener object 325 * 326 * Internal utility object, implements nsIDOMEventListener 327 * Used to catch certain DOM events needed to properly implement form fill. 328 */ 329 _domEventListener : { 330 _pwmgr : null, 331 332 QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener, 333 Ci.nsISupportsWeakReference]), 334 335 336 handleEvent : function (event) { 337 if (!event.isTrusted) 338 return; 339 340 this._pwmgr.log("domEventListener: got event " + event.type); 341 342 switch (event.type) { 343 case "DOMContentLoaded": 344 this._pwmgr._fillDocument(event.target); 345 return; 346 347 case "DOMAutoComplete": 348 case "blur": 349 var acInputField = event.target; 350 var acForm = acInputField.form; 351 352 // If the username is blank, bail out now -- we don't want 353 // fillForm() to try filling in a login without a username 354 // to filter on (bug 471906). 355 if (!acInputField.value) 356 return; 357 358 // Make sure the username field fillForm will use is the 359 // same field as the autocomplete was activated on. If 360 // not, the DOM has been altered and we'll just give up. 361 var [usernameField, passwordField, ignored] = 362 this._pwmgr._getFormFields(acForm, false); 363 if (usernameField == acInputField && passwordField) { 364 // This shouldn't trigger a master password prompt, 365 // because we don't attach to the input until after we 366 // successfully obtain logins for the form. 367 this._pwmgr._fillForm(acForm, true, true, true, null); 368 } else { 369 this._pwmgr.log("Oops, form changed before AC invoked"); 370 } 371 return; 372 373 default: 374 this._pwmgr.log("Oops! This event unexpected."); 375 return; 376 } 377 } 378 }, 379 380 381 382 383 /* ---------- Primary Public interfaces ---------- */ 384 385 386 387 388 /* 389 * addLogin 390 * 391 * Add a new login to login storage. 392 */ 393 addLogin : function (login) { 394 // Sanity check the login 395 if (login.hostname == null || login.hostname.length == 0) 396 throw "Can't add a login with a null or empty hostname."; 397 398 // For logins w/o a username, set to "", not null. 399 if (login.username == null) 400 throw "Can't add a login with a null username."; 401 402 if (login.password == null || login.password.length == 0) 403 throw "Can't add a login with a null or empty password."; 404 405 if (login.formSubmitURL || login.formSubmitURL == "") { 406 // We have a form submit URL. Can't have a HTTP realm. 407 if (login.httpRealm != null) 408 throw "Can't add a login with both a httpRealm and formSubmitURL."; 409 } else if (login.httpRealm) { 410 // We have a HTTP realm. Can't have a form submit URL. 411 if (login.formSubmitURL != null) 412 throw "Can't add a login with both a httpRealm and formSubmitURL."; 413 } else { 414 // Need one or the other! 415 throw "Can't add a login without a httpRealm or formSubmitURL."; 416 } 417 418 419 // Look for an existing entry. 420 var logins = this.findLogins({}, login.hostname, login.formSubmitURL, 421 login.httpRealm); 422 423 if (logins.some(function(l) login.matches(l, true))) 424 throw "This login already exists."; 425 426 this.log("Adding login: " + login); 427 return this._storage.addLogin(login); 428 }, 429 430 431 /* 432 * removeLogin 433 * 434 * Remove the specified login from the stored logins. 435 */ 436 removeLogin : function (login) { 437 this.log("Removing login: " + login); 438 return this._storage.removeLogin(login); 439 }, 440 441 442 /* 443 * modifyLogin 444 * 445 * Change the specified login to match the new login. 446 */ 447 modifyLogin : function (oldLogin, newLogin) { 448 this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin); 449 return this._storage.modifyLogin(oldLogin, newLogin); 450 }, 451 452 453 /* 454 * getAllLogins 455 * 456 * Get a dump of all stored logins. Used by the login manager UI. 457 * 458 * |count| is only needed for XPCOM. 459 * 460 * Returns an array of logins. If there are no logins, the array is empty. 461 */ 462 getAllLogins : function (count) { 463 this.log("Getting a list of all logins"); 464 return this._storage.getAllLogins(count); 465 }, 466 467 468 /* 469 * removeAllLogins 470 * 471 * Remove all stored logins. 472 */ 473 removeAllLogins : function () { 474 this.log("Removing all logins"); 475 this._storage.removeAllLogins(); 476 }, 477 478 /* 479 * getAllDisabledHosts 480 * 481 * Get a list of all hosts for which logins are disabled. 482 * 483 * |count| is only needed for XPCOM. 484 * 485 * Returns an array of disabled logins. If there are no disabled logins, 486 * the array is empty. 487 */ 488 getAllDisabledHosts : function (count) { 489 this.log("Getting a list of all disabled hosts"); 490 return this._storage.getAllDisabledHosts(count); 491 }, 492 493 494 /* 495 * findLogins 496 * 497 * Search for the known logins for entries matching the specified criteria. 498 */ 499 findLogins : function (count, hostname, formSubmitURL, httpRealm) { 500 this.log("Searching for logins matching host: " + hostname + 501 ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); 502 503 return this._storage.findLogins(count, hostname, formSubmitURL, 504 httpRealm); 505 }, 506 507 508 /* 509 * searchLogins 510 * 511 * Public wrapper around _searchLogins to convert the nsIPropertyBag to a 512 * JavaScript object and decrypt the results. 513 * 514 * Returns an array of decrypted nsILoginInfo. 515 */ 516 searchLogins : function(count, matchData) { 517 this.log("Searching for logins"); 518 519 return this._storage.searchLogins(count, matchData); 520 }, 521 522 523 /* 524 * countLogins 525 * 526 * Search for the known logins for entries matching the specified criteria, 527 * returns only the count. 528 */ 529 countLogins : function (hostname, formSubmitURL, httpRealm) { 530 this.log("Counting logins matching host: " + hostname + 531 ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm); 532 533 return this._storage.countLogins(hostname, formSubmitURL, httpRealm); 534 }, 535 536 537 /* 538 * uiBusy 539 */ 540 get uiBusy() { 541 return this._storage.uiBusy; 542 }, 543 544 545 /* 546 * getLoginSavingEnabled 547 * 548 * Check to see if user has disabled saving logins for the host. 549 */ 550 getLoginSavingEnabled : function (host) { 551 this.log("Checking if logins to " + host + " can be saved."); 552 if (!this._remember) 553 return false; 554 555 return this._storage.getLoginSavingEnabled(host); 556 }, 557 558 559 /* 560 * setLoginSavingEnabled 561 * 562 * Enable or disable storing logins for the specified host. 563 */ 564 setLoginSavingEnabled : function (hostname, enabled) { 565 // Nulls won't round-trip with getAllDisabledHosts(). 566 if (hostname.indexOf("\0") != -1) 567 throw "Invalid hostname"; 568 569 this.log("Saving logins for " + hostname + " enabled? " + enabled); 570 return this._storage.setLoginSavingEnabled(hostname, enabled); 571 }, 572 573 574 /* 575 * autoCompleteSearch 576 * 577 * Yuck. This is called directly by satchel: 578 * nsFormFillController::StartSearch() 579 * [toolkit/components/satchel/src/nsFormFillController.cpp] 580 * 581 * We really ought to have a simple way for code to register an 582 * auto-complete provider, and not have satchel calling pwmgr directly. 583 */ 584 autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) { 585 // aPreviousResult & aResult are nsIAutoCompleteResult, 586 // aElement is nsIDOMHTMLInputElement 587 588 if (!this._remember) 589 return null; 590 591 this.log("AutoCompleteSearch invoked. Search is: " + aSearchString); 592 593 var result = null; 594 595 if (aPreviousResult && 596 aSearchString.substr(0, aPreviousResult.searchString.length) == aPreviousResult.searchString) { 597 this.log("Using previous autocomplete result"); 598 result = aPreviousResult; 599 result.wrappedJSObject.searchString = aSearchString; 600 601 // We have a list of results for a shorter search string, so just 602 // filter them further based on the new search string. 603 // Count backwards, because result.matchCount is decremented 604 // when we remove an entry. 605 for (var i = result.matchCount - 1; i >= 0; i--) { 606 var match = result.getValueAt(i); 607 608 // Remove results that are too short, or have different prefix. 609 if (aSearchString.length > match.length || 610 aSearchString.toLowerCase() != 611 match.substr(0, aSearchString.length).toLowerCase()) 612 { 613 this.log("Removing autocomplete entry '" + match + "'"); 614 result.removeValueAt(i, false); 615 } 616 } 617 } else { 618 this.log("Creating new autocomplete search result."); 619 620 var doc = aElement.ownerDocument; 621 var origin = this._getPasswordOrigin(doc.documentURI); 622 var actionOrigin = this._getActionOrigin(aElement.form); 623 624 // This shouldn't trigger a master password prompt, because we 625 // don't attach to the input until after we successfully obtain 626 // logins for the form. 627 var logins = this.findLogins({}, origin, actionOrigin, null); 628 var matchingLogins = []; 629 630 // Filter out logins that don't match the search prefix. Also 631 // filter logins without a username, since that's confusing to see 632 // in the dropdown and we can't autocomplete them anyway. 633 for (i = 0; i < logins.length; i++) { 634 var username = logins[i].username.toLowerCase(); 635 if (username && 636 aSearchString.length <= username.length && 637 aSearchString.toLowerCase() == 638 username.substr(0, aSearchString.length)) 639 { 640 matchingLogins.push(logins[i]); 641 } 642 } 643 this.log(matchingLogins.length + " autocomplete logins avail."); 644 result = new UserAutoCompleteResult(aSearchString, matchingLogins); 645 } 646 647 return result; 648 }, 649 650 651 652 653 /* ------- Internal methods / callbacks for document integration ------- */ 654 655 656 657 658 /* 659 * _getPasswordFields 660 * 661 * Returns an array of password field elements for the specified form. 662 * If no pw fields are found, or if more than 3 are found, then null 663 * is returned. 664 * 665 * skipEmptyFields can be set to ignore password fields with no value. 666 */ 667 _getPasswordFields : function (form, skipEmptyFields) { 668 // Locate the password fields in the form. 669 var pwFields = []; 670 for (var i = 0; i < form.elements.length; i++) { 671 var element = form.elements[i]; 672 if (!(element instanceof Ci.nsIDOMHTMLInputElement) || 673 element.type != "password") 674 continue; 675 676 if (skipEmptyFields && !element.value) 677 continue; 678 679 pwFields[pwFields.length] = { 680 index : i, 681 element : element 682 }; 683 } 684 685 // If too few or too many fields, bail out. 686 if (pwFields.length == 0) { 687 this.log("(form ignored -- no password fields.)"); 688 return null; 689 } else if (pwFields.length > 3) { 690 this.log("(form ignored -- too many password fields. [got " + 691 pwFields.length + "])"); 692 return null; 693 } 694 695 return pwFields; 696 }, 697 698 699 /* 700 * _getFormFields 701 * 702 * Returns the username and password fields found in the form. 703 * Can handle complex forms by trying to figure out what the 704 * relevant fields are. 705 * 706 * Returns: [usernameField, newPasswordField, oldPasswordField] 707 * 708 * usernameField may be null. 709 * newPasswordField will always be non-null. 710 * oldPasswordField may be null. If null, newPasswordField is just 711 * "theLoginField". If not null, the form is apparently a 712 * change-password field, with oldPasswordField containing the password 713 * that is being changed. 714 */ 715 _getFormFields : function (form, isSubmission) { 716 var usernameField = null; 717 718 // Locate the password field(s) in the form. Up to 3 supported. 719 // If there's no password field, there's nothing for us to do. 720 var pwFields = this._getPasswordFields(form, isSubmission); 721 if (!pwFields) 722 return [null, null, null]; 723 724 725 // Locate the username field in the form by searching backwards 726 // from the first passwordfield, assume the first text field is the 727 // username. We might not find a username field if the user is 728 // already logged in to the site. 729 for (var i = pwFields[0].index - 1; i >= 0; i--) { 730 var element = form.elements[i]; 731 var fieldType = (element.hasAttribute("type") ? 732 element.getAttribute("type").toLowerCase() : 733 element.type); 734 if (fieldType == "text" || 735 fieldType == "email" || 736 fieldType == "url" || 737 fieldType == "tel" || 738 fieldType == "number") { 739 usernameField = element; 740 break; 741 } 742 } 743 744 if (!usernameField) 745 this.log("(form -- no username field found)"); 746 747 748 // If we're not submitting a form (it's a page load), there are no 749 // password field values for us to use for identifying fields. So, 750 // just assume the first password field is the one to be filled in. 751 if (!isSubmission || pwFields.length == 1) 752 return [usernameField, pwFields[0].element, null]; 753 754 755 // Try to figure out WTF is in the form based on the password values. 756 var oldPasswordField, newPasswordField; 757 var pw1 = pwFields[0].element.value; 758 var pw2 = pwFields[1].element.value; 759 var pw3 = (pwFields[2] ? pwFields[2].element.value : null); 760 761 if (pwFields.length == 3) { 762 // Look for two identical passwords, that's the new password 763 764 if (pw1 == pw2 && pw2 == pw3) { 765 // All 3 passwords the same? Weird! Treat as if 1 pw field. 766 newPasswordField = pwFields[0].element; 767 oldPasswordField = null; 768 } else if (pw1 == pw2) { 769 newPasswordField = pwFields[0].element; 770 oldPasswordField = pwFields[2].element; 771 } else if (pw2 == pw3) { 772 oldPasswordField = pwFields[0].element; 773 newPasswordField = pwFields[2].element; 774 } else if (pw1 == pw3) { 775 // A bit odd, but could make sense with the right page layout. 776 newPasswordField = pwFields[0].element; 777 oldPasswordField = pwFields[1].element; 778 } else { 779 // We can't tell which of the 3 passwords should be saved. 780 this.log("(form ignored -- all 3 pw fields differ)"); 781 return [null, null, null]; 782 } 783 } else { // pwFields.length == 2 784 if (pw1 == pw2) { 785 // Treat as if 1 pw field 786 newPasswordField = pwFields[0].element; 787 oldPasswordField = null; 788 } else { 789 // Just assume that the 2nd password is the new password 790 oldPasswordField = pwFields[0].element; 791 newPasswordField = pwFields[1].element; 792 } 793 } 794 795 return [usernameField, newPasswordField, oldPasswordField]; 796 }, 797 798 799 /* 800 * _isAutoCompleteDisabled 801 * 802 * Returns true if the page requests autocomplete be disabled for the 803 * specified form input. 804 */ 805 _isAutocompleteDisabled : function (element) { 806 if (element && element.hasAttribute("autocomplete") && 807 element.getAttribute("autocomplete").toLowerCase() == "off") 808 return true; 809 810 return false; 811 }, 812 813 /* 814 * _onFormSubmit 815 * 816 * Called by the our observer when notified of a form submission. 817 * [Note that this happens before any DOM onsubmit handlers are invoked.] 818 * Looks for a password change in the submitted form, so we can update 819 * our stored password. 820 */ 821 _onFormSubmit : function (form) { 822 823 // local helper function 824 function getPrompter(aWindow) { 825 var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"]. 826 createInstance(Ci.nsILoginManagerPrompter); 827 prompterSvc.init(aWindow); 828 return prompterSvc; 829 } 830 831 if (this._inPrivateBrowsing) { 832 // We won't do anything in private browsing mode anyway, 833 // so there's no need to perform further checks. 834 this.log("(form submission ignored in private browsing mode)"); 835 return; 836 } 837 838 var doc = form.ownerDocument; 839 var win = doc.defaultView; 840 841 // If password saving is disabled (globally or for host), bail out now. 842 if (!this._remember) 843 return; 844 845 var hostname = this._getPasswordOrigin(doc.documentURI); 846 if (!hostname) { 847 this.log("(form submission ignored -- invalid hostname)"); 848 return; 849 } 850 851 var formSubmitURL = this._getActionOrigin(form) 852 if (!this.getLoginSavingEnabled(hostname)) { 853 this.log("(form submission ignored -- saving is " + 854 "disabled for: " + hostname + ")"); 855 return; 856 } 857 858 859 // Get the appropriate fields from the form. 860 var [usernameField, newPasswordField, oldPasswordField] = 861 this._getFormFields(form, true); 862 863 // Need at least 1 valid password field to do anything. 864 if (newPasswordField == null) 865 return; 866 867 // Check for autocomplete=off attribute. We don't use it to prevent 868 // autofilling (for existing logins), but won't save logins when it's 869 // present. 870 // XXX spin out a bug that we don't update timeLastUsed in this case? 871 if (this._isAutocompleteDisabled(form) || 872 this._isAutocompleteDisabled(usernameField) || 873 this._isAutocompleteDisabled(newPasswordField) || 874 this._isAutocompleteDisabled(oldPasswordField)) { 875 this.log("(form submission ignored -- autocomplete=off found)"); 876 return; 877 } 878 879 880 var formLogin = new this._nsLoginInfo(); 881 formLogin.init(hostname, formSubmitURL, null, 882 (usernameField ? usernameField.value : ""), 883 newPasswordField.value, 884 (usernameField ? usernameField.name : ""), 885 newPasswordField.name); 886 887 // If we didn't find a username field, but seem to be changing a 888 // password, allow the user to select from a list of applicable 889 // logins to update the password for. 890 if (!usernameField && oldPasswordField) { 891 892 var logins = this.findLogins({}, hostname, formSubmitURL, null); 893 894 if (logins.length == 0) { 895 // Could prompt to save this as a new password-only login. 896 // This seems uncommon, and might be wrong, so ignore. 897 this.log("(no logins for this host -- pwchange ignored)"); 898 return; 899 } 900 901 var prompter = getPrompter(win); 902 903 if (logins.length == 1) { 904 var oldLogin = logins[0]; 905 formLogin.username = oldLogin.username; 906 formLogin.usernameField = oldLogin.usernameField; 907 908 prompter.promptToChangePassword(oldLogin, formLogin); 909 } else { 910 prompter.promptToChangePasswordWithUsernames( 911 logins, logins.length, formLogin); 912 } 913 914 return; 915 } 916 917 918 // Look for an existing login that matches the form login. 919 var existingLogin = null; 920 var logins = this.findLogins({}, hostname, formSubmitURL, null); 921 922 for (var i = 0; i < logins.length; i++) { 923 var same, login = logins[i]; 924 925 // If one login has a username but the other doesn't, ignore 926 // the username when comparing and only match if they have the 927 // same password. Otherwise, compare the logins and match even 928 // if the passwords differ. 929 if (!login.username && formLogin.username) { 930 var restoreMe = formLogin.username; 931 formLogin.username = ""; 932 same = formLogin.matches(login, false); 933 formLogin.username = restoreMe; 934 } else if (!formLogin.username && login.username) { 935 formLogin.username = login.username; 936 same = formLogin.matches(login, false); 937 formLogin.username = ""; // we know it's always blank. 938 } else { 939 same = formLogin.matches(login, true); 940 } 941 942 if (same) { 943 existingLogin = login; 944 break; 945 } 946 } 947 948 if (existingLogin) { 949 this.log("Found an existing login matching this form submission"); 950 951 // Change password if needed. 952 if (existingLogin.password != formLogin.password) { 953 this.log("...passwords differ, prompting to change."); 954 prompter = getPrompter(win); 955 prompter.promptToChangePassword(existingLogin, formLogin); 956 } else { 957 // Update the lastUsed timestamp. 958 var propBag = Cc["@mozilla.org/hash-property-bag;1"]. 959 createInstance(Ci.nsIWritablePropertyBag); 960 propBag.setProperty("timeLastUsed", Date.now()); 961 propBag.setProperty("timesUsedIncrement", 1); 962 this.modifyLogin(existingLogin, propBag); 963 } 964 965 return; 966 } 967 968 969 // Prompt user to save login (via dialog or notification bar) 970 prompter = getPrompter(win); 971 prompter.promptToSavePassword(formLogin); 972 }, 973 974 975 /* 976 * _getPasswordOrigin 977 * 978 * Get the parts of the URL we want for identification. 979 */ 980 _getPasswordOrigin : function (uriString, allowJS) { 981 var realm = ""; 982 try { 983 var uri = Services.io.newURI(uriString, null, null); 984 985 if (allowJS && uri.scheme == "javascript") 986 return "javascript:" 987 988 realm = uri.scheme + "://" + uri.host; 989 990 // If the URI explicitly specified a port, only include it when 991 // it's not the default. (We never want "http://foo.com:80") 992 var port = uri.port; 993 if (port != -1) { 994 var handler = Services.io.getProtocolHandler(uri.scheme); 995 if (port != handler.defaultPort) 996 realm += ":" + port; 997 } 998 999 } catch (e) { 1000 // bug 159484 - disallow url types that don't support a hostPort. 1001 // (although we handle "javascript:..." as a special case above.) 1002 this.log("Couldn't parse origin for " + uriString); 1003 realm = null; 1004 } 1005 1006 return realm; 1007 }, 1008 1009 _getActionOrigin : function (form) { 1010 var uriString = form.action; 1011 1012 // A blank or missing action submits to where it came from. 1013 if (uriString == "") 1014 uriString = form.baseURI; // ala bug 297761 1015 1016 return this._getPasswordOrigin(uriString, true); 1017 }, 1018 1019 1020 /* 1021 * _fillDocument 1022 * 1023 * Called when a page has loaded. For each form in the document, 1024 * we check to see if it can be filled with a stored login. 1025 */ 1026 _fillDocument : function (doc) { 1027 var forms = doc.forms; 1028 if (!forms || forms.length == 0) 1029 return; 1030 1031 var formOrigin = this._getPasswordOrigin(doc.documentURI); 1032 1033 // If there are no logins for this site, bail out now. 1034 if (!this.countLogins(formOrigin, "", null)) 1035 return; 1036 1037 // If we're currently displaying a master password prompt, defer 1038 // processing this document until the user handles the prompt. 1039 if (this.uiBusy) { 1040 this.log("deferring fillDoc for " + doc.documentURI); 1041 let self = this; 1042 let observer = { 1043 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), 1044 1045 observe: function (subject, topic, data) { 1046 self.log("Got deferred fillDoc notification: " + topic); 1047 // Only run observer once. 1048 Services.obs.removeObserver(this, "passwordmgr-crypto-login"); 1049 Services.obs.removeObserver(this, "passwordmgr-crypto-loginCanceled"); 1050 if (topic == "passwordmgr-crypto-loginCanceled") 1051 return; 1052 self._fillDocument(doc); 1053 }, 1054 handleEvent : function (event) { 1055 // Not expected to be called 1056 } 1057 }; 1058 // Trickyness follows: We want an observer, but don't want it to 1059 // cause leaks. So add the observer with a weak reference, and use 1060 // a dummy event listener (a strong reference) to keep it alive 1061 // until the document is destroyed. 1062 Services.obs.addObserver(observer, "passwordmgr-crypto-login", true); 1063 Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", true); 1064 doc.addEventListener("mozCleverClosureHack", observer, false); 1065 return; 1066 } 1067 1068 this.log("fillDocument processing " + forms.length + 1069 " forms on " + doc.documentURI); 1070 1071 var autofillForm = !this._inPrivateBrowsing && 1072 this._prefBranch.getBoolPref("autofillForms"); 1073 var previousActionOrigin = null; 1074 var foundLogins = null; 1075 1076 for (var i = 0; i < forms.length; i++) { 1077 var form = forms[i]; 1078 1079 // Only the actionOrigin might be changing, so if it's the same 1080 // as the last form on the page we can reuse the same logins. 1081 var actionOrigin = this._getActionOrigin(form); 1082 if (actionOrigin != previousActionOrigin) { 1083 foundLogins = null; 1084 previousActionOrigin = actionOrigin; 1085 } 1086 this.log("_fillDocument processing form[" + i + "]"); 1087 foundLogins = this._fillForm(form, autofillForm, false, false, foundLogins)[1]; 1088 } // foreach form 1089 }, 1090 1091 1092 /* 1093 * _fillform 1094 * 1095 * Fill the form with login information if we can find it. This will find 1096 * an array of logins if not given any, otherwise it will use the logins 1097 * passed in. The logins are returned so they can be reused for 1098 * optimization. Success of action is also returned in format 1099 * [success, foundLogins]. autofillForm denotes if we should fill the form 1100 * in automatically, ignoreAutocomplete denotes if we should ignore 1101 * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo 1102 * for optimization 1103 */ 1104 _fillForm : function (form, autofillForm, ignoreAutocomplete, 1105 clobberPassword, foundLogins) { 1106 // Heuristically determine what the user/pass fields are 1107 // We do this before checking to see if logins are stored, 1108 // so that the user isn't prompted for a master password 1109 // without need. 1110 var [usernameField, passwordField, ignored] = 1111 this._getFormFields(form, false); 1112 1113 // Need a valid password field to do anything. 1114 if (passwordField == null) 1115 return [false, foundLogins]; 1116 1117 // If the fields are disabled or read-only, there's nothing to do. 1118 if (passwordField.disabled || passwordField.readOnly || 1119 usernameField && (usernameField.disabled || 1120 usernameField.readOnly)) { 1121 this.log("not filling form, login fields disabled"); 1122 return [false, foundLogins]; 1123 } 1124 1125 // Need to get a list of logins if we weren't given them 1126 if (foundLogins == null) { 1127 var formOrigin = 1128 this._getPasswordOrigin(form.ownerDocument.documentURI); 1129 var actionOrigin = this._getActionOrigin(form); 1130 foundLogins = this.findLogins({}, formOrigin, actionOrigin, null); 1131 this.log("found " + foundLogins.length + " matching logins."); 1132 } else { 1133 this.log("reusing logins from last form."); 1134 } 1135 1136 // Discard logins which have username/password values that don't 1137 // fit into the fields (as specified by the maxlength attribute). 1138 // The user couldn't enter these values anyway, and it helps 1139 // with sites that have an extra PIN to be entered (bug 391514) 1140 var maxUsernameLen = Number.MAX_VALUE; 1141 var maxPasswordLen = Number.MAX_VALUE; 1142 1143 // If attribute wasn't set, default is -1. 1144 if (usernameField && usernameField.maxLength >= 0) 1145 maxUsernameLen = usernameField.maxLength; 1146 if (passwordField.maxLength >= 0) 1147 maxPasswordLen = passwordField.maxLength; 1148 1149 var logins = foundLogins.filter(function (l) { 1150 var fit = (l.username.length <= maxUsernameLen && 1151 l.password.length <= maxPasswordLen); 1152 if (!fit) 1153 this.log("Ignored " + l.username + " login: won't fit"); 1154 1155 return fit; 1156 }, this); 1157 1158 1159 // Nothing to do if we have no matching logins available. 1160 if (logins.length == 0) 1161 return [false, foundLogins]; 1162 1163 1164 // The reason we didn't end up filling the form, if any. We include 1165 // this in the formInfo object we send with the passwordmgr-found-logins 1166 // notification. See the _notifyFoundLogins docs for possible values. 1167 var didntFillReason = null; 1168 1169 // Attach autocomplete stuff to the username field, if we have 1170 // one. This is normally used to select from multiple accounts, 1171 // but even with one account we should refill if the user edits. 1172 if (usernameField) 1173 this._attachToInput(usernameField); 1174 1175 // Don't clobber an existing password. 1176 if (passwordField.value && !clobberPassword) { 1177 didntFillReason = "existingPassword"; 1178 this._notifyFoundLogins(didntFillReason, usernameField, 1179 passwordField, foundLogins, null); 1180 return [false, foundLogins]; 1181 } 1182 1183 // If the form has an autocomplete=off attribute in play, don't 1184 // fill in the login automatically. We check this after attaching 1185 // the autocomplete stuff to the username field, so the user can 1186 // still manually select a login to be filled in. 1187 var isFormDisabled = false; 1188 if (!ignoreAutocomplete && 1189 (this._isAutocompleteDisabled(form) || 1190 this._isAutocompleteDisabled(usernameField) || 1191 this._isAutocompleteDisabled(passwordField))) { 1192 1193 isFormDisabled = true; 1194 this.log("form not filled, has autocomplete=off"); 1195 } 1196 1197 // Variable such that we reduce code duplication and can be sure we 1198 // should be firing notifications if and only if we can fill the form. 1199 var selectedLogin = null; 1200 1201 if (usernameField && usernameField.value) { 1202 // If username was specified in the form, only fill in the 1203 // password if we find a matching login. 1204 var username = usernameField.value.toLowerCase(); 1205 1206 let matchingLogins = logins.filter(function(l) 1207 l.username.toLowerCase() == username); 1208 if (matchingLogins.length) { 1209 selectedLogin = matchingLogins[0]; 1210 } else { 1211 didntFillReason = "existingUsername"; 1212 this.log("Password not filled. None of the stored " + 1213 "logins match the username already present."); 1214 } 1215 } else if (logins.length == 1) { 1216 selectedLogin = logins[0]; 1217 } else { 1218 // We have multiple logins. Handle a special case here, for sites 1219 // which have a normal user+pass login *and* a password-only login 1220 // (eg, a PIN). Prefer the login that matches the type of the form 1221 // (user+pass or pass-only) when there's exactly one that matches. 1222 let matchingLogins; 1223 if (usernameField) 1224 matchingLogins = logins.filter(function(l) l.username); 1225 else 1226 matchingLogins = logins.filter(function(l) !l.username); 1227 if (matchingLogins.length == 1) { 1228 selectedLogin = matchingLogins[0]; 1229 } else { 1230 didntFillReason = "multipleLogins"; 1231 this.log("Multiple logins for form, so not filling any."); 1232 } 1233 } 1234 1235 var didFillForm = false; 1236 if (selectedLogin && autofillForm && !isFormDisabled) { 1237 // Fill the form 1238 if (usernameField) 1239 usernameField.value = selectedLogin.username; 1240 passwordField.value = selectedLogin.password; 1241 didFillForm = true; 1242 } else if (selectedLogin && !autofillForm) { 1243 // For when autofillForm is false, but we still have the information 1244 // to fill a form, we notify observers. 1245 didntFillReason = "noAutofillForms"; 1246 Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); 1247 this.log("autofillForms=false but form can be filled; notified observers"); 1248 } else if (selectedLogin && isFormDisabled) { 1249 // For when autocomplete is off, but we still have the information 1250 // to fill a form, we notify observers. 1251 didntFillReason = "autocompleteOff"; 1252 Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason); 1253 this.log("autocomplete=off but form can be filled; notified observers"); 1254 } 1255 1256 this._notifyFoundLogins(didntFillReason, usernameField, passwordField, 1257 foundLogins, selectedLogin); 1258 1259 return [didFillForm, foundLogins]; 1260 }, 1261 1262 /** 1263 * Notify observers about an attempt to fill a form that resulted in some 1264 * saved logins being found for the form. 1265 * 1266 * This does not get called if the login manager attempts to fill a form 1267 * but does not find any saved logins. It does, however, get called when 1268 * the login manager does find saved logins whether or not it actually 1269 * fills the form with one of them. 1270 * 1271 * @param didntFillReason {String} 1272 * the reason the login manager didn't fill the form, if any; 1273 * if the value of this parameter is null, then the form was filled; 1274 * otherwise, this parameter will be one of these values: 1275 * existingUsername: the username field already contains a username 1276 * that doesn't match any stored usernames 1277 * existingPassword: the password field already contains a password 1278 * autocompleteOff: autocomplete has been disabled for the form 1279 * or its username or password fields 1280 * multipleLogins: we have multiple logins for the form 1281 * noAutofillForms: the autofillForms pref is set to false 1282 * 1283 * @param usernameField {HTMLInputElement} 1284 * the username field detected by the login manager, if any; 1285 * otherwise null 1286 * 1287 * @param passwordField {HTMLInputElement} 1288 * the password field detected by the login manager 1289 * 1290 * @param foundLogins {Array} 1291 * an array of nsILoginInfos that can be used to fill the form 1292 * 1293 * @param selectedLogin {nsILoginInfo} 1294 * the nsILoginInfo that was/would be used to fill the form, if any; 1295 * otherwise null; whether or not it was actually used depends on 1296 * the value of the didntFillReason parameter 1297 */ 1298 _notifyFoundLogins : function (didntFillReason, usernameField, 1299 passwordField, foundLogins, selectedLogin) { 1300 // We need .setProperty(), which is a method on the original 1301 // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2 1302 // doesn't inherit from that, so the additional QI is needed. 1303 let formInfo = Cc["@mozilla.org/hash-property-bag;1"]. 1304 createInstance(Ci.nsIWritablePropertyBag2). 1305 QueryInterface(Ci.nsIWritablePropertyBag); 1306 1307 formInfo.setPropertyAsACString("didntFillReason", didntFillReason); 1308 formInfo.setPropertyAs…
Large files files are truncated, but you can click here to view the full file