Backed out changeset 2f36cde1694c (bug 394611, due to leaks)
[wine-gecko.git] / toolkit / components / passwordmgr / src / nsLoginManager.js
blobee6259a8207b3e9348951d0efca2a983358d5197
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 switch (event.type) {
329 case "DOMContentLoaded":
330 this._pwmgr._fillDocument(event.target);
331 return;
333 case "DOMAutoComplete":
334 case "blur":
335 var acInputField = event.target;
336 var acForm = acInputField.form;
337 // Make sure the username field fillForm will use is the
338 // same field as the autocomplete was activated on. If
339 // not, the DOM has been altered and we'll just give up.
340 var [usernameField, passwordField, ignored] =
341 this._pwmgr._getFormFields(acForm, false);
342 if (usernameField == acInputField && passwordField) {
343 // Clobber any existing password.
344 passwordField.value = "";
345 this._pwmgr._fillForm(acForm, true, true, null);
346 } else {
347 this._pwmgr.log("Oops, form changed before AC invoked");
349 return;
351 default:
352 this._pwmgr.log("Oops! This event unexpected.");
353 return;
361 /* ---------- Primary Public interfaces ---------- */
367 * addLogin
369 * Add a new login to login storage.
371 addLogin : function (login) {
372 // Sanity check the login
373 if (login.hostname == null || login.hostname.length == 0)
374 throw "Can't add a login with a null or empty hostname.";
376 // For logins w/o a username, set to "", not null.
377 if (login.username == null)
378 throw "Can't add a login with a null username.";
380 if (login.password == null || login.password.length == 0)
381 throw "Can't add a login with a null or empty password.";
383 if (login.formSubmitURL || login.formSubmitURL == "") {
384 // We have a form submit URL. Can't have a HTTP realm.
385 if (login.httpRealm != null)
386 throw "Can't add a login with both a httpRealm and formSubmitURL.";
387 } else if (login.httpRealm) {
388 // We have a HTTP realm. Can't have a form submit URL.
389 if (login.formSubmitURL != null)
390 throw "Can't add a login with both a httpRealm and formSubmitURL.";
391 } else {
392 // Need one or the other!
393 throw "Can't add a login without a httpRealm or formSubmitURL.";
397 // Look for an existing entry.
398 var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
399 login.httpRealm);
401 if (logins.some(function(l) login.matches(l, true)))
402 throw "This login already exists.";
404 this.log("Adding login: " + login);
405 return this._storage.addLogin(login);
410 * removeLogin
412 * Remove the specified login from the stored logins.
414 removeLogin : function (login) {
415 this.log("Removing login: " + login);
416 return this._storage.removeLogin(login);
421 * modifyLogin
423 * Change the specified login to match the new login.
425 modifyLogin : function (oldLogin, newLogin) {
426 this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin);
427 return this._storage.modifyLogin(oldLogin, newLogin);
432 * getAllLogins
434 * Get a dump of all stored logins. Used by the login manager UI.
436 * |count| is only needed for XPCOM.
438 * Returns an array of logins. If there are no logins, the array is empty.
440 getAllLogins : function (count) {
441 this.log("Getting a list of all logins");
442 return this._storage.getAllLogins(count);
447 * removeAllLogins
449 * Remove all stored logins.
451 removeAllLogins : function () {
452 this.log("Removing all logins");
453 this._storage.removeAllLogins();
457 * getAllDisabledHosts
459 * Get a list of all hosts for which logins are disabled.
461 * |count| is only needed for XPCOM.
463 * Returns an array of disabled logins. If there are no disabled logins,
464 * the array is empty.
466 getAllDisabledHosts : function (count) {
467 this.log("Getting a list of all disabled hosts");
468 return this._storage.getAllDisabledHosts(count);
473 * findLogins
475 * Search for the known logins for entries matching the specified criteria.
477 findLogins : function (count, hostname, formSubmitURL, httpRealm) {
478 this.log("Searching for logins matching host: " + hostname +
479 ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
481 return this._storage.findLogins(count, hostname, formSubmitURL,
482 httpRealm);
487 * countLogins
489 * Search for the known logins for entries matching the specified criteria,
490 * returns only the count.
492 countLogins : function (hostname, formSubmitURL, httpRealm) {
493 this.log("Counting logins matching host: " + hostname +
494 ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
496 return this._storage.countLogins(hostname, formSubmitURL, httpRealm);
501 * getLoginSavingEnabled
503 * Check to see if user has disabled saving logins for the host.
505 getLoginSavingEnabled : function (host) {
506 this.log("Checking if logins to " + host + " can be saved.");
507 if (!this._remember)
508 return false;
510 return this._storage.getLoginSavingEnabled(host);
515 * setLoginSavingEnabled
517 * Enable or disable storing logins for the specified host.
519 setLoginSavingEnabled : function (hostname, enabled) {
520 // Nulls won't round-trip with getAllDisabledHosts().
521 if (hostname.indexOf("\0") != -1)
522 throw "Invalid hostname";
524 this.log("Saving logins for " + hostname + " enabled? " + enabled);
525 return this._storage.setLoginSavingEnabled(hostname, enabled);
530 * autoCompleteSearch
532 * Yuck. This is called directly by satchel:
533 * nsFormFillController::StartSearch()
534 * [toolkit/components/satchel/src/nsFormFillController.cpp]
536 * We really ought to have a simple way for code to register an
537 * auto-complete provider, and not have satchel calling pwmgr directly.
539 autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
540 // aPreviousResult & aResult are nsIAutoCompleteResult,
541 // aElement is nsIDOMHTMLInputElement
543 if (!this._remember)
544 return false;
546 this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
548 var result = null;
550 if (aPreviousResult) {
551 this.log("Using previous autocomplete result");
552 result = aPreviousResult;
554 // We have a list of results for a shorter search string, so just
555 // filter them further based on the new search string.
556 // Count backwards, because result.matchCount is decremented
557 // when we remove an entry.
558 for (var i = result.matchCount - 1; i >= 0; i--) {
559 var match = result.getValueAt(i);
561 // Remove results that are too short, or have different prefix.
562 if (aSearchString.length > match.length ||
563 aSearchString.toLowerCase() !=
564 match.substr(0, aSearchString.length).toLowerCase())
566 this.log("Removing autocomplete entry '" + match + "'");
567 result.removeValueAt(i, false);
570 } else {
571 this.log("Creating new autocomplete search result.");
573 var doc = aElement.ownerDocument;
574 var origin = this._getPasswordOrigin(doc.documentURI);
575 var actionOrigin = this._getActionOrigin(aElement.form);
577 var logins = this.findLogins({}, origin, actionOrigin, null);
578 var matchingLogins = [];
580 for (i = 0; i < logins.length; i++) {
581 var username = logins[i].username.toLowerCase();
582 if (aSearchString.length <= username.length &&
583 aSearchString.toLowerCase() ==
584 username.substr(0, aSearchString.length))
586 matchingLogins.push(logins[i]);
589 this.log(matchingLogins.length + " autocomplete logins avail.");
590 result = new UserAutoCompleteResult(aSearchString, matchingLogins);
593 return result;
599 /* ------- Internal methods / callbacks for document integration ------- */
605 * _getPasswordFields
607 * Returns an array of password field elements for the specified form.
608 * If no pw fields are found, or if more than 3 are found, then null
609 * is returned.
611 * skipEmptyFields can be set to ignore password fields with no value.
613 _getPasswordFields : function (form, skipEmptyFields) {
614 // Locate the password fields in the form.
615 var pwFields = [];
616 for (var i = 0; i < form.elements.length; i++) {
617 if (form.elements[i].type != "password")
618 continue;
620 if (skipEmptyFields && !form.elements[i].value)
621 continue;
623 pwFields[pwFields.length] = {
624 index : i,
625 element : form.elements[i]
629 // If too few or too many fields, bail out.
630 if (pwFields.length == 0) {
631 this.log("(form ignored -- no password fields.)");
632 return null;
633 } else if (pwFields.length > 3) {
634 this.log("(form ignored -- too many password fields. [got " +
635 pwFields.length + "])");
636 return null;
639 return pwFields;
644 * _getFormFields
646 * Returns the username and password fields found in the form.
647 * Can handle complex forms by trying to figure out what the
648 * relevant fields are.
650 * Returns: [usernameField, newPasswordField, oldPasswordField]
652 * usernameField may be null.
653 * newPasswordField will always be non-null.
654 * oldPasswordField may be null. If null, newPasswordField is just
655 * "theLoginField". If not null, the form is apparently a
656 * change-password field, with oldPasswordField containing the password
657 * that is being changed.
659 _getFormFields : function (form, isSubmission) {
660 var usernameField = null;
662 // Locate the password field(s) in the form. Up to 3 supported.
663 // If there's no password field, there's nothing for us to do.
664 var pwFields = this._getPasswordFields(form, isSubmission);
665 if (!pwFields)
666 return [null, null, null];
669 // Locate the username field in the form by searching backwards
670 // from the first passwordfield, assume the first text field is the
671 // username. We might not find a username field if the user is
672 // already logged in to the site.
673 for (var i = pwFields[0].index - 1; i >= 0; i--) {
674 if (form.elements[i].type == "text") {
675 usernameField = form.elements[i];
676 break;
680 if (!usernameField)
681 this.log("(form -- no username field found)");
684 // If we're not submitting a form (it's a page load), there are no
685 // password field values for us to use for identifying fields. So,
686 // just assume the first password field is the one to be filled in.
687 if (!isSubmission || pwFields.length == 1)
688 return [usernameField, pwFields[0].element, null];
691 // Try to figure out WTF is in the form based on the password values.
692 var oldPasswordField, newPasswordField;
693 var pw1 = pwFields[0].element.value;
694 var pw2 = pwFields[1].element.value;
695 var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
697 if (pwFields.length == 3) {
698 // Look for two identical passwords, that's the new password
700 if (pw1 == pw2 && pw2 == pw3) {
701 // All 3 passwords the same? Weird! Treat as if 1 pw field.
702 newPasswordField = pwFields[0].element;
703 oldPasswordField = null;
704 } else if (pw1 == pw2) {
705 newPasswordField = pwFields[0].element;
706 oldPasswordField = pwFields[2].element;
707 } else if (pw2 == pw3) {
708 oldPasswordField = pwFields[0].element;
709 newPasswordField = pwFields[2].element;
710 } else if (pw1 == pw3) {
711 // A bit odd, but could make sense with the right page layout.
712 newPasswordField = pwFields[0].element;
713 oldPasswordField = pwFields[1].element;
714 } else {
715 // We can't tell which of the 3 passwords should be saved.
716 this.log("(form ignored -- all 3 pw fields differ)");
717 return [null, null, null];
719 } else { // pwFields.length == 2
720 if (pw1 == pw2) {
721 // Treat as if 1 pw field
722 newPasswordField = pwFields[0].element;
723 oldPasswordField = null;
724 } else {
725 // Just assume that the 2nd password is the new password
726 oldPasswordField = pwFields[0].element;
727 newPasswordField = pwFields[1].element;
731 return [usernameField, newPasswordField, oldPasswordField];
736 * _isAutoCompleteDisabled
738 * Returns true if the page requests autocomplete be disabled for the
739 * specified form input.
741 _isAutocompleteDisabled : function (element) {
742 if (element && element.hasAttribute("autocomplete") &&
743 element.getAttribute("autocomplete").toLowerCase() == "off")
744 return true;
746 return false;
750 * _onFormSubmit
752 * Called by the our observer when notified of a form submission.
753 * [Note that this happens before any DOM onsubmit handlers are invoked.]
754 * Looks for a password change in the submitted form, so we can update
755 * our stored password.
757 _onFormSubmit : function (form) {
759 // local helper function
760 function getPrompter(aWindow) {
761 var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
762 createInstance(Ci.nsILoginManagerPrompter);
763 prompterSvc.init(aWindow);
764 return prompterSvc;
767 var doc = form.ownerDocument;
768 var win = doc.defaultView;
770 // If password saving is disabled (globally or for host), bail out now.
771 if (!this._remember)
772 return;
774 var hostname = this._getPasswordOrigin(doc.documentURI);
775 var formSubmitURL = this._getActionOrigin(form)
776 if (!this.getLoginSavingEnabled(hostname)) {
777 this.log("(form submission ignored -- saving is " +
778 "disabled for: " + hostname + ")");
779 return;
783 // Get the appropriate fields from the form.
784 var [usernameField, newPasswordField, oldPasswordField] =
785 this._getFormFields(form, true);
787 // Need at least 1 valid password field to do anything.
788 if (newPasswordField == null)
789 return;
791 // Check for autocomplete=off attribute. We don't use it to prevent
792 // autofilling (for existing logins), but won't save logins when it's
793 // present.
794 if (this._isAutocompleteDisabled(form) ||
795 this._isAutocompleteDisabled(usernameField) ||
796 this._isAutocompleteDisabled(newPasswordField) ||
797 this._isAutocompleteDisabled(oldPasswordField)) {
798 this.log("(form submission ignored -- autocomplete=off found)");
799 return;
803 var formLogin = new this._nsLoginInfo();
804 formLogin.init(hostname, formSubmitURL, null,
805 (usernameField ? usernameField.value : ""),
806 newPasswordField.value,
807 (usernameField ? usernameField.name : ""),
808 newPasswordField.name);
810 // If we didn't find a username field, but seem to be changing a
811 // password, allow the user to select from a list of applicable
812 // logins to update the password for.
813 if (!usernameField && oldPasswordField) {
815 var logins = this.findLogins({}, hostname, formSubmitURL, null);
817 if (logins.length == 0) {
818 // Could prompt to save this as a new password-only login.
819 // This seems uncommon, and might be wrong, so ignore.
820 this.log("(no logins for this host -- pwchange ignored)");
821 return;
824 var prompter = getPrompter(win);
826 if (logins.length == 1) {
827 var oldLogin = logins[0];
828 formLogin.username = oldLogin.username;
829 formLogin.usernameField = oldLogin.usernameField;
831 prompter.promptToChangePassword(oldLogin, formLogin);
832 } else {
833 prompter.promptToChangePasswordWithUsernames(
834 logins, logins.length, formLogin);
837 return;
841 // Look for an existing login that matches the form login.
842 var existingLogin = null;
843 var logins = this.findLogins({}, hostname, formSubmitURL, null);
845 for (var i = 0; i < logins.length; i++) {
846 var same, login = logins[i];
848 // If one login has a username but the other doesn't, ignore
849 // the username when comparing and only match if they have the
850 // same password. Otherwise, compare the logins and match even
851 // if the passwords differ.
852 if (!login.username && formLogin.username) {
853 var restoreMe = formLogin.username;
854 formLogin.username = "";
855 same = formLogin.matches(login, false);
856 formLogin.username = restoreMe;
857 } else if (!formLogin.username && login.username) {
858 formLogin.username = login.username;
859 same = formLogin.matches(login, false);
860 formLogin.username = ""; // we know it's always blank.
861 } else {
862 same = formLogin.matches(login, true);
865 if (same) {
866 existingLogin = login;
867 break;
871 if (existingLogin) {
872 this.log("Found an existing login matching this form submission");
875 * Change password if needed.
877 * If the login has a username, change the password w/o prompting
878 * (because we can be fairly sure there's only one password
879 * associated with the username). But for logins without a
880 * username, ask the user... Some sites use a password-only "login"
881 * in different contexts (enter your PIN, answer a security
882 * question, etc), and without a username we can't be sure if
883 * modifying an existing login is the right thing to do.
885 if (existingLogin.password != formLogin.password) {
886 if (formLogin.username) {
887 this.log("...Updating password for existing login.");
888 this.modifyLogin(existingLogin, formLogin);
889 } else {
890 this.log("...passwords differ, prompting to change.");
891 prompter = getPrompter(win);
892 prompter.promptToChangePassword(existingLogin, formLogin);
896 return;
900 // Prompt user to save login (via dialog or notification bar)
901 prompter = getPrompter(win);
902 prompter.promptToSavePassword(formLogin);
907 * _getPasswordOrigin
909 * Get the parts of the URL we want for identification.
911 _getPasswordOrigin : function (uriString, allowJS) {
912 var realm = "";
913 try {
914 var uri = this._ioService.newURI(uriString, null, null);
916 if (allowJS && uri.scheme == "javascript")
917 return "javascript:"
919 realm = uri.scheme + "://" + uri.host;
921 // If the URI explicitly specified a port, only include it when
922 // it's not the default. (We never want "http://foo.com:80")
923 var port = uri.port;
924 if (port != -1) {
925 var handler = this._ioService.getProtocolHandler(uri.scheme);
926 if (port != handler.defaultPort)
927 realm += ":" + port;
930 } catch (e) {
931 // bug 159484 - disallow url types that don't support a hostPort.
932 // (although we handle "javascript:..." as a special case above.)
933 this.log("Couldn't parse origin for " + uriString);
934 realm = null;
937 return realm;
940 _getActionOrigin : function (form) {
941 var uriString = form.action;
943 // A blank or mission action submits to where it came from.
944 if (uriString == "")
945 uriString = form.baseURI; // ala bug 297761
947 return this._getPasswordOrigin(uriString, true);
952 * _fillDocument
954 * Called when a page has loaded. For each form in the document,
955 * we check to see if it can be filled with a stored login.
957 _fillDocument : function (doc) {
958 var forms = doc.forms;
959 if (!forms || forms.length == 0)
960 return;
962 var formOrigin = this._getPasswordOrigin(doc.documentURI);
964 // If there are no logins for this site, bail out now.
965 if (!this.countLogins(formOrigin, "", null))
966 return;
968 this.log("fillDocument processing " + forms.length +
969 " forms on " + doc.documentURI);
971 var autofillForm = this._prefBranch.getBoolPref("autofillForms");
972 var previousActionOrigin = null;
973 var foundLogins = null;
975 for (var i = 0; i < forms.length; i++) {
976 var form = forms[i];
978 // Only the actionOrigin might be changing, so if it's the same
979 // as the last form on the page we can reuse the same logins.
980 var actionOrigin = this._getActionOrigin(form);
981 if (actionOrigin != previousActionOrigin) {
982 foundLogins = null;
983 previousActionOrigin = actionOrigin;
985 this.log("_fillDocument processing form[" + i + "]");
986 foundLogins = this._fillForm(form, autofillForm, false, foundLogins)[1];
987 } // foreach form
992 * _fillform
994 * Fill the form with login information if we can find it. This will find
995 * an array of logins if not given any, otherwise it will use the logins
996 * passed in. The logins are returned so they can be reused for
997 * optimization. Success of action is also returned in format
998 * [success, foundLogins]. autofillForm denotes if we should fill the form
999 * in automatically, ignoreAutocomplete denotes if we should ignore
1000 * autocomplete=off attributes, and foundLogins is an array of nsILoginInfo
1001 * for optimization
1003 _fillForm : function (form, autofillForm, ignoreAutocomplete, foundLogins) {
1004 // Heuristically determine what the user/pass fields are
1005 // We do this before checking to see if logins are stored,
1006 // so that the user isn't prompted for a master password
1007 // without need.
1008 var [usernameField, passwordField, ignored] =
1009 this._getFormFields(form, false);
1011 // Need a valid password field to do anything.
1012 if (passwordField == null)
1013 return [false, foundLogins];
1015 // If the fields are disabled or read-only, there's nothing to do.
1016 if (passwordField.disabled || passwordField.readOnly ||
1017 usernameField && (usernameField.disabled ||
1018 usernameField.readOnly)) {
1019 this.log("not filling form, login fields disabled");
1020 return [false, foundLogins];
1023 // Need to get a list of logins if we weren't given them
1024 if (foundLogins == null) {
1025 var formOrigin =
1026 this._getPasswordOrigin(form.ownerDocument.documentURI);
1027 var actionOrigin = this._getActionOrigin(form);
1028 foundLogins = this.findLogins({}, formOrigin, actionOrigin, null);
1029 this.log("found " + foundLogins.length + " matching logins.");
1030 } else {
1031 this.log("reusing logins from last form.");
1034 // Discard logins which have username/password values that don't
1035 // fit into the fields (as specified by the maxlength attribute).
1036 // The user couldn't enter these values anyway, and it helps
1037 // with sites that have an extra PIN to be entered (bug 391514)
1038 var maxUsernameLen = Number.MAX_VALUE;
1039 var maxPasswordLen = Number.MAX_VALUE;
1041 // If attribute wasn't set, default is -1.
1042 if (usernameField && usernameField.maxLength >= 0)
1043 maxUsernameLen = usernameField.maxLength;
1044 if (passwordField.maxLength >= 0)
1045 maxPasswordLen = passwordField.maxLength;
1047 logins = foundLogins.filter(function (l) {
1048 var fit = (l.username.length <= maxUsernameLen &&
1049 l.password.length <= maxPasswordLen);
1050 if (!fit)
1051 this.log("Ignored " + l.username + " login: won't fit");
1053 return fit;
1054 }, this);
1057 // Nothing to do if we have no matching logins available.
1058 if (logins.length == 0)
1059 return [false, foundLogins];
1062 // Attach autocomplete stuff to the username field, if we have
1063 // one. This is normally used to select from multiple accounts,
1064 // but even with one account we should refill if the user edits.
1065 if (usernameField)
1066 this._attachToInput(usernameField);
1068 // Don't clobber an existing password.
1069 if (passwordField.value)
1070 return [false, foundLogins];
1072 // If the form has an autocomplete=off attribute in play, don't
1073 // fill in the login automatically. We check this after attaching
1074 // the autocomplete stuff to the username field, so the user can
1075 // still manually select a login to be filled in.
1076 var isFormDisabled = false;
1077 if (!ignoreAutocomplete &&
1078 (this._isAutocompleteDisabled(form) ||
1079 this._isAutocompleteDisabled(usernameField) ||
1080 this._isAutocompleteDisabled(passwordField))) {
1082 isFormDisabled = true;
1083 this.log("form not filled, has autocomplete=off");
1086 // Variable such that we reduce code duplication and can be sure we
1087 // should be firing notifications if and only if we can fill the form.
1088 var selectedLogin = null;
1090 if (usernameField && usernameField.value) {
1091 // If username was specified in the form, only fill in the
1092 // password if we find a matching login.
1094 var username = usernameField.value.toLowerCase();
1096 var matchingLogin;
1097 var found = logins.some(function(l) {
1098 matchingLogin = l;
1099 return (l.username.toLowerCase() == username);
1101 if (found)
1102 selectedLogin = matchingLogin;
1103 else
1104 this.log("Password not filled. None of the stored " +
1105 "logins match the username already present.");
1107 } else if (usernameField && logins.length == 2) {
1108 // Special case, for sites which have a normal user+pass
1109 // login *and* a password-only login (eg, a PIN)...
1110 // When we have a username field and 1 of 2 available
1111 // logins is password-only, go ahead and prefill the
1112 // one with a username.
1113 if (!logins[0].username && logins[1].username)
1114 selectedLogin = logins[1];
1115 else if (!logins[1].username && logins[0].username)
1116 selectedLogin = logins[0];
1117 } else if (logins.length == 1) {
1118 selectedLogin = logins[0];
1119 } else {
1120 this.log("Multiple logins for form, so not filling any.");
1123 var didFillForm = false;
1124 if (selectedLogin && autofillForm && !isFormDisabled) {
1125 // Fill the form
1126 if (usernameField)
1127 usernameField.value = selectedLogin.username;
1128 passwordField.value = selectedLogin.password;
1129 didFillForm = true;
1130 } else if (selectedLogin && !autofillForm) {
1131 // For when autofillForm is false, but we still have the information
1132 // to fill a form, we notify observers.
1133 this._observerService.notifyObservers(form, "passwordmgr-found-form", "noAutofillForms");
1134 this.log("autofillForms=false but form can be filled; notified observers");
1135 } else if (selectedLogin && isFormDisabled) {
1136 // For when autocomplete is off, but we still have the information
1137 // to fill a form, we notify observers.
1138 this._observerService.notifyObservers(form, "passwordmgr-found-form", "autocompleteOff");
1139 this.log("autocomplete=off but form can be filled; notified observers");
1142 return [didFillForm, foundLogins];
1147 * fillForm
1149 * Fill the form with login information if we can find it.
1151 fillForm : function (form) {
1152 this.log("fillForm processing form[id=" + form.id + "]");
1153 return this._fillForm(form, true, true, null)[0];
1158 * _attachToInput
1160 * Hooks up autocomplete support to a username field, to allow
1161 * a user editing the field to select an existing login and have
1162 * the password field filled in.
1164 _attachToInput : function (element) {
1165 this.log("attaching autocomplete stuff");
1166 element.addEventListener("blur",
1167 this._domEventListener, false);
1168 element.addEventListener("DOMAutoComplete",
1169 this._domEventListener, false);
1170 this._formFillService.markAsLoginManagerField(element);
1172 }; // end of LoginManager implementation
1177 // nsIAutoCompleteResult implementation
1178 function UserAutoCompleteResult (aSearchString, matchingLogins) {
1179 function loginSort(a,b) {
1180 var userA = a.username.toLowerCase();
1181 var userB = b.username.toLowerCase();
1183 if (userA < userB)
1184 return -1;
1186 if (userB > userA)
1187 return 1;
1189 return 0;
1192 this.searchString = aSearchString;
1193 this.logins = matchingLogins.sort(loginSort);
1194 this.matchCount = matchingLogins.length;
1196 if (this.matchCount > 0) {
1197 this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
1198 this.defaultIndex = 0;
1202 UserAutoCompleteResult.prototype = {
1203 QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
1204 Ci.nsISupportsWeakReference]),
1206 // private
1207 logins : null,
1209 // Interfaces from idl...
1210 searchString : null,
1211 searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
1212 defaultIndex : -1,
1213 errorDescription : "",
1214 matchCount : 0,
1216 getValueAt : function (index) {
1217 if (index < 0 || index >= this.logins.length)
1218 throw "Index out of range.";
1220 return this.logins[index].username;
1223 getCommentAt : function (index) {
1224 return "";
1227 getStyleAt : function (index) {
1228 return "";
1231 getImageAt : function (index) {
1232 return "";
1235 removeValueAt : function (index, removeFromDB) {
1236 if (index < 0 || index >= this.logins.length)
1237 throw "Index out of range.";
1239 var [removedLogin] = this.logins.splice(index, 1);
1241 this.matchCount--;
1242 if (this.defaultIndex > this.logins.length)
1243 this.defaultIndex--;
1245 if (removeFromDB) {
1246 var pwmgr = Cc["@mozilla.org/login-manager;1"].
1247 getService(Ci.nsILoginManager);
1248 pwmgr.removeLogin(removedLogin);
1253 var component = [LoginManager];
1254 function NSGetModule (compMgr, fileSpec) {
1255 return XPCOMUtils.generateModule(component);