Bug 361517 - Don't fill in passwords / usernames in readonly fields. r=gavin.
[wine-gecko.git] / toolkit / components / passwordmgr / src / nsLoginManager.js
blob60daf7d6e6076eaf4525c39d8074b75301307754
1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
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/
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.
14 * The Original Code is mozilla.org code.
16 * The Initial Developer of the Original Code is Mozilla Corporation.
17 * Portions created by the Initial Developer are Copyright (C) 2007
18 * the Initial Developer. All Rights Reserved.
20 * Contributor(s):
21 * Justin Dolske <dolske@mozilla.com> (original author)
23 * Alternatively, the contents of this file may be used under the terms of
24 * either the GNU General Public License Version 2 or later (the "GPL"), or
25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 * in which case the provisions of the GPL or the LGPL are applicable instead
27 * of those above. If you wish to allow use of your version of this file only
28 * under the terms of either the GPL or the LGPL, and not to allow others to
29 * use your version of this file under the terms of the MPL, indicate your
30 * decision by deleting the provisions above and replace them with the notice
31 * and other provisions required by the GPL or the LGPL. If you do not delete
32 * the provisions above, a recipient may use your version of this file under
33 * the terms of any one of the MPL, the GPL or the LGPL.
35 * ***** END LICENSE BLOCK ***** */
38 const Cc = Components.classes;
39 const Ci = Components.interfaces;
41 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
43 function LoginManager() {
44 this.init();
47 LoginManager.prototype = {
49 classDescription: "LoginManager",
50 contractID: "@mozilla.org/login-manager;1",
51 classID: Components.ID("{cb9e0de8-3598-4ed7-857b-827f011ad5d8}"),
52 QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager,
53 Ci.nsISupportsWeakReference]),
56 /* ---------- private memebers ---------- */
59 __logService : null, // Console logging service, used for debugging.
60 get _logService() {
61 if (!this.__logService)
62 this.__logService = Cc["@mozilla.org/consoleservice;1"].
63 getService(Ci.nsIConsoleService);
64 return this.__logService;
68 __ioService: null, // IO service for string -> nsIURI conversion
69 get _ioService() {
70 if (!this.__ioService)
71 this.__ioService = Cc["@mozilla.org/network/io-service;1"].
72 getService(Ci.nsIIOService);
73 return this.__ioService;
77 __formFillService : null, // FormFillController, for username autocompleting
78 get _formFillService() {
79 if (!this.__formFillService)
80 this.__formFillService =
81 Cc["@mozilla.org/satchel/form-fill-controller;1"].
82 getService(Ci.nsIFormFillController);
83 return this.__formFillService;
87 __observerService : null, // Observer Service, for notifications
88 get _observerService() {
89 if (!this.__observerService)
90 this.__observerService = Cc["@mozilla.org/observer-service;1"].
91 getService(Ci.nsIObserverService);
92 return this.__observerService;
96 __storage : null, // Storage component which contains the saved logins
97 get _storage() {
98 if (!this.__storage) {
100 var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
101 try {
102 var catMan = Cc["@mozilla.org/categorymanager;1"].
103 getService(Ci.nsICategoryManager);
104 contractID = catMan.getCategoryEntry("login-manager-storage",
105 "nsILoginManagerStorage");
106 this.log("Found alternate nsILoginManagerStorage with " +
107 "contract ID: " + contractID);
108 } catch (e) {
109 this.log("No alternate nsILoginManagerStorage registered");
112 this.__storage = Cc[contractID].
113 createInstance(Ci.nsILoginManagerStorage);
114 try {
115 this.__storage.init();
116 } catch (e) {
117 this.log("Initialization of storage component failed: " + e);
118 this.__storage = null;
122 return this.__storage;
125 _prefBranch : null, // Preferences service
126 _nsLoginInfo : null, // Constructor for nsILoginInfo implementation
128 _remember : true, // mirrors signon.rememberSignons preference
129 _debug : false, // mirrors signon.debug
133 * init
135 * Initialize the Login Manager. Automatically called when service
136 * is created.
138 * Note: Service created in /browser/base/content/browser.js,
139 * delayedStartup()
141 init : function () {
143 // Cache references to current |this| in utility objects
144 this._webProgressListener._domEventListener = this._domEventListener;
145 this._webProgressListener._pwmgr = this;
146 this._domEventListener._pwmgr = this;
147 this._observer._pwmgr = this;
149 // Preferences. Add observer so we get notified of changes.
150 this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
151 getService(Ci.nsIPrefService).getBranch("signon.");
152 this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
153 this._prefBranch.addObserver("", this._observer, false);
155 // Get current preference values.
156 this._debug = this._prefBranch.getBoolPref("debug");
158 this._remember = this._prefBranch.getBoolPref("rememberSignons");
161 // Get constructor for nsILoginInfo
162 this._nsLoginInfo = new Components.Constructor(
163 "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
166 // Form submit observer checks forms for new logins and pw changes.
167 this._observerService.addObserver(this._observer, "earlyformsubmit", false);
168 this._observerService.addObserver(this._observer, "xpcom-shutdown", false);
170 // WebProgressListener for getting notification of new doc loads.
171 var progress = Cc["@mozilla.org/docloaderservice;1"].
172 getService(Ci.nsIWebProgress);
173 progress.addProgressListener(this._webProgressListener,
174 Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
181 * log
183 * Internal function for logging debug messages to the Error Console window
185 log : function (message) {
186 if (!this._debug)
187 return;
188 dump("Login Manager: " + message + "\n");
189 this._logService.logStringMessage("Login Manager: " + message);
193 /* ---------- Utility objects ---------- */
197 * _observer object
199 * Internal utility object, implements the nsIObserver interface.
200 * Used to receive notification for: form submission, preference changes.
202 _observer : {
203 _pwmgr : null,
205 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
206 Ci.nsIFormSubmitObserver,
207 Ci.nsISupportsWeakReference]),
210 // nsFormSubmitObserver
211 notify : function (formElement, aWindow, actionURI) {
212 this._pwmgr.log("observer notified for form submission.");
214 // We're invoked before the content's |onsubmit| handlers, so we
215 // can grab form data before it might be modified (see bug 257781).
217 try {
218 this._pwmgr._onFormSubmit(formElement);
219 } catch (e) {
220 this._pwmgr.log("Caught error in onFormSubmit: " + e);
223 return true; // Always return true, or form submit will be canceled.
226 // nsObserver
227 observe : function (subject, topic, data) {
229 if (topic == "nsPref:changed") {
230 var prefName = data;
231 this._pwmgr.log("got change to " + prefName + " preference");
233 if (prefName == "debug") {
234 this._pwmgr._debug =
235 this._pwmgr._prefBranch.getBoolPref("debug");
236 } else if (prefName == "rememberSignons") {
237 this._pwmgr._remember =
238 this._pwmgr._prefBranch.getBoolPref("rememberSignons");
239 } else {
240 this._pwmgr.log("Oops! Pref not handled, change ignored.");
242 } else if (topic == "xpcom-shutdown") {
243 for (let i in this._pwmgr) {
244 try {
245 this._pwmgr[i] = null;
246 } catch(ex) {}
248 this._pwmgr = null;
249 } else {
250 this._pwmgr.log("Oops! Unexpected notification: " + topic);
257 * _webProgressListener object
259 * Internal utility object, implements nsIWebProgressListener interface.
260 * This is attached to the document loader service, so we get
261 * notifications about all page loads.
263 _webProgressListener : {
264 _pwmgr : null,
265 _domEventListener : null,
267 QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
268 Ci.nsISupportsWeakReference]),
271 onStateChange : function (aWebProgress, aRequest,
272 aStateFlags, aStatus) {
274 // STATE_START is too early, doc is still the old page.
275 if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
276 return;
278 if (!this._pwmgr._remember)
279 return;
281 var domWin = aWebProgress.DOMWindow;
282 var domDoc = domWin.document;
284 // Only process things which might have HTML forms.
285 if (!(domDoc instanceof Ci.nsIDOMHTMLDocument))
286 return;
288 this._pwmgr.log("onStateChange accepted: req = " +
289 (aRequest ? aRequest.name : "(null)") +
290 ", flags = 0x" + aStateFlags.toString(16));
292 // Fastback doesn't fire DOMContentLoaded, so process forms now.
293 if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) {
294 this._pwmgr.log("onStateChange: restoring document");
295 return this._pwmgr._fillDocument(domDoc);
298 // Add event listener to process page when DOM is complete.
299 domDoc.addEventListener("DOMContentLoaded",
300 this._domEventListener, false);
301 return;
304 // stubs for the nsIWebProgressListener interfaces which we don't use.
305 onProgressChange : function() { throw "Unexpected onProgressChange"; },
306 onLocationChange : function() { throw "Unexpected onLocationChange"; },
307 onStatusChange : function() { throw "Unexpected onStatusChange"; },
308 onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
313 * _domEventListener object
315 * Internal utility object, implements nsIDOMEventListener
316 * Used to catch certain DOM events needed to properly implement form fill.
318 _domEventListener : {
319 _pwmgr : null,
321 QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
322 Ci.nsISupportsWeakReference]),
325 handleEvent : function (event) {
326 this._pwmgr.log("domEventListener: got event " + event.type);
328 var doc, inputElement;
329 switch (event.type) {
330 case "DOMContentLoaded":
331 doc = event.target;
332 this._pwmgr._fillDocument(doc);
333 return;
335 case "DOMAutoComplete":
336 case "blur":
337 inputElement = event.target;
338 this._pwmgr._fillPassword(inputElement);
339 return;
341 default:
342 this._pwmgr.log("Oops! This event unexpected.");
343 return;
351 /* ---------- Primary Public interfaces ---------- */
357 * addLogin
359 * Add a new login to login storage.
361 addLogin : function (login) {
362 // Sanity check the login
363 if (login.hostname == null || login.hostname.length == 0)
364 throw "Can't add a login with a null or empty hostname.";
366 // For logins w/o a username, set to "", not null.
367 if (login.username == null)
368 throw "Can't add a login with a null username.";
370 if (login.password == null || login.password.length == 0)
371 throw "Can't add a login with a null or empty password.";
373 if (login.formSubmitURL || login.formSubmitURL == "") {
374 // We have a form submit URL. Can't have a HTTP realm.
375 if (login.httpRealm != null)
376 throw "Can't add a login with both a httpRealm and formSubmitURL.";
377 } else if (login.httpRealm) {
378 // We have a HTTP realm. Can't have a form submit URL.
379 if (login.formSubmitURL != null)
380 throw "Can't add a login with both a httpRealm and formSubmitURL.";
381 } else {
382 // Need one or the other!
383 throw "Can't add a login without a httpRealm or formSubmitURL.";
387 // Look for an existing entry.
388 var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
389 login.httpRealm);
391 if (logins.some(function(l) login.matches(l, true)))
392 throw "This login already exists.";
394 this.log("Adding login: " + login);
395 return this._storage.addLogin(login);
400 * removeLogin
402 * Remove the specified login from the stored logins.
404 removeLogin : function (login) {
405 this.log("Removing login: " + login);
406 return this._storage.removeLogin(login);
411 * modifyLogin
413 * Change the specified login to match the new login.
415 modifyLogin : function (oldLogin, newLogin) {
416 this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin);
417 return this._storage.modifyLogin(oldLogin, newLogin);
422 * getAllLogins
424 * Get a dump of all stored logins. Used by the login manager UI.
426 * |count| is only needed for XPCOM.
428 * Returns an array of logins. If there are no logins, the array is empty.
430 getAllLogins : function (count) {
431 this.log("Getting a list of all logins");
432 return this._storage.getAllLogins(count);
437 * removeAllLogins
439 * Remove all stored logins.
441 removeAllLogins : function () {
442 this.log("Removing all logins");
443 this._storage.removeAllLogins();
447 * getAllDisabledHosts
449 * Get a list of all hosts for which logins are disabled.
451 * |count| is only needed for XPCOM.
453 * Returns an array of disabled logins. If there are no disabled logins,
454 * the array is empty.
456 getAllDisabledHosts : function (count) {
457 this.log("Getting a list of all disabled hosts");
458 return this._storage.getAllDisabledHosts(count);
463 * findLogins
465 * Search for the known logins for entries matching the specified criteria.
467 findLogins : function (count, hostname, formSubmitURL, httpRealm) {
468 this.log("Searching for logins matching host: " + hostname +
469 ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
471 return this._storage.findLogins(count, hostname, formSubmitURL,
472 httpRealm);
477 * countLogins
479 * Search for the known logins for entries matching the specified criteria,
480 * returns only the count.
482 countLogins : function (hostname, formSubmitURL, httpRealm) {
483 this.log("Counting logins matching host: " + hostname +
484 ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
486 return this._storage.countLogins(hostname, formSubmitURL, httpRealm);
491 * getLoginSavingEnabled
493 * Check to see if user has disabled saving logins for the host.
495 getLoginSavingEnabled : function (host) {
496 this.log("Checking if logins to " + host + " can be saved.");
497 if (!this._remember)
498 return false;
500 return this._storage.getLoginSavingEnabled(host);
505 * setLoginSavingEnabled
507 * Enable or disable storing logins for the specified host.
509 setLoginSavingEnabled : function (hostname, enabled) {
510 // Nulls won't round-trip with getAllDisabledHosts().
511 if (hostname.indexOf("\0") != -1)
512 throw "Invalid hostname";
514 this.log("Saving logins for " + hostname + " enabled? " + enabled);
515 return this._storage.setLoginSavingEnabled(hostname, enabled);
520 * autoCompleteSearch
522 * Yuck. This is called directly by satchel:
523 * nsFormFillController::StartSearch()
524 * [toolkit/components/satchel/src/nsFormFillController.cpp]
526 * We really ought to have a simple way for code to register an
527 * auto-complete provider, and not have satchel calling pwmgr directly.
529 autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
530 // aPreviousResult & aResult are nsIAutoCompleteResult,
531 // aElement is nsIDOMHTMLInputElement
533 if (!this._remember)
534 return false;
536 this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
538 var result = null;
540 if (aPreviousResult) {
541 this.log("Using previous autocomplete result");
542 result = aPreviousResult;
544 // We have a list of results for a shorter search string, so just
545 // filter them further based on the new search string.
546 // Count backwards, because result.matchCount is decremented
547 // when we remove an entry.
548 for (var i = result.matchCount - 1; i >= 0; i--) {
549 var match = result.getValueAt(i);
551 // Remove results that are too short, or have different prefix.
552 if (aSearchString.length > match.length ||
553 aSearchString.toLowerCase() !=
554 match.substr(0, aSearchString.length).toLowerCase())
556 this.log("Removing autocomplete entry '" + match + "'");
557 result.removeValueAt(i, false);
560 } else {
561 this.log("Creating new autocomplete search result.");
563 var doc = aElement.ownerDocument;
564 var origin = this._getPasswordOrigin(doc.documentURI);
565 var actionOrigin = this._getActionOrigin(aElement.form);
567 var logins = this.findLogins({}, origin, actionOrigin, null);
568 var matchingLogins = [];
570 for (i = 0; i < logins.length; i++) {
571 var username = logins[i].username.toLowerCase();
572 if (aSearchString.length <= username.length &&
573 aSearchString.toLowerCase() ==
574 username.substr(0, aSearchString.length))
576 matchingLogins.push(logins[i]);
579 this.log(matchingLogins.length + " autocomplete logins avail.");
580 result = new UserAutoCompleteResult(aSearchString, matchingLogins);
583 return result;
589 /* ------- Internal methods / callbacks for document integration ------- */
595 * _getPasswordFields
597 * Returns an array of password field elements for the specified form.
598 * If no pw fields are found, or if more than 3 are found, then null
599 * is returned.
601 * skipEmptyFields can be set to ignore password fields with no value.
603 _getPasswordFields : function (form, skipEmptyFields) {
604 // Locate the password fields in the form.
605 var pwFields = [];
606 for (var i = 0; i < form.elements.length; i++) {
607 if (form.elements[i].type != "password")
608 continue;
610 if (skipEmptyFields && !form.elements[i].value)
611 continue;
613 pwFields[pwFields.length] = {
614 index : i,
615 element : form.elements[i]
619 // If too few or too many fields, bail out.
620 if (pwFields.length == 0) {
621 this.log("(form ignored -- no password fields.)");
622 return null;
623 } else if (pwFields.length > 3) {
624 this.log("(form ignored -- too many password fields. [got " +
625 pwFields.length + "])");
626 return null;
629 return pwFields;
634 * _getFormFields
636 * Returns the username and password fields found in the form.
637 * Can handle complex forms by trying to figure out what the
638 * relevant fields are.
640 * Returns: [usernameField, newPasswordField, oldPasswordField]
642 * usernameField may be null.
643 * newPasswordField will always be non-null.
644 * oldPasswordField may be null. If null, newPasswordField is just
645 * "theLoginField". If not null, the form is apparently a
646 * change-password field, with oldPasswordField containing the password
647 * that is being changed.
649 _getFormFields : function (form, isSubmission) {
650 var usernameField = null;
652 // Locate the password field(s) in the form. Up to 3 supported.
653 // If there's no password field, there's nothing for us to do.
654 var pwFields = this._getPasswordFields(form, isSubmission);
655 if (!pwFields)
656 return [null, null, null];
659 // Locate the username field in the form by searching backwards
660 // from the first passwordfield, assume the first text field is the
661 // username. We might not find a username field if the user is
662 // already logged in to the site.
663 for (var i = pwFields[0].index - 1; i >= 0; i--) {
664 if (form.elements[i].type == "text") {
665 usernameField = form.elements[i];
666 break;
670 if (!usernameField)
671 this.log("(form -- no username field found)");
674 // If we're not submitting a form (it's a page load), there are no
675 // password field values for us to use for identifying fields. So,
676 // just assume the first password field is the one to be filled in.
677 if (!isSubmission || pwFields.length == 1)
678 return [usernameField, pwFields[0].element, null];
681 // Try to figure out WTF is in the form based on the password values.
682 var oldPasswordField, newPasswordField;
683 var pw1 = pwFields[0].element.value;
684 var pw2 = pwFields[1].element.value;
685 var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
687 if (pwFields.length == 3) {
688 // Look for two identical passwords, that's the new password
690 if (pw1 == pw2 && pw2 == pw3) {
691 // All 3 passwords the same? Weird! Treat as if 1 pw field.
692 newPasswordField = pwFields[0].element;
693 oldPasswordField = null;
694 } else if (pw1 == pw2) {
695 newPasswordField = pwFields[0].element;
696 oldPasswordField = pwFields[2].element;
697 } else if (pw2 == pw3) {
698 oldPasswordField = pwFields[0].element;
699 newPasswordField = pwFields[2].element;
700 } else if (pw1 == pw3) {
701 // A bit odd, but could make sense with the right page layout.
702 newPasswordField = pwFields[0].element;
703 oldPasswordField = pwFields[1].element;
704 } else {
705 // We can't tell which of the 3 passwords should be saved.
706 this.log("(form ignored -- all 3 pw fields differ)");
707 return [null, null, null];
709 } else { // pwFields.length == 2
710 if (pw1 == pw2) {
711 // Treat as if 1 pw field
712 newPasswordField = pwFields[0].element;
713 oldPasswordField = null;
714 } else {
715 // Just assume that the 2nd password is the new password
716 oldPasswordField = pwFields[0].element;
717 newPasswordField = pwFields[1].element;
721 return [usernameField, newPasswordField, oldPasswordField];
726 * _isAutoCompleteDisabled
728 * Returns true if the page requests autocomplete be disabled for the
729 * specified form input.
731 _isAutocompleteDisabled : function (element) {
732 if (element && element.hasAttribute("autocomplete") &&
733 element.getAttribute("autocomplete").toLowerCase() == "off")
734 return true;
736 return false;
740 * _onFormSubmit
742 * Called by the our observer when notified of a form submission.
743 * [Note that this happens before any DOM onsubmit handlers are invoked.]
744 * Looks for a password change in the submitted form, so we can update
745 * our stored password.
747 _onFormSubmit : function (form) {
749 // local helper function
750 function getPrompter(aWindow) {
751 var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
752 createInstance(Ci.nsILoginManagerPrompter);
753 prompterSvc.init(aWindow);
754 return prompterSvc;
757 var doc = form.ownerDocument;
758 var win = doc.defaultView;
760 // If password saving is disabled (globally or for host), bail out now.
761 if (!this._remember)
762 return;
764 var hostname = this._getPasswordOrigin(doc.documentURI);
765 var formSubmitURL = this._getActionOrigin(form)
766 if (!this.getLoginSavingEnabled(hostname)) {
767 this.log("(form submission ignored -- saving is " +
768 "disabled for: " + hostname + ")");
769 return;
773 // Get the appropriate fields from the form.
774 var [usernameField, newPasswordField, oldPasswordField] =
775 this._getFormFields(form, true);
777 // Need at least 1 valid password field to do anything.
778 if (newPasswordField == null)
779 return;
781 // Check for autocomplete=off attribute. We don't use it to prevent
782 // autofilling (for existing logins), but won't save logins when it's
783 // present.
784 if (this._isAutocompleteDisabled(form) ||
785 this._isAutocompleteDisabled(usernameField) ||
786 this._isAutocompleteDisabled(newPasswordField) ||
787 this._isAutocompleteDisabled(oldPasswordField)) {
788 this.log("(form submission ignored -- autocomplete=off found)");
789 return;
793 var formLogin = new this._nsLoginInfo();
794 formLogin.init(hostname, formSubmitURL, null,
795 (usernameField ? usernameField.value : ""),
796 newPasswordField.value,
797 (usernameField ? usernameField.name : ""),
798 newPasswordField.name);
800 // If we didn't find a username field, but seem to be changing a
801 // password, allow the user to select from a list of applicable
802 // logins to update the password for.
803 if (!usernameField && oldPasswordField) {
805 var logins = this.findLogins({}, hostname, formSubmitURL, null);
807 if (logins.length == 0) {
808 // Could prompt to save this as a new password-only login.
809 // This seems uncommon, and might be wrong, so ignore.
810 this.log("(no logins for this host -- pwchange ignored)");
811 return;
814 var prompter = getPrompter(win);
816 if (logins.length == 1) {
817 var oldLogin = logins[0];
818 formLogin.username = oldLogin.username;
819 formLogin.usernameField = oldLogin.usernameField;
821 prompter.promptToChangePassword(oldLogin, formLogin);
822 } else {
823 prompter.promptToChangePasswordWithUsernames(
824 logins, logins.length, formLogin);
827 return;
831 // Look for an existing login that matches the form login.
832 var existingLogin = null;
833 var logins = this.findLogins({}, hostname, formSubmitURL, null);
835 for (var i = 0; i < logins.length; i++) {
836 var same, login = logins[i];
838 // If one login has a username but the other doesn't, ignore
839 // the username when comparing and only match if they have the
840 // same password. Otherwise, compare the logins and match even
841 // if the passwords differ.
842 if (!login.username && formLogin.username) {
843 var restoreMe = formLogin.username;
844 formLogin.username = "";
845 same = formLogin.matches(login, false);
846 formLogin.username = restoreMe;
847 } else if (!formLogin.username && login.username) {
848 formLogin.username = login.username;
849 same = formLogin.matches(login, false);
850 formLogin.username = ""; // we know it's always blank.
851 } else {
852 same = formLogin.matches(login, true);
855 if (same) {
856 existingLogin = login;
857 break;
861 if (existingLogin) {
862 this.log("Found an existing login matching this form submission");
865 * Change password if needed.
867 * If the login has a username, change the password w/o prompting
868 * (because we can be fairly sure there's only one password
869 * associated with the username). But for logins without a
870 * username, ask the user... Some sites use a password-only "login"
871 * in different contexts (enter your PIN, answer a security
872 * question, etc), and without a username we can't be sure if
873 * modifying an existing login is the right thing to do.
875 if (existingLogin.password != formLogin.password) {
876 if (formLogin.username) {
877 this.log("...Updating password for existing login.");
878 this.modifyLogin(existingLogin, formLogin);
879 } else {
880 this.log("...passwords differ, prompting to change.");
881 prompter = getPrompter(win);
882 prompter.promptToChangePassword(existingLogin, formLogin);
886 return;
890 // Prompt user to save login (via dialog or notification bar)
891 prompter = getPrompter(win);
892 prompter.promptToSavePassword(formLogin);
897 * _getPasswordOrigin
899 * Get the parts of the URL we want for identification.
901 _getPasswordOrigin : function (uriString, allowJS) {
902 var realm = "";
903 try {
904 var uri = this._ioService.newURI(uriString, null, null);
906 if (allowJS && uri.scheme == "javascript")
907 return "javascript:"
909 realm = uri.scheme + "://" + uri.host;
911 // If the URI explicitly specified a port, only include it when
912 // it's not the default. (We never want "http://foo.com:80")
913 var port = uri.port;
914 if (port != -1) {
915 var handler = this._ioService.getProtocolHandler(uri.scheme);
916 if (port != handler.defaultPort)
917 realm += ":" + port;
920 } catch (e) {
921 // bug 159484 - disallow url types that don't support a hostPort.
922 // (although we handle "javascript:..." as a special case above.)
923 this.log("Couldn't parse origin for " + uriString);
924 realm = null;
927 return realm;
930 _getActionOrigin : function (form) {
931 var uriString = form.action;
933 // A blank or mission action submits to where it came from.
934 if (uriString == "")
935 uriString = form.baseURI; // ala bug 297761
937 return this._getPasswordOrigin(uriString, true);
942 * _fillDocument
944 * Called when a page has loaded. For each form in the document,
945 * we check to see if it can be filled with a stored login.
947 _fillDocument : function (doc) {
948 var forms = doc.forms;
949 if (!forms || forms.length == 0)
950 return;
952 var formOrigin = this._getPasswordOrigin(doc.documentURI);
954 // If there are no logins for this site, bail out now.
955 if (!this.countLogins(formOrigin, "", null))
956 return;
958 this.log("fillDocument processing " + forms.length +
959 " forms on " + doc.documentURI);
961 var autofillForm = this._prefBranch.getBoolPref("autofillForms");
962 var previousActionOrigin = null;
963 var foundLogins = null;
965 for (var i = 0; i < forms.length; i++) {
966 var form = forms[i];
968 // Only the actionOrigin might be changing, so if it's the same
969 // as the last form on the page we can reuse the same logins.
970 var actionOrigin = this._getActionOrigin(form);
971 if (actionOrigin != previousActionOrigin) {
972 foundLogins = null;
973 previousActionOrigin = actionOrigin;
975 this.log("_fillDocument processing form[" + i + "]");
976 foundLogins = this._fillForm(form, autofillForm, false, foundLogins)[1];
977 } // foreach form
982 * _fillform
984 * Fill the form with login information if we can find it. This will find
985 * an array of logins if not given any, otherwise it will use the logins
986 * passed in. The logins are returned so they can be reused for
987 * optimization. Success of action is also returned in format
988 * [success, foundLogins]. autofillForm denotes if we should fill the form
989 * in automatically, ignoreAutocomplete denotes if we should ignore
990 * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo
991 * for optimization
993 _fillForm : function (form, autofillForm, ignoreAutocomplete, foundLogins) {
994 // Heuristically determine what the user/pass fields are
995 // We do this before checking to see if logins are stored,
996 // so that the user isn't prompted for a master password
997 // without need.
998 var [usernameField, passwordField, ignored] =
999 this._getFormFields(form, false);
1001 // Need a valid password field to do anything.
1002 if (passwordField == null)
1003 return [false, foundLogins];
1005 // If the fields are disabled or read-only, there's nothing to do.
1006 if (passwordField.disabled || passwordField.readOnly ||
1007 usernameField && (usernameField.disabled ||
1008 usernameField.readOnly)) {
1009 this.log("not filling form, login fields disabled");
1010 return [false, foundLogins];
1013 // If there's only a password field and it has a value, there's
1014 // nothing for us to do. (Don't clobber the existing value)
1015 if (!usernameField && passwordField.value)
1016 return [false, foundLogins];
1018 // Need to get a list of logins if we weren't given them
1019 if (foundLogins == null) {
1020 var formOrigin =
1021 this._getPasswordOrigin(form.ownerDocument.documentURI);
1022 var actionOrigin = this._getActionOrigin(form);
1023 foundLogins = this.findLogins({}, formOrigin, actionOrigin, null);
1024 this.log("found " + foundLogins.length + " matching logins.");
1025 } else {
1026 this.log("reusing logins from last form.");
1029 // Discard logins which have username/password values that don't
1030 // fit into the fields (as specified by the maxlength attribute).
1031 // The user couldn't enter these values anyway, and it helps
1032 // with sites that have an extra PIN to be entered (bug 391514)
1033 var maxUsernameLen = Number.MAX_VALUE;
1034 var maxPasswordLen = Number.MAX_VALUE;
1036 // If attribute wasn't set, default is -1.
1037 if (usernameField && usernameField.maxLength >= 0)
1038 maxUsernameLen = usernameField.maxLength;
1039 if (passwordField.maxLength >= 0)
1040 maxPasswordLen = passwordField.maxLength;
1042 logins = foundLogins.filter(function (l) {
1043 var fit = (l.username.length <= maxUsernameLen &&
1044 l.password.length <= maxPasswordLen);
1045 if (!fit)
1046 this.log("Ignored " + l.username + " login: won't fit");
1048 return fit;
1049 }, this);
1052 // Nothing to do if we have no matching logins available.
1053 if (logins.length == 0)
1054 return [false, foundLogins];
1057 // Attach autocomplete stuff to the username field, if we have
1058 // one. This is normally used to select from multiple accounts,
1059 // but even with one account we should refill if the user edits.
1060 if (usernameField)
1061 this._attachToInput(usernameField);
1063 // If the form has an autocomplete=off attribute in play, don't
1064 // fill in the login automatically. We check this after attaching
1065 // the autocomplete stuff to the username field, so the user can
1066 // still manually select a login to be filled in.
1067 var isFormDisabled = false;
1068 if (!ignoreAutocomplete &&
1069 (this._isAutocompleteDisabled(form) ||
1070 this._isAutocompleteDisabled(usernameField) ||
1071 this._isAutocompleteDisabled(passwordField))) {
1073 isFormDisabled = true;
1074 this.log("form not filled, has autocomplete=off");
1077 // Variable such that we reduce code duplication and can be sure we
1078 // should be firing notifications if and only if we can fill the form.
1079 var selectedLogin = null;
1081 if (usernameField && usernameField.value) {
1082 // If username was specified in the form, only fill in the
1083 // password if we find a matching login.
1085 var username = usernameField.value;
1087 var matchingLogin;
1088 var found = logins.some(function(l) {
1089 matchingLogin = l;
1090 return (l.username == username);
1092 if (found)
1093 selectedLogin = matchingLogin;
1094 else
1095 this.log("Password not filled. None of the stored " +
1096 "logins match the username already present.");
1098 } else if (usernameField && logins.length == 2) {
1099 // Special case, for sites which have a normal user+pass
1100 // login *and* a password-only login (eg, a PIN)...
1101 // When we have a username field and 1 of 2 available
1102 // logins is password-only, go ahead and prefill the
1103 // one with a username.
1104 if (!logins[0].username && logins[1].username)
1105 selectedLogin = logins[1];
1106 else if (!logins[1].username && logins[0].username)
1107 selectedLogin = logins[0];
1108 } else if (logins.length == 1) {
1109 selectedLogin = logins[0];
1110 } else {
1111 this.log("Multiple logins for form, so not filling any.");
1114 var didFillForm = false;
1115 if (selectedLogin && autofillForm && !isFormDisabled) {
1116 // Fill the form
1117 if (usernameField)
1118 usernameField.value = selectedLogin.username;
1119 passwordField.value = selectedLogin.password;
1120 didFillForm = true;
1121 } else if (selectedLogin && !autofillForm) {
1122 // For when autofillForm is false, but we still have the information
1123 // to fill a form, we notify observers.
1124 this._observerService.notifyObservers(form, "passwordmgr-found-form", "noAutofillForms");
1125 this.log("autofillForms=false but form can be filled; notified observers");
1126 } else if (selectedLogin && isFormDisabled) {
1127 // For when autocomplete is off, but we still have the information
1128 // to fill a form, we notify observers.
1129 this._observerService.notifyObservers(form, "passwordmgr-found-form", "autocompleteOff");
1130 this.log("autocomplete=off but form can be filled; notified observers");
1133 return [didFillForm, foundLogins];
1138 * fillForm
1140 * Fill the form with login information if we can find it.
1142 fillForm : function (form) {
1143 this.log("fillForm processing form[id=" + form.id + "]");
1144 return this._fillForm(form, true, true, null)[0];
1149 * _attachToInput
1151 * Hooks up autocomplete support to a username field, to allow
1152 * a user editing the field to select an existing login and have
1153 * the password field filled in.
1155 _attachToInput : function (element) {
1156 this.log("attaching autocomplete stuff");
1157 element.addEventListener("blur",
1158 this._domEventListener, false);
1159 element.addEventListener("DOMAutoComplete",
1160 this._domEventListener, false);
1161 this._formFillService.markAsLoginManagerField(element);
1166 * _fillPassword
1168 * The user has autocompleted a username field, so fill in the password.
1170 _fillPassword : function (usernameField) {
1171 this.log("fillPassword autocomplete username: " + usernameField.value);
1173 var form = usernameField.form;
1174 var doc = form.ownerDocument;
1176 var hostname = this._getPasswordOrigin(doc.documentURI);
1177 var formSubmitURL = this._getActionOrigin(form)
1179 // Find the password field. We should always have at least one,
1180 // or else something has gone rather wrong.
1181 var pwFields = this._getPasswordFields(form, false);
1182 if (!pwFields) {
1183 const err = "No password field for autocomplete password fill.";
1185 // We want to know about this even if debugging is disabled.
1186 if (!this._debug)
1187 dump(err);
1188 else
1189 this.log(err);
1191 return;
1194 // If there are multiple passwords fields, we can't really figure
1195 // out what each field is for, so just fill out the last field.
1196 var passwordField = pwFields[0].element;
1198 // Temporary LoginInfo with the info we know.
1199 var currentLogin = new this._nsLoginInfo();
1200 currentLogin.init(hostname, formSubmitURL, null,
1201 usernameField.value, null,
1202 usernameField.name, passwordField.name);
1204 // Look for a existing login and use its password.
1205 var match = null;
1206 var logins = this.findLogins({}, hostname, formSubmitURL, null);
1208 if (!logins.some(function(l) {
1209 match = l;
1210 return currentLogin.matches(l, true);
1213 this.log("Can't find a login for this autocomplete result.");
1214 return;
1217 this.log("Found a matching login, filling in password.");
1218 passwordField.value = match.password;
1220 }; // end of LoginManager implementation
1225 // nsIAutoCompleteResult implementation
1226 function UserAutoCompleteResult (aSearchString, matchingLogins) {
1227 function loginSort(a,b) {
1228 var userA = a.username.toLowerCase();
1229 var userB = b.username.toLowerCase();
1231 if (userA < userB)
1232 return -1;
1234 if (userB > userA)
1235 return 1;
1237 return 0;
1240 this.searchString = aSearchString;
1241 this.logins = matchingLogins.sort(loginSort);
1242 this.matchCount = matchingLogins.length;
1244 if (this.matchCount > 0) {
1245 this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
1246 this.defaultIndex = 0;
1250 UserAutoCompleteResult.prototype = {
1251 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
1252 Ci.nsISupportsWeakReference]),
1254 // private
1255 logins : null,
1257 // Interfaces from idl...
1258 searchString : null,
1259 searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
1260 defaultIndex : -1,
1261 errorDescription : "",
1262 matchCount : 0,
1264 getValueAt : function (index) {
1265 if (index < 0 || index >= this.logins.length)
1266 throw "Index out of range.";
1268 return this.logins[index].username;
1271 getCommentAt : function (index) {
1272 return "";
1275 getStyleAt : function (index) {
1276 return "";
1279 getImageAt : function (index) {
1280 return "";
1283 removeValueAt : function (index, removeFromDB) {
1284 if (index < 0 || index >= this.logins.length)
1285 throw "Index out of range.";
1287 var [removedLogin] = this.logins.splice(index, 1);
1289 this.matchCount--;
1290 if (this.defaultIndex > this.logins.length)
1291 this.defaultIndex--;
1293 if (removeFromDB) {
1294 var pwmgr = Cc["@mozilla.org/login-manager;1"].
1295 getService(Ci.nsILoginManager);
1296 pwmgr.removeLogin(removedLogin);
1301 var component = [LoginManager];
1302 function NSGetModule (compMgr, fileSpec) {
1303 return XPCOMUtils.generateModule(component);